おもちゃバコ

中身スカスカ♡

C++: パケットキャプチャもどきを作ってみた

こんにちは

ネットワークの学習として,パケットキャプチャもどきを作ってみました。
(正しいかは分からない...)


動機

  • Wiresharkをセキュリティの関係でインストール出来ない。
  • ソケット通信について勉強したい。
  • 知ってるふりして「パケットが~」って言うのが恥ずかしくなった(?)

ネットワーク関係に疎い事が気になったので,学習の取っ掛かりとして始めました。


参考

本文より100倍為になると思います。

参考文献

ほぼ参考にさせていただきました。
codezine.jp
www.keicode.com

勉強も含めてWiresharkソースコードも見てました。
理解はしてないです!!!
www.wireshark.org
github.com

参考図書

鉄板だね。



開発環境

実装にはWinSock2を利用しました。


生ソケット(raw socket)

名前通り生のソケットのこと。

各層のヘッダを分解せずに,生データのまま受信できるようにするソケットだと思ってます。
(自分でヘッダ解析はやってねってコト!?)

learn.microsoft.com
ja.wikipedia.org

プロミスキャスト・モード

自分宛以外のパケットも破棄せずに受信できるモードです。
IPAの某試験やWiresharkでよく目にするよね。

個人的にはパケット盗聴とかで使用されているイメージが強い。

ja.wikipedia.org

自分宛でないARPのパケットも破棄せずに受信できるようになるはず。
と思ったけど,WinSock2ではネットワーク層(位)までのデータまでしか受信できなさそうですね。

ARPデータリンク層なので,イーサネットフレームを読み取る必要がありそうです。
(やり方は分からない)


ソースコード

とりあえず生データを表示してみる。
スタック領域を使い過ぎだけど目を逸らした。

#include <iostream>

#include <WinSock2.h>
#include <WS2tcpip.h>
#include <mstcpip.h>
#pragma comment(lib, "ws2_32.lib") // 行儀が悪いね

