PCAPプログラミング

このテキストは、 pcapを使用したTim Carstens Programmingによる2002年の記事の翻訳です。 ロシア語のインターネットにはあまり多くのPCAP情報がありません。 この翻訳は、主にトラフィックキャプチャのトピックに興味のある人向けに作成されましたが、同時に英語を上手に話せません。 実際、カットの下では、翻訳そのものです。







エントリー



この記事の対象者を決定することから始めましょう。 明らかに、記事に記載されているコードを理解するには、Cの基本的な知識が必要です(もちろん、単に理論を理解したい場合を除きます)。しかし、プログラミングの忍者である必要はありません。すべての概念を詳細に説明します。 また、PCAPはスニッフィングを実装するためのライブラリであるため、ネットワークの操作に関する基本的な知識が理解に役立ちます。 ここで紹介するすべてのコード例は、デフォルトのカーネルを使用してFreeBSD 4.3でテストされています。







はじめに:一般的なPCAPアプリケーションフォーム



最初に理解することは、PCAPスニファーの一般的な構造です。 次のようになります。







  1. まず、トラフィックを受信するインターフェイスの識別子を定義することから始めましょう。 Linuxではeth0



    ようなもの、BSDではxl1



    などになります。 この識別子を文字列で指定するか、PCAPに提供してもらうことができます。
  2. 次に、PCAPを初期化する必要があります。 この段階で、使用するデバイスのPCAP名を転送する必要があります。 必要に応じて、複数のデバイスからトラフィックをキャプチャできます。 セッション記述子を使用してそれらを区別します。 ファイルの操作中と同様に、トラフィックキャプチャセッションに名前を付けて、他の同様のセッションと区別できるようにする必要があります。
  3. 特定のトラフィック(たとえば、TCP / IPパケットのみ、またはポート23からのみのパケットなど)を受信する場合は、一連のルールを作成し、それらを「コンパイル」して、特定のセッションに適用する必要があります。 これは3段階の密接に関連したプロセスです。 一連のルールは最初は行にあり、その後、理解可能なPCAP形式にコンパイルされます。 コンパイルは、プログラム内で関数を呼び出すことで実行され、外部アプリケーションの使用とは関係ありません。 次に、必要なセッションにこのフィルターを適用するようにPCAPに指示します。
  4. 最後に、PCAPにトラフィックのキャプチャを開始するよう指示します。 pcap_loop



    を使用する場合、PCAPは、指定した数のパケットを受信するまで動作します。 彼は新しいパッケージを受け取るたびに、定義した関数を呼び出します。 この関数は何でもできます。 彼女はパッケージを読み、その情報をユーザーに転送したり、ファイルに保存したり、何もしなかったりすることができます。
  5. キャプチャジョブが完了したら、セッションを閉じることができます。

    これは実際には非常に簡単なプロセスです。 5つのステップのみで、そのうちの1つはオプションです(ステップ3)。 各ステップとその実装を見てみましょう。


デバイス定義



とても簡単です。 聞きたいデバイスを決定する方法は2つあります。







1つ目は、トラフィックをキャプチャするデバイスの名前をユーザーにプログラムに伝えることです。 次のコードを検討してください。







 #include <stdio.h> #include <pcap.h> int main(int argc, char *argv[]) { char *dev = argv[1]; printf("Device: %s\n", dev); return(0); }
      
      





ユーザーは、プログラムの最初の引数としてデバイスの名前を指定してデバイスを定義します。 現在、 dev



行には、PCAPが理解できる形式でリッスンするインターフェイスの名前が含まれています(もちろん、ユーザーがインターフェイスの実際の名前を提供した場合)







2番目の方法も非常に簡単です。 プログラムを見てみましょう。







 #include <stdio.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, "Couldn't find default device: %s\n", errbuf); return(2); } printf("Device: %s\n", dev); return(0); }
      
      





この場合、PCAPは単にデバイス名を独自に設定します。 「でも待って、ティム」とあなたは言う。 「 errbuf



