パケットの取得
WinPcapを使用してパケットを取得するには、次のステップを踏みます。
それでは、各ステップについて順番に見ていきましょう。
ステップ1 アダプタ一覧の取得
コンピュータには、複数のネットワークが設定されている場合があります。NICが2つ以上設定されている場合や、LAN接続されているコンピュータでダイアルアップ接続した場合などです。パケットのモニターは、1つのインターフェースを指定してパケットを採取するため、複数のネットワークがある場合は、これを選択する必要があります。サンプルプログラムでは、複数のネットワークが存在した場合、それを選択させるダイアログを表示しています。
WinPcapでモニターを開始するためには、インターフェースをオープンする際にデバイス名を指定する必要がありますが、デバイス名は、WinPcapのライブラリ関数であるpcap_findalldevs
で簡単に取得できます。pcap_findalldevs
で取得するpcap_if_t
構造体には、デバイス名のほかにIPアドレスの情報も含まれています。サンプルプログラムでは、ダイアログ表示のためにIPアドレスも併せて取得しています。
pcap_if_t *AddDevs; if(pcap_findalldevs(&AddDevs,ErrorBuf) != -1){ pcap_if_t *d = AddDevs; while(d){ // IPアドレスの情報が有効なものだけをターゲットにする if(d->addresses!=NULL){ // sockaddr_inにキャストする sockaddr_in *sa = (sockaddr_in*)d->addresses->addr; // 現在有効でないデバイスは、 // アドレスが0になっているので排除する if(sa->sin_addr.s_addr!=0){ // sa->sin_addr.s_addr にIPアドレスが取得できている // d->name にアダプタ名が取得できている } d=d->next; } // デバイス情報のバッファを開放する pcap_freealldevs(AddDevs); } }
ステップ2 インターフェースのオープン
キャプチャのための物理インターフェイスをオープンするためにpcap_open_live
を使用します。
pcap_t* pcap_open_live( char *device, //オープンするデバイス名 int snaplen, //キャプチャする最大バイト数 int promisc, //プロミスキャス(無差別)モード int to_ms, //読み込みのタイプアウト値(ミリ秒) char *ebuf // エラーもしくは警告文字列を返すためのバッファ )
device
には、ステップ1で取得したアダプタ名を指定し、promisc
は、プロミスキャスモードで使用する際にtrue
に設定します。
int timeout = 20; if((fp=pcap_open_live(AdapterName,MAX_RECV_SIZE, true/*プロミスキャスモード*/,timeout,ErrorBuf))== NULL) return FALSE;//エラー
サンプルプログラムの中では、「Capture.cpp」のStart
でインターフェースのオープン処理を行っています。
ステップ3 パケットの取得
パケットを取得するためには、ステップ2でオープンしたオブジェクト識別子を指定しpcap_next_ex
をコールします。
int pcap_next_ex( pcap_t *p, // オブジェクト識別子 struct pcap_pkthdr ** pkt_header, // 受信データの情報をあらわす構造体 u_char **pkt_data //受信データ )
pcap_next_ex
の戻り値は次のとおりです。
1 | パケットが滞りなく読み込まれた |
0 | タイムアウトが経過(この場合はpkt_header とpkt_data は有効なパケットを返しません) |
-1 | エラーが発生 |
-2 | オフラインキャプチャからの読み込みがEOFに達した |
pcap_next_ex
が成功すると、pkt_data
に取得したデータが格納され、pkt_header->caplen
にそのバイト数が保存されます。
int ret; u_char *pkt_data; struct pcap_pkthdr *pkt_header; if(0<=(ret=pcap_next_ex(fp,&pkt_header, &pkt_data))){ if(ret==1){ //パケットが滞りなく読み込まれた // この時点で pkt_header->caplen に // 受信バイト数 pkt_data に受信したデータが含まれています。 } return TRUE; } return FALSE;// エラー
サンプルプログラムの中では、「Capture.cpp」のRecv
でパケットの受信処理を行っています。
ステップ4 インターフェースのクローズ
使用したオブジェクト識別子は、pcap_close
で閉じます。pcap_close
は、オブジェクト識別子に関連付けられているファイルなどをすべてクローズし、割り当てたリソースを開放します。
サンプルプログラムの中では、「Capture.cpp」のStop
でドライバの終了処理を行っています。
パケットの解析
pcap_next_ex
で取得したデータは、1つのパケットです。そして、これを解析することでプロトコルが判明するのです。パケットの解析は、Ethernetフレームの解析から始まります。
Ethernetフレームの解析
Ethernetで伝達されるパケットは、すべてEthernetフレームとして運ばれています。Ethernetフレームのフォーマットは、図に示す2種類に大別することができます。
Ethernet II(DIX規格)フレーム
2つのフレームを区別する方法は、Ethernet IIのフレームフォーマットに照らし合わせて確認することができます。Ethernet IIのフレームフォーマットは、図のように「送り先MACアドレス」「送り元MACアドレス」「上位プロトコルのタイプ」「データ」「CRC」という形式になっていますが、この「上位プロトコルのタイプ」の部分を見ることで、ネットワーク層のプロトコルが判明するのです。IEEE802.3では、この「上位プロトコルのタイプ」のところに「データの長さ」を格納しています。Ethernetのデータの最大の長さは1500(05DCh)であるため、タイプ番号にはこれより大きい値が利用されています。そうです、この値が1500よりも大きければ、Ethernet IIフレームであると判断できるのです。タイプの値には、IP(0x0800)・ARP(0x0806)・RARP(0x8035)・NetBIOS(0x0840)・IPX(0x8137)などが定義されています。
サンプルプログラムでは、当初、Ethernetヘッダを定義した構造体にデータへのポインタをキャストして、送り先(元)MACおよび上位プロトコルのタイプを取得し、次の上位データへアクセスするためにポインタをインクリメントしています。
// 受信データへのポインタを取得する unsigned char *p = Buffer; // Ethernetヘッダへキャストする LP_PROTOCOL_ETHER protocol_ether = (LP_PROTOCOL_ETHER)p; // Ethernetヘッダの内容を取得する処理 // 上位のデータにアクセスするためにポインタをEthernetのヘッダ分 // (14オクテット)だけ移動する p+=14;
なお、サンプルプログラムでは、見通しが良くなるように一般的に使用されているEthernet IIフレームのIP(0800)およびARP(0806)プロトコルのみを対象にして解析しています。
IEEE802.3(802.3 Raw)フレーム
「802.3 Raw」 のフレームフォーマットは、上位(ネットワーク層)のプロトコル識別する部分が欠落しているため、その後IEEE802.2のLSAPの概念をとりいれ、LLCヘッダを追加した「802.3 with LLC」が作られました。また、LLC では、識別しきれないプロトコルに対応するため、さらにSNAPヘッダを追加した「802.3 with SNAP」もあります。
IEEE802.3フレームと判断された場合は、そのデータ部分の先頭にLLC のヘッダを当てはめて確認します。LLCヘッダは「DSAP(宛先の上位プロトコル)」「SSAP(送信先の上位プロトコル)」「CTRL(制御)」の3バイトの形をしています。 また、「DSAP」および「SSAP」は、IP(06h)・IPX(E0h)・NetBIOS(F0h)などが定義されています。「802.3 with LLC」だと仮定して、DSAPとSSAPに06h、E0h、F0hなどがあれば、この仮定が正しいと判断されるのです。また同時にネットワーク層のタイプも取得できます。
「DSAP」「SSAP」は、最上位ビットが予約されているので、7ビット(128種類)のプロトコルしか表せません。そこで、「802.3 with SNAP」を使っている場合は、その「しるし」として「DSAP」「SSAP」に「AAh」を使います。 SNAPヘッダは、「DSAP」「SSAP」「CTRL(制御)」「プロトコルID」「タイプ」という形式ですが、この「タイプ」の部分は、「Ethernet II」のフレームで使用されていた、「上位プロトコルのタイプ」とまったく同じものが入ります。
「802.3 with SNAP」 のフレームは、「タイプ」の部分を見ることでネットワーク層のタイプを取得できるわけです。
最後に、LLCヘッダの確認で「DSAP」「SSAP」 に有効な数値が見つけられなかった場合は、「802.3 Raw」 フレームだということになります。この場合、上位プロトコルの種類は分かりません。「802.3 Raw」はNetWareだけで使われているので、おそらく「IPX」あろう、と推測できます。
MACアドレスは48ビットで表現されていますが、このうち上位24ビットはベンダー固有のコードであり、IEEEが割り当てています。そして、ベンダー内で下位24ビットにユニークな値を与えて、ベンダーの責任で全世界でユニークなMACアドレスを作り出しているのです。
下記のページで、MACアドレスからの検索および現在割り当てられている一覧を取得できます。
http://standards.ieee.org/regauth/oui/index.shtml
IP(TCP/UDP/ICMP)/ARPパケットの解析
取得したデータのEthernetヘッダ部分を解析した後、ヘッダ分の14バイトをインクリメントしたポインタは、次の上位のプロトコルのヘッダの先頭にあります。そこで、Ethernetヘッダで取得した「プロトコル」に従って適切なヘッダを表現した構造体へキャストします。
サンプルプログラムでは、EthernetヘッダでプロトコルがIPの場合とARPの場合に、それぞれ対応するヘッダを表現した構造体へキャストして、データを取得しています。また、IPヘッダの中で表現される上位のプロトコルにあわせて、TCP、UDPおよびICMPへのキャストもしています。
// Ethernetヘッダよりプロトコルを取得 unsigned int Type = protocol_ether->Type; if(Type==0x0800){ // IPパケット // IPパケットヘッダへキャストする LP_PROTOCOL_IP protocol_ip = (LP_PROTOCOL_IP)p; // ここでIPヘッダの内容を取得する処理を行う p+=(protocol_ip->Ver_Len & 0x0F)*4; // IPヘッダのサイズ分だけ移動 if(protocol_ip->Protocol==6){ //TCP // TCPパケットヘッダへキャストする LP_PROTOCOL_TCP protocol_tcp= (LP_PROTOCOL_TCP)p; // ここでTCPヘッダの内容を取得する処理を行う }else if(protocol_ip->Protocol==17){ //UDP // UDPパケットヘッダへキャストする LP_PROTOCOL_UDP protocol_udp= (LP_PROTOCOL_UDP)p; // ここでUDPヘッダの内容を取得する処理を行う }else if(protocol_ip->Protocol==1){ //ICMP // ICMPパケットヘッダへキャストする LP_PROTOCOL_ICMP protocol_icmp= (LP_PROTOCOL_ICMP)p; // ここでICMPヘッダの内容を取得する処理を行う }else{ // サンプルプログラムのため、TCP、UDPおよびICMP以外は、未対応 ; } }else if(Type==0x0806){ // ARP // ARPパケットヘッダへキャストする LP_PROTOCOL_ARP protocol_arp= (LP_PROTOCOL_ARP)p; // ここでARPパケットの内容を取得する処理を行う }else{ // サンプルプログラムのため、プロトコルIPおよびARP以外は、未対応 ; }
このほかのプロトコルに関しても同様に、下位プロトコルから順に適切なヘッダを表現した構造体にキャストしてポインタをずらしていくことで、すべてのデータを解析することができます。
サンプルプログラムの中では、各プロトコルのヘッダを表現した構造体は「Packet.h」に定義されており、「Unit1.cpp」のDisp1
およびDisp2
でこの解析が行われています。
しかし、開発環境によっては最適化のために、構造体のデータ表現を一定のサイズにアライメント(整列)してコンパイルされてしまいます。
C++ Builder 6およびVisual C++ .NET 2003におけるデータアライメントの設定は、以下のメニューから行えます。
- C++ Builder 6の場合
- Visual C++ .NET 2003の場合
まとめ
本稿では、WinPcapを使用したパケットモニターのサンプルを通して、WinPcapの利用方法および取得したパケットの解析の方法について紹介しました。ネットワーク関連のプログラムを作成する際の参考として、わずかながらでもお役に立てれば幸いです。