パケットキャプチャツールのつくり方【C言語】 〜IPヘッダー解析〜

このページはこんな方におすすめです
  • 自分でパケットキャプチャツールを作ってみたい
  • パケットの構造に興味がある
  • ネットワークの知識を深めたい

「パケットキャプチャツールをつくる」と聞くと難しそうな印象を持ってしまうかもしれませんが、そんなことはありません。

はじめは高機能なものは全然必要なくて最初は単純なもので大丈夫です。簡単なものであれば受信したパケットの中身を解析してヘッダーの内容を表示するくらいのものがC言語100行くらいで書けます。

対象とするOS

  • macOS
  • Linux
  • BSD

対象はUnix系OSですが、Windowsでも基本部分は変わらないと思います。なお、C言語の勉強が目的ではないのでC言語についての解説はしません。

パケットキャプチャツールのつくり方

パケット受信処理方法の違い

macOSとBSDは/dev/bpfというデバイスファイルを開いて読むことでパケットを受信することができます。それに対してLinuxはsocket(2)を使います。

パケットの受信は低レベルな処理なので、どうしてもOSによって差異が生じてしまいます。そこでOSの差異を吸収する仕組みとして生み出されたものがlibpcapです。

libpcapは元々tcpdmpのために作られたライブラリです。

libpcapを使う事によりOSに関係なく統一したコードを書くことができるようになります。今回はこのlibpcapを使ってLinuxとmacOS、BSDで動く共通コードを書くことにします。

libpcapが導入されていない場合は別途インストールする必要があります。わたしの手元のCentOSは未導入でしたので以下のようにしてインストールをおこないました。

# yum -y install libpcap-devel

Ubuntu/Debianでは以下のようにインストールします。

$ sudo apt install libpcap-dev

パケットキャプチャの流れ

パケットキャプチャは以下の流れでおこないます。

デバイス(ソケット)を開く
ループしながらパケットを受信する
受信したパケットをIPヘッダーの構造体にマッピングする
IPヘッダーのプロトコルタイプを調べて、TCPならTCPヘッダー、UDPならUDPヘッダーという感じで構造体にマッピングする
各ヘッダー値を画面に出力する

ポインタが解っていれば問題なく書けると思います。古い本ですが、わたしは「Cプログラミング専門課程」を読んで一発でポインタを理解しました。超オススメの良書です。

パケットを扱うときの注意点

パケットキャプチャのコードを書くにあたって、ひとつだけ注意する点があります。それはバイトオーダーです。バイトーオーダーは「リトルエンディアン」と「ビッグエンディアン」があります。

バイトオーダーはデータをどのような順でメモリに格納するのか、というものです。たとえば0x12345678という4バイトのデータをメモリに格納する場合を想定すると以下のようになります。

ビッグエンディアン ⇒ 12 34 56 78
リトルエンディアン ⇒ 78 56 34 12

このようにビッグエンディアンとリトルエンディアンはメモリへのデータ格納順が逆です。そのためバイトオーダーが異なると同じデータを受信しても値がまったく異なる結果となります。

そのためネットワークの世界では「ネットワークバイトオーダー」でデータを送信するという決まりがあります

ネットワークバイトオーダーに対してローカルのバイトオーダーを「ホストバイトオーダー」といいます。

IPヘッダーやTCPヘッダーなど、2バイト以上の値は必ずバイトオーダーの変換が必要になるので注意してください。

このバイトオーダーの差異を吸収するのがhtons、htonl、htonll、ntohs、ntohl、ntohllです。

ローカルホストのバイトオーダーをネットワークバイトオーダーへ変換するときはhtonXを使い、ネットワークバイトオーダーをローカルホストのバイトオーダーへ変換するときはntohXを使います。

Xの部分は変換するデータのサイズに合わせます。2バイトのデータはshortなので「s」、4バイトのデータはlongなので「l」、8バイトのデータはlong longなので「ll」となります。この後に出てくるサンプルコードを見れば、どういうことか解ると思います。