文字列をどうするか」。 ほとんどのPCAPコマンドでは、引数の1つとして文字列を渡すことができます。 どんな目的のために? コマンドが失敗した場合、PCAPは送信された文字列にエラーの説明を書き込みます。 この場合、 pcap_lookupdev()



が失敗すると、エラーメッセージがerrbuf



に配置されます。 かっこいいですね。 これは、トラフィックをキャプチャするためのデバイス名の設定方法です。







スニッフィング用のデバイスをセットアップする



トラフィックキャプチャセッションを作成するタスクも非常に簡単です。 このために、 pcap_open_live()



関数を使用します。 この関数のプロトタイプ:







 pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)
      
      





最初の引数は、前のセクションで定義したデバイスの名前です。 snaplen



は、PCAPがキャプチャできる最大バイト数を定義する整数です。 promisc



true



に設定すると、デバイスが判読不能モードに設定されます(とにかく、たとえfalse



に設定されていても、場合によってはインターフェースが判読不能モードになることがあります)。 to_ms



はミリ秒単位の読み取り時間です(値0はタイムアウトなしを意味します;少なくとも一部のプラットフォームでは、これらのパケットの分析を終了する前に十分なパケットを待機してスニッフィングを停止できることを意味します。したがって、ゼロ以外の時間を使用する必要があります)。 最後に、 ebuf



はエラーメッセージを保存できる行です(以前errbuf



行ったように)。 この関数は、セッションハンドルを返します。







実証するために、次のコードを検討してください。







 #include <pcap.h> ... pcap_t *handle; handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf); if (handle == NULL) { fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf); return(2); }
      
      





このコードは、 dev



変数に配置されたデバイスを開き、 BUFSIZ



BUFSIZ



で定義されている定数)で指定された数のバイトを読み取るように指示します。 エラーが発生するまでトラフィックをキャプチャするためにデバイスを非可聴モードに切り替え、エラーの場合はその説明をerrbuf



行に入れます。 そして、エラーが発生した場合、この行を使用して、問題のあったメッセージを表示します。







読みやすい/聞き取れないスニッフィングモードに関する注意:2つの方法はスタイルが大きく異なります。 通常、インターフェイスは読みやすいモードで、送信されたトラフィックのみをキャプチャします。 スニファーによってキャプチャされるのは、そこから送信されるトラフィック、またはそこにルーティングされるトラフィックのみです。 逆に、非可聴モードでは、ケーブルを通過するすべてのトラフィックがキャプチャされます。 非スイッチング環境では、これはすべてのネットワークトラフィックになります。 この方法の明らかな利点は、トラフィックキャプチャの目的に応じて、より多くのパケットをキャプチャできることです。 ただし、欠点もあります。 判読不能モードは簡単に検出され、一方のノードは他方が判読不能モードであるかどうかを明確に判断できます。 また、非スイッチ環境(ハブ、APRを使用するルーターなど)でのみ機能します。 もう1つの欠点は、トラフィックが多いネットワークでは、システムリソースがすべてのパケットをキャプチャして分析するのに十分でない可能性があることです。







すべてのデバイスが、読み取ったパケットに同じリンク層ヘッダーを提供するわけではありません。 イーサネットデバイス、および一部の非イーサネットデバイスは、イーサネットヘッダーを提供できますが、BSDおよびOS Xの短絡デバイス、PPPインターフェイス、モニタリングモードのWi-Fiインターフェイスなどの他のタイプのデバイスは提供しません。







デバイスが提供するリンク層ヘッダーのタイプを判別し、それを使用してパケットのコンテンツを分析する必要があります。 pcap_datalink()



は、リンク層ヘッダーのタイプを返します。 ( リンク層ヘッダー値のリストを参照してください。戻り値は、このリストのDHT_値です)







プログラムがデバイスによって提供されるリンクレベルヘッダーをサポートしていない場合、同様のコードを使用して動作を停止する必要があります。







 if (pcap_datalink(handle) != DLT_EN10MB) { fprintf(stderr, "Device %s doesn't provide Ethernet headers -not supported\n", dev); return(2); }
      
      





デバイスがイーサネットヘッダーをサポートしていない場合に機能します。 これは、イーサネットヘッダーを使用する以下のコードで機能する場合があります。







