Notes

Do not read codes but patch binary.

windowsでパケットをinterceptするツール

windivert

年末年始休暇を利用して、windowsでパケットをインターセプト(capture + drop)する方法を探していてwindivert(https://github.com/basil00/Divert)というライブラリを見つけた。
npcap/winpcap辺りだと、dropができないようなので。良いツールだが、日本語の紹介記事が無いので書いている。

そもそもの目的としては、解析に辺り、以下の様な点を解消してくれるツールを探していた。

  1. c&cサーバとの通信のプロキシ
    c&cサーバとの通信パケットの中身を別サーバでフックしたい。
  2. SSL通信の可視化
    所謂,SSL inspection。
  3. c&cサーバが死んでいる場合の擬似サーバ設置
    通信したc&cサーバの接続先IPが変更された場合も解析したい。

自動化難易度的には1<2<3である。 windivertは、少なくとも1に関しては、そのままでも満たせそうなツールだ。

プロキシの例

examplesの中でも最も秀逸だと思った物が

Divert/streamdump.c at master · basil00/Divert · GitHub

やっていることは、いわゆる、NAPT(ポートフォワーディング + IP変換)によるプロキシサーバ生成。
コードの簡潔さがすごい。

まず、ハイレベルな文法でcapture対象のパケットを指定する。 今回は、トランスポート層における3つの送受信元先のポート宛てのパケットをcaptureする。

"tcp and "
        "(tcp.DstPort == %d or tcp.DstPort == %d or tcp.DstPort == %d or "
         "tcp.SrcPort == %d or tcp.SrcPort == %d or tcp.SrcPort == %d)",

それぞれ、

  1. captureしたい宛先ポート
  2. proxy serverの受信元ポート
  3. 代理クライアントの送信先ポート

代理クライアントの送信先ポートは実際に送信が行われるポートでは無いが、captureしたいポートへの出力への出力はforwardingされる為、 裏の出口ポートとして設定されている。詳しくは後述。

基本的には、

 handle = WinDivertOpen(filter, WINDIVERT_LAYER_NETWORK, priority, 0);

でcaptureするレイヤーを決定(今回は、L3(IPヘッダ以上)がcaptureできる)し、

WinDivertRecv(handle, packet, sizeof(packet), &packet_len, &addr)

...

WinDivertSend(handle, packet, packet_len, NULL, &addr)

recvとsendの間にfilterする処理を書くという流れ。
recvとsendの間にpacketの内容を書き換えたり、条件により、sendを行わないことでdropできる。

今回の例では、規則として、以下を記述している。

  • 外部への通信(IPヘッダの送信元=ホストのIP & 送信先/=ホストのIP)の場合

規則1. 宛先ポートの通信をインバウンドに変換

送信先ポート : 監視したいポート -> proxy serverのポート
IP : 送信元IP <-> 送信先IP

規則2. プロキシサーバから外部への通信の送信元の書き換え

送信元ポート : proxy serverのポート-> フック対象ポート
IP : 送信元IP <-> 送信先IP

規則3. 代替ポートへの外部向けの送信をフック対象ポートに戻す

送信先ポート : 代替ポート -> フック対象ポート

  • 外部から内部への通信

規則4. フック対象ポートからの通信を代替ポートからの通信へと偽装


以上のルールの元、proxy server(以下,server1), また、フック対象への通信ごとに、別スレッドで肩代わりするクライアント(client0)を設け、 外部のサーバとの接続確立を行い、送受信用に2つスレッドを設ける。
- 内部から外部への通信のためのスレッド(以下,client1)
- 外部から内部への通信のためのスレッド(以下,client2)

TCP handshakeの例

TCPの場合を例に出す。説明のために、フック対象ポートを443とする。

server1 の役割

server1はフック対象元のポートに通信を行ったソケットからの接続を代替する。

  1. あるプロセスから443への接続が行われる。
  2. 通信を行ったsynパケットが規則1により、送信先ポートが変換され、IPも送信先元が反転されるため、proxy serverのポートでlisten中のサーバに届く。
  3. proxy serverは(というかドライバ側で)、syn/ackパケットを1で通信を行ったクライアントの送信元ポートに送信する。
  4. syn/ackパケットは、送信元ポートがproxy serverのポートであり、かつ、2のIP反転により外部向けの通信になっている。 そのため、規則2により、送信元ポートが443に置き換えられ、IPアドレスの送信先/元も反転される。
  5. 1で接続を行ったプロセスがackパケットを再度サーバに送る。
  6. 1と同じ原理で、proxy serverのソケットにパケットが届く。
  7. この段階で、サーバのapplicationレイヤーのaccept処理(https://github.com/basil00/Divert/blob/master/examples/streamdump/streamdump.c#L302)が返る。

client0の役割を説明

7のaccept後、1つの新しく生成されたスレッドと、同スレッドそれぞれでソケットが生成される。

1つは、外部の443への接続を肩代わりする物である。

  1. この通信先は、application側では代替ポート宛てに通信を行うが、 ドライバで規則3によりフック対象ポート443に書き換えられる。規則3はIPの変換はなく、通信はそのまま外部に出ていく。
  2. 生の通信先からのsyn/ackが帰ってくる。通信は、規則4により代替ポートからの通信と解釈される。
  3. 代替ポートに、ackを送り、connect(https://github.com/basil00/Divert/blob/master/examples/streamdump/streamdump.c#L360)関数が返る。

なお、接続が確立すると、スレッドを生成し、1つで実際のサーバとのやりとり、もう一方で、クライアントとのやりとりが行われる。 なお、通常のクライアントは、recvしたソケットに再度sendを行うが、この場合、
送信側 : proxyサーバのソケットでrecvし、(パケットに対して任意の処理を行った後、)実際のサーバとのやりとりに使う外部向けソケットにsendする
受信側 : 外部向けソケットでrecvし、proxyサーバのポート(を送信先)として、(acceptしたポート宛てに)sendする

となっている。

sendした場合

  1. あるプロセスから接続確立済み、外部443ポートへのサーバへパケットを送信する。
  2. 外部向けの通信は規則1により、proxy server向けの通信に変換される。
  3. 生成されたデータは、proxyサーバ上で待つ1つのスレッドに補足され、そのサーバは、代替ポート先へ再度sendを行う。
  4. 代替ポートへのsendは、規則3により443に変換され、送信される。

recvした場合

  1. あるプロセスの接続先サーバからパケットが届く。
  2. 規則4により送信元(サーバの)ポートが代替ポートに変換される。
  3. 代替ポート先にrecvを行っていたスレッドの処理を起こし、そのスレッドが、proxyサーバのソケットを利用してsendする。この場合、送信元がproxy serverになる。なお、IPアドレスは、受信先を送信先に設定するので、外部向け通信となる。
  4. proxyサーバのポートからの外部向けは、規則2より、内部向け、かつ、フック対象ポート(443)に形式上変換される。

補足

  • l2レイヤー(MACアドレス)のcapture,interceptはできません。
  • クライアントの送信元ポートが指定されているプロトコルだと上手く行きません。
  • 自分の理解の為にまとめたけど、実際にpacket captureしながら触ってみないとわかりずらいかも。。