RAW Socket / BPF(Berkeley Packet Filter)を用いたパケットキャプチャーツールの実装

パケットキャプチャーツールは、ネットワークを流れるすべてのパケットを受け取り解析します。 NIC(Network Interface Card)のほとんどはプロミスキャスモードとよばれるモードをサポートしており、これを有効にすることでアドレスにかかわらずNICはすべてのパケットをホストに渡します。 ソフトウェアとハードウェアが連携して動作するため、扱っているレイヤーが低く環境によってInterfaceに差異があります。

tcpdumpの開発者によってつくられたlibpcapというライブラリはUNIXのシステムの差異を吸収します。またWindowsにもWinPcapという名前で移植されています。 もしパケットキャプチャーを作る際にはlibpcapを利用することが一般的かと思いますが、今回は勉強も兼ねて LinuxmacOS で動作するパケットキャプチャーをlibpcapを使わずに1からC言語で実装してみました。

※ BPF VM(Berkeley Packet Filter Virtual Machine)によるFilteringの仕組みには今回は触れません。

f:id:nwpct1:20181028135417g:plain

目次

xpcap のソースコード(Github)

github.com

最近作りたいなと思っているパケットキャプチャー関連のソフトわがありそちらはGo言語で実装しているのですが、せっかくなら移植性を考えてPure Goで実装したいと思っています。こういったレイヤーのプログラムをいきなりCのAPIがベースにあってそれをGoで書くとドキュメントを追うのも大変なので、C言語でまずは書いてみたものがこちらです。

プロトコルは今のところARPIPv4IPv6TCPUDP、ICMPに対応していて、Ethernet Frameからパースした結果を標準出力に書き出します。

RAW SOCKETを用いたキャプチャー (Linux)

LinuxMACアドレスEthernet Frameのヘッダー情報までプログラムで扱うには、RAW Socketが必要です。 ソケットディスクリプタを取得する際には、アドレスファミリーとして AF_PACKET 、ソケットタイプとして SOCK_RAW そして第3引数のprotocolには htons(EATH_P_ALL) を指定します。全部を説明すると長くなるので手順と呼び出さないといけない関数を次に示します。 xpcapのソースコードと合わせてご覧ください。

  1. socket() ディスクリプタの取得
    • int soc = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)))
  2. en0 などのインターフェイス名を指定してインターフェイスの情報を取得
    • ioctl(soc, SIOCGIFINDEX, &if_req)
  3. ソケットディスクリプターをインターフェイスにバインド
    • bind(soc, (struct sockaddr *) &sa, sizeof(sa))
  4. インターフェイスのフラグを取得
    • ioctl(soc, SIOCGIFFLAGS, &if_req)
  5. プロミスキャスモードを有効にし、インターフェイスをUP(動作中)にする
    • ioctl(soc, SIOCSIFFLAGS, &if_req)

これで準備が完了です。あとは selectepoll でソケットディスクリプターへの書き込みを監視しready担った状態で recv(2) で読み出せばOKです。

struct timeval timeout;
fd_set mask;
int width, len, ready;
while (g_gotsig == 0) {
    FD_ZERO(&mask);
    FD_SET(soc, &mask);
    width = doc + 1;

    timeout.tv_sec = 8;
    timeout.tv_usec = 0;
    ready = select(width, &mask, NULL, NULL, &timeout);
    if (ready == -1) {
        perror("select");
        break;
    } else if (ready == 0) {
        fprintf(stderr, "select timeout");
        break;
    }

    if (FD_ISSET(sniffer->fd, &mask)){
        if ((len = recv(soc, buffer, >buf_len, 0)) == -1){
            perror("recv:");
            return -1;
        }
    }
}

自分は Linuxネットワークプログラミングバイブル で勉強しましたが、この書籍以外にもLinuxで動くRAW SOCKETを使ったシンプルなパケットキャプチャーの作り方を解説している資料は多くあります。一方でBSD系のOSではアドレスファミリーとして AF_PACKET を指定できません。BSD系のOSでEthernet frameを読み出す方法を確認しましょう。

BPF(Berkeley Packet Filter)によるキャプチャー (macOS, BSD系)

これらはBPF(Berkeley Packet Filter)という仕組みを使う必要があります。 BPFにはBPF Virtual Machineという仕組みを使ってパケットをKernel側でフィルタリングすることで必要ないものまでユーザー空間に移さずオーバーヘッドを減らす仕組みのようです。読み出しには BPFデバイスというのを用います。ひとまずすべてキャプチャーするならBPF VMについては気にする必要はありません。

BPFデバイスは、 /dev/bpf* に存在します。これらを順にopenしながら、使用可能なBPFデバイスを探さなくてはいけません。

$ ls /dev/bpf?
/dev/bpf0 /dev/bpf1 /dev/bpf2 /dev/bpf3 /dev/bpf4 /dev/bpf5 /dev/bpf6 /dev/bpf7 /dev/bpf8 /dev/bpf9

手元では bpf255 ぐらいまで存在しますが、google/gopacketなどの実装では99までチェックしているようです。 NICの数以上に必要になるケースはほとんどなさそうなので99個は十分に余裕を持った値なんだと思います。

gopacket/bsd_bpf_sniffer.go at a35e09f9f224786863ce609de910bc82fc4d4faf · google/gopacket · GitHub