メモ
  • 送信時はネットワークバイトオーダーに変換
  • 受信時はホストバイトオーダーに変換

パケットを受信する方法

デバイスを開く

pcapでデバイスを指定して開くには、以下のようにします。

/* 受信用のデバイスを開く */
if ((handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf)) == NULL) {
    fprintf(stderr, "Couldn't open device %s: %sn", dev, errbuf);
    exit(EXIT_FAILURE);
}
/* イーサネットのみ */
if (pcap_datalink(handle) != DLT_EN10MB) {
    fprintf(stderr, "Device not support: %sn", dev);
    exit(EXIT_FAILURE);
}

今回はサンプルとして ICMPパケットのみ受信するようにフィルターの設定をします。ここで設定しているフィルターの文字列は、普段tcpdumpを使用するときに指定するフィルターの文字列と同じです。

/* ICMPのみ受信するフィルターをおこなう */
if (pcap_compile(handle, &fp, "icmp", 0, net) == -1) {
    fprintf(stderr, "Couldn't parse filter: %sn", pcap_geterr(handle));
    return -1;
}
if (pcap_setfilter(handle, &fp) == -1) {
    fprintf(stderr, "Couldn't install filter: %sn", pcap_geterr(handle));
    return -1;
}

ループしながらパケットを受信する

libpcapでパケットを受信する方法はいくつかあるのですが、今回は単純なpcap_next()を使用します。

/* ループでパケットを受信 */
while (1) {
    if ((packet = pcap_next(handle, &header)) == NULL)
        continue;

    /* イーサネットヘッダーとIPヘッダーの合計サイズに満たなければ無視 */
    if (header.len < sizeof(struct ether_header)+sizeof(struct ip))
        continue;
    print_ipheader((char *)(packet+sizeof(struct ether_header)));
}

受信したパケットをIPヘッダーの構造体にマッピングする

受信したパケットからIPヘッダーの値へアクセスするには以下のようにしてIPヘッダーの構造体にマッピングします。

struct ip *ip;

p = (struct ip *)p;

各ヘッダー値を画面に出力する

IPヘッダーの構造体にマッピングできたら、あとは値を取得して画面へ表示するだけです。先述したとおりntohXでホストバイトオーダーに変換します。IPヘッダーの場合は基本的に2バイトの値がメインなのでntohsを使うことになると思います。

printf("ip_v = 0x%xn", ip->ip_v);
printf("ip_hl = 0x%xn", ip->ip_hl);
printf("ip_tos = 0x%.2xn", ip->ip_tos);
printf("ip_len = %d bytesn", ntohs(ip->ip_len));
printf("ip_id = 0x%.4xn", ntohs(ip->ip_id));
printf("ip_off = 0x%.4xn", ntohs(ip->ip_off));
printf("ip_ttl = 0x%.2xn", ip->ip_ttl);
printf("ip_p = 0x%.2xn", ip->ip_p);
printf("ip_sum = 0x%.4xn", ntohs(ip->ip_sum));
printf("ip_src = %sn", inet_ntoa(ip->ip_src));
printf("ip_dst = %sn", inet_ntoa(ip->ip_dst));
printf("n");

サンプルコードと実行例

サンプルコード

サンプルコードはGitHubからもダウンロードできます。

/*
 * パケットキャプチャサンプルコード
 * by shj@netwiz.jp
 */

#include <stdio.h>
#include <stdlib.h>
#include <pcap.h>
#include <netinet/ip.h>
#include <net/ethernet.h>
#include <arpa/inet.h>

