さて前回に続き、「自力でsyn scan」に挑戦します。こっそり敷居を下げるため、さりげなくタイトルが変わっていますけれど。
ちなみにsyn scanとは、「相手からのsyn+ackを受け取った時点で、ackを送らずrstを送り、3way handshakeを中断する」というポートスキャンの一種です。3way handshakeが成功していないので、接続が確立しておらず、ログに残らない、というメリット(誰の?)があるようです。
こういった少し怪しげな技術の是非はそっちのけで、自らのネタを完結させるべく話を進めます。
さて、「syn scanができました」というためには、「syn scan」するサンプルプログラムを書かないといけません。それを目標に頑張ってみます。頑張り手順を列挙していきます。
1. TCP/IPのヘッダ情報を調べる
Webで検索したりTCP/IP関連の書籍をみてみて、TCP/IPのヘッダ構成を理解する。ついでに、LinuxやFreeBSDのヘッダファイルを眺めてみる。FreeBSDだとsrc/sys/netinet/ip.hとtcp.hを、Linuxだとsrc/linux-source-2.6.22/include/net/ip.hとtcp.h(どちらもcvsupとかapt-getとかでソースを落としてから)。
2. socket(SOCK_RAW)の使い方を調べる
通常のTCP通信であればsocket(PF_INET, SOCK_STREAM)なのだけど、記憶の片隅に「synパケットなどを手作りするには生ソケット(SOCK_RAW)を使う必要あり」という情報があったので、socket(PF_INET, SOCK_RAW)の使い方を調べてみる。今回はIPパケットから手作りしたかったのでIPPROTO_RAWを使ってみることに。
3. 適当にコードを書いてみる
とりあえず雰囲気をつかむため、TCP/IPヘッダ構造体を書いて勢いをつけ、適当にsynパケットを投げるコードを書いてみる。setsockopt(IPPROTO_IP, IP_HDRINCL)っておくとIPパケットの細かい設定(チェックサム付与を含む)は自動的にやってくれるらしい。TCPパケットのチェックサムはとりあえず後回し。
4. Linux(local loopback)でいきなり動いた
TCPヘッダのチェックサムにダミー値を入れているにも関わらず、ダミーのTCPサーバーからsyn+ackが帰ってきた。
$ sudo tcpdump -i lo -nX -s 4096 -v tcp tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 4096 bytes 23:31:32.935103 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40) 192.168.100.2.6543 > 192.168.100.2.8080: S, cksum 0x9999 (incorrect (-> 0xbb63), 12345678:12345678(0) win 4096 0x0000: 4500 0028 0000 4000 4006 f17a c0a8 6402 E..(..@.@..z..d. 0x0010: c0a8 6402 198f 1f90 00bc 614e 0000 0000 ..d.......aN.... 0x0020: 5002 1000 9999 0000 P....... 23:31:32.935430 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 44) 192.168.100.2.8080 > 192.168.100.2.6543: S, cksum 0xf963 (correct), 800247823:800247823(0) ack 12345679 win 327920x0000: 4500 002c 0000 4000 4006 f176 c0a8 6402 E..,..@.@..v..d. 0x0010: c0a8 6402 1f90 198f 2fb2 d00f 00bc 614f ..d...../.....aO 0x0020: 6012 8018 f963 0000 0204 400c `....c....@.
まぁ、local loopbackだから信用しているようで、対remote hostの場合、syn+ackは帰ってこなかった。けれども、一応手ごたえあり。
5. FreeBSDではまる
手元にFreeBSDもあったので、勢いでどちらでも動くようにしてみる。ところが、sendto()でパケットを投げようとすると失敗してしまう。悩む…
こんなことならLinuxだけ使っておけば良かった…と公開しつつ、Webで情報を探る。すると、「BSD系の場合、IPヘッダのtotal Lengthはnetwork byte orderではなく、hostのbyte orderなのだ(ちょっとうろおぼえ)」という情報を見つける。恐る恐るhtons()を外してみると、sendto()成功!でもFreeBSDのほうはチェックサムが不正だとlocal loopbackでもsyn+ackを返さなかった。
6. TCPチェックサムを付与
えーっとTCPチェックサムは、TCPヘッダの前に擬似ヘッダがあるものと想定して16ビット毎の1の補数和について1の補数和をとったものらしい。よくわからんのでtcpdumpのソースなどを見てみる。どこかでチェックサム計算しているだろうという推測で。
どうやらprint-ip.cのin_cksum()という関数がそれらしい。1の補数和って1の補数を足しこんでいくわけでは無いのね…
これを参考にしつつ試行錯誤の結果チェックサムOK(実はチェックサム値をhtons()し忘れてかなりはまってた)。
7. syn+ackパケットの受信
この時点で、正常なsynパケットを投げられるようになっていて、TCPサーバー側からはsyn+ackパケットが返ってきているのを確認している(tcpdumpで)。でも、syn scannerというからにはsyn+ackパケットを受信してフラグを判断したいところ。まず、Linuxで普通にrecvfrom()をしてみると…
だめだ〜recvfrom()から抜けてこない。「man 7 raw」してみると「An IPPROTO_RAW socket is send only.」という一文が。もうだめだ…
そこで、socket(PF_INET, SOCK_RAW, IPPROTO_TCP)路線に変更。そしたら受信できた。
一応、念のためFreeBSDで試してみると…こちらはrecvfrom()から抜けてこないのであった。もう、この辺でFreeBSDで頑張ることをあきらめることを決意。
「あきらめる勇気」、響きはなんかかっこよい。響きはね。
8. ソースはこんな感じ
#include#include #include #include #include #include struct tcp { u_int16_t src_port; /* source port */ u_int16_t dst_port; /* destination port */ u_int32_t seq; /* sequence number */ u_int32_t ack; /* acknowledgement number */ #if BYTE_ORDER == LITTLE_ENDIAN u_int8_t dummy:4; /* (unused) */ u_int8_t offset:4; /* data offset */ #elif BYTE_ORDER == BIG_ENDIAN u_int8_t offset:4; /* data offset */ u_int8_t dummy:4; /* (unused) */ #endif u_int8_t flags; /* FIN 0x01 | SYN 0x02 * RST 0x04 | PUSH 0x08 * ACK 0x10 | URG 0x20 */ u_int16_t win_size; /* window */ u_int16_t cksum; /* checksum */ u_int16_t urg_ptr; /* urgent pointer */ }; struct ptcp { struct in_addr src_addr; /* source address */ struct in_addr dst_addr; /* dest address */ u_int8_t zero; /* zero */ u_int8_t ptcl; /* protocol */ u_int16_t snd_len; /* tcp header snd_len */ }; static u_int32_t cksum(u_int32_t last_sam, u_int16_t *buff, int snd_len) { u_int32_t sum = last_sam; while (snd_len > 0) { if (snd_len > 1) sum += *buff++; else sum += *(u_int8_t *)buff; snd_len -= 2; } return sum; } static u_int16_t tcp_cksum(struct ptcp *ptcp, u_int16_t *tcp, int tcp_len) { u_int16_t *ptr; u_int32_t sum; sum = cksum(0, (u_int16_t *)ptcp, sizeof(struct ptcp)); sum = cksum(sum, tcp, tcp_len); sum = (sum & 0xffff) + (sum >> 16); sum = (sum & 0xffff) + (sum >> 16); return (u_int16_t)~sum; } int main(int argc, char *argv[]) { struct sockaddr_in src, dst; char buff[4096]; struct tcp *tcp; struct ptcp ptcp; int snd_len = 0, rcv_len = 0, fromlen; int sock; int on = 1; #define ERROR(name) do { perror(name); exit(1); } while (0); if (argc != 5) { fprintf(stderr, "usage: my_tcp src_ip src_port dst_ip dst_portn"); exit(2); } memset(&src, 0, sizeof(struct sockaddr_in)); src.sin_family = PF_INET; src.sin_addr.s_addr = inet_addr(argv[1]); src.sin_port = atoi(argv[2]); memset(&dst, 0, sizeof(struct sockaddr_in)); dst.sin_family = PF_INET; dst.sin_addr.s_addr = inet_addr(argv[3]); dst.sin_port = atoi(argv[4]); if ((sock = socket(PF_INET, SOCK_RAW, IPPROTO_TCP)) == -1) ERROR("socket"); tcp = (struct tcp *) buff; tcp->src_port = htons(src.sin_port); tcp->dst_port = htons(dst.sin_port); tcp->seq = htonl(12345678); tcp->ack = 0; tcp->offset = sizeof(struct tcp) / 4; tcp->flags = 0x02; tcp->win_size = htons(4096); tcp->cksum = 0; tcp->urg_ptr = 0; snd_len += sizeof(struct tcp); ptcp.src_addr.s_addr = src.sin_addr.s_addr; ptcp.dst_addr.s_addr = dst.sin_addr.s_addr; ptcp.zero = 0; ptcp.ptcl = IPPROTO_TCP; ptcp.snd_len = htons(sizeof(struct tcp)); tcp->cksum = tcp_cksum(&ptcp, (u_int16_t *)tcp, sizeof(struct tcp)); if (sendto(sock, buff, snd_len, 0, (struct sockaddr *) &dst, sizeof(dst)) == -1) ERROR("sentto"); fromlen = sizeof(struct sockaddr); if ((rcv_len = recvfrom(sock, buff, sizeof(buff), 0, (struct sockaddr *) &dst, &fromlen)) == -1) ERROR("recvfrom"); #define SIZE_OF_IP_HEADER 20 #define FLAG_SYN_ACK (0x02 | 0x10) if (((struct tcp *)&buff)->flags == FLAG_SYN_ACK) printf("okn"); else printf("ngn"); exit(0); }
これを適当なファイル名(仮にmy_tcp.c)をつけ保存し、以下のようなmakefileを書いてmakeを叩けば実行可能ファイルができる(はず)。
CFLAGS = -g all: my_tcp
9. 実行例
送信元(192.168.100.2 6543)から送信先(192.168.100.5 80)にsynパケットを送る(送信先がlisten()している場合)
$ sudo ./my_tcp 192.168.100.2 6543 192.168.100.5 80 ok
送信元(192.168.100.2 6543)から送信先(192.168.100.5 81)にsynパケットを送る(送信先がlisten()していない場合)
$ sudo ./my_tcp 192.168.100.2 6543 192.168.100.5 81 ng
これだけ…
tcpdumpの結果と照らし合わせてみたほうが面白いです。
10. 本当は
相手からsyn+ackパケットを受信したら、ackパケットを送って「自力で3way handshake」をやってやろうと思っていたのですが、Kernelが勝手にrstパケットを送ってしまうのでした。
FreeBSDだとsysctl net.inet.tcp.blackhole=2でrst抑止できそうなんですけどねぇ。Linuxはどうなのかしら。
気が向いたらもうちょっと頑張ってみますが、今は気が向いていないので今回はここまで。
※このエントリはZDNetブロガーにより投稿されたものです。朝日インタラクティブ および ZDNet編集部の見解・意向を示すものではありません。