RAW Socket / BPF(Berkeley Packet Filter)を用いたパケットキャプチャーツールの実装
パケットキャプチャーツールは、ネットワークを流れるすべてのパケットを受け取り解析します。 NIC(Network Interface Card)のほとんどはプロミスキャスモードとよばれるモードをサポートしており、これを有効にすることでアドレスにかかわらずNICはすべてのパケットをホストに渡します。 ソフトウェアとハードウェアが連携して動作するため、扱っているレイヤーが低く環境によってInterfaceに差異があります。
tcpdumpの開発者によってつくられたlibpcapというライブラリはUNIXのシステムの差異を吸収します。またWindowsにもWinPcapという名前で移植されています。 もしパケットキャプチャーを作る際にはlibpcapを利用することが一般的かと思いますが、今回は勉強も兼ねて Linux と macOS で動作するパケットキャプチャーをlibpcapを使わずに1からC言語で実装してみました。
※ BPF VM(Berkeley Packet Filter Virtual Machine)によるFilteringの仕組みには今回は触れません。
目次
- xpcap のソースコード(Github)
- RAW SOCKETを用いたキャプチャー (Linux)
- BPF(Berkeley Packet Filter)によるキャプチャー (macOS, BSD系)
- 実行方法
- 参考ソースコード
xpcap のソースコード(Github)
最近作りたいなと思っているパケットキャプチャー関連のソフトわがありそちらはGo言語で実装しているのですが、せっかくなら移植性を考えてPure Goで実装したいと思っています。こういったレイヤーのプログラムをいきなりCのAPIがベースにあってそれをGoで書くとドキュメントを追うのも大変なので、C言語でまずは書いてみたものがこちらです。
プロトコルは今のところARPやIPv4、IPv6、TCP、UDP、ICMPに対応していて、Ethernet Frameからパースした結果を標準出力に書き出します。
RAW SOCKETを用いたキャプチャー (Linux)
LinuxでMACアドレスやEthernet Frameのヘッダー情報までプログラムで扱うには、RAW Socketが必要です。
ソケットディスクリプタを取得する際には、アドレスファミリーとして AF_PACKET
、ソケットタイプとして SOCK_RAW
そして第3引数のprotocolには htons(EATH_P_ALL)
を指定します。全部を説明すると長くなるので手順と呼び出さないといけない関数を次に示します。
xpcapのソースコードと合わせてご覧ください。
socket()
ディスクリプタの取得int soc = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)))
en0
などのインターフェイス名を指定してインターフェイスの情報を取得ioctl(soc, SIOCGIFINDEX, &if_req)
- ソケットディスクリプターをインターフェイスにバインド
bind(soc, (struct sockaddr *) &sa, sizeof(sa))
- インターフェイスのフラグを取得
ioctl(soc, SIOCGIFFLAGS, &if_req)
- プロミスキャスモードを有効にし、インターフェイスをUP(動作中)にする
ioctl(soc, SIOCSIFFLAGS, &if_req)
これで準備が完了です。あとは select
や epoll
でソケットディスクリプターへの書き込みを監視し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します。その後次のような手順が準備に必要になります。
- bpfデバイスのopen
fd = open(params.device, O_RDWR)
- バッファ長の設定 or 取得。
BIOCSBLEN
の変更は、BPFデバイスにNICをアサインするBIOCSETIF
より先に呼び出される必要があるので注意してください。これになかなか気づかず結構はまってしまいました。ioctl(fd, BIOCSBLEN, ¶ms.buf_len)
: 設定ioctl(fd, BIOCGBLEN, ¶ms.buf_len)
: 取得
- BPFデバイスとネットワークインターフェイスをバインド
ioctl(fd, BIOCSETIF, &if_req)
- プロミスキャスモードの有効化
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.. ================================================================================
参考ソースコード
困ったときは次のコードが参考になりました。
- GitHub - bpk-t/packet_capture
- gopacket/bsd_bpf_sniffer.go at master · google/gopacket · GitHub
- net/bpf.h Source
またLinuxネットワークプログラミングバイブルはかなりおすすめの書籍です。
- 作者: 小俣光之,種田元樹
- 出版社/メーカー: 秀和システム
- 発売日: 2014/10/07
- メディア: Kindle版
- この商品を含むブログ (1件) を見る
実践 パケット解析 第3版 ―Wiresharkを使ったトラブルシューティング
- 作者: Chris Sanders,高橋基信,宮本久仁男,岡真由美
- 出版社/メーカー: オライリージャパン
- 発売日: 2018/06/16
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る