最近在帮助用户调试一个网络问题的时候,发现一个很好用的发包工具scapy,记录一下使用方法。
我们在调试TCP/IP时, 有时需要发送一些报文,用的工具一般有ping, nping, netcat 等。 如果需要再深入微调IP或传输层的域,或发送一些畸型的报文,以上工具可能就不一定能做到了。 此时我们可以自己用socket 编程,不过太麻烦了,迭代周期太长了。 此时 scapy 是一种不错的选择。
安装
从 https://github.com/secdev/scapy 上直接下载压缩包,再解压到任意目录即可。 如果您用的是Linux, 很有可能已经有了相应的安装包。 详见 https://scapy.readthedocs.io/en/latest/installation.html 。
运行
根据您的主机上安装的是python2 还是python3, 运行其中的 run_scapy_py2 或 run_scapy_py3 即可:
./run_scapy_py2
进入交互式Python 界面。 运行一些简单的函数感觉一下 scapy 的功能:
>>> str(IP())
'E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01'
>>> IP(_)
可以看到IP 头结构:
<IP version=4 ihl=5 tos=0x0 len=20 id=1 flags= frag=0 ttl=64
proto=hopopt chksum=0x7ce7 src=127.0.0.1 dst=127.0.0.1 |>
我们发送一个ICMP 消息:
>>> send(IP(dst="172.25.52.34")/ICMP())
Sent 1 packets.
注意‘/’的使用,它用来连接不同的协议层。 换句话说,‘/’后面的数据是前一层的负载。
一个实例
用户的问题是这样的:主机A 向主机B发送了一个2千多字节的UDP消息,由于MTU的限制, 该消息在主机A上就被分成了两个IP分片, 经过中间路由器的时候,第一个IP分片又被分成了两片,到达主机B的时候是3个IP分片, 结果上层应用没有收到。 要找到原因在哪里,是在IP层还是UDP层被丢弃的。
因为主机B是我所在公司的产品, 我需要验证,这样的3个IP分片包在主机B上受到了怎样的待遇。它们没被上层应用收到的原因是什么。
直接ping一个大包到主机B说明不了问题,必须要用用户原始的数据。 用户已经抓包了,存放在/tmp/fragments.pcap 里, 我们可以用scapy 的rdpcap()读取抓包。
>>> p = rdpcap("/tmp/fragments.pacp")
>>> p
<fragments.pcap: TCP:0 UDP:1 ICMP:0 Other:2>
>>> len(p)
3
p是一个list, 每个成员是一个IP分片包。 我们可以看一下它们的长度:
>>> for i in range(len(p)):
... len(p[i].load)
...
1448
24
875
和内容:
>>> for i in range(len(p)):
... p[i].load
...
p[i].load 就是用户发送的数据(不包括IP头和UDP头)。然后我们可以利用这些数据构造自己的IP分片:
>>> a=IP(dst="172.25.52.34",id=1,flags="MF",frag=0,ttl=255)/UDP(sport=5059,dport=5060,len=2355,chksum=0x8061)/p[0].load
>>> b=IP(dst="172.25.52.34",id=1,flags="MF",frag=182,ttl=255,proto=17)/p[1].load
>>> c=IP(dst="172.25.52.34",id=1,frag=185,ttl=255,proto=17)/p[2].load
根据IP分片的规范, 我们设置了IP头的一些域: dst 指定了目标IP地址, 所有的分片要有一个相同的id,我们在此设置为1, 前两个分片设置了标志MF(more fragments),表示后续还有分片,最后一片不需要设置该标志。 frag 设置的是分片在整个数据包中的偏移量,第一个分片偏移量当然为0, 第二个分片的偏移量是第一个分片中数据的长度,注意frag 在IP头中是一个13位的域,表示的是以8字节为单位的偏移,因此要把实际长度除8, 得到182; 第三个分片的偏移量是第一个和第二个分片是数据的长度相加,再除8, 得到185。 proto为17,表示传输层是UDP。 ttl 设成最大值255,表示IP分片失效的秒数。
另外,第一分片要有一个UDP头,其余分片只要IP头加上数据就可以了, 所以我们构造了一个UDP层:
UDP(sport=5059,dport=5060,len=2355,chksum=0x8061)
这里的源端口、目的端口,长度和 chksum 都是从抓包信息里读出的。
构造好三个分片后,我们就可以发送了:
>>> send(a)
>>> send(b)
>>> send(c)
也可以写一个简单的函数一下子发送:
>>> def send_all():
... send(a)
... send(b)
... send(c)
>>> send_all()
在发送的同时,我们用wireshark 抓包,可以看到IP报文的确发出去了。 但在目的主机 172.25.52.34 上建立的UDP 服务器没有收到这个UDP消息。
由于目标主机上的网络协议就是自己公司的(实验的目的也是为了调试网络协议), 加入调用代码后发现是UDP层检查checksum失败, 把UDP消息丢弃了。 再回过头来看我构造的第一个分片:
>>> a=IP(dst="172.25.52.34",id=1,flags="MF",frag=0,ttl=255)/UDP(sport=5059,dport=5060,len=2355,chksum=0x8061)/p[0].load
这是的 chksum=0x8061 ,我是从用户的抓包里看到的,直接拿来用了。 其实是不对的 。 查资料找到UDP checksum的构成: 它先要构造一个伪IP头,包含目的IP、源IP, UDP长度。 然后这个伪IP头再和UDP头和数据合在一起计算checksum, 由于用户的IP地址和我的环境不同,checksum 当然不同了。 幸好还可以强制让协议栈不计算checksum, 这样至少可以看到一次成功的发收包, 把checksum 设为0:
a=IP(dst="172.25.52.34",id=1,flags="MF",frag=0,ttl=255)/UDP(sport=5059,dport=5060,len=2355,chksum=0)/p[0].load
再发送 a,b,c 这回对方收到UDP消息了。 这说明IP层没有问题,分片都是正确的,问题很可能出在UDP的checksum上。 再参考这个网页https://scapy.readthedocs.io/en/latest/functions.html
计算一下UDP 的checksum, 果然不是0x8061。 很有可能用户的主机在发送UDP的时候,checksum 计算错了。