RawSocketで簡単なパケットモニタを作る方法

たまにはハッカーっぽく、Binary 2.0っぽい記事を書こう。


Windows 2000/XPでは、RawSocketという仕組みが用意されていて、パケットをかなり自由に受信したり・送信することができます。
RawSocketでは、いわゆる「生パケット」が扱えるのです。
悪い言い方をすると、パケットを覗きみたり、パケットを偽装することも可能ということになります。


以前までは、ドライバレベルでコーディングしないとできなかったようなのですが、WinSock 2.2以降では、これがとても簡単にできます。
今日は、これを利用して簡単なパケットモニタツールを作ってみようと思います。
ビルドは cygwin+mingwです。


大きな流れは以下のとおりです。

  1. Socketの初期化
  2. IPアドレスの取得(必須ではありません)
  3. バインド
  4. プロミスキャスモードに設定
  5. 受信

では1つずつ見ていきましょう

1.Socketの初期化

    // Socket初期化
    if (INVALID_SOCKET == (this->socket = ::socket(AF_INET, SOCK_RAW, IPPROTO_IP)))
    {
        TRACE("INVALID_SOCKET\n");
        return false;
    }

2. IPアドレスの取得

Windowsは、複数のIPアドレスを持っている可能性があるので、そのIPアドレスを列挙します。
なお今回は、1個目に見つかったアドレスを使用するようにしています。
GUIであれば、IPアドレスを選択できるようにするのもよいでしょう。

    SOCKET_ADDRESS_LIST* addressList;
    DWORD d;
    char buffer[1024];

    // アダプタの列挙
    if (WSAIoctl(this->socket, SIO_ADDRESS_LIST_QUERY, NULL, 0, buffer, 1024, &d, NULL, NULL) != 0)
    {
        TRACE("INVALID_SOCKET\n");
        return 0;
    }

    addressList = (SOCKET_ADDRESS_LIST*)buffer;

    if (addressList->iAddressCount == 0)
    {
        TRACE("address not found\n");
        return 0;
    }

    // 1番目のアドレスを使う(手抜き)
    return ((SOCKADDR_IN*)addressList->Address[0].lpSockaddr)->sin_addr.s_addr;

3.バインド

2.で取得したアドレスを元にbind()

    // bind
    SOCKADDR_IN addr_in;
    addr_in.sin_addr.s_addr = this->GetIPAddress(); // IPアドレス
    addr_in.sin_family      = AF_INET;              // IPv4
    addr_in.sin_port        = htons(0);             // 0で初期化らしい
    if (SOCKET_ERROR == bind(this->socket, (SOCKADDR*)&addr_in, sizeof(addr_in)))
    {
        TRACE("bind error\n");
        return false;
    }

4.プロミスキャスモードに設定

NIC(ネットワークインターフェースカード)は、通常自分宛でないパケットは破棄するようになっていますが、プロミスキャスモードという動作モードに設定可能で、これをすると自分宛以外のパケットも受信できるようになります。
今回はパケットモニターを作っているのでこのモードに設定します。

    // プロミスキャスモードの設定
    #define SIO_RCVALL _WSAIOW(IOC_VENDOR, 1)
    unsigned long optval=1; //PROMISC
    if (SOCKET_ERROR != WSAIoctl(this->socket, SIO_RCVALL, &optval, sizeof(optval), NULL, 0, &d, NULL, NULL))
    {
        TRACE("SIO_RCVALL\n");
        return false;
    }

5.受信

いよいよ受信です。WSARecv関数を使用して受信します。
なお今回はブロックモードで受信しています。
ブロックモードとは、パケットがくるまでスレッドがブロックされるモードのことです。
パケット到着まで他の作業はできません。

    unsigned long length;
    unsigned long flags = 0;
    WSABUF wsb;
    wsb.buf = (PTCHAR)this->readBuffer;
    wsb.len = MAX_RECV_SIZE;
    ZeroMemory(wsb.buf, wsb.len);

    if (SOCKET_ERROR == WSARecv(this->socket, &wsb, 1, &length, &flags, NULL, NULL))
    {
        TRACE("RECIEVE ERROR\n");
        return false;
    }

パケットモニタ

上記手順を踏めば、ほんの100行足らずでパケットを受信することができてしまいます。


今回は、この処理を抽象化して以下のように使えるようにしてみました。
RawSocketクラスのインスタンスに対して、Open/Read/Closeするだけです。

int main(int argc, char *argv[])
{
    byte buffer[1024];
    dword length;
    RawSocket socket;

    if (!socket.Open()) {
        printf("Socket Open Error\n");
        return -1;
    }

    for (int i = 0; i < 3; i++)
    {
        printf("\nWaiting for Packet...\n");
        socket.Read(buffer, 1024, &length);

        IP::Header* header = (IP::Header*)buffer;

        printf("protocol number %x\n", header->prot);
        printf("source   address ");
        printAddress(header->srcip);

        printf("dest     address ");
        printAddress(header->dstip);

        printf("data     ");
        for (int j = 0; j < 10; j++)
        {
            printf("%02x ", header->data[j]);
        }
        printf("\n");
    }

    if (!socket.Close()) {
        printf("Socket Close Error\n");
        return -1;
    }

    return 0;
}

実際に動作させると

Waiting for Packet...
protocol number 1
source   address 192.168.11.2
dest     address 192.168.11.2
data     00 00 61 ffffffce 6c 0e 05 00 3c ffffff86

こんな感じでパケットの中身を読むことができます。
プロトコルごとにさらに詳しく解析するといろいろと楽しいことができそうですね。

ソースとか

ソースと実行ファイルを置いておきます。
使用は自己責任でお願いします。実行ファイルは管理者権限でないと動作しないと思います。
rawsocket.zip

資料

はてなブックマーク - winsockに関するhigeponのブックマーク

マスタリングTCP/IP 入門編

マスタリングTCP/IP 入門編

あとがき

この試みはもちろんMonaのネットワーク対応の一環です。
つい調子に乗って勢いで書いてしまいました。
次は、パケット送信にチャレンジしようと思っていますが、需要が少ないためか受信に比べて資料が少ないようです。


これのLinux版を作って、Windows版も同梱して、SwigPerlとかから使えたら便利だよね。
まぁほかにいろいろ手段あるから必要ないか・・・。>Perlの偉い人