パケットジェネレータのサンプルとしてUDPパケットを送信します。パケットをつくって送信するためにIPヘッダ−、UDPヘッダーを自ら設定します。
パケットジェネレータの作成はパケットキャプチャと比べて難易度が上がります。パケットキャプチャの場合はネットワークを流れるIPヘッダーやUDPヘッダーを単純に表示させるだけでしたが、パケットジェネレータは自らヘッダーの値を設定しなければなりません。そのためには、ヘッダーの構造だけでなくヘッダー値の意味を理解していなくてはなりません。
今回はパケットジェネレータの作成に必要な知識を羅列し、最後にサンプルコードと実行例を掲載します。サンプルコードのコンパイルと実行はOS X El CapitanとCentOS 7でおこなっています。
目次
パケット送信の流れ
パケットの送信は以下のような流れになります。
- Raw Socket(生ソケット)を作成する
- IPヘッダーを作成する
- UDPヘッダーを作成する
- 送信する
Raw Socket(生ソケット)を作成する
IPデータグラムを送信するためにRaw Socket(生ソケット)を作成する必要があります。今回はUDPパケットを作成して送信しますが、IPヘッダーも含めるため通常のソケットでは実現することができません。IPヘッダーを含めて送信するためにはRaw Socket(生ソケット)が必要です。セキュリティの関係上、ルート権限が必要になります。
ソケットオプション
ブロードキャストパケットを送信する際などにソケットオプションを設定しますが、今回はIPヘッダーを送信パケットに含める必要があるため、ソケットオプション(IP_HDRINCL)を設定しています。なお、Linuxはこのオプションを設定しなくてもIPヘッダーを含めてパケットを送信できます。BSD(Mac OS X)はソケットオプションが必須です。
IPヘッダー作成
基本的にIPヘッダーの構造がわかっていれば問題ありません。重要な点は「ip_len」「ip_off」の取り扱いです。IPヘッダーを作成するときのバイトオーダーについて明記されたドキュメントがないのですが、LinuxはすべてのIPヘッダー値をネットワークバイトオーダーで設定する必要があるのに対して、BSD(Mac OS X)は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など)を設定しても問題なく送信できます。
UDPヘッダー作成
UDPヘッダーの作成で注意するのは、チェックサムの計算です。UDPはチェックサムを0にしても良いのですが、チェックサム計算の方法を理解するためにも、自分で計算してみましょう。
疑似ヘッダー
UDPのチェックサム計算には疑似ヘッダーというものを使います。
1 2 3 4 5 6 7 8 |
0 7 8 15 16 23 24 31 +--------+--------+--------+--------+ | source address | +--------+--------+--------+--------+ | destination address | +--------+--------+--------+--------+ | zero |protocol| UDP length | +--------+--------+--------+--------+ |
疑似ヘッダーの末尾に、送信するUDPヘッダーとデータを付加します。そして疑似ヘッダーとUDPヘッダー、データを含めてチェックサムの計算をおこないます。つまり、次のようなデータ構造を用意してチェックサムの計算をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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(Mac OS X)はLinuxと違って厳密なため、IPヘッダーで設定したip_lenの値とsendto(2)へ渡す送信サイズに差異があるとエラーになりますから注意してください。それ以外は特別なことはありません。では、次のセクションでサンプルコードと実行例を掲載します。
サンプルコードと実行例
サンプルコード
UDPはヘッダー構造が単純ですが、それでもコード行数はキャプチャに比べてかなり増えています。なお、コードを単純にする目的でIPヘッダーやUDPヘッダー値の大半をハードコーディングしています。慣れてきたら引数でヘッダー値を設定できるようにすると、本格的なパケットジェネレータとなってくるはずです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
/* * udp-gen.c * UDPパケット送信サンプルコード * by shj@netwiz.jp */ #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 %s\n", 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つを引数に設定します。プログラムの実行はMacでおこないました。ルート権限が必要であることに注意してください。
1 2 3 4 5 |
$ cc udp-gen.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はリモートのCentOS 7なので、CentOS 7でtcpdumpの結果を確認します。
1 2 3 4 5 6 7 8 9 10 11 12 |
# 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内であらゆる事ができるようになります。イーサネットヘッダーの送信については、また別の機会に書きたいと思います。