【第4回】TCPヘッダーを設定して送信するツールをC言語で作成する

第4回目はTCPヘッダーを設定して送信するツールをC言語で作成します。第1回・第2回・第3回の続きとなっているので、過去の記事と重複する箇所の解説(IPヘッダーやバイトオーダーなど)は省略します。

第3回ではTCPの受信のみ実装しました。今回はTCPの送信となります。ただ、第2回でUDPの送信を自ら実装した方であればTCPも同じことをやるので、難しいところはないはずです。今回はTCPオプションを付けてみたりセッションフラグを変えてみたりしたいと思います。

記事を読み進めるにあたり、次のスキルセットを持っていることを前提にしています。

  • IP、TCP、UDPの違いを知っている
  • C言語でのプログラミング経験がある

なお、本記事ではコードを追うことを容易にするためできるだけ関数化を避けています。またエラー処理も極力省いていますので、ご自身でコーディングされる場合は適宜エラー処理を追加したり関数化したりしてください。

IPv4ヘッダーとTCPヘッダーを設定して送信する

TCPヘッダーフォーマット

TCPのヘッダーフォーマットは次のようになっています。TCPヘッダーを自ら設定する場合は各フィールドの意味を理解していないと正しく送信できませんので注意してください。

各フィールドの意味については第3回の記事をご覧ください。

 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 |       |C|E|U|A|P|R|S|F|                               |
| Offset| Rsrvd |W|C|R|C|S|S|Y|I|            Window             |
|       |       |R|E|G|K|H|T|N|N|                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           [Options]                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               :
:                             Data                              :
:                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

LinuxではTCPヘッダーの構造体は/usr/include/netinet/tcp.hで定義されています。メンバー名の先頭に「th_」が付くものと付かないものが定義されていますが、本記事では先頭に「th_」が付く方を使います。

struct tcphdr
  {
    __extension__ union
    {
      struct
      {
        uint16_t th_sport;      /* source port */
        uint16_t th_dport;      /* destination port */
        tcp_seq th_seq;         /* sequence number */
        tcp_seq th_ack;         /* acknowledgement number */
# if __BYTE_ORDER == __LITTLE_ENDIAN
        uint8_t th_x2:4;        /* (unused) */
        uint8_t th_off:4;       /* data offset */
# endif
# if __BYTE_ORDER == __BIG_ENDIAN
        uint8_t th_off:4;       /* data offset */
        uint8_t th_x2:4;        /* (unused) */
# endif
        uint8_t th_flags;
# define TH_FIN 0x01
# define TH_SYN 0x02
# define TH_RST 0x04
# define TH_PUSH        0x08
# define TH_ACK 0x10
# define TH_URG 0x20
        uint16_t th_win;        /* window */
        uint16_t th_sum;        /* checksum */
        uint16_t th_urp;        /* urgent pointer */
      };
      struct
      {
        uint16_t source;
        uint16_t dest;
        uint32_t seq;
        uint32_t ack_seq;
# if __BYTE_ORDER == __LITTLE_ENDIAN
        uint16_t res1:4;
        uint16_t doff:4;
        uint16_t fin:1;
        uint16_t syn:1;
        uint16_t rst:1;
        uint16_t psh:1;
        uint16_t ack:1;
        uint16_t urg:1;
        uint16_t res2:2;
# elif __BYTE_ORDER == __BIG_ENDIAN
        uint16_t doff:4;
        uint16_t res1:4;
        uint16_t res2:2;
        uint16_t urg:1;
        uint16_t ack:1;
        uint16_t psh:1;
        uint16_t rst:1;
        uint16_t syn:1;
        uint16_t fin:1;
# else
#  error "Adjust your <bits/endian.h> defines"
# endif
        uint16_t window;
        uint16_t check;
        uint16_t urg_ptr;
      };
    };
};

チェックサムの計算は疑似ヘッダーを使う

TCPはチェックサムの計算が必須です。チェックサムの計算にはUDPのときと同じように疑似ヘッダーを使って計算します。

 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 Address                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination Address                        |
+---------------+---------------+---------------+---------------+
|      zero     |    protocol   |           TCP length          |
+---------------+---------------+---------------+---------------+