トラフィックフィルタリング



多くの場合、特定の種類のトラフィックのみをキャプチャすることに関心があります。 たとえば、必要なのは、ポート23(telnet)からのトラフィックをキャプチャしてパスワードを検索することだけです。 または、ポート21(FTP)を介して送信されたファイルをインターセプトすることもできます。 DNSトラフィック(UDPポート53)のみをキャプチャしたい場合があります。 ただし、すべてのインターネットトラフィックを盲目的にキャプチャしたい場合はまれです。 関数pcap_compile()



およびpcap_setfilter()



見てみましょう。







プロセスは非常に簡単です。 pcap_open_live()



を呼び出して、スニッフィングセッションを実行した後、フィルターを適用できます。 あなたは、なぜ通常のif



/ else if



式を使用しないのですか? 2つの理由:1つ目は、BAPを直接フィルタリングするため、PCAPフィルターはより効率的です。 したがって、BPFドライバーがこれを直接行うため、必要なリソースははるかに少なくなります。 2番目は、PCAPフィルターが単純であるということです。







フィルターを適用する前に、コンパイルする必要があります。 フィルター条件は、通常の文字列(またはchar



配列)に含まれています。 構文はtcpdump.orgのホームページにかなり詳しく文書化されています。 私はあなた自身の考慮のためにそれをあなたに任せます。 ただし、単純なテスト式を使用します。おそらく、上記の例からこれらの条件の構文規則を独立して導き出すのに十分賢いでしょう。







フィルターをコンパイルするには、 pcap_compile()



関数を呼び出します。 プロトタイプでは、この関数を次のように定義しています。







 int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)
      
      





最初の引数はセッション記述子です(前の例ではpcap_t* handle



)。 次は、コンパイル済みバージョンのフィルターを保存する場所へのポインターです。 次は、通常の文字列形式の式自体です。 次に、フィルター式を最適化するかどうかを決定する整数が入ります(0-いいえ、1-はい)。 最後に、フィルターを適用するネットワークのネットワークマスクを定義する必要があります。 関数はエラー時に-1を返します。 他のすべての値は成功を意味します。







フィルターをコンパイルした後、それを適用する時間です。 pcap_setfilter()



呼び出します。 PCAPの説明形式に従って、この関数のプロトタイプを検討する必要があります。







 int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
      
      





それは非常に単純明快です。 最初の引数はセッション記述子で、2番目はフィルターのコンパイル済みバージョンへのポインターです(これは前のpcap_compile()



関数と同じ変数でなければなりません)。







おそらく、この例は理解を深めるのに役立つでしょう。







PCAPフィルターの設定、コンパイル、および適用の例
 #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, "Can't get netmask for device %s\n", dev); net = 0; mask = 0; } handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf); if (handle == NULL) { fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf); return(2); } if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) { fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle)); return(2); } if (pcap_setfilter(handle, &fp) == -1) { fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle)); return(2); }
      
      





このプログラムは、混合モードでrl0



デバイスのポート23を通過するトラフィックを探知するように構成されています。







前の例にはまだ説明していない関数が含まれていることに気づくかもしれません。 pcap_lookupnet()



は、デバイス名を受け取ると、IPv4ネットワーク番号と対応するネットワークマスクを返す関数です(ネットワーク番号は、ネットワークマスクとのIPv4 ANDアドレスであるため、アドレスのネットワーク部分のみが含まれます)。 フィルターを適用するにはネットマスクを知る必要があるため、これは不可欠です。







私の経験では、このフィルターは一部のオペレーティングシステムでは機能しません。 私のテスト環境では、カーネルを備えたOpenBSD 2.9はデフォルトでこのタイプのフィルターをサポートしていますが、デフォルトのカーネルを備えたFreeBSD 4.3はサポートしていません。 あなたの経験は異なる場合があります。







本物のスニッフィング



現在の段階では、デバイスを識別し、トラフィックをキャプチャするために準備し、フィルターを適用する方法を学びました。 今こそ、いくつかのパッケージを入手するときです。 パケットをキャプチャするには、主に2つの方法があります。 1つのパケットをキャプチャするか、n個のパケットがキャプチャされるまで実行されるループに入ることができます。 まず、1つのパッケージをキャプチャする方法を示し、次にループの使用方法を検討します。 プロトタイプpcap_next()



見てpcap_next()









 u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
      
      





最初の引数はセッションハンドルです。 2番目は、パケットに関する一般情報、具体的には、パケットがキャプチャされた時間、パケットの長さ、および特定の部分の長さ(断片化されている場合など)を含む構造へのポインターです。 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, "Couldn't find default device: %s\n", errbuf); return(2); } /*    */ if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) { fprintf(stderr, "Couldn't get netmask for device %s: %s\n", dev, errbuf); net = 0; mask = 0; } /*      */ handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf); if (handle == NULL) { fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf); return(2); } /*     */ if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) { fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle)); return(2); } if (pcap_setfilter(handle, &fp) == -1) { fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle)); return(2); } /*   */ packet = pcap_next(handle, &header); /*    */ printf("Jacked a packet with length of [%d]\n", header.len); /*   */ pcap_close(handle); return(0); }
      
      





アプリケーションは、 pcap_loockupdev()



を介して受信したデバイスのトラフィックをキャプチャし、無差別モードにします。 パケットがポート23(telnet)上にあることを検出し、ユーザーにパケットのサイズ(バイト単位)を伝えます。 繰り返しますが、プログラムにはpcap_close()



呼び出しが含まれていますが、これについては後で説明します(ただし、かなり理解しやすいものです)。







トラフィックをキャプチャする2番目の方法は、 pcap_loop()



またはpcap_dispatch()



を使用することpcap_loop()



(これらはpcap_loop()



使用します)。 これら2つの関数の使用を理解するには、コールバック関数の概念を理解する必要があります。







コールバック関数は新しいものではなく、多くのAPIで一般的なものです。 コールバック関数の背後にある概念は非常に単純です。 特定の種類のイベントを待機しているプログラムがあるとします。 例として、プログラムがキーの押下を待機するとします。 ユーザーがキーを押すたびに、私のプログラムはこのキーストロークを処理する関数を呼び出します。 これはコールバック関数です。 これらの関数はPCAPで使用されますが、キーが押されたときに呼び出すのではなく、PCAPがパケットをキャプチャするときに呼び出されます。 コールバック関数は、この点で非常に似ているpcap_loop()およびpcap_dispatch()でのみ使用できます。 それらはそれぞれ、パケットがフィルターを通過するたびにコールバック関数を呼び出します(もちろんフィルターがない場合。そうでない場合、キャプチャされたすべてのパケットがコールバック関数を呼び出します)。







プロトタイプpcap_loop()



以下に示します。







 int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
      
      





最初の引数はセッションハンドルです。 次に、キャプチャされる必要があるパケットpcap_loop()



数をpcap_loop()



伝える整数が来ます(負の値は、エラーが発生する前にループを実行する必要があることを示します)。 3番目の引数は、コールバック関数の名前(識別子のみ、パラメーターなし)です。 最後の引数は一部のアプリケーションで役立ちますが、ほとんどの場合、単にNULLに設定されます。 pcap_loop()



が渡す引数に加えて、コールバック関数に渡す引数があるとします。 最後の引数は、それを行う場所にすぎません。 明らかに、正しい結果を得るために、それらをu_char *



型にキャストする必要があります。 後で見るように、PCAPはu_char *



形式で情報を送信するいくつかの興味深い方法を使用します。 PCAPがこれを行う方法の例を示した後、この時点でこれを行う方法が明らかになります。 そうでない場合は、Cヘルプテキストを参照してください。ポインターの説明はこのドキュメントの範囲外です。 pcap_dispatch()



使用法pcap_dispatch()



ほぼ同じです。 pcap_dispatch()



pcap_loop()



の唯一の違いは、 pcap_dispatch()



はシステムから受信した最初の一連のパケットのみを処理し、pcap_loop()はカウンターがなくなるまでパケットまたはバッチを処理し続けることです。 違いの詳細については、公式のPCAPドキュメントを参照してください。







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()



