Help us understand the problem. What is going on with this article?

パケットキャプチャツールをつくる

はじめに

ネットワークと C 言語の勉強を兼ねて、簡易的なパケットキャプチャツールをつくってみました。参考にしたのは「ルーター自作でわかるパケットの流れ」という書籍です。

image.png

表紙に書かれている「ネットワークはどのようにつながるのかパケットの気持ちになって考えてみたことはありますか?」というコメントに妻が若干引いておりましたが、こういったディープな内容の本は中々ないので有り難かったです。なお、この本はタイトルのとおりルータを自作することがゴールになっていて、パケットキャプチャツールの作成はそのための練習という位置付けです。

また、特別講座 ネットワークプログラミング ( FWをつくろう )というサイトも非常に参考になりました。図入りで説明されていてとても分かりやすかったです。

ちなみに、C 言語は大学の時に少しかじったものの、ほぼ初心者に近い状態だったので Udemy の「イメージでわかる!基礎知識ゼロからのC言語。現役エンジニアが教えるC言語完全攻略コース」という講座を受けて学び直しました。C 言語のメモリの扱いが分かりやすく説明されていて、オススメです。

そもそもパケットキャプチャとは

パケットキャプチャとは、通信ネットワーク上を流れるデータ (パケット) を採取 (キャプチャ) することです。また、キャプチャしたパケットを可視化して、各パケットの通信の種類、送信元・送信先、データの中身などを確認できます。パケットキャプチャツールとしては Wireshark や tcpdump など高性能なソフトウェアがありますが、今回は各レイヤーでのプロトコル解析の練習のため、簡易的なパケットキャプチャツールを自作しました。

以下で作成するのは、Ethernet ヘッダ / IP ヘッダ / UDP ヘッダの一部を表示するプログラムです。

パケットキャプチャツールをつくる

処理の流れは以下のとおりです。

  1. データリンク層を扱うためのディスクリプタを準備する
  2. ループしながらパケットを受信する
  3. Ethernet ヘッダの送信元 MAC アドレスと送信先 MAC アドレスを表示する
  4. IP ヘッダの送信元 IP アドレスと送信先 IP アドレスを表示する
  5. UDP ヘッダの送信元ポート番号と送信先ポート番号とチェックサムの値を表示する

1. データリンク層を扱うためのディスクリプタを準備する

今回は手元の Raspberry Pi 上で C 言語で実装したプログラムを動かしました。Linux では socket() でデータリンク層も扱えるので、TCP/UDP のソケットプログラミングと同じ要領でプログラムを記述できます。以下のように socket() の第一引数に PF_PACKET を、第二引数に SOCK_RAW を指定することで、データリンク層のパケットを扱うことができます。また、第三引数はプロトコルの指定で、ETH_P_IP を指定すると IP パケットのみを受信できます。 (ETH_P_ALL を指定すると全てのパケットを受信できます。)

soc = socket(PF_PACKET,SOCK_RAW,htons(ETH_P_IP));

なお、第三引数のプロトコルは IEEE 802.3 プロトコル番号をネットワークバイトオーダーで指定します。そのため、ホストバイトオーダーをネットワークバイトオーダーに変換する htons() 関数を使用します。ざっくりと説明すると、ネットワークの世界とコンピュータの世界で、(2 バイト以上の) データをメモリ上へ展開したりどこかへ転送したりするときのデータの並び順が異なるため相互に変換が必要になるのです。(前者をネットワークバイトオーオーダー、後者をホストバイトオーダーと呼びます。)

2. ループしながらパケットを受信する

こんな感じでループさせます。

    while(1){
        read(soc,buf,sizeof(buf));
        AnalyzePacket(buf);
    }

無限ループを抜ける際は、Ctrl + C でプログラムを停止します。

3. Ethernet ヘッダの送信元 MAC アドレスと送信先 MAC アドレスを表示する

受信したデータポインタを Ethernet ヘッダの構造体 ether_header にキャストして、構造体のメンバを呼び出して表示します。ether_header は Linux では、/usr/include/net/ethernet.h に定義されています。

char *ether_ntoa(u_char *hwaddr){
    static char str[18];
    snprintf(str,sizeof(str),"%02x:%02x:%02x:%02x:%02x:%02x",hwaddr[0],hwaddr[1],hwaddr[2],hwaddr[3],hwaddr[4],hwaddr[5]);
    return str;
}

