はじめに
本稿では、WinSockでパケットモニターを作成し、WindowsでRAWソケットを扱う方法を紹介します。また、GUIプログラムを作成する場合に必須となる「非ブロッキングモード」や、すべてのパケットを取得するための「プロミスキャスモード」についても触れます。
取得したパケットをプロトコルにしたがって表示する処理については、WinPcapを使用したパケットモニターの作成で解説しましたので、詳しくはそちらを参考にしてください。
対象読者
- WindowsでC++を使用してネットワーク関連のプログラムを作成される方。
- パケットモニターの作成に興味をお持ちの方。
必要な環境
- サンプルプログラムは、Windows 2000以降のOSで動作します。
- サンプルコードは、C++ Builder 6およびVisual C++ .NET 2003でコンパイルが可能です。
WinSockのRAWソケットについて
UNIXを使用する場合、ソケットプログラムで自由にRAWソケットを扱うことができます。しかし、Windows上のWinSockでRAWソケットが本格的に使用できるようになったのは、Windows2000以降です。それまでのWindowsプラットフォームでは、まったく使用不可能であったり、一部のプロトコルだけが使用可能であるという状況が続いていました。本稿では、Windows2000以降のプラットフォームをターゲットにすることでRAWソケットを扱っています。
WinSockでソケット作成時に通信方式をSOCK_RAW
にした時、指定可能なプロトコルの一覧は、次のとおりです。
プロトコル | Winsock 1.1 | Winsock2(Windows2000以前) | Winsock2(Windows2000以降) |
IPPROTO_IP | × | × | ○ |
IPPROTO_ICMP | × | ○ | ○ |
IPPROTO_IGMP | × | ○ | ○ |
IPPROTO_UDP | × | ○ | ○ |
IPPROTO_TCP | × | × | × |
IPPROTO_RAW | × | × | × |
※ SOCK_RAW
を指定した場合は、IPPROTO_TCP
は正常に動作できません。TCPを扱う場合は、IPPROTO_IP
を指定して、TCPヘッダを自前で用意する必要があります。
DLL名 | ヘッダファイル | ライブラリ | 備考 |
WINSOCK.DLL | WINSOCK.H | WINSOCK.LIB | 16ビットのWinsock1.1 |
WSOCK32.DLL | WINSOCK.H | WSOCK32.LIB | 32ビットのWinsock1.1 |
WS2_32.DLL | WINSOCK2.H | WS2_32.LIB | 32ビットのWinsock2 |
パケットの取得
WinSockでパケットを取得するには、次のステップを踏みます。
それでは、各ステップについて順番に見ていきましょう。
ステップ1 WinSockの初期化
初期化は、WinSockを使用する時に必要な処理です。具体的には、利用するバージョンおよびWSADATA
構造体へのポインタを用意して、WSAStartup
を実行し「Ws_32.dll」を初期化しています。今回のサンプルでは、バージョン2.0の機能を使用しますので第1パラメータにMAKEWORD(2,0)
を指定しています。WSAStartup
が成功すると、WSADATA
構造体にバージョン情報などの初期化情報が返されます。
WSADATA wsd; if(0!=WSAStartup(MAKEWORD(2,0),&wsd)) return FALSE; //エラー return TRUE;
サンプルプログラムの中では、「Capture.cpp」のStartup
でWinSockの初期化を行っています。
ステップ2 アダプタ一覧の取得
コンピュータには、複数のネットワークが設定されている場合があります。NICが2つ以上設定されている場合や、LAN接続されているコンピュータでダイアルアップ接続した場合などです。パケットのモニターは、1つのインターフェースを指定してパケットを採取するため、複数のネットワークがある場合は、これを選択する必要があります。サンプルプログラムでは、複数のネットワークが存在した場合、それを選択させるダイアログを表示しています。
WinSockでモニターを開始するためには、ソケットのbind
時にモニター対象のIPアドレスを与える必要があるため、利用可能なネットワークアダプタを一覧し、それぞれのIPアドレスを取得します。IPアドレスを取得するには、IP Helperを使用するなどのさまざまな方法がありますが、今回はWinSock2のWSAIoctl
を使用する方法を紹介します。
char Buffer[1024]; DWORD d=0; if(0 == WSAIoctl(sock,SIO_ADDRESS_LIST_QUERY,NULL, 0,Buffer,1024,&d,NULL,NULL)){ // SOCKET_ADDRESS_LISTへのキャスト SOCKET_ADDRESS_LIST *slist = (SOCKET_ADDRESS_LIST *)Buffer; // iAddressCount個のデータが取得されている for(int i=0;i<slist->iAddressCount && (*max)<MAX_ADAPTER;i++){ sockaddr *so = (sockaddr*)(slist->Address[i].lpSockaddr); //アドレスファミリがTCP/IPであるかどうかの確認 if(so->sa_family==AF_INET){ // sockaddr_inへのキャスト sockaddr_in *s = (sockaddr_in*)(slist->Address[i].lpSockaddr); // この時点で、IPアドレスが // s->sin_addr.s_addr に取得できている } } }
WSAIoctl
に制御コードとしてSIO_ADDRESS_LIST_QUERY
(0x48000022)を送ることで現在利用可能なすべてのアドレスの一覧(AF_INET
を指定した場合、ループバックアドレスは除かれます)を取得することができます。
WSAIoctl
は、ソケットに関連する操作のパラメータをセットしたり検索したりするために利用でき、そのプロトタイプは次のようになっています。
int WSAIoctl( SOCKET s, // ソケット DWORD dwIoControlCode, // 制御コード LPVOID lpvInBuffer, // 情報を送るバッファ DWORD cbInBuffer, // 上記バッファのサイズ LPVOID lpvOutBuffer, // 情報を受け取るバッファ DWORD cbOutBuffer, // 上記バッファのサイズ LPDWORD lpcbBytesReturned, // 受け取ったデータのサイズ LPWSAOVERLAPPED lpOverlapped, //WSAOVERLAPPED構造体へのポインタ // 実行完了時に呼ばれる関数 LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
また、制御コードに使用したSIO_ADDRESS_LIST_QUERY
は、「Winsock2.h」の中で次のように定義されており、結果的に「0x48000022」となっています。
#define IOC_WS2 0x08000000 #define IOC_OUT 0x40000000 #define _WSAIOR(x,y) (IOC_OUT|(x)|(y)) #define SIO_ADDRESS_LIST_QUERY _WSAIOR(IOC_WS2,22)
取得できる一覧は、SOCKET_ADDRESS_LIST
構造体へのポインタですが、このSOCKET_ADDRESS_LIST
構造体のメンバであるiAddressCount
には、取得したアドレス情報の数が入っており、Address
は、SOCKET_ADDRESS
構造体の配列になっています。そして、SOCKET_ADDRESS
構造体のメンバであるlpSockaddr
は、sockaddr
へのポインタです。sockaddr
は、各種プロトコルのアドレスを表現する包括的な構造体ですが、IPv4では、sockaddr_in
型でデータが格納されます。
サンプルでは、いったんsockaddr
にキャストして、アドレスファミリ(sa_family
)がAF_INET
であることを確認できた場合、改めてsockaddr_in
にキャストしなおしています。sockaddr_in
のsin_addr
には、目的の4バイトのIPアドレスが格納されています。
下図でこの関係を示しましたのでsockaddr
構造体とsockaddr_in
構造体の関係などの参考にしてください。
typedef struct _SOCKET_ADDRESS_LIST { INT iAddressCount; // 取得したアドレスの数(4) SOCKET_ADDRESS Address[1]; // アドレスを表す構造体(8) } SOCKET_ADDRESS_LIST, FAR * LPSOCKET_ADDRESS_LIST; typedef struct _SOCKET_ADDRESS { LPSOCKADDR lpSockaddr; // sockaddr へのポインタ(4) INT iSockaddrLength; // sockaddr のサイズ(4) 常に16になります。 } SOCKET_ADDRESS, *PSOCKET_ADDRESS; struct sockaddr { u_short sa_family; // プロトコルファミリ(2) char sa_data[14]; // アドレスデータ }; struct sockaddr_in { short sin_family; // プロトコルファミリ(2) unsigned short sin_port; // ポート番号(2) struct in_addr sin_addr; // IPアドレス(4) char sin_zero[8]; // 未使用(8) };
サンプルプログラムの中では、「Capture.cpp」のEnumAdapter
でアダプタ一覧の処理を実装しています。詳しくはそちらを参照してください。
WSAIoctl
にSIO_ADDRESS_LIST_QUERY
を与えてアダプタ一覧の情報を取得しています。デバイスへコマンドを送るのであればioctlsocket
が利用できそうですが、これを使用した場合どうしてもWSAEINVAL
のエラーが発生するようです。WSAIoctl
は、WinSock2に含まれる「WS2_32.DLL」がエクスポートするファンクションですが、C++ Builderで使用する場合、「Ws2_32.lib」のリンクが必要になります。ステップ3 ソケットの作成
ステップ3では、いよいよパケットをモニターするためのソケットを作成しますが、ソケットの初期化という意味で、このステップでは「プロミスキャスモード」への移行と「非ブロッキングモード」への移行を併せて行うことにします。
ソケット作成
ソケット識別子を取得するためには、通常のソケットプログラムと同様にsocket
を使用しますが、このとき、通信方式にSOCK_RAW
、プロトコルにIPPROTO_IP
を指定します。
//SOCKETの生成 if( INVALID_SOCKET == (sock = socket(AF_INET,SOCK_RAW,IPPROTO_IP))) return FALSE; //エラー //SOCKET のデータセット addr_in.sin_addr.s_addr=ip_address;//アダプタ一覧で取得したIPアドレス addr_in.sin_family = AF_INET; //IPv4 addr_in.sin_port = htons(0); //0で初期化 //bind if(bind(sock,(SOCKADDR *)&addr_in,sizeof(addr_in))==SOCKET_ERROR) return FALSE; // エラー
「Winsock2.h」には、ソケットの通信方式として次の5種類が定義されてますが、アドレスファミリーにAF_INET(IPv4)を指定した場合は、SOCK_STREAM
(TCP)、SOCK_DGRAM
(UDP)、SOCK_RAW
(RAW)の3つだけが使用可能です。
#define SOCK_STREAM 1 /* stream socket */ #define SOCK_DGRAM 2 /* datagram socket */ #define SOCK_RAW 3 /* raw-protocol interface */ #define SOCK_RDM 4 /* reliably-delivered message */ #define SOCK_SEQPACKET 5 /* sequenced packet stream */
SOCK_RDM
はWSAESOCKTNOSUPPORT
(10044)、SOCK_SEQPACKET
はWSAEAFNOSUPPORT
(10047)のエラーが出て使用できません また、ソケットプロトコルには下記の各種プロトコルが定義されていますが、パケットモニターのようにすべてのプロトコルを取得したい場合は、IPPROTO_IP
を指定することになります。なお、ヘッダの中にはIPPROTO_RAW
というものもありますが、これを指定した場合、エラーは発生しませんがパケット一切は取得できません。これはWinSock2で未対応なためだと思われます。
#define IPPROTO_IP 0 /* dummy for IP */ #define IPPROTO_ICMP 1 /* control message protocol */ #define IPPROTO_IGMP 2 /* internet group management protocol */ #define IPPROTO_GGP 3 /* gateway^2 (deprecated) */ #define IPPROTO_TCP 6 /* tcp */ #define IPPROTO_PUP 12 /* pup */ #define IPPROTO_UDP 17 /* user datagram protocol */ #define IPPROTO_IDP 22 /* xns idp */ #define IPPROTO_IPV6 41 /* IPv6 */ #define IPPROTO_ND 77 /* UNOFFICIAL net disk proto */ #define IPPROTO_ICLFXBM 78 #define IPPROTO_RAW 255 /* raw IP packet */ #define IPPROTO_MAX 256
非ブロッキングモード
デフォルトでは、データ受信のためのrecv
を呼び出すと、何らかのデータが届くまで制御が戻りません(ブロッキング)。これではGUIのプログラムとして非常に使いにくいものとなります。そこで、非ブロッキングモードに設定することにします。
WinSockでは、ioctlsocket
にFIONBIO
を渡すことにより、ブロッキング/非ブロッキングの設定を変更することができます。
// 0:ブロッキングモード(デフォルト) 1:非ブロッキングモード unsigned long arg = 1L; if(0!=ioctlsocket(sock,FIONBIO,&arg)) return FALSE; // エラー
プロミスキャスモード
パケットモニターのように、他の端末あて(自分あてではない)パケットを全部取得するには、ネットワークカードを「プロミスキャス」という特別なモードに設定する必要があります。Winsockで「プロミスキャスモード」を設定する方法は、次のようになります。
optval=1; //PROMISC if(0!=ioctlsocket(sock,SIO_RCVALL,&optval)) return FALSE; // エラー
ioctlsocket
に制御コードとしてSIO_RCVALL
を送っていますが、これは「MSTcpIp.h」および「WinSock2.h」の中で次のように定義されており、結果的に「0x98000001」となります。
#define IOC_VENDOR 0x18000000 #define IOC_IN 0x80000000 /* copy in parameters */ #define _WSAIOW(x,y) (IOC_IN|(x)|(y)) #define SIO_RCVALL _WSAIOW(IOC_VENDOR,1)
しかし、ここで注意が必要なのはSIO_RCVALL
の定義が入っている「MSTcpIp.h」は、Visual C++ .NET 2003やC++ Builder 6では提供されていないことです。「MSTcpIp.h」はWinSockの拡張用のヘッダファイルであり、Platform SDKをインストールすると含まれています。サンプルプログラムでは、必要な定義を#define
することで、「MSTcpIp.h」がない環境でもコンパイルできるようにしています。
なお、「MSTcpIp.h」は、非常に短いヘッダファイルですので、参考のためここで紹介しておきます。
// Copyright (c) Microsoft Corporation. All rights reserved. #if _MSC_VER > 1000 #pragma once #endif /* Argument structure for SIO_KEEPALIVE_VALS */ struct tcp_keepalive { u_long onoff; u_long keepalivetime; u_long keepaliveinterval; }; // New WSAIoctl Options #define SIO_RCVALL _WSAIOW(IOC_VENDOR,1) #define SIO_RCVALL_MCAST _WSAIOW(IOC_VENDOR,2) #define SIO_RCVALL_IGMPMCAST _WSAIOW(IOC_VENDOR,3) #define SIO_KEEPALIVE_VALS _WSAIOW(IOC_VENDOR,4) #define SIO_ABSORB_RTRALERT _WSAIOW(IOC_VENDOR,5) #define SIO_UCAST_IF _WSAIOW(IOC_VENDOR,6) #define SIO_LIMIT_BROADCASTS _WSAIOW(IOC_VENDOR,7) #define SIO_INDEX_BIND _WSAIOW(IOC_VENDOR,8) #define SIO_INDEX_MCASTIF _WSAIOW(IOC_VENDOR,9) #define SIO_INDEX_ADD_MCAST _WSAIOW(IOC_VENDOR,10) #define SIO_INDEX_DEL_MCAST _WSAIOW(IOC_VENDOR,11) // Values for use with SIO_RCVALL* options #define RCVALL_OFF 0 #define RCVALL_ON 1 #define RCVALL_SOCKETLEVELONLY 2
http://www.microsoft.com/downloads/details.aspx?familyid=EBA0128F-A770-45F1-86F3-7AB010B398A3&displaylang=en
SIO_RCVALL
に関して次のようにアナウンスされています。SIO_RCVALL
で、ソケットにネットワーク上の全てのIPパケットを受け取らせることができます。WSAIoctl
に渡されるソケット・ハンドルは、AF_INET
アドレスファミリ、SOCK_RAW
ソケット・タイプ、IPPROTO_IP
プロトコルでなければなりません。- ソケットは明確なローカル・インタフェースが必要です。したがって
INADDR_ANY
は使用できません。 - 一度ソケットを
ioctl
でセットしたならば、WSARecv
やrecv
関数を呼び出して、ネットワークインターフェースを通過するIPパケットを取得することができます。 - 十分に大きいバッファを用意することに注意してください。
ioctl
にSIO_RCVALL
をセットするには、ローカル・コンピュータ上でAdministrator権限が必要です。SIO_RCVALL
は、Windows 2000以降のバージョンで利用できます。
サンプルプログラムの中では、「Capture.cpp」のStartでこれらの初期化処理を実装しています。詳しくはそちらを参照してください。
ステップ4 パケットの取得
パケットの受信は、recv
で行うことができます。非ブロッキングモードでrecv
を呼ぶと、受信データがなくてもタイムアウトで制御が戻ります。しかし、この場合、戻り値はSOCKET_ERROR
になります。そこで、SOCKET_ERROR
のエラーが返されたときはWSAGetLastError
でエラーの内容を確認し、エラーの種類がWSAEWOULDBLOC
であった場合は、タイムアウトしたということで受信バイト数0で処理を継続します。
// RecvSize 受信バイト数 // Buffer 受信バッファ // MAX_RECV_SIZE 受信バッファのサイズ if(SOCKET_ERROR==(RecvSize = recv(sock,(char *)Buffer,MAX_RECV_SIZE,0))){ if(WSAEWOULDBLOCK==WSAGetLastError()){ RecvSize=0; return TRUE; // 0バイト受信(成功) }else{ return FALSE; // エラー } } return TRUE; // RecvSizeバイトのデータを受信(成功)
サンプルプログラムの中では、「Capture.cpp」のRecv
でこの処理を実装しています。詳しくはそちらを参照してください。
ステップ5 ソケットの終了処理とWinSockの終了処理
使用したソケット識別子は、closesocket
で閉じます。また、WinSockの終了処理には、WSACleanup
を使用します。
サンプルプログラムの中では、「Capture.cpp」のStop
およびCleanup
でこの処理を実装しています。詳しくはそちらを参照してください。
その他の情報
管理者以外のユーザがRAWソケットを使用する方法
デフォルトでは、RAWソケットにアクセスできるのは管理者だけですが、下記のレジストリ値をTRUE
に設定すると、管理者以外のユーザがRAWソケットにアクセスできるようになります。
- AllowUserRawAccess
- キー: 「Tcpip\Parameters」
- 値の種類:
REG_DWORD
-ブール値 - 有効範囲:
0
または1
(FALSE
またはTRUE
) - 既定値:
0
(FALSE
)
参照:Microsoft Windows 2000 TCP/IP 実装詳細
送信パケットが取得できない
RAWソケットを使用したプログラムは、Window XPなどで自分の送信パケットが取得できない場合があります。この場合は、以下の対策を行ってみてください。
- Internet Connection Firewall(ICF)/Internet Connection Sharing(ICS)のサービスを開始
- Personal Firewallを停止
WinSockを使用したパケットモニターの限界
上記で紹介したとおりWindows 2000以降のWinSock2でも、SOCK_RAW
を指定してプロトコルにIPPROTO_RAW
を指定することはできません。したがって、プログラムで取り扱えるのはIPヘッダまでということになります。そうです、IPパケット以外(ARPパケットなど)の採取はできないのです。また、MACアドレスなどを含んだEthernetヘッダを取得することもできません。
IPヘッダパケット以外のパケットや、Ethernetヘッダも対象にしたい場合は、WinSock を経由せずに、トランスポートデータインターフェース(TDI)かネットワークデバイスインタフェース仕様(NDIS)層をターゲットにする必要があります。
このアプローチで現在最も一般的に利用されているのは、WinPcapドライバでしょう。WinPcapの使用方法に関しては、「WinPcapを使用したパケットモニターの作成」を参照してください。
まとめ
本稿では、WinSock2.0を使用したパケットモニターのサンプルをとおして、WinSockでRAWソケットを扱うプログラムについて書きました。ネットワーク関連のプログラムを作成する際の参考として、わずかながらでもお役に立てれば幸いです。