Windows Raw Socket Programming ■0x01.) はじめに  「WindowsもついにRawSocketが使えるようになったかぁ」とWindowsの進化 に少し感動したのが、つい最近のようですが、Windows2000/XPはご存知、 RawSocketを微妙にサポートしています。「微妙に」というあいまいな言葉を 使ったのにはわけがあって、サポートしてるわりにはなんかやりたいことが実 現できなかったり、原因不明かつ意味不明な動作で正常に動作しなかったりと、 まだまだ個人的に気に食わないところがあるからです。でもMSは「サポートし てます」といっているので、とりあえず、今回はサポートしているということ にして、このテキストを読んでください(笑)。  動作確認は、VC++.NETとWindowsXPで行っていますが、Windows2000でもOKだ と思います。ただWindows9x系ではWinsock2ではなくRawSocketをサポートして ないらしいので無理かもしれません。コンパイラに関しては、何でもOKです。 多分Borland C++ Compiler 5.5でもOKだと思います。  一応、読者対象としては、ソケットを利用したネットワークプログラミング を行ったことがあることを前提としています。しかし、まぁ大して難しいこと をやっているわけでもないので、気合と根性と少し優れた検索能力があれば、 なんとかなるはずです(^^;。 ■0x02.) 実行!  さて、いきなりですがソースコードとEXEファイルをDLして実行してくださ い。 http://ruffnex.oc.to/kenji/socket/source.zip  EXEファイルも一緒に添付されているので、まぁとりあえず実行してくださ い。「どんなプログラムかもわからんようなEXEファイルなんて実行できませ ん」という方は以下のソース解説を読んだのち実行してください。  実行すると、Windowが表示され、上部の入力欄にIPアドレスが表示されます。 もし表示されない場合は入力してください。そして開始ボタンをクリックする と、プログラムがネットワークパケットを取得し始めます。パケットの取得を 確認するために、適当なホームページをブラウザで閲覧してみてください。ネッ トワーク上でパケットが流れると、ずらずらっと16進数で、流れたパケットが 表示されます。  動作をみてだいたい分かりますが、一応スニッファもどきです。「もどき」 というのは受信パケットしか取得してくれないからです。送信パケットまで取 得したい場合はWinpcapなどを利用するのが良いと思います。  では、ソースの解説へ移ることにしましょう。 ■0x03.) ソースコード解説(ウォーミングアップ篇)  まずはウォーミングアップ程度にちゃっちゃと読んでしまいましょう。 Windowsプログラミングとしては基本的なことで、大したことはやってません。 ----- recv_sniffer.cpp int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { HWND hWnd = FindWindow( _T("#32770"), _T("RecvSniffer")); if(IsWindow(hWnd)) { SendMessage(hWnd, ESM_POKECODEANDLOOKUP, 0, 0); return 0; } DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, Dlg_Proc); return 0; } -----  まず実行されると最初にWinMain関数から処理されます。なので、プログラ ムの二重起動防止処理はこの部分に書くのが適切でしょう。二重起動防止の処 理はいろいろとありますが、私が一番利用するのは、FindWindowや共有データ セグメントです。  二重起動防止に関しては「CreateSemaphore」を利用する方法や 「CreateMutex」を利用する方法や「共有データセグメント」を利用する方法 や「FindWindow」を利用する方法や、もうありとあらゆる方法が存在します。 個人的には、どの方法も似たようなものなので、何を使ってもあまり関係ない と思います。二重起動防止で検索すると、たくさんのサイトがヒットするので、 どのくらい方法があるのか調べるのも面白いかもしれません。  プログラムの内容は「RecvSniffer」というタイトル名のWindowが存在すれ ばFindWindowからハンドルが戻ってきて、IsWindowが「真」を返すので、二重 起動ということで、SendMessageでESM_POKECODEANDLOOKUPメッセージを送って 終了します。もしIsWindowが「偽」を返したら、FindWindowが失敗し、NULLを 返したということなので「RecvSniffer」というタイトル名のWindowは存在し ないということで、DialogBox関数でWindowを生成します。 ----- BOOL CALLBACK Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { static DATA data; switch (uMsg) { // Window作成時の処理、初期化などを担当 case WM_INITDIALOG: Init_Dlg(hWnd, data); break; -----  Windowが生成されると、まずWM_INITDIALOGメッセージがきて、それ以下が 実行されますので、Init_Dlg関数が実行されることになります。ここで定義さ れているdata構造体は、ソースの上部で以下のように定義されています。 ----- typedef struct{ HWND hWnd; // Windowハンドル HANDLE LoggingThread; // ログをとるスレッドのハンドル BOOL ThreadFlag; // スレッドの起動終了を管理するフラグ TCHAR IPAdress[32]; // 自分のIPアドレス SOCKET sock; // ソケット }DATA, *PDATA; -----  この構造体は、プログラム全体に渡って利用されるデータの集まりです。 hWndにはWindowハンドルを、LoggingThreadにはパケットを取得するスレッド (後に生成する)のハンドルを、ThreadFlagにはLoggingThreadハンドルのス レッドが生きているか死んでいるかを表す値を、IPAdressには自PCのIPアドレ スを、sockにはパケット取得を受け持っているソケットハンドルをそれぞれ設 定します。とりあえずは、Init_Dlg関数でこの構造体を初期化します。 ----- BOOL Init_Dlg(HWND hWnd, DATA &data) { data.hWnd = hWnd; data.ThreadFlag = FALSE; TCHAR buf[1024]; ZeroMemory(buf, sizeof(buf)); if(GetIPaddress(data.hWnd, buf, sizeof(buf))) { return TRUE; } -----  Init_Dlg関数の途中までです。data構造体は参照渡しでこの関数に渡されて いますので、そのままこの関数内で利用することができます。data.hWndには Windowのハンドルを設定、data.ThreadFlagはあとでパケット取得スレッドを 起動させるためのフラグです。とりあえずまだスレッドは起動させていないの で、FALSEとしておきます。GetIPaddress関数は、その名の通り実行されたPC のIPアドレスを取得する関数です。  以下、一度Init_Dlgの解説を中断してGetIPaddressの解説へ進みます。 ----- BOOL GetIPaddress(HWND hWnd, PTCHAR buf, DWORD size) { WSADATA wsaData; if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { (エラー処理「省略」) } -----  まずは、WSAStartupにてソケットを初期化しています。WinSockのバージョ ンは現時点で(多分)最新版である2.2を使います。ソースコード内ではエラー 処理もちゃんと書いていますが、ここでは、説明のためにエラー処理のソース は省略します。 ----- SOCKET sock; if((sock = WSASocket(AF_INET, SOCK_RAW, IPPROTO_IP, NULL, 0, WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET) { (エラー処理「省略」) } -----  ソケット生成です。ポイントは、まずWSASocket関数を利用するということ、 そして引数に、SOCK_RAWやIPPROTO_IPといった値を設定することです。この辺 がRawSocketっぽくて良い感じです。WSASocket関数に関しては、 「http://msdn.microsoft.com/library/default.asp?url=/library/en-us/win sock/winsock/wsasocket_2.asp」を参照してください。RawSocket用のソケッ トを戻り値として返してくれるようです。 ----- DWORD dWord; if(WSAIoctl(sock, SIO_ADDRESS_LIST_QUERY, NULL, 0, buf, size - 1, &dWord, NULL, NULL) == SOCKET_ERROR) { (エラー処理「省略」) } -----  WSAIoctl関数についてはMSDNの「http://msdn.microsoft.com/library/defaul t.asp?url=/library/en-us/winsock/winsock/wsaioctl_2.asp」を参照してく ださい。第二引数に「SIO_ADDRESS_LIST_QUERY」を指定すると、bufにIPアド レスのリストを格納してくれます。MSDNを見ると他にもいろいろと使用方法が あります。なかなか面白い使い方ができそうです。 ----- EndConnect(sock); return FALSE; } -----  最後にEndConnectという分かりやすい関数を呼び出していますが、これは自 前の関数で、ソケットを閉じる関数です。ソースの下の方に定義されています。 ----- void EndConnect(SOCKET sock) { shutdown(sock, SD_BOTH); closesocket(sock); WSACleanup(); } -----  EndConnectを呼び出すと、shutdown、closesocket、WSACleanupというお決 まりのパターンでソケットが閉じられます。  さて、GetIPaddress関数が終了したこの瞬間は、つまりWSAIoctl関数が GetIPaddress関数に渡した引数bufの領域にIPアドレスのリストを格納してく れてる状態です。正確には、まだIPアドレスのリストなので、IPアドレスを取 得しているわけじゃないのですが、まぁ同じことです。この状態で処理が Init_Dlgへ戻ります。 ----- SOCKET_ADDRESS_LIST *slist = (SOCKET_ADDRESS_LIST *)buf; if((slist->iAddressCount) > 0) { TCHAR text[16]; wsprintf(text, _T("%s"), inet_ntoa(((SOCKADDR_IN *) slist->Address[0].lpSockaddr)->sin_addr)); Edit_SetText(GetDlgItem(hWnd, IDC_EDIT2), text); } -----  さて、再びInit_Dlg関数へ戻ってきました。GetIPaddressを呼び出したあと の処理を続けます。とりあえずGetIPaddressへ渡したbuf配列には、IPアドレ スのリストが入っていますので、それをSOCKET_ADDRESS_LIST型のポインタ slistとして新しく作成します。SOCKET_ADDRESS_LIST型はMSDNで以下のように 定義されています。 ----- typedef struct _SOCKET_ADDRESS_LIST { INT iAddressCount; SOCKET_ADDRESS Address[1]; } SOCKET_ADDRESS_LIST, FAR * LPSOCKET_ADDRESS_LIST; -----  なんかよくわからん構造体ですが、Address[1];と定義されてるってことは、 一個しかIPアドレスを保存できないわけで、「全然アドレス『リスト』になっ てへんやん」と突っ込めます。ちなみにMSは結構突っ込みどころ満載な企業で す。もしかするとそこが人気の秘密かもしれません。しかも、どうせ1個しか 保存できんのにiAddressCountとかいうカウンタまでついてます。「これ意味 ないじゃん!」。でも多分これは今後、MSがWinSockのバージョンアップを重 ね、この構造体もさらに改良を重ねていくのだろうということだと思います。 多分(^^;  ということで、slist->iAddressCountは0より大きくなり(1となり) Edit_SetTextによって、見事、取得したIPアドレスがWindow上部のエディット ボックスに表示されることとなります。これでInit_Dlg関数は終了です。  では、次はWM_COMMANDの部分を見ていくことにします。 ----- case WM_COMMAND: switch (LOWORD(wParam)) { case IDOK: StartandStop_Logging(data); break; case IDCANCEL: EndProgram(data); break; } -----  今度はWM_COMMANDメッセージの部分、つまりは何かしらのユーザーからのメッ セージを受け取ったときに実行する処理についてです。まぁまずはWindowの仕 組みとして、IDCANCELメッセージが来たときにプログラムを終了させなければ なりませんので、そのための関数を自前で作っています。EndProgram関数です。 ----- void EndProgram(DATA &data) { if(data.ThreadFlag) { data.ThreadFlag = FALSE; EndConnect(data.sock); } EndDialog(data.hWnd, NULL); } -----  一応data構造体を参照渡ししており、パケットログ取得のスレッドが生きて いたらその接続を終了させて、プログラム自体を終了させています。 ■0x04.) ソースコード解説(メイン処理篇)  いよいよ楽しみなところに入ってきました。StartandStop_Logging関数です。 この関数は「開始」ボタン(「終了」ボタンとも言える)をクリックしたとき の処理を受け持ちます。まさに本プログラムのメイン部分です。ちょっと前に でてきたdata構造体を確認しながら読んでいきましょう。 ----- BOOL StartandStop_Logging(DATA &data) { if(data.ThreadFlag == TRUE) { data.ThreadFlag = FALSE; EndConnect(data.sock); return FALSE; } -----  まず、data.ThreadFlagはパケットログをとるスレッドの起動終了を管理す るフラグです。最初にボタンが押されたときはFALSEです。もしTRUEならば、 スレッドが「起動中」ということなので、そのスレッドを停止させます。逆に FALSEならば、スレッドが「停止中」ということなので、スレッドを開始させ ます。これは、ボタンのキャプションが「開始」であるか「終了」であるかに 起因しています。つまり、ボタンのキャプションが「開始」だったならば、 FALSEとなっており、「終了」だったならTRUEとなっているわけです。よって 「終了」ボタンが押されたら、if文以下のプログラムが実行され、スレッドを 停止させることになります。 ----- ZeroMemory(data.IPAdress, sizeof(data.IPAdress)); Edit_GetText(GetDlgItem(data.hWnd, IDC_EDIT2), data.IPAdress, sizeof(data.IPAdress) - 1); if((data.sock = MakingSocket(data.hWnd, data.IPAdress, 0)) == -1)   (エラー処理) -----  さて、次に進むと、いきなりZeroMemory関数でdata.IPAdressはクリアされ ます。これは自らのIPアドレスを格納するフィールドです。そしてそのフィー ルドにIDC_EDIT2(Windowの上部のエディットボックス)の値が格納されます。 つまりIPアドレスが格納されるわけです。IDC_EDIT2には、プログラム起動直 後に実行されるInit_Dlg関数内で、IPアドレスが設定されていました。それを 今度はEdit_GetTextを使って取得してくるわけです。  取得したらそのIPアドレスを引数としてMakingSocketという自前の関数を呼 び出します。この関数は、接続に関する決まりごとをやりつつ、bind関数を呼 び出しポートを見張るように設定します。MakingSocketの引数には、Windowハ ンドル、IPアドレス、見張るべきポートを指定します。そして戻り値にそのソ ケットを返します。一応、MakingSocket関数の戻り値がパケット取得のための ソケットとなります。 ----- SOCKET MakingSocket(HWND hWnd, PTCHAR IPorHost, USHORT Port) {   ....(ネットワークプログラムの決まりごとなので省略).... DWORD dWord; UINT optval=1; if(WSAIoctl(sock, SIO_RCVALL, &optval, sizeof(optval), NULL, 0, &dWord, NULL, NULL) == SOCKET_ERROR) (エラー処理) return sock; } ----- SIO_RCVALLについて、MSDN(http://msdn.microsoft.com/library/default.asp? url=/library/en-us/winsock/winsock/wsaioctl_2.asp)では以下のような説 明になっています。 ----- MSDNから引用 SIO_RCVALL  Enables a socket to receive all IP packets on the network. The socket handle passed to the WSAIoctl function must be of AF_INET address family, SOCK_RAW socket type, and IPPROTO_IP protocol. -----  「ソケットがネットワーク上のIPパケットをすべて受け取ることを可能にし ます。WSAIoctl関数には、ソケットハンドル、AF_INET、SOCK_RAW、 IPPROTO_IPを指定しなければなりません。」とあります。そしてさらに、 -----  Once the socket is bound and the ioctl set, calls to the WSARecv or recv functions return IP datagrams passing through the given interface. Note that you must supply a sufficiently large buffer. -----  「一度ソケットをioctlでセットしたならば、WSARecvやrecv関数を呼び出し て、ネットワークインターフェースを通るIPパケットを取得することができま す。その場合、十分に大きいバッファを供給しなければならないことに注意し てください。」と続きます。  ではStartandStop_Logging関数へ戻ります。 ----- data.ThreadFlag = TRUE; if((data.LoggingThread = CreateThread(NULL, 0, LogThread, (LPVOID)&data, 0, NULL)) == NULL) {   (エラー処理) } -----  いよいよここでスレッドを生成します。ロギングを行うソケットが生成済み なので、あとはスレッド起動してそのスレッドにソケットを監視させます。 data.ThreadFlagをTRUEにし、data.LoggingThreadのハンドルを設定します。 スレッドとして利用する関数は、LogThread関数です。 ----- CloseHandle(data.LoggingThread); return FALSE; } -----  スレッドを生成したら、あとは「終了」ボタンが押されるまでユーザーから のレスポンスを待つだけです。この時点で、data構造体のすべてのデータが埋 まりました。 ----- HWND hWnd; // Windowハンドル HANDLE LoggingThread; // ログをとるスレッドのハンドル BOOL ThreadFlag; // スレッドの起動終了を管理するフラグ TCHAR IPAdress[32]; // 自分のIPアドレス SOCKET sock; // ソケット -----  現時点でこのプログラムは2つのスレッドによって管理されています。ひと つはソケットからパケットのログを監視するスレッド。もうひとつはユーザー からのメッセージ(例えば「終了」ボタンが押されるとか)を監視するスレッ ドです。しかし、2つのスレッドがあっても、それぞれは同じdata構造体の値 を参照し、それに従って処理を続けることになります。  では、パケットロギングを行うスレッドを追ってみることにします。 ----- DWORD WINAPI LogThread(LPVOID pData) { DATA *data = (DATA *)pData; Button_SetText(GetDlgItem(data->hWnd, IDOK), _T("終了")); Edit_Enable(GetDlgItem(data->hWnd, IDC_EDIT2), FALSE); PUCHAR recvData = new UCHAR[MAX_RECV_SIZE]; PTCHAR PrintData = new TCHAR[MAX_PRINT_SIZE]; -----  パケットロギングを行う関数LogThreadは、まず最初にdata構造体へのポイ ンタを取得します。その後、ボタンのテキストを「終了」へ変更し、 IDC_EDIT2への入力を拒否します。さらに、受信するデータを格納するメモリ 領域を確保します。recvDataは受信するデータを格納するメモリ領域で、 PrintDataはテキストとして出力するデータを格納するメモリ領域です。 ----- while(data->ThreadFlag) { Sleep(10); WSABUF wsb; wsb.buf = (PTCHAR)recvData; wsb.len = MAX_RECV_SIZE; ZeroMemory(wsb.buf, wsb.len); DWORD Len, Flags = 0; if(WSARecv(data->sock, &wsb, 1, &Len, &Flags, NULL, NULL) == SOCKET_ERROR) { break; } -----  いよいよログ監視のループに入ります。ループの脱出条件はdata構造体の ThreadFlagフラグのFALSE、もしくはエラー時のみです。この値をFALSEにする のは、基本的にユーザー側を監視しているスレッドです。従ってこちら側のス レッドは、ユーザーから終了のメッセージを受け取るまで延々とパケットを監 視し続けます。  実際パケットデータを監視するためには、WSABUF型の構造体を定義し、格納 メモリのポインタとその長さをセットし、WSARecvへ渡さなければなりません。 すると結果的にwsb.bufへ、つまりrecvDataへ監視したデータを格納されるこ とになります。 ----- MyPrintBuffer(recvData, Len, PrintData, MAX_PRINT_SIZE); HWND hEdit = GetDlgItem(data->hWnd, IDC_EDIT1); DWORD Edit_Len = Edit_GetTextLength(hEdit) * sizeof(TCHAR); PTCHAR EditData = new TCHAR[Edit_Len + MAX_PRINT_SIZE]; Edit_GetText(hEdit, EditData, Edit_Len + 1); lstrcat(EditData, PrintData); Edit_SetText(hEdit, EditData); Edit_Scroll(hEdit, Edit_GetLineCount(hEdit), 0); delete[]EditData; } -----  この部分は主に取得したパケットデータの整形と出力にあります。 MyPrintBuffer関数は、受信したデータを出力用のデータへ変換しPrintDataへ 格納します。PrintDataには出力用のデータは入っているわけですが、それを 出力する前に、すでにIDC_EDIT1エディットボックスには、前回出力したデー タが表示されている可能性がありますので、それを取得しなければなりません。 前回までの取得データをEditDataへ格納し、lstrcatでその後にPrintDataを追 加します。そしてEditDataを再び表示することによって、エディットボックス にデータが追加されたカタチとなります。最後にEdit_Scrollで、エディット ボックスを最下行までスクロールさせて、表示(データの更新)完了となりま す。whileループなので再びパケットログの取得へと戻ります。 ----- // whileを抜ける理由は2つ // 1. data->ThreadFlagがFALSEになったか? // 2. WSARecvが失敗したか? // よって2.となった場合は、フラグをここでFALSEにする if(data->ThreadFlag != FALSE) { data->ThreadFlag = FALSE; } -----  さて、この位置に処理がきているということは、whileループを抜けてきた ことになります。その理由を検査しています。まず、「ThreadFlagがFALSEに なった」ならば、当然ThreadFlagはFALSEです。いやホント当たり前ですけど ね。しかし、WSARecvが失敗したことによってwhileループを抜けていたら、 「ThreadFlagがTRUEのまま」whileを抜けたことになります。よってここで、 ThreadFlagをFALSEにしているわけです。 ----- delete[]recvData; delete[]PrintData; // ボタンのテキストを「開始」に変更、その他。 Button_SetText(GetDlgItem(data->hWnd, IDOK), _T("開始")); Edit_Enable(GetDlgItem(data->hWnd, IDC_EDIT2), TRUE); return 0; } -----  最後に後始末をしてスレッド終了となります。さて、では、最後にこの LogThreadで利用していたMyPrintBufferを見ていくことにします。 ----- BOOL MyPrintBuffer(PUCHAR buf, DWORD Len, PTCHAR PrintData, DWORD size) { ZeroMemory(PrintData, size); -----  ポイントは、bufに受信したパケットデータが入っていて、それをテキスト として出力できるデータへ変換しPrintDataへ格納することです。 ----- // 一行に16バイト分表示させる DWORD x = (Len / 16) + 1; DWORD y = (Len % 16); -----  まずは、受信したデータを16で割った商と余りを、それぞれxとyへ代入しま す。例えば、230バイト受信したとしてbufに230バイト分のデータが入ってい たとします。Lenは当然230という値です。230/16は商14、余り6、です。しか し16列×14行用意したのでは足りません。余り6の分だけ余ってしまいます。 なので、16列×15行を用意しなければならなくなります。つまり16個を1列と 考えて、商+1行分確保しなくてはならなくなります。そのための+1です。 ----- DWORD i, j; for(i=0; i < x; i++) { // 最後の一行は16バイトに満たないかもしれないので // 満たない場合は、余りを代入 DWORD max = ((i == (x - 1)) ? y : 16); // パケットデータを文字列(数値)に変換 for(j=0; j < max; j++) { TCHAR strNum[4]; wsprintf(strNum, "%02x ", buf[(i*16)+j]); lstrcat(PrintData, strNum); } -----  xには商+1の値が入っています。もし「i == (x - 1)」であるならば、それ はつまり、最後の一行であることを意味しているので、maxに余りyを代入する ことになります。それ以外ならば16を代入します。そしてパケットデータを文 字列へ変換していきます。 ----- // 16バイトに満たなかった場合に、空白で埋める if(max != 16 && max != 0) { TCHAR space[16 * 3]; memset(space, ' ', sizeof(space)); space[((16 - max) * 3)] = '\0'; lstrcat(PrintData, space); } -----  加算した一行によって、余りまですべてを収めることはできましたが、今度 は、最後の一行が16個ではなく、余り6個分しかないので、「16 - 6 = 10」の 残り10個を空白で埋めなければなりません。その処理を行っています。ちなみ に変数maxが0がもしくは16であった場合は、その限りではありません。 ----- TCHAR strStr[20]; ZeroMemory(strStr, sizeof(strStr)); // 出力可能な文字ならばそのまま出力し、不可能ならば'.'を出力 for(j=0; j < max; j++) { strStr[j] = (isprint(buf[(i*16)+j]) ? buf[(i*16)+j] : '.'); } strStr[j] = '\r', strStr[j+1] = '\n'; lstrcat(PrintData, strStr); } -----  16バイト分のデータ1行の最後に、文字として表示できるものは表示させる ようにします。 ----- // 最後に改行を追加 lstrcat(PrintData, _T("\r\n")); return FALSE; } -----  最後に改行をいれて、無事終了です。 ■0x04.) さいごに  さて、いかがだったでしょうか。実は、このツールはTCP/IPの教科書の CD-ROMに入っている受信パケットスニッファです。本当は、このツールのソー スコード的な解説も本書の中で行いたかったのですが、いろいろと問題があり できなかったので、WizardBibleに投稿するというカタチになりました。楽し んでいただけたなら幸いです。  では、また会う日まで... ■0x05.) 参考サイト ・「サッポロワークス」(http://homepage2.nifty.com/spw/tips/PacketDump.html)