void PrintEtherHeader(u_char *buf){
    struct ether_header *eth;
    eth = (struct ether_header *)buf;
    printf("MAC ADDRESS : %17s > ",ether_ntoa(eth->ether_shost));
    printf("%17s | ",ether_ntoa(eth->ether_dhost));
}

AnalyzePacket() 内の処理は以下。

void AnalyzePacket(u_char *buf){
    PrintEtherHeader(buf);
}

4. IP ヘッダの送信元 IP アドレスと送信先 IP アドレスを表示する

IP ヘッダを解析するには、データポインタを Ethernet ヘッダ構造体 ether_header のサイズ分進めてから先ほどと同じように IP ヘッダ用構造体 iphdr にキャストして、構造体のメンバを呼び出して表示します。iphdr は Linux では、/usr/include/netinet/ip.h に定義されています。

char *ip_ntoa(u_int32_t ipaddr){
    u_char *d = (u_char *)&ipaddr;
    static char str[16];
    snprintf(str,sizeof(str),"%d.%d.%d.%d",d[0],d[1],d[2],d[3]);
    return str;
}

void PrintIpHeader(u_char *buf){
    struct iphdr *ip;
    ip= (struct iphdr *)buf;
    printf("IP ADDREDD : %s > ",ip_ntoa(ip->saddr));
    printf("%s | ",ip_ntoa(ip->daddr));
}

AnalyzePacket() 内に処理を追加して以下のようにします。

void AnalyzePacket(u_char *buf){
    u_char *ptr;
    struct iphdr *ip;
    PrintEtherHeader(buf);
    ptr = buf;
    ptr += sizeof(struct ether_header);
    PrintIpHeader(ptr);
}

5. UDP ヘッダの送信元ポート番号と送信先ポート番号とチェックサムの値を表示する

IP ヘッダの protocol フィールド の値が 17 であれば UDP のパケットであることが確認できます。UDP ヘッダを解析するには、更に IP ヘッダの長さ分ポインタを進めて、UDP ヘッダ用構造体 udphdr にキャストして、構造体のメンバを呼び出して表示します。なお、IP ヘッダの長さは、IP ヘッダ用構造体 iphdrihl メンバに格納されている値を 4 倍すると得られます。(ihl には IP ヘッダの長さ ÷4 の値が格納されているため。)
udphdr は Linux では、/usr/include/netinet/udp.h に定義されています。

void PrintUdpHeader(u_char *buf){
    struct udphdr *ptr;
    ptr = (struct udphdr *)buf;
    printf("PORT NUMBER : %u > %u |CHECK SUM : %u\n",ntohs(ptr->source),ntohs(ptr->dest),ntohs(ptr->check));
}

AnalyzePacket() 内に処理を追加して以下のようにします。

void AnalyzePacket(u_char *buf){
    u_char *ptr;
    struct iphdr *ip;
    PrintEtherHeader(buf);
    ptr = buf;
    ptr += sizeof(struct ether_header);
    PrintIpHeader(ptr);
    ip = (struct iphdr *)ptr;

    if(ip->protocol==17){
        ptr += ((struct iphdr *)ptr)->ihl*4;
        PrintUdpHeader(ptr);
    }
}

なお、UDP ではなく TCP パケットのヘッダを表示したい場合はプロトコル番号が 6 のパケットを TCP ヘッダ用の構造体 tcphdr でキャストします。

ソースコード

完成したソースコードは以下です。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <sys/ioctl.h>
#include <netinet/ip.h>
#include <netinet/if_ether.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <netpacket/packet.h>

char *ether_ntoa(u_char *hwaddr){
    static char str[18];
    snprintf(str,sizeof(str),"%02x:%02x:%02x:%02x:%02x:%02x",hwaddr[0],hwaddr[1],hwaddr[2],hwaddr[3],hwaddr[4],hwaddr[5]);
    return str;
}

char *ip_ntoa(u_int32_t ipaddr){
    u_char *d = (u_char *)&ipaddr;
    static char str[16];
    snprintf(str,sizeof(str),"%d.%d.%d.%d",d[0],d[1],d[2],d[3]);
    return str;
}

void PrintEtherHeader(u_char *buf){
    struct ether_header *eth;
    eth = (struct ether_header *)buf;
    printf("MAC ADDRESS : %17s > ",ether_ntoa(eth->ether_shost));
    printf("%17s | ",ether_ntoa(eth->ether_dhost));
}


