SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

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

WinPcapの利用方法、および取得したパケットの解析方法


パケットの取得

 WinPcapを使用してパケットを取得するには、次のステップを踏みます。

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

ステップ1 アダプタ一覧の取得

 コンピュータには、複数のネットワークが設定されている場合があります。NICが2つ以上設定されている場合や、LAN接続されているコンピュータでダイアルアップ接続した場合などです。パケットのモニターは、1つのインターフェースを指定してパケットを採取するため、複数のネットワークがある場合は、これを選択する必要があります。サンプルプログラムでは、複数のネットワークが存在した場合、それを選択させるダイアログを表示しています。

モニターするアダプタを選択するダイアログ
モニターするアダプタを選択するダイアログ
 ※複数のアドレスが検出されない場合は、このダイアログは表示されません。

 WinPcapでモニターを開始するためには、インターフェースをオープンする際にデバイス名を指定する必要がありますが、デバイス名は、WinPcapのライブラリ関数であるpcap_findalldevsで簡単に取得できます。pcap_findalldevsで取得するpcap_if_t構造体には、デバイス名のほかにIPアドレスの情報も含まれています。サンプルプログラムでは、ダイアログ表示のために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_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をコールします。

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_headerpkt_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種類に大別することができます。

2種類のEthernetフレーム
2種類のEthernetフレーム

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および上位プロトコルのタイプを取得し、次の上位データへアクセスするためにポインタをインクリメントしています。

Ethernetヘッダの解析
// 受信データへのポインタを取得する
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フレームの構造
各種のIEEE802.3フレームの構造

 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アドレスのベンダー表示
 各種のパケットモニター製品では、MACアドレスと共にそのメーカ名が表示されているものがありますが、これは、MACアドレスの中のベンダー固有のコードをデータベースから検索して表示しています。
 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の場合
  • [プロジェクト]→[プロジェクトのプロパティ]→[C/C++]→[コード生成]→[構造体メンバのアライメント]
 サンプルプログラムでは、上記のアライメントを有効にしてコンパイルすると、ARPヘッダの解析に失敗することを確認できます。

まとめ

 本稿では、WinPcapを使用したパケットモニターのサンプルを通して、WinPcapの利用方法および取得したパケットの解析の方法について紹介しました。ネットワーク関連のプログラムを作成する際の参考として、わずかながらでもお役に立てれば幸いです。

参考資料

  1. WinPcap: the Free Packet Capture Library for Windows
  2. WinPcapドキュメント
  3. WinPcap解説資料(日本語)
  4. IEEE Standards Association (MACのベンダコードの検索および一覧のダウンロード)
  5. WidePackets (ネットワーク技術のドキュメント)
  6. プロトコル番号の情報
  7. WinPcapの対応するネットワークカードの情報
  8. WinSock2を使用したパケットモニターの作成

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
特集記事連載記事一覧

もっと読む

この記事の著者

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

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

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/126 2007/12/10 13:51

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー