- 自分でパケットを作って送信してみたい
- パケットの構造に興味がある
パケットジェネレータのサンプルとしてIPヘッダ−とUDPヘッダーを自ら設定しUDPパケットを送信する方法を解説します。
まず最初にパケットジェネレータの作成に必要な知識を解説し、最後にサンプルコードと実行例を掲載します。サンプルコードのコンパイルと実行はOS X El CapitanとCentOS 7でおこなっています。
パケットジェネレータのつくり方
パケット送信の流れ
ルート権限が必要です
バイトオーダーに注意します
チェックサムは不要ですが自ら設定する方法を解説します
IPヘッダー作成のポイント
Raw Socket(生ソケット)を作成する
IPヘッダーを含めて送信するためにはRaw Socket(生ソケット)を作成する必要があります。
Raw Socketを使うにはセキュリティの関係上、ルート権限が必要になります。
ソケットオプションについて
IPヘッダーを送信パケットに含めるためソケットオプション(IP_HDRINCL)を設定する必要があります。
Linuxはこのオプションを設定しなくてもIPヘッダーを含めてパケットを送信できますがBSD(macOS)はソケットオプションが必須です。
バイトオーダー
IPヘッダーを作成する上で重要な点は「ip_len」「ip_off」の取り扱いです。
IPヘッダーを作成するときのバイトオーダーについて明記されたドキュメントはありませんが、LinuxはすべてのIPヘッダー値をネットワークバイトオーダーで設定する必要があるのに対して、BSD(macOS)はip_lenとip_offをホストバイトオーダーで設定し、それ以外はネットワークバイトオーダーで設定する必要があります(UNIXネットワークプログラミング 第2版 Vol.1 p.635「25.3 rawソケットからの出力」)。
ただしLinuxはip_lenを設定しても無視しsendto(2)で引数にとる送信サイズをip_lenへ自動的に設定します。そのため今回のサンプルではip_lenとip_offは両方ともホストバイトオーダーで設定しています。
補足情報としてNmapはLinuxとBSDのバイトオーダー設定方法の差異を吸収するBSDFIX/BSDUFIXというマクロを使用していましたが、3.90からこのマクロを削除しています(Nmap Change Log)。
またチェックサムを計算して設定していますがIPヘッダーのチェックサムはOSがパケット送信時に設定しますので、適当な値(123など)を設定しても問題なく送信できます。
- Linuxはすべてネットワークバイトオーダー
- BSDは ip_len と ip_off をホストバイトオーダー、その他はネットワークバイトオーダーで送信する
- IPヘッダーのチェックサムはOSが計算する
UDPヘッダー作成のポイント
UDPヘッダーの作成で注意するのはチェックサムの計算です。
UDPはチェックサムを0に設定しても問題なく動作するのですが、チェックサム計算の方法を理解するためにも自分で計算してみましょう。
疑似ヘッダー
UDPのチェックサム計算には疑似ヘッダーというものを使います。
0 7 8 15 16 23 24 31 +--------+--------+--------+--------+ | source address | +--------+--------+--------+--------+ | destination address | +--------+--------+--------+--------+ | zero |protocol| UDP length | +--------+--------+--------+--------+
疑似ヘッダーの末尾に送信するUDPヘッダーとデータを付加します。そして疑似ヘッダーとUDPヘッダー、データを含めてチェックサムの計算をおこないます。つまり次のようなデータ構造を用意してチェックサムの計算をおこないます。
0 7 8 15 16 23 24 31 +--------+--------+--------+--------+ | source address | +--------+--------+--------+--------+ | destination address | +--------+--------+--------+--------+ | zero |protocol| UDP length | +--------+--------+--------+--------+ | Source | Destination | | Port | Port | +--------+--------+--------+--------+ | | | | Length | Checksum | +--------+--------+--------+--------+ | | data octets ... +---------------- ...
チェックサムの計算に必要な関数は、BSDのping.cを流用します。
パケットの送信方法
パケットの送信にはsendto(2)を使用します。
BSD(macOS)はLinuxと違って厳密なためIPヘッダーで設定したip_lenの値とsendto(2)へ渡す送信サイズに差異があるとエラーになりますから注意してください。
では、次のセクションでサンプルコードと実行例を掲載します。
サンプルコードと実行例
サンプルコード
UDPはヘッダー構造が単純ですが、それでもコード行数はキャプチャに比べてかなり増えています。なお、コードを単純にする目的でIPヘッダーやUDPヘッダー値の大半をハードコーディングしています。慣れてきたら引数でヘッダー値を設定できるようにすると本格的なパケットジェネレータとなってくるはずです。
/* * udp-gen.c * UDPパケット送信サンプルコード */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #ifndef __FAVOR_BSD # define __FAVOR_BSD #endif #include <netinet/udp.h> #include <netinet/ip.h> #include <err.h> /* チェックサム計算用UDP疑似ヘッダー */ struct pseudo_hdr { struct in_addr src; struct in_addr dst; unsigned char zero; unsigned char proto; unsigned short len; }; static void usage(char *prog) { fprintf(stderr, "Usage: %s <src ip> <dst ip> <port> <string>n", prog); exit(EXIT_FAILURE); } /* * チェックサム計算コード * ping.cから流用 */ static unsigned short in_cksum(unsigned short *addr, int len) { int nleft, sum; unsigned short *w; union { unsigned short us; unsigned char uc[2]; } last; unsigned short answer; nleft = len; sum = 0; w = addr; /* * Our algorithm is simple, using a 32 bit accumulator (sum), we add * sequential 16 bit words to it, and at the end, fold back all the * carry bits from the top 16 bits into the lower 16 bits. */ while (nleft > 1) { sum += *w++; nleft -= 2; } /* mop up an odd byte, if necessary */ if (nleft == 1) { last.uc[0] = *(unsigned char *)w; last.uc[1] = 0; sum += last.us; } /* add back carry outs from top 16 bits to low 16 bits */ sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */ sum += (sum >> 16); /* add carry */ answer = ~sum; /* truncate to 16 bits */ return(answer); } static void build_udp(char *p, struct in_addr *src, struct in_addr *dst, unsigned short dport, char *data) { char *ubuf; struct ip *ip; struct udphdr *udp; struct pseudo_hdr *pse; int needlen; /* チェックサム計算用にUDPヘッダーとデータ、疑似ヘッダの合計サイズを計算する */ needlen = sizeof(struct pseudo_hdr) + sizeof(struct udphdr) + strlen(data); if ((ubuf = malloc(needlen)) == NULL) errx(1, "malloc"); memset(ubuf, 0, needlen); pse = (struct pseudo_hdr *)ubuf; pse->src.s_addr = src->s_addr; pse->dst.s_addr = dst->s_addr; pse->proto = IPPROTO_UDP; pse->len = htons(sizeof(struct udphdr) + strlen(data)); udp = (struct udphdr *)(ubuf + sizeof(struct pseudo_hdr)); udp->uh_sport = htons(65001); udp->uh_dport = htons(dport); udp->uh_ulen = pse->len; udp->uh_sum = 0; /* データ部分の書き込み */ memcpy((char *)udp + sizeof(struct udphdr), data, strlen(data)); /* チェックサム計算 */ udp->uh_sum = in_cksum((unsigned short *)ubuf, needlen); /* UDPヘッダーとデータ部分をIPヘッダーの後ろへ書き込む */ ip = (struct ip *)p; memcpy(p + (ip->ip_hl << 2), udp, needlen - sizeof(struct pseudo_hdr)); free(ubuf); } static void build_ip(char *p, struct in_addr *src, struct in_addr *dst, size_t len) { struct ip *ip; ip = (struct ip *)p; ip->ip_v = 4; /* もちろんIPv4 */ ip->ip_hl = 5; /* IPオプションは使わないので5に決め打ち */ ip->ip_tos = 1; /* TOSが設定されることを確認するため1に設定 */ ip->ip_len = len; /* 今回送信する全パケット長 */ ip->ip_id = htons(getpid()); /* IDは何でもいいので、今回はプロセスIDとする */ ip->ip_off = 0; /* フラグメント化させない */ ip->ip_ttl = 0x40; /* TTL */ ip->ip_p = IPPROTO_UDP; /* UDPなので */ ip->ip_src = *src; /* 送信元IPアドレス */ ip->ip_dst = *dst; /* 送信先IPアドレス */ /* チェックサム計算 */ ip->ip_sum = 0; ip->ip_sum = in_cksum((unsigned short*)ip, ip->ip_hl << 2); } int main(int argc, char *argv[]) { int sd; int on = 1; char *data; char *buf; struct in_addr src, dst; struct sockaddr_in to; socklen_t tolen = sizeof(struct sockaddr_in); size_t packetsiz; unsigned short dport; if (argc != 5) usage(argv[0]); dport = atoi(argv[3]); data = argv[4]; packetsiz = sizeof(struct ip) + sizeof(struct udphdr) + strlen(data); if ((buf = malloc(packetsiz)) == NULL) errx(1, "malloc"); /* RAWソケット */ if ((sd = socket(PF_INET, SOCK_RAW, IPPROTO_RAW)) < 0) errx(1, "socket"); /* 送信パケットにIPヘッダーを含めるためのソケットオプション */ if (setsockopt(sd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) errx(1, "setsockopt"); src.s_addr = inet_addr(argv[1]); dst.s_addr = inet_addr(argv[2]); build_ip(buf, &src, &dst, packetsiz); build_udp(buf, &src, &dst, dport, data); memset(&to, 0, sizeof(struct sockaddr_in)); to.sin_addr = dst; to.sin_port = htons(dport); to.sin_family = AF_INET; printf("Sending to %s from %sn", argv[2], argv[1]); if (sendto(sd, buf, packetsiz, 0, (struct sockaddr *)&to, tolen) < 0) { perror("sendto"); } close(sd); free(buf); return 0; }
実行例
実行するためには「送信元IPアドレス」「送信先IPアドレス」「送信先UDPポート番号」「データ文字列」の4つを引数に設定します。
プログラムの実行はmacOSでおこないました。ルート権限が必要であることに注意してください。
$ cc packet_gen_udp.c $ sudo ./a.out 1.2.3.4 192.168.2.6 12345 DEADBEEF Password: Sending to 192.168.2.6 from 1.2.3.4 $
パケットの送信先である192.168.2.6でtcpdumpを実行します。
# tcpdump -i wlp2s0 -vvv -nn -X udp and port 12345 tcpdump: listening on wlp2s0, link-type EN10MB (Ethernet), capture size 65535 bytes 20:27:48.815355 IP (tos 0x1,ECT(1), ttl 64, id 59995, offset 0, flags [none], proto UDP (17), length 36) 1.2.3.4.65001 > 192.168.2.6.12345: [udp sum ok] UDP, length 8 0x0000: 4501 0024 ea5b 0000 4011 c9b8 0102 0304 E..$.[..@....... 0x0010: c0a8 0206 fde9 3039 0010 fde1 4445 4144 ......09....DEAD 0x0020: 4245 4546 BEEF ^C 1 packet captured 1 packet received by filter 0 packets dropped by kernel #
IPヘッダーが正しく設定できていることを確認するために、念のためTOSを1に設定しました。
ご覧のとおりtcpdumpの結果を見るとTOSが1になっています。送信元IPアドレスも1.2.3.4となっていて送信成功であることがわかります。
まとめ
パケットジェネレータの作成はパケットキャプチャに比べると難易度は高いですが、UDPパケットの送信ならばさほど難しくないと思います。
TCPはヘッダー構造が異なるだけでやることは同じですから応用してTCPにも挑戦してみてください。
更にイーサネットヘッダーまで送信できるようになるとLAN内であらゆる事ができるようになります。イーサネットヘッダーの送信については、また別の機会に書きたいと思います。
最後にチェックポイントをまとめておきます。
- LinuxとBSD(Mac)の仕様の違いに注意する
- ソケットオプションを忘れない
- バイトオーダーについて注意する
パケットジェネレーターに関するドキュメントは少ないため経験則に頼る場面が少なくありません。本格的にパケットジェネレータを作成する場合は「UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI・ソケットとXTI」を是非読んでください。
古い本ですが、わたしが知る限り最高の技術書です。