[ TCP - SYN PortScan - ] 動作確認はすべて Linux で行っています。 TCPポートへの接続は 3way handshake と呼ばれる方法で確立される。 まずはTCPヘッダの詳細を知りたいので RFC 793 を読む。 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sequence Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Acknowledgment Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data | |U|A|P|R|S|F| | | Offset| Reserved |R|C|S|S|Y|I| Window | | | |G|K|H|T|N|N| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Checksum | Urgent Pointer | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | data | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ TCP Header Format IPヘッダやUDPヘッダと同じような要素があることが分かる。 3way handshake は 1、クライアントからサーバへ「 SYN を 1 」にしたパケットを送信する。 2、サーバからクライアントへ「 SYN を 1 , ACK を 1 」にしたパケットを送信する。 3、クライアントからサーバへ「 ACK を 1 」にしたパケットを送信する。(コネクションが確立する) つまりTCPで通信が行われた場合、最低でも3回はパケットが移動してることになる。 簡単なプログラムを書いてパケットを観察する。 connect.pl ------------------------------------------------------------------------------ #!/usr/bin/perl use Socket; $host = '123.123.123.***'; $port = 8080; $iaddr = inet_aton($host) or die "error inet_aton.\n"; $addr = pack_sockaddr_in($port, $iaddr); socket(SOCKET, PF_INET, SOCK_STREAM, 0) or die "error socket.\n"; connect(SOCKET, $addr) or die "error connect.\n"; sleep(100); ------------------------------------------------------------------------------ tcp]$ chmod 755 connect.pl sleep(100);というのは「コネクションの確立」だけのパケットを観察するためだ。 これが無いとプログラムの終了と同時にコネクションが切断され、 そのときのパケット移動も tcpdump から出力されるからだ。 接続するサーバは何でも良いが、私は適当に見つけてきたProxyサーバで試してみた。 80ポートだと他がちょっとうるさいので 8080 を選んだ。 パケット監視には tcpdump を使用した。 tcp]$ su Passward: root..tcp]$ root..tcp]$ ./tcpdump -s 1600 -x -i eth0 port 8080 tcpdump: listening on eth0 myhost.com > target.com: S 3236324651:3236324651(0) win 5840 (DF) 4500 003c aac2 4000 4006 2f29 **** **** **** **** 80fd 1f90 c0e6 652b 0000 0000 a002 16d0 39d1 0000 0204 05b4 0402 080a 000c cfe9 0000 0000 0103 0300 target.com > myhost.com: S 906207661:906207661(0) ack 3236324652 win 65535 4500 003c 5eab 0000 3406 c740 **** **** **** **** 1f90 80fd 3603 a1ad c0e6 652c a012 ffff 894a 0000 0204 05b4 0103 0300 0101 080a 0002 f293 000c cfe9 myhost.com > target.com: . ack 1 win 5840 (DF) 4500 0034 aac3 4000 4006 2f30 **** **** **** **** 80fd 1f90 c0e6 652c 3603 a1ae 8010 16d0 9e2c 0000 0101 080a 000c cffb 0002 f293 3 packets received by filter 0 packets dropped by kernel root..tcp]$ では解析していこう。 とりあえず最初のパケットを見てみる。 4500 003c aac2 4000 4006 2f29 **** **** **** **** 80fd 1f90 c0e6 652b 0000 0000 a002 16d0 39d1 0000 0204 05b4 0402 080a 000c cfe9 0000 0000 0103 0300 '****'の部分がIPアドレスなのでその後からがTCPヘッダだ。 80fd 1f90 はそれぞれ使用したポートだ。1f90 は10進数で 8080 だ。 c0e6 652b はシーケンス番号(SEQ番号)で 0000 0000 は確認応答番号(ACK番号)だ。 これらはなかなか興味深いものだ。3つのパケットのこの部分だけ切り取ってくると c0e6 652b 0000 0000 3603 a1ad c0e6 652c c0e6 652c 3603 a1ae 「 1つ目のパケットのシーケンス番号(SEQ番号) + 1 」が「 2つ目のパケットの確認応答番号(ACK番号) 」 「 2つ目のパケットのシーケンス番号(SEQ番号) + 1 」が「 3つ目のパケットの確認応答番号(ACK番号) 」 となっている。 そして、確認応答番号(ACK番号)の後の a0 は 10100000 で前 4Bits(10) が DataOffset 。 これはTCPヘッダ(data部も含む)の長さを4Bytes単位で表したものだ。 つまり IPアドレス の後から 40Bytes のデータがあるということを示している。 その次の 02 がポイントだ! TCPヘッダのフォーマットと見比べてみると、 02 は 10進数で 2 つまり 00000010 なので TCPヘッダを見ると「 SYN が 1 」ということになる。 では2つ目のパケットの同じ場所を見ると 12 だ! 12 は 10進数で 18 つまり 00010010 なので「ACK が 1 and SYN が 1 」だ!! ではでは3つ目のパケットは.... 10 .... 16 .... 00010000 ...「 ACK が 1 」だ! 見事 3way handshake を行っていることが分かる。 PortScan について ポートスキャンはとてもシンプルな仕組みだ。 プログラム的には ただ Port 1つ1つに connect要求 を行っていくだけだからだ。 しかし、これだと膨大なログが残ることになり攻撃者が特定されやすい。 そこでポートスキャンのテクニックの1つとして 「ステルススキャン」(ハーフスキャン、SYNスキャンとも呼ばれる)がある。 これはつまり 3way handshake の最後のステップである「接続元からのACK送信」をしないということだ。 SYN を送信したあとに サーバから SYN/ACK がくれば、開いている。 それ以外(もしくはRST/ACK)なら開いていない。という判定方法で実現される。 これは接続を確立するわけではないのでサーバにログが残りにくいのだ。(絶対に残らないとは言えないが) そしてさらに接続元を分かりにくくする方法として 送信元を偽装したパケットをたくさん送る方法がある。 例えば 送信元を偽装した100個の SYNパケット(それぞれ違うアドレスに偽装されている)を サーバに送り、そのなかの1個だけを送信元のアドレス(本当のアドレス)にする。 もちろん偽装されてる99個のパケットに対してのサーバからのレスポンス(SYN/ACK)は 送信元のPCには(偽装対象にされてしまったPCに送信されるので)返ってこない。 しかし1個だけは返って来る。 これによって開いているかどうかを判定すれば良い。 こうするとサーバのログには(もしステルススキャン対策をしていれば) それぞれ別々のアドレスからの100個のスキャン記録が残ることになる。 もちろんその中には(1つだけ)本当の攻撃者のアドレスがあるのだが 特定することは容易ではない。しかもこれが1000個なら?もう救いようがないだろう。 では これまでの知識を集めて「ステルススキャン」のプログラムを書いてみようと思う。 ただ たくさんのパケットを送って さらに自分のIPを特定しにくくするといった実装はしない。 単純に SYN パケット を(1つ)送信し SYN/ACK なら開いている、それ以外なら閉じている。 という判別を行う PortScan を書いてみる。 しかし、必ずしもサーバがレスポンスを返してくれるとは限らないので、 一定の時間が経ってもレスポンスがこない場合は TimeOut と表示させるようにする。 まずは netinet/tcp.h から便利な構造体をさがしてくる。 tcp.h (抜粋) ------------------------------------------------------------------------------ struct tcphdr { u_int16_t source; u_int16_t dest; u_int32_t seq; u_int32_t ack_seq; # if __BYTE_ORDER == __LITTLE_ENDIAN u_int16_t res1:4; u_int16_t doff:4; u_int16_t fin:1; u_int16_t syn:1; u_int16_t rst:1; u_int16_t psh:1; u_int16_t ack:1; u_int16_t urg:1; u_int16_t res2:2; # elif __BYTE_ORDER == __BIG_ENDIAN u_int16_t doff:4; u_int16_t res1:4; u_int16_t res2:2; u_int16_t urg:1; u_int16_t ack:1; u_int16_t psh:1; u_int16_t rst:1; u_int16_t syn:1; u_int16_t fin:1; # else # error "Adjust your defines" # endif u_int16_t window; u_int16_t check; u_int16_t urg_ptr; }; ------------------------------------------------------------------------------ 以下が プログラムだ。 select関数により TimeOut を判別しいる。 syn.c ------------------------------------------------------------------------------ #include #include #include #include #include #include #include #include #include #include #include unsigned short in_cksum(unsigned short *data, int size); int host2ip(char *hostname, struct in_addr *addr); struct _rData{ struct iphdr ip; struct tcphdr tcp; } rData; struct _pHeader{ unsigned int source; unsigned int dest; unsigned char zero; unsigned char protocol; unsigned short tcp_len; struct tcphdr tcp; } pHeader; int main(int argc, char *argv[]) { int sock; struct tcphdr tcp; struct in_addr addr; struct sockaddr_in sin; int len, port; fd_set rfds; struct timeval tv; int retval; if(argc < 3){ fprintf(stderr, "%s sourcehost dsthost port\n", argv[0]); exit(1); } if((port = (unsigned short)atoi(argv[3])) == 0){ perror("port"); exit(1); } /* set the tcp header fields */ tcp.dest = htons(port); tcp.source = getpid(); tcp.seq = getpid(); tcp.ack_seq = 0; tcp.res1 = 0; tcp.doff = 5; tcp.fin = 0; tcp.syn = 1; tcp.rst = 0; tcp.psh = 0; tcp.ack = 0; tcp.urg = 0; tcp.res2 = 0; tcp.window = htons(512); tcp.check = 0; tcp.urg_ptr = 0; if(host2ip(argv[1], &addr) < 0){ perror("host2ip argv[1]"); exit(1); } pHeader.source = addr.s_addr; if(host2ip(argv[2], &addr) < 0){ perror("host2ip argv[2]"); exit(1); } pHeader.dest = addr.s_addr; sin.sin_family = AF_INET; bcopy(&(addr.s_addr), &(sin.sin_addr), sizeof(struct in_addr)); /* set the pseudo header fields */ pHeader.zero = 0; pHeader.protocol = 6; pHeader.tcp_len = htons(20); bcopy((char *)&tcp, (char *)&pHeader.tcp, 20); tcp.check = in_cksum((unsigned short *)&pHeader, 32); if((sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP)) < 0){ perror("socket"); exit(1); } len = sizeof(sin); if((sendto(sock, &tcp, sizeof(tcp), 0, (struct sockaddr *)&sin, len)) < 0){ perror("send to"); exit(1); } /* TIME OUT */ FD_ZERO(&rfds); FD_SET(sock, &rfds); tv.tv_sec = 3; tv.tv_usec = 0; retval = select(sock + 1, &rfds, NULL, NULL, &tv); if(retval == -1){ perror("select"); exit(1); }else if(retval){ recvfrom(sock, &rData, sizeof(rData), 0, (struct sockaddr *)&sin, &len); if(rData.tcp.syn == 1 && rData.tcp.ack == 1) printf("Open\n"); else printf("Close\n"); }else{ printf("TimeOut\n"); } return 0; } unsigned short in_cksum(unsigned short *data, int size) { unsigned long sum = 0; while(size > 1){ sum += *(data++); size -= 2; } if(size > 0) sum += (*data) & 0xff00; sum = (sum & 0xffff) + (sum >> 16); return ((~(short)((sum >> 16) + (sum & 0xffff)))); } int host2ip(char *hostname, struct in_addr *addr) { if((inet_aton(hostname, addr)) == 0){ struct hostent *h; if((h=gethostbyname(hostname)) == NULL) return(-1); bcopy(h->h_addr, (char *)&addr->s_addr, h->h_length); } return 0; } ------------------------------------------------------------------------------ tcp]$ gcc syn.c -o syn tcp]$ su Password: root..tcp]$ ./syn myhost.com targethost.com 8080 Open root..tcp]$ ./syn myhost.com targethost.com 1345 TimeOut root..tcp]$ 実際に自分でサーバを立ててみてログが残るかどうかを試してみるといい。 これは(Open,Close,TimeOutだけの)シンプルな情報しか表示しないが、ちょっと手を加えれば レスポンスがきたパケットを表示させるように改良を加えたりできるだろう。 あとはすべての Port を検査するように変更し、 さらに1つの Port につき数百の偽装パケットを送信する仕組みを実装すれば なかなか実用的なものになるのではないだろうか。 End. written by kenji aiko 2003/09/09 2004/02/04 修正 Copyright (C) 2003 kenji aiko All Rights Reserved