この疑似ヘッダーはUDPのときと同じ構造です。疑似ヘッダーの後ろに送信するTCPヘッダー(とペイロード)を配置してチェックサムの計算をおこないます。

 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 Address                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination Address                        |
+---------------+---------------+---------------+---------------+
|      zero     |    protocol   |           TCP length          |
+---------------+---------------+---------------+---------------+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Data |       |C|E|U|A|P|R|S|F|                               |
| Offset| Rsrvd |W|C|R|C|S|S|Y|I|            Window             |
|       |       |R|E|G|K|H|T|N|N|                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           [Options]                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               :
:                             Data                              :
:                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

チェックサムの計算はOpenBSDのping.cに含まれる関数をそのまま使います。疑似ヘッダーとTCPヘッダーを設定して、このチェックサムの関数を通すとチェックサム値が得られます。

int
in_cksum(u_short *addr, int len)
{
	int nleft = len;
	u_short *w = addr;
	int sum = 0;
	u_short answer = 0;

	/*
	 * 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) {
		*(u_char *)(&answer) = *(u_char *)w ;
		sum += answer;
	}

	/* 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);
}

サンプルプログラム

パケットの送信はこれまでの記事で解説した内容と同じくバイトオーダーに注意が必要です。バイトオーダーについて分からない場合は、第1回の記事と第2回の記事、それと低レイヤーのネットワークプログラミング時の注意点に関する記事をご覧ください。

参考:低レイヤーのネットワークプログラミング時の注意点|LinuxとBSDの違いを考慮する

今回のポイントは2つあります。

  1. セッションフラグを設定する
  2. TCPオプションを設定する

セッションフラグはSYNを設定しています。SYNというのはTCPコネクションを確立するとき最初に設定するフラグです。このフラグによって受信側の応答が変わってきます。また、受信側のポートが開いている場合と閉じている場合でも応答が変わります。この値を色々と変えてみて、どのような応答が返るのか観察するとTCPの理解が深まります。その際はシーケンス番号とアクノレッジ番号にも注目してください。

tcp->th_flags = TH_SYN;

次に、TCPオプションを設定するのですが、非常にシンプルにNOPを3つとEOLを1つ、合計4つ付けています。

    /* TCPオプション */
    p = (char *)tcp + sizeof (struct tcphdr);
    *p++ = TCPOPT_NOP;
    *p++ = TCPOPT_NOP;
    *p++ = TCPOPT_NOP;
    *p = TCPOPT_EOL;

注意点として、オプションの合計サイズは4の倍数である必要があります。NOPとEOLはオプション長とデータを持たずオペレーションコードのみ、合計1バイトです。そのためNOPを3つとEOLを1つ、合計4つ付けています。

そして、オプションを含める場合はTCPヘッダーのサイズが大きくなります。そのためTCPヘッダーのth_offを正しく設定しなければなりません。今回はオプションが4バイトなのでTCPヘッダーのサイズは元の20バイト+4バイトで合計24バイトとなります。th_offの値は4バイト単位で表すのでth_offの値を6に設定しています。

tcp->th_off = 6; /* オプションをつけるので4バイト分確保 */

解説は以上です。TCPヘッダーを設定して送信するサンプルプログラムを以下に掲載します。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <sys/socket.h>

/* チェックサム計算用疑似ヘッダー */
struct pseudo_hdr {
    struct in_addr src;
    struct in_addr dst;
    unsigned char zero;
    unsigned char proto;
    unsigned short len;
};

int
in_cksum(u_short *addr, int len) {
    int nleft = len;
    u_short *w = addr;
    int sum = 0;
    u_short answer = 0;

    /*
     * 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) {
        *(u_char * )(&answer) = *(u_char *) w;
        sum += answer;
    }

    /* 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);
}

int
main(int argc, char *argv[]) {
    int sd;
    int needlen;
    int sendlen;
    int on;
    int res;
    struct ip *ip;
    struct sockaddr_in src_addr;
    struct sockaddr_in dst_addr;
    struct tcphdr *tcp;
    struct pseudo_hdr *pse;
    char buf[65535];
    unsigned short port;
    char *tbuf, *p;
    unsigned long seq;

    if (argc != 4) {
        fprintf(stderr, "Usage: %s <src ip address> <dst ip address> <port>\n", argv[0]);
        exit(1);
    }

    /* 送信先ポート番号 */
    port = atoi(argv[3]);

    /*
     * 送信元IPアドレスと宛先IPアドレスを設定する
     */
    if (inet_pton(AF_INET, argv[1], &src_addr.sin_addr) < 0) {
        perror("inet_pton");
        exit(1);
    }

    memset(&dst_addr, 0, sizeof(struct sockaddr_in));
    dst_addr.sin_family = AF_INET;
    if (inet_pton(AF_INET, argv[2], &dst_addr.sin_addr) < 0) {
        perror("inet_pton");
        exit(1);
    }

    if ((sd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) < 0) {
        perror("socket");
        exit(1);
    }
    /*
     * ソケットオプション
     * Linuxは設定しなくてもOK
     */
    on = 1;
    if (setsockopt(sd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) {
        perror("setsockopt");
        exit(1);
    }

    /*
     * IPヘッダーを作成する
     */
    ip = (struct ip *) buf;
    ip->ip_v = 4;                     /* IPv4 */
    ip->ip_hl = 5;                    /* オプションがないのでヘッダーサイズは20バイト */
    ip->ip_tos = 0;                   /* TOS */
    ip->ip_len = 0;                   /* 後で計算する(Linuxはこの値を設定しなくてもOK) */
    ip->ip_id = 0;                    /* IPID(OSに任せる) */
    ip->ip_off = htons(0);  /* フラグメントオフセット */
    ip->ip_ttl = 64;                  /* TTL */
    ip->ip_p = IPPROTO_TCP;           /* プロトコル番号 */
    ip->ip_sum = 0;                   /* チェックサムはOSが計算して設定する */
    ip->ip_src = src_addr.sin_addr;   /* 送信元IPアドレス */
    ip->ip_dst = dst_addr.sin_addr;   /* 宛先IPアドレス */

    /* 疑似乱数でシーケンス番号を生成 */
    srand((unsigned int) time(NULL));
    seq = rand();

    /*
     * TCPヘッダー設定
     * この時点ではチェックサムを0に設定する
     */
    tcp = (struct tcphdr *) (buf + (ip->ip_hl << 2));
    tcp->th_sport = htons(65000);
    tcp->th_dport = htons(port);
    tcp->th_seq = htonl(seq);
    tcp->th_ack = 0;
    tcp->th_off = 6; /* オプションをつけるので4バイト分確保 */
    tcp->th_flags = TH_SYN;
    tcp->th_win = htons(8192);
    tcp->th_sum = 0;
    tcp->th_urp = 0;
    /* TCPオプション */
    p = (char *)tcp + sizeof (struct tcphdr);
    *p++ = TCPOPT_NOP;
    *p++ = TCPOPT_NOP;
    *p++ = TCPOPT_NOP;
    *p = TCPOPT_EOL;

    /*
     * 疑似ヘッダーを作成する準備
     * TCPヘッダーと疑似ヘッダの合計サイズを計算し、メモリーを確保する
     */
    needlen = sizeof(struct pseudo_hdr) + (tcp->th_off << 2);
    if ((tbuf = malloc(needlen)) == NULL) {
        perror("malloc");
        exit(1);
    }
    memset(tbuf, 0, needlen);

    /*
     * 疑似ヘッダー設定
     * サイズの指定は送信するUDPヘッダーとデータ部分の合計値とし、疑似ヘッダーのサイズは含めない
     */
    pse = (struct pseudo_hdr *) tbuf;
    pse->src = ip->ip_src;
    pse->dst = ip->ip_dst;
    pse->proto = IPPROTO_TCP;
    pse->len = htons(tcp->th_off << 2);
    
    /* チェックサム計算 */
    memcpy(tbuf + sizeof(struct pseudo_hdr), tcp, tcp->th_off << 2);
    tcp->th_sum = in_cksum((unsigned short *) tbuf, needlen);

    /* チェックサム計算が終わり疑似ヘッダーは不要になったのでメモリーを解放する */
    free(tbuf);

    /*
     * ip_len設定
     * ネットワークバイトオーダーで設定する
     * Linuxは設定しなくてもOK
     */
    sendlen = needlen - sizeof(struct pseudo_hdr) + (ip->ip_hl << 2);
    ip->ip_len = htons(sendlen);

    if ((res = sendto(sd, buf, sendlen, 0, (struct sockaddr *) &dst_addr, sizeof(dst_addr))) < 0) {
        perror("sendto");
        exit(1);
    }
    close(sd);
    printf("%d bytes sended\n", res);

    return 0;
}

このCコードを「send_ipv4-tcp_packet.c」として保存しました。このCコードはLinuxと*BSDでコンパイル・実行できます。コンパイルする際にオプションは不要です。

cc send_ipv4-tcp_packet.c

実行するにはroot権限が必要です。送信元IPアドレスに「8.8.8.8」を設定し、送信先ポート番号を「12345」と設定して送信してみます。送信先IPアドレスは「192.168.218.133」に設定しました。これは同じLANにある別のマシンです。

$ sudo ./a.out 8.8.8.8 192.168.218.133 12345
44 bytes sended
$

この通信を観察するために受信側でtcpdumpを実行してみると、オプションが設定通り送信されている様子が見てとれます(「nop,nop,nop,eol」の箇所)。また、この通信に対して192.168.218.133はリセットパケットを返送していることが分かります。リセットパケットを返送しているのは、ポート番号12345が閉じているからです。

openbsd# tcpdump -i em0 -t -nn host 8.8.8.8
tcpdump: listening on em0, link-type EN10MB
8.8.8.8.65000 > 192.168.218.133.12345: S 1819754239:1819754239(0) win 8192 <nop,nop,nop,eol>
192.168.218.133.12345 > 8.8.8.8.65000: R 0:0(0) ack 1819754240 win 0 (DF)
^C
28 packets received by filter
0 packets dropped by kernel
openbsd#

それでは22番にパケットを送信するとどうなるでしょうか?

$ sudo ./a.out 8.8.8.8 192.168.218.133 22
44 bytes sended
$

ポート番号22番はSSHで、送信先のホストではポートが開いています。そのため192.168.218.133はSYN+ACKを返送しています。そして3つ目のパケットでは8.8.8.8がリセットパケットを返送しています。これは、8.8.8.8が未知のSYN+ACKを受け取ったためです。

openbsd# tcpdump -i em0 -t -nn host 8.8.8.8
tcpdump: listening on em0, link-type EN10MB
8.8.8.8.65000 > 192.168.218.133.22: S 98398403:98398403(0) win 8192 <nop,nop,nop,eol>
192.168.218.133.22 > 8.8.8.8.65000: S 1776964986:1776964986(0) ack 98398404 win 16384 <mss 1460> (DF)
8.8.8.8.65000 > 192.168.218.133.22: R 98398404:98398404(0) win 32767
^C
15 packets received by filter
0 packets dropped by kernel
openbsd#

ちなみに、このようなIPアドレスを偽装した通信はインターネットに向けて送信しても失敗することが多いでしょう。

ISPでは発信元のIPアドレスをチェックしているので、矛盾したIPアドレスはIPスプーフィングと判断して破棄されます。矛盾したIPアドレスというのは、たとえばISP網内から発信されているのに送信元IPアドレスが網外のIPアドレスだったり、逆にISP網外から発信されているのに送信元IPアドレスがISP網内のIPアドレスである場合です。

まとめ

TCPはUDPと比較すると設定できるヘッダーフィールドが多いので、値を変化させてみて送信先での反応を見てみるとTCPの理解が深まります。

今回のサンプルプログラムは応用すれば独自のポートスキャナーや通信テストツールを開発できるようになるので、ご自身でヘッダー値を変えながらテストしたり機能を拡張したりしてみてください。

この記事は役に立ちましたか?

もし参考になりましたら、下記のボタンで教えてください。

関連記事

コメント

この記事へのコメントはありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)