パケット解析、入門してみました。〜その1〜

今年度に入って艦これ始めました。楽しいです。

艦これをやってくうちに、2017夏イベントに突入したのもあってログを取りたいと思ったので自前でロガーを作ろうと思い立ったわけです。 知り合いにソフトを教えてもらったりもしたのですが、そのソフトはWindows用のバイナリを配布しているだけで、 主にLinuxを使ってる自分は使えなかったので(mono1とか使えばいけるのかも知れませんが)。

ログを取る手段は色々あります。既存のものではスクリーンショットを取って解析するのが主流なんですかね、あまり知りませんが。 画像解析はノウハウは持ってないし勉強するのもめんどくさそうだったので、自分はパケット解析をする手段を取ろうと思います。 なんせ、艦これの通信はSSL/TLSで暗号化してないので。

パケット解析ツール?

軽く調べてみたら、Linuxでパケット解析する手法としてはtcpdump2Wireshark3を使うのが一番お手頃らしいです。 tcpdumpで生パケットをファイルに保存して、Wiresharkでそのファイルに書き込まれたパケットを解析するという感じらしいです。

しかしこの方法だと一度ファイルに保存する必要があり、リアルタイムで解析するのは難しいように思えたので、個人的には腑に落ちませんでした。 ですので、tcpdumpで使われているlibpcapというライブラリを使って自分でプログラムを書くことにします (tcpdumpで使われているというより、tcpdumpの副産物的なライブラリという方が正しいですかね)。

libpcap

libpcapを使うにあたり、適度に解説してくれてる日本語のページが見つからなかったため公式ホームページにある使い方4を参考にしました。 以下は、ここの内容を要約したものになると思います。

ではlibpcapの関数をいくつか紹介していきましょう。

pcap_lookupdev

デフォルトのネットワークデバイス名を取得する関数です。

宣言

char *pcap_lookupdev(char *errbuf);

デフォルトのデバイス名を返します。 エラーが起きた場合は、errbufにエラー文字列を格納し、NULLを返します。

サンプルコード

#include <stdio.h>
#include <pcap.h>

int main(int argc, char** argv) {
    char *dev, errbuf[PCAP_ERRBUF_SIZE];

    dev = pcap_lookupdev(errbuf);
    if (dev == NULL) {
        fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
        return 2;
    }

    printf("Device: %s\n", dev);

    return 0;
}

このコードをコンパイルして実行すると、デフォルトのネットワークデバイス名を表示してくれます(環境によってはroot権限を要求されるかも)。 コンパイル時に-lpcapでリンクするのをお忘れなく。

pcap_lookupnet

バイスの属するネットワークのIPv4ネットワークアドレスとネットマスクを取得する関数です。 デバイスIPv4アドレスを取得するわけではないので注意してください。

宣言

int
pcap_lookupnet(const char *device,
               bpf_u_int32 *net,
               bpf_u_int32 *mask,
               char *errbuf);

devで渡された名前のデバイスのネットワークアドレスとネットマスクをそれぞれnetmaskに格納します。 エラーが起きた場合は、errbufにエラー文字列を格納し、-1を返します。

サンプルコード

#include <stdio.h>
#include <pcap.h>

int main(int argc, char** argv) {

    /* ここは上と同じ */

    bpf_u_int32 net, mask;
    if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
        fprintf(stderr, "Can't get netmask for device %s: %s\n", dev, errbuf);
        return 2;
    }

    printf("net:  %d.%d.%d.%d\n",
           net&0xff, (net>>8)&0xff, (net>>16)&0xff, (net>>24)&0xff);
    printf("mask: %d.%d.%d.%d\n",
           mask&0xff, (mask>>8)&0xff, (mask>>16)&0xff, (mask>>24)&0xff);

    return 0;
}

これを実行すると、ネットワークアドレスとネットマスクが表示されます。

pcap_open_live

バイスを扱うハンドルを開放する関数です(こんな日本語で良いのか?)。

宣言

pcap_t *
pcap_open_live(const char *device,
               int snaplen,
               int promisc,
               int to_ms,
               char *errbuf);

