- 自分でパケットキャプチャツールを作ってみたい
- パケットの構造に興味がある
前回はパケットキャプチャでIPヘッダーの表示を行いましたので、今回はUDPヘッダーの解析と表示をおこないます。
UDPヘッダーは単純な構造のためパケットキャプチャツール作成の入門に最適です。UDPヘッダーを解析できるようになればTCPやICMPも同様に解析できるようになるでしょう。
UDPヘッダーの構造については次の記事を参照してください。
TCP/IPヘッダ構造パケットキャプチャツールのつくり方
ヘッダーファイル取り扱いについての注意
macOS、BSDとLinuxで共通のコードを記述する場合はUDPヘッダーの構造体定義が異なるためコンパイルエラーに注意する必要があります。
幸いにもLinuxにはBSD形式の構造体が用意されていて「__FAVOR_BSD」を定義することで使用することができるようになります。
具体的にどうするのかというと、以下のようにnetinet/udp.hをincludeする前に__FAVOR_BSDを定義します。
#ifndef __FAVOR_BSD # define __FAVOR_BSD #endif
UDPヘッダーのマッピング方法
この方法はTCPやICMPでも同様の方法です。
パケットの中でUDPヘッダーの先頭へアクセスするためにはIPヘッダーのサイズを知る必要があります。
IPヘッダーは可変長です。そのためIPヘッダーの長さを知るために ip_hl を使用します。ip_hl はIPヘッダーのサイズを4オクテット単位で表しているためip_hlの値を4倍することによってIPヘッダーのサイズを知ることができます。
今回のサンプルコードでは以下のようにしてUDPヘッダーが格納されているメモリアドレスを計算しています。
struct udphdr *udp; udp = (struct udphdr *)((char *)ip + (ip->ip_hl<<2));
このようにip_hlを左へ2ビットシフトした値をポインタipに加算しています。
上位プロトコルの判定
IPヘッダーの上位プロトコルはIPヘッダーの ip_p を調べることによって知ることができます。
どのような値が定義されているのかについてはヘッダーファイルのnetinet/in.hを見てください。
サンプルコードでは以下のようにしてプロトコルによる処理の振り分けをおこなっています(まだUDPしか処理していません)。
switch(ip->ip_p) { case IPPROTO_UDP: print_udpheader(ip); break; case IPPROTO_TCP: case IPPROTO_ICMP: default: break; }
UDPヘッダー値を表示する
UDPヘッダーの表示はIPヘッダーでのやり方と変わりません。バイトオーダーに注意して各フィールドの値を表示するだけです。
サンプルコードでは、以下のようにしています。
printf("uh_sport = %d\n", ntohs(udp->uh_sport)); printf("uh_dport = %d\n", ntohs(udp->uh_dport)); printf("uh_ulen = %d bytes\n", ntohs(udp->uh_ulen)); printf("uh_sum = 0x%.4x\n", ntohs(udp->uh_sum));
サンプルコードと実行例
/* * UDPパケットキャプチャサンプルコード */ #include <stdio.h> #include <stdlib.h> #include <pcap.h> #include <netinet/ip.h> #ifndef __FAVOR_BSD # define __FAVOR_BSD #endif #include <netinet/udp.h> #include <net/ethernet.h> #include <arpa/inet.h> static void print_udpheader(struct ip *ip) { struct udphdr *udp; udp = (struct udphdr *)((char *)ip + (ip->ip_hl<<2)); printf("uh_sport = %d\n", ntohs(udp->uh_sport)); printf("uh_dport = %d\n", ntohs(udp->uh_dport)); printf("uh_ulen = %d bytes\n", ntohs(udp->uh_ulen)); printf("uh_sum = 0x%.4x\n", ntohs(udp->uh_sum)); printf("\n\n"); } static void print_ipheader(char *p) { struct ip *ip; ip = (struct ip *)p; printf("ip_v = 0x%x\n", ip->ip_v); printf("ip_hl = 0x%x\n", ip->ip_hl); printf("ip_tos = 0x%.2x\n", ip->ip_tos); printf("ip_len = %d bytes\n", ntohs(ip->ip_len)); printf("ip_id = 0x%.4x\n", ntohs(ip->ip_id)); printf("ip_off = 0x%.4x\n", ntohs(ip->ip_off)); printf("ip_ttl = 0x%.2x\n", ip->ip_ttl); printf("ip_p = 0x%.2x\n", ip->ip_p); printf("ip_sum = 0x%.4x\n", ntohs(ip->ip_sum)); printf("ip_src = %s\n", inet_ntoa(ip->ip_src)); printf("ip_dst = %s\n", inet_ntoa(ip->ip_dst)); printf("\n"); switch(ip->ip_p) { case IPPROTO_UDP: print_udpheader(ip); break; case IPPROTO_TCP: case IPPROTO_ICMP: default: break; } } static void usage(char *prog) { fprintf(stderr, "Usage: %s <device> <packet filter>\n", prog); exit(EXIT_FAILURE); } int main(int argc, char *argv[]) { pcap_t *handle; const unsigned char *packet; char *dev, *filter; 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 ((filter = argv[2]) == NULL) usage(argv[0]); /* 受信用のデバイスを開く */ if ((handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf)) == NULL) { fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf); exit(EXIT_FAILURE); } /* イーサネットのみ */ if (pcap_datalink(handle) != DLT_EN10MB) { fprintf(stderr, "Device not support: %s\n", dev); exit(EXIT_FAILURE); } /* パケットフィルター設定 */ if (pcap_compile(handle, &fp, filter, 0, net) == -1) { fprintf(stderr, "Couldn't parse filter: %s\n", pcap_geterr(handle)); return -1; } if (pcap_setfilter(handle, &fp) == -1) { fprintf(stderr, "Couldn't install filter: %s\n", pcap_geterr(handle)); return -1; } /* ループでパケットを受信 */ 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_capture_udp.c -lpcap $ sudo ./a.out en1 'udp and port 53' Password: ip_v = 0x4 ip_hl = 0x5 ip_tos = 0x00 ip_len = 57 bytes ip_id = 0xc54e ip_off = 0x0000 ip_ttl = 0x40 ip_p = 0x11 ip_sum = 0xe2a9 ip_src = 192.168.2.4 ip_dst = 8.8.8.8 uh_sport = 62290 uh_dport = 53 uh_ulen = 37 bytes uh_sum = 0x7c27
今回のサンプルコードはパケットフィルターを引数で指定できるようにしているので、実行時に「udp and port 53」を引数として渡すことで、DNSパケットのみ受信しています。
まとめ
UDPヘッダーは単純なため、UDPヘッダーを表示させられるようになればTCPやICMPも簡単に対応できますから興味のある方は是非UDPヘッダーを自在に表示できるようになってください。
更に詳細な知識を得たい場合は「UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI・ソケットとXTI」がおすすめです。