【第3回】パケットを受信してTCPヘッダーを解析するツールをC言語で作成する

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

今回はTCPパケットの送信はおこないません。TCPヘッダーの解析だけで長くなるので、TCPパケットの送信は別の回で解説します。

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

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

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

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

TCPヘッダーフォーマット

TCPヘッダーはUDPと比較すると複雑な構造となっています。TCPはコネクションの確立したりフロー制御をおこなったりするため、このような構造となっています。また、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 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                              :
:                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • Source Port(16bit):送信元ポート番号
  • Destination Port(16bit):送信先ポート番号
  • Sequence Number(32bit):シーケンス番号
  • Acknowledgment Number(32bit):アクノレッジ番号
  • Data Offset(4bit):TCPのデータ部分へのオフセットを4バイト単位で表す
  • Rsrvd(4bit):予約
  • CWR(1bit):輻輳ウィンドウフラグ
  • ECE(1bit):ECNエコーフラグ
  • URG(1bit):緊急ポインタフラグ
  • ACK(1bit):確認応答フラグ
  • PSH(1bit):プッシュフラグ
  • RST(1bit):リセットフラグ
  • SYN(1bit):シーケンス番号同期フラグ
  • FIN(1bit):終了フラグ
  • Window(16bit):ウィンドウ
  • Checksum(16bit):チェックサム
  • Urgent Pointer(16bit):URGフラグが立っている際、このフィールドは緊急ポインタの最後のオクテットを指し示します(RFC1011で明記)。URGフラグとUrgent PointerはRFC793での説明が曖昧だったため、誤った実装をしていたり、そもそも実装していない(無視する)場合があります。現在はRFC793が更新されましたが、相互で正しい実装をする必要があるため注意が必要です。TelnetやFTPなどはURGのTCP緊急メカニズムに依存していますが、相互運用性の問題が発生したり望ましくない動作を引き起こしたりする可能性があります。

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オプションのオペレーション番号もtcp.hで定義されています。

パケットを解析してIPv4ヘッダーとTCPヘッダーの値を表示する

サンプルプログラム

今回はTCPオプションを含めて解析していきたいと思います。そのため少し長くなりますが、お付き合いください。TCPオプションの解析を自分で実装できるようになると、普段tcpdumpやWiresharkで何気なく見ていたTCPオプションを深く理解できるようになるはずです。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <error.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <net/ethernet.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#ifndef TH_ECE
#define TH_ECE    0x40
#endif
#ifndef TH_CWR
#define TH_CWR    0x80
#endif
#ifndef TCPOPT_SACKOK
#define TCPOPT_SACKOK 4
#endif
#ifndef TCPOPT_WSCALE
#define TCPOPT_WSCALE 3
#endif
#ifndef TCPOPT_ECHO
#define TCPOPT_ECHO 6
#endif
#ifndef TCPOPT_ECHOREPLY
#define TCPOPT_ECHOREPLY 7
#endif
#ifndef TCPOPT_CC
#define TCPOPT_CC 11
#endif
#ifndef TCPOPT_CCNEW
#define TCPOPT_CCNEW 12
#endif
#ifndef TCPOPT_CCECHO
#define TCPOPT_CCECHO 13
#endif
#ifndef TCPOPT_SIGNATURE
#define TCPOPT_SIGNATURE 19
#endif
#ifndef TCPOPT_SCPS
#define TCPOPT_SCPS 20
#endif
#ifndef TCPOPT_UTO
#define TCPOPT_UTO 28
#endif
#ifndef TCPOPT_TCPAO
#define TCPOPT_TCPAO 29
#endif
#ifndef TCPOPT_MPTCP
#define TCPOPT_MPTCP 30
#endif
#ifndef TCPOPT_FASTOPEN
#define TCPOPT_FASTOPEN 34
#endif
#ifndef TCPOPT_EXPERIMENT2
#define TCPOPT_EXPERIMENT2 254
#endif