static void
print_ipheader(char *p)
{
    struct ip *ip;

    ip = (struct ip *)p;
    printf("ip_v = 0x%xn", ip->ip_v);
    printf("ip_hl = 0x%xn", ip->ip_hl);
    printf("ip_tos = 0x%.2xn", ip->ip_tos);
    printf("ip_len = %d bytesn", ntohs(ip->ip_len));
    printf("ip_id = 0x%.4xn", ntohs(ip->ip_id));
    printf("ip_off = 0x%.4xn", ntohs(ip->ip_off));
    printf("ip_ttl = 0x%.2xn", ip->ip_ttl);
    printf("ip_p = 0x%.2xn", ip->ip_p);
    printf("ip_sum = 0x%.4xn", ntohs(ip->ip_sum));
    printf("ip_src = %sn", inet_ntoa(ip->ip_src));
    printf("ip_dst = %sn", inet_ntoa(ip->ip_dst));
    printf("n");
}

static void
usage(char *prog)
{

    fprintf(stderr, "Usage: %s <device>n", prog);
    exit(EXIT_FAILURE);
}

int
main(int argc, char *argv[])
{
    pcap_t *handle;
    const unsigned char *packet;
    char *dev;
    char errbuf[PCAP_ERRBUF_SIZE];
    struct pcap_pkthdr header;
    struct bpf_program fp;
    bpf_u_int32 net;

    if ((dev = argv[1]) == NULL)
        usage(argv[0]);

    /* 受信用のデバイスを開く */
    if ((handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf)) == NULL) {
        fprintf(stderr, "Couldn't open device %s: %sn", dev, errbuf);
        exit(EXIT_FAILURE);
    }
    /* イーサネットのみ */
    if (pcap_datalink(handle) != DLT_EN10MB) {
        fprintf(stderr, "Device not support: %sn", dev);
        exit(EXIT_FAILURE);
    }    

    /* ICMPのみ受信するフィルターの設定をおこなう */
    if (pcap_compile(handle, &fp, "icmp", 0, net) == -1) {
        fprintf(stderr, "Couldn't parse filter: %sn", pcap_geterr(handle));
        exit(EXIT_FAILURE);
    }
    if (pcap_setfilter(handle, &fp) == -1) {
        fprintf(stderr, "Couldn't install filter: %sn", pcap_geterr(handle));
        exit(EXIT_FAILURE);
    }

    /* ループでパケットを受信 */
    while (1) {
        if ((packet = pcap_next(handle, &header)) == NULL)
            continue;

        /* イーサネットヘッダーとIPヘッダーの合計サイズに満たなければ無視 */
        if (header.len < sizeof(struct ether_header)+sizeof(struct ip))
            continue;
        print_ipheader((char *)(packet+sizeof(struct ether_header)));
    }

    /* ここに到達することはない */
    pcap_close(handle);
    return 0;
}

サンプルコードのコンパイルと実行例

コンパイルするには以下のようにします。libpcapとリンクするために”-l pcap”が必要であることに注意してください。

$ cc packet_cape_icmp.c -lpcap

コンパイルは一般ユーザーで構いませんが実行はルート権限でおこないます。手元のmacOSでは以下のようにして実行しました。

$ sudo ./a.out en1
Password:
ip_v = 0x4
ip_hl = 0x5
ip_tos = 0x00
ip_len = 84 bytes
ip_id = 0x5f71
ip_off = 0x0000
ip_ttl = 0x40
ip_p = 0x01
ip_sum = 0x95dd
ip_src = 192.168.2.4
ip_dst = 192.168.2.6

コンパイルしたプログラムを実行後、ICMP通信を受信するとIPヘッダーの値を表示します。

まとめ

パケット解析のコツは /usr/include/netinet 以下にあるip.hやtcp.hなどを読むことです。これが読めれば、受信したデータを構造体にマッピングして値を表示するだけです。

ネットワークプログラミングに興味があれば「UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI・ソケットとXTI」を読むことをおすすめします。著者、翻訳者共に世界最高レベルのハッカーが書いた名著です。

コメントを残す

メールアドレスが公開されることはありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)