プログラミング with libpcap
以下は、Programming with pcap(http://www.tcpdump.org/pcap.htm)を日本語に直しただけです。誤訳等々有ると思いますがご容赦願います。ご指摘頂ければ直します。
関数マニュアルは、libpcap リファレンスで。
はじめに:pcapアプリケーション
最初に、pcapアプリケーションを作成するに当たって、手順の概要を説明する。
- まず、どのインタフェースを監視対象にするかを決める。Linuxではeth0、FreeBSDではxl1の様な名前になると思われる。インタフェース名は、文字列で与えるか、pcapが自動的に決定するかのいずれかの方法がある。
- pcapを初期化する。つまり実際にどのインタフェースを監視対象にするかを決定する。複数のインタフェースを監視対象にすることも可能である。その場合どのように識別するかというと、ファイルハンドルを用いる。ちょうど読み書きするためにファイルを開くように、セッションを用いて他のセッションとを識別する。
- 必要なトラフィックのみを抽出するために、ルールを設定する必要がある。(例えば、TCP/IPパケットで、port 23のみの通信だけとりたい 等)ルールの設定自体は、pcapに含まれる関数を呼び出すだけで終了する。他のアプリケーションは必要とはしない。そして次に、どのセッション(ディバイス)に対してそのルールを適用するかを指定する。
- ここまできて、ようやくpcapは実行ループに入る。この状態では、pcapはパケットを待ち受けながら、取得すべきパケットかどうかを判断する。pcapはパケットを受け取る毎に、あらかじめ定義された関数を呼ぶ。関数自体は、プログラマが自由に設定できる。つまり、詳細にパケットを解析したり、コンソールに出力したり、ファイルに書き出したり、何もしなかったりと、自由にできる。
- あとは、終了させる
監視対象ディバイスの設定
至ってシンプルであり、監視対象にするディバイスの指定方法には2種類ある。
引数で渡す方法
#include <stdio.h> #include <pcap.h> int main(int argc, char *argv[]) { char *dev = argv[1]; printf("ディバイス: %s\n", dev); return(0); }
この方法では、プログラム起動時に引数として監視対象インタフェース名を渡す。当然認識されているインタフェース名でなければならない。
一方、以下のような方法もある。
自動でサーチする
#include <stdio.h> #include <stdlib.h> #include <pcap.h> int main(int argc, char *argv[]) { char *dev, errbuf[PCAP_ERRBUF_SIZE]; dev = pcap_lookupdev(errbuf); if (dev == NULL) { fprintf(stderr, "ディバイスが見つかりませんでした: %s\n", errbuf); exit(1); } printf("ディバイス: %s\n", dev); return 0; }
この方法では、libpcapは自動的に監視対象インタフェースを決定する。
pcap_lookupdev()関数に渡す引数として、errbufを渡しているが、libpcapの関数のほとんどはこの引数を必要とする。この文字列には、関数が失敗した(エラーが出た)場合に、エラーメッセージが格納される。この場合、pcap_lookupdev()関数がエラーを出した場合にエラーメッセージが格納される。すばらしいでしょう?
こうやってディバイスをセットする。
監視対象ディバイスのオープン
監視対象にするためのセッションを作成する手順は至って簡単。pcap_open_live()関数を呼び出すだけである。この関数のプロトタイプは以下のようになっている(pcap man pageより抜粋)
pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)
- 第一引数(char *device)
- さきほどのセクションで説明したインタフェース名へのポインタを渡す。
- 第二引数(int snaplen)
- 取得したいパケットの最大長を入れる。256と指定すれば、1500byteのパケットを受け取った場合は、256byteに切りつめられることになる。
- 第三引数(int promisc)
- インタフェースをプロミスキャスモードにするかどうかを決める。TRUEを指定すれば、プロミスキャスモードになる。(しかし、FALSEを指定した場合でも、ある条件下ではとりあえずプロミスキャスモードで動作する)
- 第四引数(int to_ms)
- 読み込みタイムアウト時間をミリ秒で指定する。(0を指定すれば、永遠に待ち続ける。つまり、いくつかのプラットフォーム上では、受信したパケットを見る前に、十分にパケットを蓄える(待つ)ものがある。(そのため、0を指定すると、十分蓄えるまで待ってしまうため、極端な話、永遠に待ち続けるおそれがある。)そのため、極力0以外の値を指定した方がよい。)
- 第五引数(char *ebuf)
- 関数実行中にエラーが発生した場合に、エラーメッセージを格納するための変数へのポインタを指定する。
この関数の戻り値は、パケットキャプチャディスクリプタである。
コードの一部分は以下のような感じになる。
#include <pcap.h> ... pcap_t *handle; handle = pcap_open_live(somedev, BUFSIZ, 1, 1000, errbuf); if (handle == NULL) { fprintf(stderr, "ディバイス「%s」を開けませんでした: %s\n", somedev, errbuf); exit(1); }
このサンプルコード(もちろんコードの一部だけど)は、権限のあるsomedevを開き、BUFSIZで定義されている分だけのbyteを読み込むように伝える。(BUFSIZはpcap.hで定義されている)ディバイスをプロミスキャスモードで開くように命令し、エラーが発生しないかぎり監視し続ける。なにかエラーがあるとerrbufにエラー内容が格納され、エラーメッセージを出力するのに利用できる。
プロミスキャスモードと非プロミスキャスモード
これら2つは大いに異なるものである。一般に非プロミスキャスモードでの動作は、監視対象のホストに関連するパケットのみを取得する。つまり、ホスト自身へのパケット、ホスト自身から送信されるパケット、ルータとして動作している場合は、ホストによって転送されるパケットのみである。一方、プロミスキャスモードでは、インタフェースに到達するパケット全てを取得する。言うまでもなく、プロミスキャスモードは全てのパケットを取得することができるため、ネットワークを監視するのには有効かも知れない。(場合によっては有効でないかも知れないが)しかし、不利な点もある。プロミスキャスモードでの監視は他のホストから検出可能である。ホストは、ネットワーク上で誰かがプロミスキャスモードでネットワークを見張っているか否かを高い信頼度で検出できる。次に、これはスイッチ(ハブやARPを漏らすスイッチとか)がない環境においてのみ可能である。最後に、広帯域ネットワークにおいてはシステムリソースにたいして、多大な負担をかける可能性が出る。
トラフィックフィルタリング
特定トラフィックのみを取得したいと思うことがしばしばある。たとえば、パスワードの検索のためにport 23(TELNET)の通信のみ全てを監視したいということがあるかもしれない。もしくは、port 21(FTP)でやりとりされているファイルをハイジャックしたいかもしれない。DNSのトラフィックのみ(UDP port 53)を監視したいかもしれない。いずれにせよ、無差別に全トラフィックを監視したいなんて事はほとんど無い。
pcap_compile()関数とpcap_setfilter()関数に進んでいこう。
実行自体は至って簡単である。すでにpcap_open_live()関数を実行し、有効なセッションを持っているはずで、それにたいしてフィルタを適用するだけである。なぜ独自でif/else構文を利用しないかというと、2つの理由がある。まず1つ目は、pcapのフィルタはBPFフィルタとともに直接有効になるため、非常に効率的であることが挙げられる。BPFドライバそのものを直接いじる為に必要ないくつものステップを全て排除している。2つ目の理由は、様々な点でこっちの方が簡単であると考えるからである。
フィルタのコンパイル
フィルタを有効にする前に「コンパイル」という作業が必要になる。フィルタ表現形式は一般的な文字列(char型配列)を保っている。文法はtcpdumpのドキュメントを参照していただければ非常に詳しく載っている。各々そちらを参照していただきたい。しかしながら、以下に出てくるテストのための表現を見ていただければ、十分理解できると思う。
プログラムをコンパイルするためにpcap_compile()関数を呼び出す。プロトタイプは以下の通り宣言されている
int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)
- 第一引数(pcap_t *p)
- 今まで出てきたように、パケットキャプチャディスクリプタを入れる。
- 第二引数(struct bpf_program *fp)
- フィルタをコンパイルしたものを格納すべき変数へのポインタを入れる。
- 第三引数(char *str)
- フィルタの表現形式の文字列そのものへのポインタである。
- 第四引数(int optimize)
- 表現形式が最適化されているか否かを指定する。(0はされていない。1はされていることを示す。ありきたりですね ('-'*)
- 第五引数(bpf_u_int32 netmask)
- フィルタを適用させるネットワークのネットマスクを指定する。
関数の戻り値は、-1でエラー、成功すると-1以外の数値である。
フィルタの適用
フィルタのコンパイルが終わったら、フィルタを適用させる番である。pcap_setfilter()関数へ行ってみよう。以下がそのプロトタイプである。
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
実に簡単である。
- 第一引数(pcap_t *p)
- パケットキャプチャディスクリプタ。
- 第二引数(struct bpf_program *fp)
- フィルタをコンパイルしたものを格納すべき変数へのポインタである。(まぁpcap_compile()関数の第二引数と同じものを指定しておけばよい)
理解を手助けできると思われるサンプルコードを以下に示す。
#include <pcap.h> ... pcap_t *handle; /* パケットキャプチャディスクリプタ */ char dev[] = "rl0"; /* 監視ディバイス名 */ char errbuf[PCAP_ERRBUF_SIZE]; /* エラー理由格納用文字配列 */ struct bpf_program fp; /* コンパイル済みフィルタ用構造体 */ char filter_exp[] = "port 23"; /* フィルタ表記用文字配列 */ bpf_u_int32 mask; /* 監視ディバイスのネットマスク */ bpf_u_int32 net; /* 監視ディバイスのIPアドレス */ if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) { fprintf(stderr, "ディバイスのネットマスクを取得できませんでした:%s\n", dev); net = 0; mask = 0; } handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf); if (handle == NULL) { fprintf(stderr, "ディバイス「%s」を開けませんでした: %s\n", somedev, errbuf); exit(1); } if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) { fprintf(stderr, "フィルタ「%s」の解析ができませんでした: %s\n", filter, pcap_geterr(handle)); exit(1); } if (pcap_setfilter(handle, &fp) == -1) { fprintf(stderr, "フィルタ「%s」の組み込みができませんでした: %s\n", filter_exp, pcap_geterr(handle)); exit(1); }
これで、インタフェース「rl0」で、プロミスキャスモードにより23番ポートを監視する下準備ができた。
上の例において、まだ説明していない関数が出てきたのに気づかれたかも知れない。pcap_lookupnet()関数は引数にインタフェース名などを取り、IPアドレスとサブネットマスクを返す関数である。これは、フィルタを適用するネットマスクを知るために必要不可欠なものである。この関数の説明はこのドキュメントの最後にある「その他の節」で行うことにする。
経験上、フィルタの表記は全てのOSで共通というわけではない。libpcap開発時のテスト環境で、デフォルトカーネルのOpenBSD2.9はこのタイプのフィルタが利用できることが分かっている。しかし、デフォルトカーネルのFreeBSD4.3では利用できなかった。効果が変わるかも知れないのでご注意を。
実際の監視
現在まで、インタフェースを指定する方法、監視を始める準備をする方法、どれを監視ししてどれを監視しないかというフィルタを適用する方法をみてきた。次は実際にパケットをいくつか取得してみよう。
大きく分けて2つの方法がある。1つのパケットを取得する方法、もしくはループに入って一度にn個のパケットを取得する方法とがある。まずは1つのパケットを取得する方法から見ていくことにしよう。pcap_next()関数を使用する。
pcap_nextを利用する
pcap_next()関数は以下のようなプロトタイプで定義されている。
u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
- 第一引数(pcap_t *p)
- パケットキャプチャディスクリプタがはいる。
- 第二引数(struct pcap_pkthdr *h)
- パケットに関する一般的な情報が入りうる構造体を入れる。一般的な情報とは以下の通りである
- パケットが取得された時間
- パケット長
- 実際取得されたパケット長(フラグメントされている可能性有り)
pcap_next()関数は第二引数で指定されている構造体で記述されるパケットへのポインタを返す(u_char型)。後ほど取得したパケットをどのように読み込んでいくかを見ていくことにする。
以下がpcap_next()関数を用いてパケットを取得する簡単な例である。
#include <pcap.h> #include <stdio.h> int main(int argc, char *argv[]) { pcap_t *handle; /* パケットキャプチャディスクリプタ */ char *dev; /* 監視ディバイス */ char errbuf[PCAP_ERRBUF_SIZE]; /* エラー格納用文字配列 */ struct bpf_program fp; /* コンパイル済みフィルタ構造体 */ char filter_exp[] = "port 23"; /* フィルタ表記用文字列 */ bpf_u_int32 mask; /* ネットマスク */ bpf_u_int32 net; /* IPアドレス */ struct pcap_pkthdr header; /* pcap用ヘッダ構造体 */ const u_char *packet; /* 実際のパケットへのポインタ */ /* ディバイスの定義 */ dev = pcap_lookupdev(errbuf); if (dev == NULL) { fprintf(stderr, "デフォルトのディバイスを発見できませんでした: %s\n", errbuf); exit(1) } /* ディバイスの属性を指定 */ if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) { fprintf(stderr, "ディバイス「%s」のネットマスクを発見できませんでした: %s\n", dev, errbuf); net = 0; mask = 0; } /* プロミスキャスモードでセッションを開く */ handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf); if (handle == NULL) { fprintf(stderr, "ディバイス「%s」を開けませんでした: %s\n", somedev, errbuf); exit(1) } /* フィルタをコンパイルして適用する */ if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) { fprintf(stderr, "フィルタ「%s」を解析できませんでした: %s\n", filter_exp, pcap_geterr(handle)); exit(1) } if (pcap_setfilter(handle, &fp) == -1) { fprintf(stderr, "フィルタ「%s」の組み込みができませんでした: %s\n", filter_exp, pcap_geterr(handle)); exit(1) } /* パケットを捕まえる */ packet = pcap_next(handle, &header); /* 取得パケット長を表示 */ printf("取得したパケット長 [%d]\n", header.len); /* セッションを閉じる */ pcap_close(handle); return(0); }
このアプリケーションはプロミスキャスモードでpcap_lookupdev()関数が返すインタフェース上のものは何でも取得する。そして、port 23(TELNET)にきた最初のパケットが見つかったらユーザに対しパケットbyteサイズを表示する。またまたこのプログラムはpcap_closeという新たな関数を含んでいる。これもまた後ほど説明することにしよう。
もう1つの方法はより複雑ではあるが、非常に役立つ方法である。少数の取得(べつにいくつでもかまわないが…)には実際pcap_next()関数が使われている。しばしばpcap_loop()関数やpcap_dispatch()関数(pcap_dispatch()関数自身はpcap_loop()関数を用いている)が使われる。これら2つの関数を理解するには、コールバック関数というものについて理解しないといけない。
callback function
コールバック関数という物自体は目新しいものではなく、多くのAPIで一般的なものである。コールバック関数の背景にある概念はおおよそシンプルなものである。何度かソートを行うために処理を待つプログラムがあると仮定してみよう。このサンプルプログラムの目的は、ユーザがキーボードのキーを押させたいというプログラムの模倣である。ユーザが何かキーを押す毎にどのような挙動を起こすかという事を決定するための関数を呼び出したい。このとき利用する関数がコールバック関数である。ユーザがキーを押す度にこのプログラムはコールバック関数を呼び出している。コールバックはpcapでも使われており、ユーザがキーを押す度に呼び出す代わりにpcapがパケットを取得する度に呼び出すようになっている。そのコールバック関数を定義できる2つの関数が、pcap_loop()関数とpcap_dispatch()関数である。これら2つの関数はコールバック関数に使用方法が似ている。両関数とも、フィルタが要求するパケットに出会う毎にコールバック関数を呼び出すようになっている。(当然ながら、フィルタがあれば・・・の話であるが。もしフィルタがなければ、全パケットがコールバック関数に送られる)
pcap_loopを利用する
pcap_loop()関数のプロトタイプは以下のようになっている。
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
- 第一引数(pcap_t *p)
- パケットキャプチャディスクリプタが入る。
- 第二引数(int cnt)
- pcap_loop()関数が戻り値を返すまでに、いくつのパケットを取得可能かというパケット数を入れる。(マイナス値を入れると、エラーが発生するまで取得を続ける)
- 第三引数(pcap_handler callback)
- コールバック関数の名前を入れる。(関数名のみで引数の記述は必要ない)
- 第四引数(u_char *user)
- 特定にアプリケーションには有用な引数だが、単にNULLを指定しても良い。pcap_loop()関数の引数以外に、コールバック関数に追加で引数を送りたいことを考えてみよう。ここでそれができる。きちんと当てはまるように、明らかにu_charへのポインタ型に型をあわせなければいけない。後述しているが、pcapはu_char型へのポインタの型において、受け渡される情報の意味を生かせるような使用方法をしている。どのようにそれを行っているかという例を見せれば、それがどのようになっているかを明確化できるだろう。もしわからなければ、C言語のリファレンステキストでも参照するとよい。
pcap_dispatch()関数は使用方法はほとんど同じである。両者の違いといえば、pcap_dispatch()関数が最初のパケット群に対してバッチ的な処理を行うのみであるということである。一方pcap_loop()関数はint cntで与えられた数を処理するまで処理を続ける。両者の違いに関するさらなる議論はpcapのmanを参照していただきたい。
pcap_loop()関数の使用例を示す前にコールバック関数のフォーマットを説明しておく。好き勝手にコールバック関数のプロトタイプを宣言してはいけない。つまり、pcap_loop()関数はその関数をどう利用するかを知るすべがない。そのため、以下に示すフォーマットに従ってコールバック関数のプロトタイプを定義する必要がある。
void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet);
さて、より詳細に見ていく。まず、関数の戻り値がvoid型なのに気づいただろうか。これは当然のことである。なぜなら、pcap_loop()はどんな値が返ってくるのか知る余地がないのだから。第一引数はpcap_loop()関数の最後の引数に対応している。pcap_loop()関数の最後の引数の値は、常にコールバック関数が呼ばれる時にコールバック関数の第一引数として呼ばれる。第二引数はpcapヘッダ変数である。これはパケットがいつ取得されたか、パケット長情報などが格納されている。pcap_pkthdr構造体はpcap.hの中に以下のように定義されている
struct pcap_pkthdr { struct timeval ts; /* タイムスタンプ */ bpf_u_int32 caplen; /* 得られたパケットの長さ */ bpf_u_int32 len; /* 元々のパケットの長さ */ };
それぞれの値に関しては、見れば分かるだろう。さて、興味深く、皆がよく戸惑う最後の引数であるが、これはもう一つのu_char型へのポインタであり、pcap_loop()によって取得されたパケット全体の先頭バイトを指している。
では、この「*packet」という変数をどうやって使えばいいのだろうか?パケットは多くの属性を持っている。想像にたやすいが、文字列のみではなく構造体のあつまりである。(たとえば、TCP/IPパケットはEthernetヘッダ、IPヘッダ、TCPヘッダ、最後にパケットのペイロードを含んでいる)このu_char型ポインタはこれら構造体の一連の流れを指している。それらを使うときは、型変換(キャスト)行いながら利用しなければならない。
型変換を行う前にまず、実際の構造体の定義をおこなう。以下の例はTCP/IP over Ethernetを描いた場合の定義である。
/* イーサネットアドレス(MACアドレス)は6バイト*/ #define ETHER_ADDR_LEN 6 /* イーサネットヘッダ */ struct sniff_ethernet { u_char ether_dhost[ETHER_ADDR_LEN]; /* 送信先ホストアドレス */ u_char ether_shost[ETHER_ADDR_LEN]; /* 送信元ホストアドレス */ u_short ether_type; /* IP? ARP? RARP? など */ }; /* IPヘッダ */ struct sniff_ip { u_char ip_vhl; /* バージョン(上位4ビット)、ヘッダ長(下位4ビット) */ u_char ip_tos; /* サービスタイプ */ u_short ip_len; /* パケット長 */ u_short ip_id; /* 識別子 */ u_short ip_off; /* フラグメントオフセット */ #define IP_RF 0x8000 /* 未使用フラグ(必ず0が立つ) */ #define IP_DF 0x4000 /* 分割禁止フラグ */ #define IP_MF 0x2000 /* more fragments フラグ */ #define IP_OFFMASK 0x1fff /* フラグメントビットマスク */ u_char ip_ttl; /* 生存時間(TTL) */ u_char ip_p; /* プロトコル */ u_short ip_sum; /* チェックサム */ struct in_addr ip_src,ip_dst; /* 送信元、送信先IPアドレス */ }; #define IP_HL(ip) (((ip)->ip_vhl) & 0x0f) #define IP_V(ip) (((ip)->ip_vhl) >> 4) /* TCP header */ struct sniff_tcp { u_short th_sport; /* 送信元ポート */ u_short th_dport; /* 送信先ポート */ tcp_seq th_seq; /* シーケンス番号 */ tcp_seq th_ack; /* 確認応答番号 */ u_char th_offx2; /* データオフセット、予約ビット */ #define TH_OFF(th) (((th)->th_offx2 & 0xf0) >> 4) u_char th_flags; #define TH_FIN 0x01 #define TH_SYN 0x02 #define TH_RST 0x04 #define TH_PUSH 0x08 #define TH_ACK 0x10 #define TH_URG 0x20 #define TH_ECE 0x40 #define TH_CWR 0x80 #define TH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR) u_short th_win; /* ウインドサイズ */ u_short th_sum; /* チェックサム */ u_short th_urp; /* 緊急ポインタ */ };
注意:これをテストしたSlackware Linux 8.0のPC(kernel 2.2.19)では、上記の構造体ではコンパイルできなかった。判明した問題は、include/features.hにおいて_BSD_SOURCEが定義されずにPOSIXインタフェースが実装されていた。それが定義されていないと、他のTCPヘッダ構造体の定義を使わなければならない。一般的な解決方法としては、
#define _BSD_SOURCE 1
の定義をどのヘッダファイルよりも先に来るようにすることである。これをすることで確実にAPIがBSDスタイルを使用するようにできる。繰り返すが、このようにしたくなければ単に他の違ったTCPヘッダ構造体の定義を用いればよい。
で、これがどうpcapやよくわからないu_char型ポインタに関わってくるのだろうか?これらの構造体はパケットのデータの中で現れるヘッダとして定義される。で、どうやってバラバラにするんだろうか?ポインタの利用方法でもっとも実用的な使用方法の一つを見る準備をしてみよう。(C言語にポインタは要らないとかいっている全てのCプログラマの皆さん、ガツンとやるよ)
再度言うと、TCP/IP over Ethernetのパケットを扱うと仮定している。このテクニックは他のどんなパケットにも適用可能である。つまり構造体のタイプが違うだけである。さて、変数の定義からはじめて、パケットデータを分解する必要がある定義をコンパイルしてみようか。
/* イーサネットヘッダは常にちょうど14バイト */ #define SIZE_ETHERNET 14 const struct sniff_ethernet *ethernet; /* イーサネットヘッダ */ const struct sniff_ip *ip; /* IPヘッダ */ const struct sniff_tcp *tcp; /* TCPヘッダ */ const char *payload; /* パケットペイロード */ u_int size_ip; u_int size_tcp;
以下のように型変換(キャスト)を工夫してみた
ethernet = (struct sniff_ethernet*)(packet); ip = (struct sniff_ip*)(packet + SIZE_ETHERNET); size_ip = IP_HL(ip)*4; if (size_ip < 20) { printf(" * 不正なIPヘッダ長: %u bytes\n", size_ip); return; } tcp = (struct sniff_tcp*)(packet + SIZE_ETHERNET + size_ip); size_tcp = TH_OFF(tcp)*4; if (size_tcp < 20) { printf(" * 不正なTCPヘッダ長: %u bytes\n", size_tcp); return; } payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);
動いだろうか?メモリ内のパケットデータレイアウトを考慮してみよう。u_charポインタは本当に、単にメモリ内のアドレスを含む変数である。それがポインタというもの、つまりポインタはメモリ内の場所を示しているにすぎない。
簡略化のために、このポインタのアドレスを変数Xに代入する。もし3つの構造が一列に並んでいたとしたら、最初にsniff_ethernet構造体が来ているはずである。そして、そのアドレスのすぐ後に構造体のアドレスを見つけることができるだろう。つまり、そのアドレスはX + イーサネットヘッダ長(14 か SIZE_ETHERNET)である。
同様に、あるヘッダのアドレスを得ることができれば、その後に続くヘッダのアドレスはヘッダ長を足せば良いことになる。IPヘッダは、Ethernetヘッダとは違って、固定長ではない。その長さは、IPヘッダのヘッダ長フィールドで4バイトワードとして与えられる。4バイトワードを計算するには、与えられたサイズを4倍しないといけない。IPヘッダの最小ヘッダ長は20バイトである。
TCPヘッダもまた可変長である。その長さは「データオフセット」フィールドで4バイトワードで与えられる。最小ヘッダ長は20バイトである。
表にすると、以下の通りになる。
変数 | アドレス(バイト) |
---|---|
sniff_ethernet | X |
sniff_ip | X + SIZE_ETHERNET |
sniff_tcp | X + SIZE_ETHERNET + {IP header length} |
payload | X + SIZE_ETHERNET + {IP header length} + {TCP header length} |
sniff_ethernet構造体の最初のアドレスは単にXの場所にある。sniff_ethernetの後に続くsniff_ipはXにイーサネットヘッダが消費するサイズ(14 もしくは SIZE_ETHERNET)を加えた位置に存在する。sniff_tcpはsniff_ethernetとsniff_ipの両方の後にある。それはつまり、Xにsniff_ethernetヘッダとIPヘッダのサイズ(各々、14バイトとIPヘッダ長フィールドに格納されている長さ)を加えた後に位置していると言うことである。最後に、ペイロードがある。ペイロードは固有の構造は持たず、TCPの上位に位置しているプロトコルに依存した内容になっている)
さて、現時点では我々はどのようにコールバック関数を準備し、それを呼び出し、内容を確認したいパケットに関する属性をどうやって見つければよいかと言うことを知っている。それはつまり、あなた方が有用なパケットスニファを書くときが来た!ということである。ソースコードが長いため、この本文中にソースコードは掲載しなかった。ダウンロードはこちら(http://www.tcpdump.org/sniffex.c)から行って、試してみてほしい。
最後に
現時点で、pcapを使ったパケットスニファプログラムを書けるようになっただろう。pcapセッションを開いた後の概念や、パケットを見る方法、フィルタを適用する方法、コールバック関数を利用する方法に関する一般的な特性を学んできた。今こそそれらを使って電線の中を盗聴してみよう!
This document is Copyright 2002 Tim Carstens. All rights reserved.
修正するしないにかかわらず、再掲載したり利用したりする際は、以下の条件を満たすこと
- 再掲載するときは上記のコピーライトおよびこの条件リストを含めること
- Tim Carstens の名前を、個別に書面による事前の許可を得ずに、この文章から生じるプロダクトを保証したり、プロモーションするために使ってはいけない
参考文献
- Programming with pcap(http://www.tcpdump.org/pcap.htm)
Slackware
なぜか、コンパイルすると
エラーがでる
http://rpmfind.net//linux/RPM/fedora/devel/x86_64/libpcap-0.8.3-7.i386.html
おすすめ書籍
- マスタリングTCP/IP(入門編)第3版(http://item.rakuten.co.jp/book/1420352/)
- 定番中の定番。ネットワークを勉強し始める上での登竜門。これでパケットの構造を勉強しつつ、プログラミングを行うと理解が深まると思います。
最終更新時間:2007年03月17日 11時39分26秒