void PrintIpHeader(u_char *buf){
    struct iphdr *ip;
    ip= (struct iphdr *)buf;
    printf("IP ADDREDD : %s > ",ip_ntoa(ip->saddr));
    printf("%s | ",ip_ntoa(ip->daddr));
}


void PrintUdpHeader(u_char *buf){
    struct udphdr *ptr;
    ptr = (struct udphdr *)buf;
    printf("PORT NUMBER : %u > %u |CHECK SUM : %u\n",ntohs(ptr->source),ntohs(ptr->dest),ntohs(ptr->check));
}

void AnalyzePacket(u_char *buf){
    u_char *ptr;
    struct iphdr *ip;
    PrintEtherHeader(buf);
    ptr = buf;
    ptr += sizeof(struct ether_header);
    PrintIpHeader(ptr);
    ip = (struct iphdr *)ptr;

    if(ip->protocol==17){
        ptr += ((struct iphdr *)ptr)->ihl*4;
        PrintUdpHeader(ptr);
    }
}

int main(){
    int soc;
    u_char buf[65535];
    soc = socket(PF_PACKET,SOCK_RAW,htons(ETH_P_IP));
    while(1){
        read(soc,buf,sizeof(buf));
        AnalyzePacket(buf);
    }
}

パケットキャプチャツールを実行する

作成したプログラム pcap.c をコンパイルして実行します。

pi@raspberrypi:~ $ cc -g -Wall pcap.c -o pcap
pi@raspberrypi:~ $ sudo ./pcap

別コンソールを立ち上げて、Raspberry pi 上で、(UDP 上で動作する) NTP のリクエストを送信します。

pi@raspberrypi:~ $ sudo ntpdate -v ntp.nict.jp

先ほどのコンソールに戻って表示内容を確認します。123 番ポートで通信ができていることが確認できます。

MAC ADDRESS : 00:00:00:00:00:00 > 00:00:00:00:00:00 | IP ADDREDD : 127.0.0.1 > 127.0.0.1 | PORT NUMBER : 53985 > 53 |CHECK SUM : 65080
MAC ADDRESS : 00:00:00:00:00:00 > 00:00:00:00:00:00 | IP ADDREDD : 127.0.0.1 > 127.0.0.1 | PORT NUMBER : 53985 > 53 |CHECK SUM : 65080
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 192.168.3.1 > 192.168.3.28 | PORT NUMBER : 53 > 14731 |CHECK SUM : 65495
MAC ADDRESS : 00:00:00:00:00:00 > 00:00:00:00:00:00 | IP ADDREDD : 127.0.0.1 > 127.0.0.1 | PORT NUMBER : 53 > 53985 |CHECK SUM : 65263
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 192.168.3.1 > 192.168.3.28 | PORT NUMBER : 53 > 39759 |CHECK SUM : 34994
MAC ADDRESS : 00:00:00:00:00:00 > 00:00:00:00:00:00 | IP ADDREDD : 127.0.0.1 > 127.0.0.1 | PORT NUMBER : 53 > 53985 |CHECK SUM : 65141
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.243 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 30179
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.164 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 25045
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.244 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 35276
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.163 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 32936
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 61.205.120.130 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 38592
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.243 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 19473
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.164 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 36554
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.244 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 28117
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.163 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 19369
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 61.205.120.130 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 23324
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.243 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 46579
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.164 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 64861
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.244 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 4243
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.163 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 16498
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 61.205.120.130 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 27594
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.243 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 27751
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.164 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 1263
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.244 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 44970
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 133.243.238.163 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 43629
MAC ADDRESS : 14:a5:1a:cd:d5:53 > dc:a6:32:ab:77:4e | IP ADDREDD : 61.205.120.130 > 192.168.3.28 | PORT NUMBER : 123 > 123 |CHECK SUM : 29785

おわりに

パケットキャプチャツールをつくるというと難しく聞こえますが、今回のような簡単なものであれば 100 行以下のプログラムで実装可能です。でも、もしかしたらバグがあるかも。また、その他記載事項に間違いがあればご指摘いただけますと助かります。

また、勉強したことは発信することでより理解が深まることが実感できたので、今後も学んだことをブログに書いていこうと思います。もっとディープな内容が書けるように頑張ります。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away