【第1回】パケットを受信してIPヘッダーを解析したりIPヘッダーを設定して送信したりするツールをC言語で作成する

この記事では、ネットワークを流れる通信を受信してIPヘッダーを解析したり、自らIPv4ヘッダーを設定してネットワークに送信したりするための方法を解説します。

ただし、すべてを解説すると非常に長くなるので最低限知っておかなければいけないパケット受信・送信の基礎的な知識を本記事では解説しています。

なお、開発環境はLinuxを想定していますが、macOSや*BSDでも方法は同じです。LinuxとmacOS・*BSDではいくつか注意しなければならない違いがありますが、その点も詳しく解説しています。

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

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

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

IPv4ヘッダーフォーマット

インターネットで送受信されるパケットの先頭にはIPv4ヘッダーがあります。

    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
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |Version|  IHL  |Type of Service|          Total Length         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         Identification        |Flags|      Fragment Offset    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Time to Live |    Protocol   |         Header Checksum       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                       Source Address                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Destination Address                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

  • Version(4bit):IPバージョン(バージョンは4です)
  • IHL(4bit):4オクテット単位でIPヘッダーサイズ表す(IPヘッダーは最低20バイトなので、ここの値は最低でも5が格納される)
  • Type of Service(8bit):TOS値
  • Total Length(16bit):パケット全体のサイズ
  • Identification(16bit):ID番号(パケットがフラグメント化された際はこのID番号を基に再構築される)
  • Flags(3bit):フラグメント状態
  • Fragment Offset(13bit):ペイロードの先頭パケットからのオフセット値を8オクテット単位で表す
  • Time to Live(8bit):TTL値
  • Protocol(8bit):上位プロトコルのプロトコル番号
  • Heaer Checksum(16bit):IPヘッダーのチェックサム
  • Source Address(32bit):送信元IPアドレス
  • Destination Address(32bit):送信先IPアドレス
  • Options(可変):タイムスタンプやソールルーティングなどオプションがあればここに格納される
  • Padding(可変):IPヘッダーのオプションは可変長のため4の倍数に収まるようにパディングする

重要な箇所を説明すると、IHLはIPヘッダーサイズを4オクテット単位で表します。この値は4ビットなので最大値は60となります。IPヘッダーの最小値は20バイトですから、IPオプションは40バイトまで追加できる事が分かります。IPオプションはソースルーティングなどで使用されますが、実際にはIPオプションはほとんど使われていません。

IdentificationはID番号と呼ばれるものでIPパケットの識別に使われます。パケットをフラグメントした際はこの値を基に再構築されます。

Fragment Offsetはパケットをフラグメントした際にペイロード部分の先頭からのオフセットを8オクテット単位で表します。そのためフラグメント化した先頭のパケットはFragment Offsetの値が「0」になります。

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

この章ではパケットを解析してIPv4ヘッダーをコンソール画面に表示させる、簡易的なtcpdumpのようなプログラムを作成します。

プログラムを作成する前にC言語のヘッダーファイルでIPv4ヘッダーの構造を確認しておくと理解が深まります。Linuxの場合/usr/include/netinet/ip.hにIPv4ヘッダーの構造体が定義されています。また、macOSや*BSD(FreeBSDやOpenBSD、NetBSDなど)も同様の場所にヘッダーファイルがあるはずです。

Linuxの場合、2種類の構造体が用意されています。最初に掲載するのはLinux固有の構造体です。Linuxだけ扱う場合はこの構造体でも悪くないのですが、macOSや*BSDなどマルチプラットフォームでコンパイルできるようにするためにはもうひとつの構造体を使った方が良いでしょう。

struct iphdr
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int ihl:4;
    unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
    unsigned int version:4;
    unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
    uint8_t tos;
    uint16_t tot_len;
    uint16_t id;
    uint16_t frag_off;
    uint8_t ttl;
    uint8_t protocol;
    uint16_t check;
    uint32_t saddr;
    uint32_t daddr;
    /*The options start here. */
  };

以下はBSD由来の構造体です。本記事ではこちらの構造体を使います。

struct ip
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int ip_hl:4;               /* header length */
    unsigned int ip_v:4;                /* version */
#endif
#if __BYTE_ORDER == __BIG_ENDIAN
    unsigned int ip_v:4;                /* version */
    unsigned int ip_hl:4;               /* header length */
#endif
    uint8_t ip_tos;                     /* type of service */
    unsigned short ip_len;              /* total length */
    unsigned short ip_id;               /* identification */
    unsigned short ip_off;              /* fragment offset field */
#define IP_RF 0x8000                    /* reserved fragment flag */
#define IP_DF 0x4000                    /* dont fragment flag */
#define IP_MF 0x2000                    /* more fragments flag */
#define IP_OFFMASK 0x1fff               /* mask for fragmenting bits */
    uint8_t ip_ttl;                     /* time to live */
    uint8_t ip_p;                       /* protocol */
    unsigned short ip_sum;              /* checksum */
    struct in_addr ip_src, ip_dst;      /* source and dest address */
  };