最後の引数で渡される値に関係なく、コールバック関数の最初の引数で渡されます。 2番目の引数はPCAPヘッダーです。これには、パケットがキャプチャされた時期、パケットの大きさなどに関する情報が含まれます。 pcap_pkthdr



構造pcap_pkthdr



、pcap.hファイルで次のように定義されています。







 struct pcap_pkthdr { struct timeval ts; /*   */ bpf_u_int32 caplen; /*   */ bpf_u_int32 len; /*   */ };
      
      





これらの値は合理的に明確でなければなりません。 最後の議論はすべての中で最も興味深いものであり、初心者のプログラマーが理解するのが最も難しいものです。 これはu_char



への別のポインタであり、 pcap_loop()



によってキャプチャされたパケットに含まれるデータセクションの最初のバイトを指します。







しかし、この変数(パケットと呼ばれる)をプロトタイプでどのように使用できますか? パケットには多くの属性が含まれているため、ご想像のとおり、これは文字列ではなく、構造のセットです(たとえば、TCP / IPパケットにはイーサネットヘッダー、IPヘッダー、TCPヘッダー、最後にデータが含まれます)。 このu_char



ポインターは、これらの構造のシリアル化されたバージョンを指します。 それらのいずれかの使用を開始するには、いくつかの興味深い型変換を行う必要があります。







まず、データを構造に取り込む前に、構造自体を決定する必要があります。 TCP/IP Ethernet.







Ethernet, IP, TCP
 /* Ethernet    6  */ #define ETHER_ADDR_LEN 6 /*  Ethernet */ 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 header */ struct sniff_ip { u_char ip_vhl; /*  << 4 |   >> 2 */ u_char ip_tos; /*   */ u_short ip_len; /*   */ u_short ip_id; /*  */ u_short ip_off; /*    */ #define IP_RF 0x8000 /* reserved   */ #define IP_DF 0x4000 /* dont   */ #define IP_MF 0x2000 /* more   */ #define IP_OFFMASK 0x1fff /*     */ u_char ip_ttl; /*   */ u_char ip_p; /*  */ u_short ip_sum; /*   */ struct in_addr ip_src,ip_dst; /*      */ }; #define IP_HL(ip) (((ip)->ip_vhl) & 0x0f) #define IP_V(ip) (((ip)->ip_vhl) >> 4) /* TCP header */ typedef u_int tcp_seq; struct sniff_tcp { u_short th_sport; /*   */ u_short th_dport; /*   */ tcp_seq th_seq; /*   */ tcp_seq th_ack; /*   */ u_char th_offx2; /*  , rsvd */ #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; /*   */ };
      
      





PCAP u_char



? , . ? ( : ).







, , TCP/IP Ethernet. . — , . , . .







 /*  Ethernet    14  */ #define SIZE_ETHERNET 14 const struct sniff_ethernet *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(" * Invalid IP header length: %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(" * Invalid TCP header length: %u bytes\n", size_tcp); return; } payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);
      
      





どのように機能しますか? . u_char



— .







, ,



. , , — sniff_ethernet



,



, . —



Ethernet , 14, SIZE_ETHERNET



.







, , — . IP, Ethernet, . 4- IP. 4- , 4, . 20 .







TCP , 4- , " " TCP, 20 .







, :







VARIABLE LOCATION(in bytes)
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



, ,



. sniff_ip



, sniff_ethernet



,



, sniff_ethernet



(14 SIZE_ETHERNET



). sniff_tcp



, — X



Ethernet, IP . (14 , 4 IP). , ( ) .







, , , . . sniffer.c .







完了



PCAP. PCAP , , , . .







2002. . , :

:

, , .



This document is Copyright 2002 Tim Carstens. All rights reserved. Redistribution and use, with or without modification, are permitted provided that the following conditions are met:

Redistribution must retain the above copyright notice and this list of conditions.

The name of Tim Carstens may not be used to endorse or promote products derived from this document without specific prior written permission.

/ Insert 'wh00t' for the BSD license here /



All Articles