unsigned long
get_u4b(unsigned char *p) {
    return ((uint32_t) ntohl(*(uint32_t * )(p)));
}

unsigned short
get_u2b(unsigned char *p) {
    return ((uint16_t) ntohs(*(uint16_t * )(p)));
}

unsigned char
get_u1b(unsigned char *p) {
    return (uint8_t) * p;
}

char *
get_optname(unsigned char opt) {
    switch (opt) {
        case TCPOPT_EOL:
            return "eol";
        case TCPOPT_NOP:
            return "nop";
        case TCPOPT_MAXSEG:
            return "mss";
        case TCPOPT_WSCALE:
            return "wscale";
        case TCPOPT_SACK:
            return "sack";
        case TCPOPT_SACKOK:
            return "sackok";
        case TCPOPT_ECHO:
            return "echo";
        case TCPOPT_ECHOREPLY:
            return "echoreply";
        case TCPOPT_TIMESTAMP:
            return "ts";
        case TCPOPT_CC:
            return "cc";
        case TCPOPT_CCNEW:
            return "ccnew";
        case TCPOPT_CCECHO:
            return "ccecho";
        case TCPOPT_SIGNATURE:
            return "md5";
        case TCPOPT_SCPS:
            return "scps";
        case TCPOPT_UTO:
            return "uto";
        case TCPOPT_TCPAO:
            return "tcp-ao";
        case TCPOPT_MPTCP:
            return "mptcp";
        case TCPOPT_FASTOPEN:
            return "tfo";
        case TCPOPT_EXPERIMENT2:
            return "exp";
        default:
            return "unknown";
    }
}