ip_hl:4ip_v:4:4の箇所は4ビットという意味です。また、#if __BYTE_ORDER == __LITTLE_ENDIANでバイトオーダーによってip_hlip_vを入れ替えています。後で改めて解説しますが、IP通信はネットワークバイトオーダーで通信が流れています。ネットワークバイトオーダーはビックエンディアンと同じです。そのためx86などリトルエンディアンのCPUを使う場合は常にバイトオーダーに注意する必要があります。

通信を受信する方法はOSによって異なる

HTTP通信などを受信する場合とは異なり、IPv4ヘッダーを含めて通信を受信する方法はOSによって異なります。

Linuxの場合はroot権限でsocketを開き通信を受信します。macOSや*BSDは/dev/bpfというデバイスファイルをroot権限で開いてデバイスから通信を読み出します。

このようにOSによって差異があるため、その差異を吸収するためにtcpdumpで使われるlibpcapという便利なライブラリが存在します。本来はlibpcapを使った方がプログラムの作成が楽なのですが今回はライブラリに頼らず自力で通信を受信する方法を解説します。

LinuxではIPヘッダーを含めて受信する場合はsocket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))のように記述してソケットを作成します。見慣れないhtons(ETH_P_ALL)というものがありますが、これはすべての通信を対象にするという意味です。

そのためIPv4以外の通信(たとえばIPv6やARP)も受信しますからif (ip->ip_v != 0x4)のようにしてIPv4でない場合は無視しています。

パケットを扱う際はバイトオーダーに注意する

また、パケットを扱う際に注意しなければいけないのはバイトオーダーです。さきほど、インターネットはネットワークバイトオーダーで流れていると解説しました。ネットワークバイトオーダーはビッグエンディアンと同じであるためリトルエンディアンの環境ではバイトの並び順が一致しません。

たとえばビッグエンディアンの「0x1234」という16bitのデータはリトルエンディアンの環境では「0x3412」とメモリー上で表現されます。また、ビッグエンディアンの「0x12345678」という32ビットのデータはリトルエンディアンの環境では「0x78563412」となります。

  • ビッグエンディアン:12 34 56 78
  • リトルエンディアン:78 56 34 12

そのためネットワークバイトオーダーで受信したデータをリトルエンディアンの環境で正しく扱うためにバイトの並びを変換するntohs()を使う必要があります。

ntohs()はネットワークバイトオーダーのデータをホストバイトオーダーに変換します。ntohsのnは「ネットワークバイトオーダー(Network Byte Order)」の事でtoは「To」です。そしてhは「ホストバイトオーダー(Host Byte Order)」で最後のsは「Short」となります。つまり、Short(16bit)のデータをネットワークバイトオーダーからホストバイトオーダーに変換する、という意味です。

ちなみに32bitのデータをネットワークバイトオーダーからホストバイトオーダーに変換する場合はntohl()のようにLongを意味する「l」が付きます。

IPv4パケットを受信してヘッダー値を表示するサンプルプログラム

次のCコードは非常にシンプルですが、IPv4通信を送受信するとコンソール画面にIPv4ヘッダーの値を表示します。

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