引数として、デバイス名(device)、読み込む最大バイト数(snaplen)、プロミスキャス・モードで開放するかどうかのフラグ(promisc)、 タイムアウトの時間[ミリ秒](to_ms)、エラー文字列を格納するポインタ(errbuf)を取ります。

返り値はデバイスのハンドルです。 エラーが起きたときはerrbufにエラー文字列を格納し、NULLを返します。

今後はここで返されるpcap_t*型のハンドルを用いてパケット解析をしていくことになります。

サンプルコード

#include <stdio.h>
#include <pcap.h>

int main(int argc, char** argv) {

    /* ここは上と同じ */

    pcap_t *handle;
    handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
    if (handle == NULL) {
        fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
        return 2;
    }

    return 0;
}

エラーが起きなければ、さっきと出力は同じです。 自分の環境ではroot権限で実行しないとOperation not permittedと怒られました。

pcap_datalink

この関数は渡されたデバイスの(OSI参照モデルでの)データリンク層の種類を返します。

宣言

int pcap_datalink(pcap_t *handle);

引数にハンドルのポインタを受け取り、データリンク層の種類をint型で返します。 データリンク層の種類の一覧はこのページにあります。

サンプルコード

#include <stdio.h>
#include <pcap.h>

int main(int argc, char** argv) {

    /* ここは上と同じ */

    if (pcap_datalink(handle) == DLT_EN10MB) {
        printf("Device %s provides Ethernet headers\n", dev);
    } else {
        printf("Device %s doesn't provide Ethernet headers\n", dev);
    }

    return 0;
}

これを実行すると、デフォルトのデバイスデータリンク層Ethernetかどうかを表示します。

pcap_compile

この関数はハンドルで取得するパケットのフィルターを生成します。

宣言

int
pcap_compile(pcap_t *handle,
             struct bpf_program *fp,
             const char *filter_exp,
             int optimize,
             bpf_u_int32 netmask);

handleで取得するパケットをfilter_expで表されるフィルターをコンパイルし、fpに格納します。 optimizeは最適化するかどうかを示すフラグで、0なら偽、1なら真です。 netmaskはこのフィルターを適用するネットワーク範囲を指定します。

フィルターのコンパイルに失敗した場合は-1を返します。

filter_expで渡す文字列はtcpdumpに渡すexpressionと同じものです。詳しくはman tcpdumpを参照してください。

pcap_setfilter

pcap_compileで生成したフィルターをハンドルに設定します。

宣言

int pcap_setfilter(pcap_t *handle, struct bpf_program *fp);

フィルターfpをハンドルhandleに設定します。 失敗したときは-1を返します。

pcap_geterr

ハンドルで起きた直近のエラー文字列を返します(たぶん)。

宣言

char *pcap_geterr(pcap_t *handle);

サンプルコード

#include <stdio.h>
#include <pcap.h>

int main(int argc, char** argv) {
    char *dev, errbuf[PCAP_ERRBUF_SIZE];
    bpf_u_int32 net, mask;
    pcap_t *handle;
    struct bpf_program fp;
    char filter_exp[] = "tcp and host x.x.x.x";

    dev = pcap_lookupdev(errbuf);
    if (dev == NULL) {
        fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
        return 2;
    }

    if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
        fprintf(stderr, "Can't get netmask for device %s: %s\n", dev, errbuf);
        return 2;
    }

    handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
    if (handle == NULL) {
        fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
        return 2;
    }

    if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
        fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
        return 2;
    }

    if (pcap_setfilter(handle, &fp) == -1) {
        fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
        return 2;
    }

    return 0;
}

エラーが起きたときしか出力しませんが、フィルターをハンドルに設定しています。 フィルターを表す文字列"tcp and host x.x.x.x"は適宜変更してください。 "x.x.x.x"の部分を艦これサーバーのIPアドレスにすれば、艦これサーバーとのTCP通信を見ることができます。

つづく

今回はここまでにしたいと思います。 実際にパケットのデータを扱うのは次回。