ScapyでpcapファイルのTCPシーケンスを再現する

Scapyを使って、PCAP形式ファイル(以降、pcapファイル)に記録されたTCPシーケンスを再現できないか試してみました。結論としては、それなりにTCPシーケンスを再現できた、という感じでしょうか。きちんとTCPシーケンスを再現できるとは思いませんが、「同じTCPセッションを再現したい」ときの一つの方法としてメモしておきます。

ちなみに、普段コードを書いていないため、Pythonスクリプトには自信がないです(--; 間違い等がありましたら、ご指摘いただけると嬉しいです。

動作環境

TCPシーケンスを再現させた動作環境は下図の通りです。ノートPCからルータのWeb管理画面にアクセスしたときのHTTP通信をpcapファイルに保存して、それをScapyで再現しました。使用したpcapファイルをここにおいておきます。

ScapyによるTCPシーケンスの再現

まずScapyを送信するパケットを、pcapファイルから切り出します。送信元IPアドレス(192.168.0.104)を基にフィルタして、別途pcapファイルに保存します。以下のPythonスクリプトで、このpcapファイルを送信します。

このPythonスクリプトは、次の流れで動作します。

  1. パケットからIPおよびTCPヘッダのチェックサムを削除する(チェックサムを再計算させるため)。
  2. iptablesにRSTパケットを破棄するルールを追加する(SANS Diary の記事*1を参照)。
  3. TCPシーケンスのパケットを一つずつ送信する。
    1. multi、timeoutパラメータを付与して、srp()を実行する。
    2. SYN-ACKパケットを受信した場合:
      • 次に送信するパケットのACK番号=受信したTCPパケットのシーケンス番号+1
    3. SYN-ACKパケット以外を受信した場合:
      • 次に送信するパケットのACK番号=シーケンス受信したパケットのうち最も大きいシーケンス番号+TCPペイロード
  4. iptablesから2で追加したルールを削除する。
#!/usr/bin/env python

import os
from scapy.all import *

packets=rdpcap("./192.168.0.1_http_src.pcap")

### 1
for i in range(len(packets)):
	del(packets[i][IP].chksum)
	del(packets[i][TCP].chksum)

### 2
rstrule="-p tcp --sport " + str(packets[0].sport) + " --tcp-flags RST RST -j DROP"
os.system("iptables -A OUTPUT " + rstrule)

### 3
acknum = 0
for i in range(len(packets)):
	try:
		### 3.1
		ans,unans = srp(packets[i],multi=1,timeout=0.1)

		if len(packets) == i+1:
			break

		if len(ans) == 0:
			packets[i+1][TCP].ack = acknum
		else:
			for j in range(len(ans)):
				### 3.2
				if ans[j][1][TCP].flags == 18:
					packets[i+1][TCP].ack = ans[j][1][TCP].seq+1
					acknum = packets[i+1][TCP].ack
					break;

				### 3.3
				if ans[j][1][TCP].seq >= acknum:
					if "Raw" in ans[j][1]:
						packets[i+1][TCP].ack = ans[j][1][TCP].seq+len(ans[j][1][Raw])
					else:
						packets[i+1][TCP].ack = ans[j][1][TCP].seq
					acknum = packets[i+1][TCP].ack
	except:
		print traceback.format_exc(sys.exc_info()[2])
		break;

### 4
os.system("iptables -D OUTPUT " + rstrule)


上記Pythonスクリプトを実行すると、次のような結果となります。ScapyによるTCPシーケンスを再現したときに改めてパケットキャプチャを実施しました。そのときのpcapファイルをここにおいておきます。いくつかのパケットもロストしていますね:(

root@root:~# python -V
Python 2.6.5
root@root:~# python tcpsess_replay.py
Begin emission:
Finished to send 1 packets.
*
Received 1 packets, got 1 answers, remaining 0 packets
Begin emission:
Finished to send 1 packets.

Received 0 packets, got 0 answers, remaining 1 packets
Begin emission:
Finished to send 1 packets.
****
Received 4 packets, got 4 answers, remaining 0 packets
(snip)
Received 0 packets, got 0 answers, remaining 1 packets
Begin emission:
Finished to send 1 packets.

Received 0 packets, got 0 answers, remaining 1 packets

おまけ:Scapyメモ

Scapyについては、下記にドキュメントがあります。

個人的にメモしておきたい点だけ以下にまとめました。ls(), lsc() だけ忘れなければ、それらの出力結果からドキュメントを調べればよいと思いました。

root@root:~# scapy
Welcome to Scapy (2.1.0)
>>> 

>>> ls()
ARP        : ARP
ASN1_Packet : None
BOOTP      : BOOTP
CookedLinux : cooked linux
DHCP       : DHCP options
DHCP6      : DHCPv6 Generic Message)
(snip)

>>> lsc()
arpcachepoison      : Poison target's cache with (your MAC,victim's IP) couple
arping              : Send ARP who-has requests to determine which hosts are up
bind_layers         : Bind 2 layers on some specific fields' values
corrupt_bits        : Flip a given percentage or number of bits from a string
corrupt_bytes       : Corrupt a given percentage or number of bytes from a string
defrag              : defrag(plist) -> ([not fragmented], [defragmented],
(snip)

>>> ls(IP)
version    : BitField             = (4)
ihl        : BitField             = (None)
tos        : XByteField           = (0)
len        : ShortField           = (None)
id         : ShortField           = (1)
flags      : FlagsField           = (0)
frag       : BitField             = (0)
ttl        : ByteField            = (64)
proto      : ByteEnumField        = (0)
chksum     : XShortField          = (None)
src        : Emph                 = (None)
dst        : Emph                 = ('127.0.0.1')
options    : PacketListField      = ([])
>>> ls(TCP)
sport      : ShortEnumField       = (20)
dport      : ShortEnumField       = (80)
seq        : IntField             = (0)
ack        : IntField             = (0)
dataofs    : BitField             = (None)
reserved   : BitField             = (0)
flags      : FlagsField           = (2)
window     : ShortField           = (8192)
chksum     : XShortField          = (None)
urgptr     : ShortField           = (0)
options    : TCPOptionsField      = ({})

>>> help(rdpcap)

Help on function rdpcap in module scapy.utils:

rdpcap(filename, count=-1)
    Read a pcap file and return a packet list
    count: read only <count> packets

*1:"Second, because these are crafted packet, the real TCP stack on your host will send RESETS to the unexpected responses. You have to add some IPTABLES rules to block these RESETS from the real TCP stack."という記述があります