int
main(void) {
    int sd;
    int i;
    int hlen;        /* 受信したTCPヘッダーのサイズ */
    int tcp_datalen; /* TCPのデータサイズ */
    int read_len;    /* 受信したサイズ */
    char buf[65535];
    struct ip *ip;
    struct tcphdr *tcp;

    if ((sd = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))) < 0) {
        perror("socket");
        exit(-1);
    }

    while (1) {
        if ((read_len = read(sd, buf, sizeof(buf))) < 0) {
            perror("read");
            exit(-1);
        }

        ip = (struct ip *) buf;
        if (ip->ip_v != 0x4)
            continue;
        if (ip->ip_p != IPPROTO_TCP)
            continue;

        tcp = (struct tcphdr *) ((unsigned char *) ip + (ip->ip_hl << 2));
        tcp_datalen = read_len - ((ip->ip_hl<<2) + (tcp->th_off<<2));
        
        printf("======== read_len: %d bytes ========\n", read_len);
        printf("ip_v: %d\n", ip->ip_v);
        printf("ip_hl: %d\n", ip->ip_hl);
        printf("ip_tos: %d\n", ip->ip_tos);
        printf("ip_len: %d\n", ntohs(ip->ip_len));
        printf("ip_id: %d\n", ntohs(ip->ip_id));
        printf("ip_off: %d\n", ntohs(ip->ip_off));
        printf("ip_ttl: %d\n", ip->ip_ttl);
        printf("ip_p: %d\n", ip->ip_p);
        printf("ip_sum: %d\n", ntohs(ip->ip_sum));
        printf("ip_src: %s\n", inet_ntoa(ip->ip_src));
        printf("ip_dst: %s\n", inet_ntoa(ip->ip_dst));

        printf("======== TCP Header ========\n");
        printf("th_sport: %d\n", ntohs(tcp->th_sport));
        printf("th_dport: %d\n", ntohs(tcp->th_dport));
        printf("th_seq: %u", ntohl(tcp->th_seq));
        if (tcp_datalen > 0) {
            printf(":%u", ntohl(tcp->th_seq) + tcp_datalen);
        }
        printf("\n");
        printf("th_ack: %u\n", ntohl(tcp->th_ack));
        printf("th_off: %d\n", tcp->th_off);
        printf("th_flags: 0x%.2x[FIN:%d SYN:%d RST:%d PUSH:%d ACK:%d URG:%d ECE:%d CWR:%d]\n",
               tcp->th_flags,
               tcp->th_flags & TH_FIN ? 1 : 0,
               tcp->th_flags & TH_SYN ? 1 : 0,
               tcp->th_flags & TH_RST ? 1 : 0,
               tcp->th_flags & TH_PUSH ? 1 : 0,
               tcp->th_flags & TH_ACK ? 1 : 0,
               tcp->th_flags & TH_URG ? 1 : 0,
               tcp->th_flags & TH_ECE ? 1 : 0,
               tcp->th_flags & TH_CWR ? 1 : 0);
        printf("th_window: %d\n", ntohs(tcp->th_win));
        printf("th_sum: %d\n", ntohs(tcp->th_sum));
        printf("th_urp: %d\n", ntohs(tcp->th_urp));
        
        hlen = tcp->th_off << 2;
        /*
         * TCPオプションがある場合の処理
         */
        if (hlen > sizeof(struct tcphdr)) {
            char ch = '\0';
            unsigned int len;     /* TCPヘッダーから取得したオプション長(1byte) */
            unsigned int opt;     /* Tオプション番号 */
            unsigned int datalen; /* 計算用 */
            unsigned char *cp = (unsigned char *) tcp + sizeof(struct tcphdr); /* オプションへのポインタ */

            printf("options:");
            /* オプションのサイズを求める */
            hlen -= sizeof(struct tcphdr);
            while (hlen > 0) {
                /* TCPヘッダーからオプション番号を取得する */
                opt = get_u1b(cp);
                cp++;

                /* EOLとNOPはオプションデータが存在しない */
                if (opt == TCPOPT_EOL || opt == TCPOPT_NOP) {
                    len = 1;
                    hlen -= 1;
                } else {
                    /* EOL/NOPでない場合はヘッダーからオプションの長さを取得する */
                    len = get_u1b(cp);
                    cp++;
                    hlen -= 2;
                }
                datalen = 0;

                if (ch != '\0')
                    printf("%c", ch);

                printf(" %s", get_optname(opt));
                switch (opt) {
                    case TCPOPT_MAXSEG:
                        datalen = 2;
                        printf(" %u", get_u2b(cp));
                        break;
                    case TCPOPT_WSCALE:
                        datalen = 1;
                        printf(" %u", get_u1b(cp));
                        break;
                    case TCPOPT_SACK:
                        int i;
                        unsigned int left, right;

                        datalen = len - 2;
                        printf(" %u", datalen / 8);

                        for (i = 0; i < datalen; i += 8) {
                            left = get_u4b(cp + i);
                            right = get_u4b(cp + i + 4);
                            printf("{%u:%u}", left, right);
                        }
                        break;
                    case TCPOPT_CC:
                    case TCPOPT_CCNEW:
                    case TCPOPT_CCECHO:
                    case TCPOPT_ECHO:
                    case TCPOPT_ECHOREPLY:
                        datalen = 4;
                        printf(" %lu", get_u4b(cp));
                        break;
                    case TCPOPT_TIMESTAMP:
                        datalen = 8;
                        printf(" val=%lu,ecr=%lu", get_u4b(cp), get_u4b(cp + 4));
                        break;
                    case TCPOPT_SCPS:
                        datalen = 2;
                        printf(" cap=%.02x,id=%.02x", get_u1b(cp), get_u1b(cp + 1));
                        break;
                    case TCPOPT_EOL:
                    case TCPOPT_NOP:
                    case TCPOPT_SACKOK:
                        break;
                    default:
                        datalen = len - 2;
                        for (int i = 0; i < datalen; ++i) {
                            printf(" %.02x", get_u1b(cp + i));
                        }
                        break;
                } /* end of switch */

                /* 次のポプションへポインタを進める */
                cp += datalen;
                hlen -= datalen;
                
                if (opt == TCPOPT_EOL)
                    break;

                ch = ',';
            }
            printf("\n");
            printf("data-length: %d\n", tcp_datalen);
        }
    }
}

