自力でsyn scan

こまつ みつのり(Mitsunori Komatsu) 2008-02-29 21:04:16

さて前回に続き、「自力で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 32792 

  0x0000:  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編集部の見解・意向を示すものではありません。

SpecialPR

  • デジタル変革か?ゲームセットか?

    デジタルを駆使する破壊的なプレーヤーの出現、既存のビジネスモデルで競争力を持つ
    プレイヤーはデジタル活用による変革が迫られている。これを読めばデジタル変革の全体像がわかる!

  • 【3/31まで早期割引受付中!】「IBM Watson Summit 2017」開催

    日本IBMが主催する最大の国内総合イベント。テクノロジー・リーダーの疑問を紐解く「企業IT、セキュリティー、モバイル、データ解析などの進化を探る」詳細はこちらから!