こんにちは
ネットワークの学習として,パケットキャプチャもどきを作ってみました。
(正しいかは分からない...)
動機
- Wiresharkをセキュリティの関係でインストール出来ない。
- ソケット通信について勉強したい。
- 知ってるふりして「パケットが~」って言うのが恥ずかしくなった(?)
ネットワーク関係に疎い事が気になったので,学習の取っ掛かりとして始めました。
参考
本文より100倍為になると思います。
参考文献
ほぼ参考にさせていただきました。
codezine.jp
www.keicode.com
勉強も含めてWiresharkのソースコードも見てました。
理解はしてないです!!!
www.wireshark.org
github.com
参考図書
鉄板だね。
開発環境
- Windows11 Home 23H2 22631.2715
- Microsoft Visual Studio Community 2022 Version 17.7.0
- Visual C++ 2022 Microsoft Visual C++ 2022
実装にはWinSock2を利用しました。
生ソケット(raw socket)
名前通り生のソケットのこと。
各層のヘッダを分解せずに,生データのまま受信できるようにするソケットだと思ってます。
(自分でヘッダ解析はやってねってコト!?)
learn.microsoft.com
ja.wikipedia.org
プロミスキャスト・モード
自分宛以外のパケットも破棄せずに受信できるモードです。
IPAの某試験やWiresharkでよく目にするよね。
個人的にはパケット盗聴とかで使用されているイメージが強い。
自分宛でない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宛でフィルターをかけると...?