IPヘッダーとTCPヘッダーの表示は第1回・第2回と同じことをしています。バイトオーダーに注意しつつヘッダーの値を表示させるだけです。

TCPフラグについては視覚的にどのビットが立っているのか分かりやすくするために以下のような処理にしています。

        printf("th_flags: 0x%.2x[FIN:%d SYN:%d RST:%d PUSH:%d ACK:%d URG:%d ECE:%d CWR:%d]\n",
               tcp->th_flags,
               tcp->th_flags & TH_FIN ? 1 : 0,
               tcp->th_flags & TH_SYN ? 1 : 0,
               tcp->th_flags & TH_RST ? 1 : 0,
               tcp->th_flags & TH_PUSH ? 1 : 0,
               tcp->th_flags & TH_ACK ? 1 : 0,
               tcp->th_flags & TH_URG ? 1 : 0,
               tcp->th_flags & TH_ECE ? 1 : 0,
               tcp->th_flags & TH_CWR ? 1 : 0);

TCPフラグはパケット解析の際に重要となるので、知識としてだけでなく自分で実装してみてTCPフラグの変化を観察することをおすすめします。併せてシーケンス番号とアクノレッジ番号の変化も追いかけてみることをおすすめします。

TCPオプションは非常にややこしいので、NOPやMSS、ウィンドウ・スケール、タイムスタンプあたりをおさえておけば問題ないと思います。実際のパケット解析でもNOPやMSS、ウィンドウ・スケール、タイムスタンプとSACKくらいしか目にしないはずです。

TCPオプションは1バイトのオペレーションコード、1バイトのオプション長、可変長サイズのデータという構造をしています。データはすべてのオプションに存在しているわけではなくて、NOPとEOLにはデータが含まれません(オプション長もなし)。オプション長が「2」よりも大きければデータが存在するということが分かり、その長さも同時に分かります。

[opcode(1byte)][len(1byte)][data ...]

パケット解析でよく見かけるNOPというのは「No Operation」の略で特に意味は無くパディングに使われます。MSSは「Max Segment Size」の略で、デバイスが受け入れ可能なサイズです。ウィンドウ・スケールは受信ウィンドウサイズを64KBよりも大きくする場合に使われます。タイムスタンプはカーネルがTCP 接続のラウンドトリップ時間 (RTT) をより適切に推定する場合などに使われます。

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

cc linux-read_ipv4-tcp_packet.c

実行する際はroot権限が必要になります。無限ループするので終了する際はCtrl-Cで終了してください。

$ sudo ./a.out
======== read_len: 60 bytes ========
ip_v: 4
ip_hl: 5
ip_tos: 0
ip_len: 60
ip_id: 9389
ip_off: 16384
ip_ttl: 64
ip_p: 6
ip_sum: 59927
ip_src: 192.168.218.130
ip_dst: 183.79.217.124
======== TCP Header ========
th_sport: 60924
th_dport: 443
th_seq: 1447047116
th_ack: 0
th_off: 10
th_flags: 0x02[FIN:0 SYN:1 RST:0 PUSH:0 ACK:0 URG:0 ECE:0 CWR:0]
th_window: 64240
th_sum: 11302
th_urp: 0
options: mss 1460, sackok, ts val=492195458,ecr=0, nop, wscale 7
data-length: 0

TCPオプションが含まれている場合、このようにオプションが表示されるはずです。

まとめ

TCPは非常に複雑です。パケット解析をする際はTCPヘッダーだけでなくTCPの仕様を知っていないと問題解決は難しいです。専門書を読んでも理解することは難しいので、理解を深めるためには自分でパケット解析ツールを作成してヘッダー値やオプションの変化を観察していみるという方法が個人的にはおすすめです。

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

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

関連記事

コメント

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

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