int
main(void)
{
    int sd;
    int read_len;
    char buf[256];
    struct ip *ip;

    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;

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

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

cc linux-read_ipv4_packet.c

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

$ sudo ./a.out
======== read_len: 84 bytes ========
ip_v: 4
ip_hl: 5
ip_tos: 0
ip_len: 84
ip_id: 23691
ip_off: 16384
ip_ttl: 64
ip_p: 1
ip_sum: 6778
ip_src: 192.168.0.250
ip_dst: 1.1.1.1
======== read_len: 84 bytes ========
ip_v: 4
ip_hl: 5
ip_tos: 0
ip_len: 84
ip_id: 27958
ip_off: 0
ip_ttl: 55
ip_p: 1
ip_sum: 21199
ip_src: 1.1.1.1
ip_dst: 192.168.0.250
^C
$

この表示は1.1.1.1へpingを実行した際のものです。そのためip_pの値はICMPを意味する「1」となっています。

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

IPv4パケットのフォーマットについて理解を深めることができました。それでは次にIPv4パケットを送信します。

IPv4パケットの送信は応用がきくので、このスキルをマスターするとネットワークハッキングが楽しくなるはずです。それでは始めましょう。

IPv4パケットの送信はsocketを使う

IPv4パケットを送信する際はLinuxでもmacOSでも*BSDでもsocketを使います。この部分に関してはUnix系OSであれば差異がありません。

ただし、LinuxとBSD系のOSではIPヘッダーの値の設定方法が異なるため注意が必要となります。本来はライブラリを使うべきところですが、この章でも自力で実装していきます。

OSごとに異なる仕様

LinuxとmacOSや*BSDでは微妙に仕様が異なります。これを知らないと正しく実装できないので、それらの違いについて解説します。

主な違いは以下のとおりです。

  • macOS、*BSDはIP_HDRINCLオプションが必須
  • LinuxはIP_HDRINCLオプションが自動的に有効化される(rawソケット作成時にIPPROTO_RAWを指定した場合)
  • BSDはip_lenの値とsendto(2)で指定する送信サイズが一致している必要がある
  • Linuxはsendto(2)で指定する送信サイズがip_lenに自動設定される
  • macOS、*BSDはip_offip_lenはホストバイトオーダー、それ以外はネットワークバイトオーダーで送信する

C言語でIPヘッダーを含めて送信する際はソケットオプションでIP_HDRINCLを追加する必要がありますが、Linuxはrawソケット作成時にIPPROTO_RAWを指定すると自動的にIP_HDRINCLオプションが有効化されます。

ip_lenはIPパケット全体のサイズを指定するものですが、macOSや*BSDはsendto(2)の引数で指定する送信サイズと一致しなければなりません。Linuxの場合はIP_HDRINCLオプションが有効化されている場合、sendto(2)の引数で指定する送信サイズが自動的に設定されます。

ip_offip_lenについては特に重要です。Linuxの場合は何も考えずネットワークバイトオーダーで送信すれば良いのですが、macOSや*BSDの場合はip_offip_lenをホストバイトオーダーで設定してip_idはネットワークバイトオーダーで設定する必要があります。

バイトオーダーの取り扱いについては「低レイヤーのネットワークプログラミング時の注意点|LinuxとBSDの違いを考慮する」をご覧ください。NetBSDを利用されている場合は注意が必要です。

バイトオーダーの変更はhtons()を使います。パケットを受信するときに使ったntohs()とは逆ですね。Short(16bit)のデータをホストバイトオーダーからネットワークバイトオーダーに変換します。

また、LinuxとmacOS、*BSDはどれもIPヘッダーのチェックサムをOSが設定するので自ら計算する必要がありません。

IPv4パケットを作成して送信するサンプルプログラム

次のコードは非常にシンプルですが、IPヘッダーを自在に変更してパケットを送信することができます。

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

int
main(int argc, char *argv[])
{
    int sd;
    int on;
    struct ip *ip;
    struct sockaddr_in src_addr;
    struct sockaddr_in dst_addr;
    char buf[256];

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

    /*
     * 送信元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 = htons(sizeof(struct ip)); /* Linuxはこの値を設定しなくてもOK */
    ip->ip_id = htons(1234);               /* IPID */
    ip->ip_off = htons(0);                 /* フラグメントオフセット */
    ip->ip_ttl = 64;                       /* TTL */
    ip->ip_p = IPPROTO_RAW;                /* プロトコル番号 */
    ip->ip_sum = 0;                        /* チェックサムはOSが計算して設定する */
    ip->ip_src = src_addr.sin_addr;        /* 送信元IPアドレス */
    ip->ip_dst = dst_addr.sin_addr;        /* 宛先IPアドレス */

    if (sendto(sd, buf, sizeof(struct ip), 0, (struct sockaddr *)&dst_addr, sizeof(dst_addr)) < 0) {
        perror("sendto");
        exit(1);
    }
    close(sd);

    return 0;
}

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

cc send_ipv4_packet.c

実行する際はroot権限が必要になります。LAN内であれば送信元IPアドレスを偽装することもできます。試しに送信元IPアドレスを「1.2.3.4」に設定して送信してみます。1番目の引数が送信元IPアドレス、2番目の引数が送信先IPアドレスです。

sudo ./a.out 1.2.3.4 192.168.0.250

前半に作成したIPv4パケットを受信してコンソール画面に表示するプログラムで正常に送信できているのか確認します。

======== read_len: 20 bytes ========
ip_v: 4
ip_hl: 5
ip_tos: 0
ip_len: 20
ip_id: 4660
ip_off: 0
ip_ttl: 255
ip_p: 255
ip_sum: 58126
ip_src: 1.2.3.4
ip_dst: 192.168.0.250

成功です!送信元IPアドレスを偽装できていますね。IPヘッダーの値を書き換えて、どのように送信されるのか色々と試してみてください。

まとめ

今回は基礎中の基礎であるIPヘッダーを取り上げました。非常にシンプルなサンプルプログラムを掲載しましたが、応用させると高度なパケットアナライザーやパケットジェネレーターを作成できます。

まずはIPヘッダーを自在に操れるようにしておき、それからICMPやUDP、TCPへと発展させていきましょう。低レベルレイヤーのネットワークプログラミングに興味がある方はUNIXネットワークプログラミングが大変おすすめです。

次回は「【第2回】パケットを受信してUDPヘッダーを解析したりUDPヘッダーを設定してUDPパケットを送信するツールをC言語で作成する」でUDPを扱います。

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

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

関連記事