// C6262: スタック領域使い過ぎ
int main()
{
    // WinSock初期化
    WSADATA wsaData{};
    const WORD winsockVerSion = MAKEWORD(2, 2); // WinSock2.2
    if (WSAStartup(winsockVerSion, &wsaData) != 0)
    {
        std::cout << "WSAStartup is error: " << WSAGetLastError() << std::endl;
        return 1;
    }

    // socket作成
    const int addressFamily = AF_INET; // IPv4
    const int socketType = SOCK_RAW;   // 生ソケット(管理者権限が必要)
    const int protocol = IPPROTO_IP;   // ダミー(全プロトコル取得)
    SOCKET mySocket = socket(addressFamily, socketType, protocol);
    if (mySocket == INVALID_SOCKET)
    {
        std::cout << "socket is error: " << WSAGetLastError() << std::endl;
        return 1;
    }

    // NICのパラメータを取得(IPアドレス)
    char outBuffer[1024] = {}; // 出力先のバッファ
    DWORD outBufferBytesReturned = 0; // 出力の実際のバイト数
    const DWORD dwIoControlCode = SIO_ADDRESS_LIST_QUERY; // バインド可能なソケットのプロトコルファミリのアドレス一覧を取得
    if (WSAIoctl(mySocket, dwIoControlCode,
        NULL, 0, // 入力先
        outBuffer, sizeof(outBuffer), &outBufferBytesReturned, // 出力先
        NULL, // WSAOVERLAPPED構造体へのポインタ
        NULL  // コールバック関数?
    ) != 0)
    {
        std::cout << "WSAIoctrl is error: " << WSAGetLastError() << std::endl;
        return 1;
    }
    // 取得した情報を表示
    SOCKET_ADDRESS_LIST* const pSocketAddressList = reinterpret_cast<SOCKET_ADDRESS_LIST*>(outBuffer);
    for (uint8_t i = 0u; i < pSocketAddressList->iAddressCount; ++i)
    {
        SOCKADDR_IN* const pNicAddr = reinterpret_cast<SOCKADDR_IN*>(pSocketAddressList->Address[i].lpSockaddr);
        if (pNicAddr == NULL) { return 1; }
        char ipAddressBuffer[1024 * 4] = {}; // IPv4(16文字以上), IPv6(46文字以上)
        PCSTR pIpStr = inet_ntop(addressFamily, &(pNicAddr->sin_addr), ipAddressBuffer, sizeof(ipAddressBuffer));
        if (pIpStr == NULL)
        {
            std::cout << "inet_ntop is error: " << WSAGetLastError() << std::endl;
            return 1;
        }
        std::cout << pIpStr << " <-> " << ipAddressBuffer << std::endl;
    }

    // ソケット設定
    // - NICから取得したIPアドレスを直接指定してるから,分かっていればIPアドレス直書きでもいいかもね
    sockaddr_in address = {};
    SOCKADDR_IN* const pNicAddr = reinterpret_cast<SOCKADDR_IN*>(pSocketAddressList->Address[0].lpSockaddr);
    address.sin_addr.S_un.S_addr = pNicAddr->sin_addr.S_un.S_addr; // 覗き見対象のIPアドレス
    address.sin_family = addressFamily;
    address.sin_port = htons(0); // ネットワークバイトオーダ(ビッグエンディアン)に変換

    // バインド
    if (bind(mySocket, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0)
    {
        std::cout << "bind is error: " << WSAGetLastError() << std::endl;
        return 1;
    }

    // NICを通過するIPv4/IPv6の全パケットを受信(プロミスキャストモード)
    // - これを設定しないと受信できない
    ULONG rcvallOption = RCVALL_ON;
    if (WSAIoctl(mySocket, SIO_RCVALL,
        &rcvallOption, sizeof(rcvallOption),
        NULL, 0, &outBufferBytesReturned,
        NULL, NULL) != 0)
    {
        std::cout << "WSAIoctrl is error: " << WSAGetLastError() << std::endl;
        return 1;
    }

    while (true)
    {
        // データ受信
        char recvBuffer[1024 * 20] = {};
        const int receiveByte = recv(mySocket, recvBuffer, sizeof(recvBuffer), 0);
        if (receiveByte == SOCKET_ERROR)
        {
            // 10040(WSAEMSGSIZE): 受信メッセージがバッファに収まりきらなかった
            std::cout << "recv is error: " << WSAGetLastError() << std::endl;
            break;
        }

        // データ表示(無加工)
        std::cout << receiveByte << " " << outBuffer << std::endl;
        for (uint64_t i = 0ull; i < receiveByte; ++i)
        {
            std::cout << recvBuffer[i];
        }
        std::cout << "\n" << std::endl;
    }

    // WinSock終了
    if (WSACleanup() != 0)
    {
        std::cout << "WSACleanup is error: " << WSAGetLastError() << std::endl;
        return 1;
    }

    return 0;
}

プログラム設計は気にしないで...。

実行結果

大体文字化けしてるけど,SSDPとか所々読めるところがあるね。

192.168.87.12 <-> 192.168.87.12
192.168.4.124 <-> 192.168.4.124
128 
メD誾g...

IPヘッダ(IPv4)

受信できてそうなので,受信データを解析してみます。
とりあえずインターネット層から。

IPヘッダ情報はWikipediaを参考にしました。
ja.wikipedia.org

struct IPv4Header
{
    uint8_t  version = 0u;               // バージョン: 4bit
    uint8_t  internetHeaderLength = 0u;  // ヘッダ長さ: 4bit
    uint8_t  typeOfService = 0u;         // サービス種別: 8bit
    uint16_t totalLength = 0u;           // 全長: 16bit
    uint16_t identification = 0u;        // 識別子: 16bit
    uint8_t  versionControlFlags = 0u;   // フラグ: 3bit
    uint16_t fragmentOffset = 0u;        // 断片位置: 13bit
    uint8_t  protocol = 0u;              // プロトコル: 8bit
    uint8_t  timeToLive = 0u;            // 生存時間: 8bit
    uint16_t headerChecksum = 0u;        // チェックサム: 16bit
    uint8_t  sourceAddress[4] = {};      // 送信元アドレス: 32bit
    uint8_t  destinationAddress[4] = {}; // 宛先アドレス: 32bit
    uint32_t options = 0u;               // 拡張情報: 32bit

    void Init(const char* const pBuff)
    {
        if (pBuff == NULL) { return; }

        // 元がchar型なのでキャスト時に注意
        version = (pBuff[0] & 0xF0u) >> 4u;
        internetHeaderLength = (pBuff[0] & 0x0Fu);
        typeOfService = pBuff[1];
        totalLength = ((static_cast<uint16_t>(pBuff[2]) & 0x00FFu) << 8u) | (static_cast<uint16_t>(pBuff[3]) & 0x00FFu);
        identification = ((static_cast<uint16_t>(pBuff[4]) & 0x00FFu) << 8u) | (static_cast<uint16_t>(pBuff[5]) & 0x00FFu);
        versionControlFlags = (pBuff[6] & 0xE0u) >> 5u;
        fragmentOffset = ((static_cast<uint16_t>(pBuff[6]) & 0x007Fu) << 8u) | (static_cast<uint16_t>(pBuff[7]) & 0x00FFu);
        timeToLive = pBuff[8];
        protocol = pBuff[9];
        headerChecksum = ((static_cast<uint16_t>(pBuff[10]) & 0x00FFu) << 8u) | (static_cast<uint16_t>(pBuff[11]) & 0x00FFu);
        sourceAddress[0] = pBuff[12]; sourceAddress[1] = pBuff[13]; sourceAddress[2] = pBuff[14]; sourceAddress[3] = pBuff[15];
        destinationAddress[0] = pBuff[16]; destinationAddress[1] = pBuff[17]; destinationAddress[2] = pBuff[18]; destinationAddress[3] = pBuff[19];

        // ヘッダ長が4オクテット単位
        const uint32_t dataStartIndex = (internetHeaderLength * 4u);
        if (dataStartIndex > 20)
        {
            // 多分使われてないので,20オクテットより大きいなら添字20から読めばイケると思う(雑)
            options = ((static_cast<uint32_t>(pBuff[20]) & 0x000000FFu) << 24u) | ((static_cast<uint32_t>(pBuff[21]) & 0x000000FFu) << 16u) | ((static_cast<uint32_t>(pBuff[22]) & 0x000000FFu) << 8u) | ((static_cast<uint32_t>(pBuff[23]) & 0x000000FFu));
        }
    }
    void Print()
    {
        std::cout << "-- IPv4 Header --" << std::endl;
        std::cout << "Version: " << +version << std::endl;
        std::cout << "InternetHeaderLength: " << +internetHeaderLength << std::endl;
        std::cout << "TypeOfService: " << +typeOfService << std::endl;
        std::cout << "TotalLength: " << +totalLength << std::endl;
        std::cout << "Identification: " << +identification << std::endl;
        std::cout << "VersionControlFlags: " << +versionControlFlags << std::endl;
        std::cout << "FragmentOffset: " << +fragmentOffset << std::endl;
        std::cout << "TimetoLive: " << +timeToLive << std::endl;
        std::cout << "Protocol: " << +protocol << std::endl;
        std::cout << "HeaderChecksum: " << +headerChecksum << std::endl;
        std::cout << "SrcAddress: " << +sourceAddress[0] << "." << +sourceAddress[1] << "." << +sourceAddress[2] << "." << +sourceAddress[3] << std::endl;
        std::cout << "DstAddress: " << +destinationAddress[0] << "." << +destinationAddress[1] << "." << +destinationAddress[2] << "." << +destinationAddress[3] << std::endl;
        std::cout << "Options: " << +options << std::endl;
    }
};

char -> unsigned charのキャストでビット演算に手間取ってしまった。

雑な感じだけど一応読み取れてそう(?)
IPペイロード部分は次の層へ。

実行結果はこんな感じ

-- IPv4 Header --
Version: 4
InternetHeaderLength: 5
TypeOfService: 0
TotalLength: 88
Identification: 24435
VersionControlFlags: 2
FragmentOffset: 17394
TimetoLive: 64
Protocol: 6
HeaderChecksum: 0
SrcAddress: 192.168.87.11
DstAddress: 172.217.174.110
Options: 0

TCPヘッダ

TCPヘッダ情報はWikipediaを参考にしました。
ja.wikipedia.org

struct TCPHeader
{
    IPv4Header ipHeader{};
    uint16_t sourcePort = 0u;            // 送信元ポート: 16bit
    uint16_t destinationPort = 0u;       // 送信先ポート: 16bit
    uint32_t sequenceNumber = 0u;        // シーケンス番号: 32bit
    uint32_t acknowledgementNumber = 0u; // 確認応答番号: 32bit
    uint8_t  dataOffset = 0u;            // ヘッダ長: 4bit
    uint8_t  reserved = 0u;              // 予約: 3bit
    uint16_t controlBit = 0u;            // 制御ビット列: 1bit * 9
    uint16_t windowSize = 0u;            // ウィンドウサイズ: 16bit
    uint16_t checkSum = 0u;              // チェックサム: 16bit
    uint16_t urgentPointer = 0u;         // 緊急ポインタ: 16bit
    uint32_t options = 0u;               // オプション(Padding込み): 32bit

    void Init(const char* const pBuff)
    {
        if (pBuff == NULL) { return; }

        ipHeader.Init(pBuff);

        const uint16_t startIndex = ipHeader.internetHeaderLength * 4u;
        sourcePort = ((static_cast<uint16_t>(pBuff[startIndex + 0u]) & 0x00FFu) << 8u) | (static_cast<uint16_t>(pBuff[startIndex + 1u]) & 0x00FFu);
        destinationPort = ((static_cast<uint16_t>(pBuff[startIndex + 2u]) & 0x00FFu) << 8u) | (static_cast<uint16_t>(pBuff[startIndex + 3u]) & 0x00FFu);
        sequenceNumber = ((static_cast<uint32_t>(pBuff[startIndex + 4u]) & 0x000000FFu) << 24u) | ((static_cast<uint32_t>(pBuff[startIndex + 5u]) & 0x000000FFu) << 16u) | ((static_cast<uint32_t>(pBuff[startIndex + 6u]) & 0x000000FFu) << 8u) | ((static_cast<uint32_t>(pBuff[startIndex + 7u]) & 0x000000FFu));
        acknowledgementNumber = ((static_cast<uint32_t>(pBuff[startIndex + 8u]) & 0x000000FFu) << 24u) | ((static_cast<uint32_t>(pBuff[startIndex + 9u]) & 0x000000FFu) << 16u) | ((static_cast<uint32_t>(pBuff[startIndex + 10u]) & 0x000000FFu) << 8u) | ((static_cast<uint32_t>(pBuff[startIndex + 11u]) & 0x000000FFu));
        dataOffset = (pBuff[startIndex + 12u] & 0xF0) >> 4u;
        reserved = (pBuff[startIndex + 12u] & 0x0E) >> 1u;
        controlBit = ((static_cast<uint16_t>(pBuff[startIndex + 12u]) & 0x0001u) << 8u) | (static_cast<uint16_t>(pBuff[startIndex + 13u]) & 0x00FFu);
        windowSize = ((static_cast<uint16_t>(pBuff[startIndex + 14u]) & 0x00FFu) << 8u) | (static_cast<uint16_t>(pBuff[startIndex + 15u]) & 0x00FFu);
        checkSum = ((static_cast<uint16_t>(pBuff[startIndex + 16u]) & 0x00FFu) << 8u) | (static_cast<uint16_t>(pBuff[startIndex + 17u]) & 0x00FFu);
        urgentPointer = ((static_cast<uint16_t>(pBuff[startIndex + 18u]) & 0x00FFu) << 8u) | (static_cast<uint16_t>(pBuff[startIndex + 19u]) & 0x00FFu);

        // ヘッダ長が32ビットワード単位(4オクテットってコト?!)
        const uint32_t dataStartIndex = (dataOffset * 4u);
        if (dataOffset > 20u)
        {
            options = 0xffu; // 面倒になったので何か入れとく
        }
    }
    void Print()
    {
        std::cout << "-- TCP Header --" << std::endl;
        std::cout << "SrcPort: " << sourcePort << std::endl;
        std::cout << "DstPort: " << destinationPort << std::endl;
        std::cout << "SeqNum: " << sequenceNumber << std::endl;
        std::cout << "AckNum: " << acknowledgementNumber << std::endl;
        std::cout << "DataOffset: " << +dataOffset << std::endl;
        std::cout << "Reserved: " << +reserved << std::endl;
        std::cout << "ControlBit: " << controlBit << std::endl;
        std::cout << "WindowSize: " << windowSize << std::endl;
        std::cout << "CheckSum: " << checkSum << std::endl;
        std::cout << "URGPointer: " << urgentPointer << std::endl;
        std::cout << "Options: " << options << std::endl;
        ipHeader.Print();
    }
};

段々適当になってきた。
それっぽい数値が入っているけど,多分何か間違えている。
(ヘッダ長が4ワードの時がある...)

実行結果は省略。
UDPは...今回はパスで

最終形態

ソースコードはこうなりました。

何か間違ってる気がする...。

実行結果はこんな感じ

-- TCP Header --
SrcPort: 49887
DstPort: 443
SeqNum: 88542682754
AckNum: 1143207688
DataOffset: 5
Reserved: 0
ControlBit: 24
WindowSize: 1032
CheckSum: 193249
URGPointer: 0
Options: 0
-- IPv4 Header --
Version: 4
InternetHeaderLength: 5
TypeOfService: 0
TotalLength: 88
Identification: 63
VersionControlFlags: 2
FragmentOffset: 1684
TimetoLive: 128
Protocol: 6
HeaderChecksum: 0
SrcAddress: ***
DstAddress: ***
Options: 0
-- DATA --
fdsa&3Vhreqasdウチy|reqSL巷エ&ラyuht∫Ig

感想

簡単なパケットキャプチャもどきを作成したことで,ソケット通信への理解が深まった気がします。
今までTCP/IP階層モデル(OSI基本参照モデル)とか分かってますけど的な雰囲気で過ごしてきましたが,想像以上に理解できていないことが理解できて良かったです。

これからはネットワーク上を流れるデータを何でもかんでもパケットと言うのは控えようと思いました。
(なるべくパケットの意味を意識したいね)


おまけ

mDNS宛でフィルターをかけると...?