Shoeisha Technology Media

CodeZine(コードジン)

記事種別から探す

WinSock2を使用したパケットモニターの作成

WindowsでRAWソケットを扱う方法

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/07/27 12:00

本稿では、WinSockでパケットモニターを作成し、WindowsでRAWソケットを扱う方法を紹介します。また、GUIプログラムを作成する場合に必須となる「非ブロッキングモード」や、すべてのパケットを取得するための「プロミスキャスモード」についても触れます。

サンプルプログラムの実行例
サンプルプログラムの実行例

はじめに

 本稿では、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にした時、指定可能なプロトコルの一覧は、次のとおりです。

通信方式にSOCK_RAWを使用した場合に利用可能なプロトコル
プロトコルWinsock 1.1Winsock2(Windows2000以前)Winsock2(Windows2000以降)
IPPROTO_IP××
IPPROTO_ICMP×
IPPROTO_IGMP×
IPPROTO_UDP×
IPPROTO_TCP×××
IPPROTO_RAW×××

 ※ SOCK_RAWを指定した場合は、IPPROTO_TCPは正常に動作できません。TCPを扱う場合は、IPPROTO_IPを指定して、TCPヘッダを自前で用意する必要があります。

WinSockのバージョン
 WinSockのバージョンには、1.1と、2があります。また、バージョン1.1には16ビット版と32ビット版が存在しています。WinSockの各バージョンにおけるDLL名とヘッダファイルの一覧は次のとおりです。
WinSockの各バージョン
DLL名ヘッダファイルライブラリ備考
WINSOCK.DLLWINSOCK.HWINSOCK.LIB16ビットのWinsock1.1
WSOCK32.DLLWINSOCK.HWSOCK32.LIB32ビットのWinsock1.1
WS2_32.DLLWINSOCK2.HWS2_32.LIB32ビットのWinsock2

パケットの取得

 WinSockでパケットを取得するには、次のステップを踏みます。

 それでは、各ステップについて順番に見ていきましょう。

ステップ1 WinSockの初期化

 初期化は、WinSockを使用する時に必要な処理です。具体的には、利用するバージョンおよびWSADATA構造体へのポインタを用意して、WSAStartupを実行し「Ws_32.dll」を初期化しています。今回のサンプルでは、バージョン2.0の機能を使用しますので第1パラメータにMAKEWORD(2,0)を指定しています。WSAStartupが成功すると、WSADATA構造体にバージョン情報などの初期化情報が返されます。

WinSockの初期化
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を使用する方法を紹介します。

アダプタ一覧(IPアドレス)を取得する
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は、ソケットに関連する操作のパラメータをセットしたり検索したりするために利用でき、そのプロトタイプは次のようになっています。

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」となっています。

SIO_ADDRESS_LIST_QUERYに関する定義(「Winsock2.h」より)
#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_insin_addrには、目的の4バイトのIPアドレスが格納されています。

 下図でこの関係を示しましたのでsockaddr構造体とsockaddr_in構造体の関係などの参考にしてください。

SOCKET_ADDRESS_LIST、SOCKET_ADDRESS、sockaddr、およびsockaddr_inの関係
SOCKET_ADDRESS_LIST、SOCKET_ADDRESS、sockaddr、およびsockaddr_inの関係
各構造体の定義(「Winsock2.h」より)

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でアダプタ一覧の処理を実装しています。詳しくはそちらを参照してください。

「Ws2_32.lib」のリンクについて
 サンプルプログラムでは、WSAIoctlSIO_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つだけが使用可能です。

通信方式(「Winsock2.h」より)
#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_RDMWSAESOCKTNOSUPPORT(10044)、SOCK_SEQPACKETWSAEAFNOSUPPORT(10047)のエラーが出て使用できません

 また、ソケットプロトコルには下記の各種プロトコルが定義されていますが、パケットモニターのようにすべてのプロトコルを取得したい場合は、IPPROTO_IPを指定することになります。なお、ヘッダの中にはIPPROTO_RAWというものもありますが、これを指定した場合、エラーは発生しませんがパケット一切は取得できません。これはWinSock2で未対応なためだと思われます。

ソケットプロトコル(「Winsock2.h」より)
#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では、ioctlsocketFIONBIOを渡すことにより、ブロッキング/非ブロッキングの設定を変更することができます。

非ブロッキングモードの設定
// 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」となります。

SIO_RCVALLに関する定義(「MSTcpIp.h」および「Winsock2.h」より)
#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」は、非常に短いヘッダファイルですので、参考のためここで紹介しておきます。

「MSTcpIp.h」(Platform SDKより)
//  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
Platform SDKのダウンロード
 ※Platform SDKは、下記のページよりダウンロードが可能です。
 http://www.microsoft.com/downloads/details.aspx?familyid=EBA0128F-A770-45F1-86F3-7AB010B398A3&displaylang=en
MicrosoftのドキュメントによるSIO_RCVALLに関する記述
 Microsoftのドキュメントには、SIO_RCVALLに関して次のようにアナウンスされています。
  • SIO_RCVALLで、ソケットにネットワーク上の全てのIPパケットを受け取らせることができます。
  • WSAIoctlに渡されるソケット・ハンドルは、AF_INETアドレスファミリ、SOCK_RAWソケット・タイプ、IPPROTO_IPプロトコルでなければなりません。
  • ソケットは明確なローカル・インタフェースが必要です。したがってINADDR_ANYは使用できません。
  • 一度ソケットをioctlでセットしたならば、WSARecvrecv関数を呼び出して、ネットワークインターフェースを通過するIPパケットを取得することができます。
  • 十分に大きいバッファを用意することに注意してください。
  • ioctlSIO_RCVALLをセットするには、ローカル・コンピュータ上でAdministrator権限が必要です。
  • SIO_RCVALLは、Windows 2000以降のバージョンで利用できます。
 ※参考 http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winsock/winsock/wsaioctl_2.asp

 サンプルプログラムの中では、「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などで自分の送信パケットが取得できない場合があります。この場合は、以下の対策を行ってみてください。

  1. Internet Connection Firewall(ICF)/Internet Connection Sharing(ICS)のサービスを開始
  2. 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ソケットを扱うプログラムについて書きました。ネットワーク関連のプログラムを作成する際の参考として、わずかながらでもお役に立てれば幸いです。

参考資料

  1. Winsock Programmer's FAQ
  2. Winsock Programmer's FAQ(日本語訳)
  3. SecurityFriday「Capture packets with Winsock」
  4. WinPcap: the Free Packet Capture Library for Windows
  5. Platform SDK: Windows Sockets 2
  6. Platform SDK: Windows Server 2003 SP1 Platform SDK Full Download
  7. WinPcapを使用したパケットモニターの作成
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 平内 真一(ヒラウチ シンイチ)

     クラスメソッド株式会社 モバイルアプリサービス部所属  仕事では、iOSアプリの開発を行っております。 会社ブログ 個人ブログ

All contents copyright © 2005-2017 Shoeisha Co., Ltd. All rights reserved. ver.1.5