BPFデバイスが決まったらOpenします。その後次のような手順が準備に必要になります。

  1. bpfデバイスのopen
    • fd = open(params.device, O_RDWR)
  2. バッファ長の設定 or 取得。BIOCSBLEN の変更は、BPFデバイスNICアサインする BIOCSETIF より先に呼び出される必要があるので注意してください。これになかなか気づかず結構はまってしまいました。
    • ioctl(fd, BIOCSBLEN, &params.buf_len) : 設定
    • ioctl(fd, BIOCGBLEN, &params.buf_len) : 取得
  3. BPFデバイスとネットワークインターフェイスをバインド
    • ioctl(fd, BIOCSETIF, &if_req)
  4. プロミスキャスモードの有効化
    • ioctl(fd, BIOCPROMISC, NULL)

こちらはデバイスファイルなので recv(2) ではなく read(2) で読み出します。 読み出すとイーサネットのフレームではなくBPFパケットというものにくるまれています。 ヘッダーをパースするとデータ長が乗っているため、それをもとに次のBPFパケットの位置を求めてパースを繰り返していきます。

typedef struct {
    int fd;
    char device[11];
    unsigned int buf_len;
    char *buffer;
    unsigned int last_read_len;
    unsigned int read_bytes_consumed;
} Sniffer;

int
parse_bpf_packets(Sniffer *sniffer, CapturedInfo *info)
{
    if (sniffer->read_bytes_consumed + sizeof(sniffer->buffer) >= sniffer->last_read_len) {
        return 0;
    }

    info->bpf_hdr = (struct bpf_hdr*)((long)sniffer->buffer + (long)sniffer->read_bytes_consumed);
    info->data = sniffer->buffer + (long)sniffer->read_bytes_consumed + info->bpf_hdr->bh_hdrlen;
    sniffer->read_bytes_consumed += BPF_WORDALIGN(info->bpf_hdr->bh_hdrlen + info->bpf_hdr->bh_caplen);
    return info->bpf_hdr->bh_datalen;
}

あとはごりごりパースしていくのですが、そこはプラットフォームに変わらず同じです。 ゴリゴリ実装していくだけで解説してもしかたないのでマスタリングTCP/IPなどを頼りにソースコードを読んでみてください。

実行方法

build.sh でビルドできます。Vagrantfileも用意しているのでLinuxで試したいmacOSユーザーの方はご利用ください。実行結果は次のような感じです。

$ ./build.sh
$ ./xpcap en0 -v
device = en0, verbose = 1, port = 0

================================================================================
[TCP6]
ether_header--------------------------------------------------------------------
ether_dhost = XX:XX:XX:XX:XX:XX
ether_shost = XX:XX:XX:XX:XX:XX
ether_type = 86DD(IPv6)
ip6-----------------------------------------------------------------------------
ip6_vfc = 96
ip6_flow = 2363892320
ip6_plen = 15104
(TCP), ip6_hlim = 56
ip6_src = xxxx:xxxx:xxxx:x::xxxx:xxxx
ip6_dst = yyyy:yy:yyyy:yyyy:yyyy:yyyy:yyyy:yyyy
tcphdr--------------------------------------------------------------------------
source: 47873
destination: 59083
sequence number: 1148644729
ack number = 2897299570
data offset = 5, control flag = 24, window = 49152, checksum = 54057, urgent pointer = 0
data----------------------------------------------------------------------------
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ..something..data..
================================================================================

================================================================================
[ARP]
ether_header--------------------------------------------------------------------
ether_dhost = XX:XX:XX:XX:XX:XX
ether_shost = XX:XX:XX:XX:XX:XX
ether_type = 806(Address resolution)
ether_arp-----------------------------------------------------------------------
arp_hrd = 1(Ethernet 10/100Mbps.), arp_pro = 2048(IP)
arp_hln = 6, arp_pln = 4, arp_op = 1(ARP request.)
arp_sha = 34:76:C5:77:5D:4C
arp_spa = 192.168.0.1
arp_tha = 00:00:00:00:00:00
arp_tpa = 192.168.0.8
================================================================================

================================================================================
[UDP]
ether_header--------------------------------------------------------------------
ether_dhost = XX:XX:XX:XX:XX:XX
ether_shost = XX:XX:XX:XX:XX:XX
ether_type = 800(IP)
ip------------------------------------------------------------------------------
ip_v = 4, ip_hl = 5, ip_tos = 0, ip_len = 149
ip_id = 29282, ip_off = 0, 0
ip_ttl = 255, ip_p = 17(UDP), ip_sum = 42831
ip_src = yyy.yyy.yyy.yyy
ip_dst = xxx.xxx.xxx.xxx
udphdr--------------------------------------------------------------------------
source = 5353, dest = 5353
len = 129, check = 38825
data----------------------------------------------------------------------------
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ..something..data..
================================================================================

参考ソースコード

困ったときは次のコードが参考になりました。

またLinuxネットワークプログラミングバイブルはかなりおすすめの書籍です。

Linuxネットワークプログラミングバイブル

Linuxネットワークプログラミングバイブル

実践 パケット解析 第3版 ―Wiresharkを使ったトラブルシューティング

実践 パケット解析 第3版 ―Wiresharkを使ったトラブルシューティング