Scapy 实战
2020-07-06 15:19:27
Zeron Wang
在之前的文章中,我们曾简单介绍过 Scapy 这款强大的网络问题复现工具。然而,越强大则意味着越复杂,甚至让人望而却步。本期将通过几个典型的场景和案例来跟大家分享一些使用 Scapy 的技巧和心得。
牛刀小试:发送 Gratuitous ARP
BIG-IP在做 Failover 的时候,会发送 Gratuitous ARP 来通告相邻设备更新其 ARP 缓存。有时候会遇到 Failover 后流量并没有及时切换到新 Active 设备上的问题,这很有可能就是 ARP 缓存问题引起的。假设本机的 IP 地址是 192.168.1.1,MAC 地址是 01:23:45:67:89:00,要复现这个问题,可以通过下列 Scapy 脚本发 GARP 包(GARP 的格式请另行参考相关资料):
from scapy.* import all
my_mac = "01:23:45:67:89:00"
my_ip = "192.168.1.1"
sendp(
Ether(
dst = ETHER_BROADCAST, # ff:ff:ff:ff:ff:ff
src = my_mac
)/
ARP(
op = 1,
hwsrc = my_mac,
psrc = my_ip,
hwdst = ETHER_BROADCAST,
pdst = my_ip
)
)
一旦运行成功,就能在网络接口上抓到如下的包:
可以看到发出的 GARP 包完全符合自定义的内容。
需要注意的是,因为 ARP 工作在第二层,而 send() 是工作在第三层的,因此得用 sendp() 通过二层来发送。
send() / sendp() 还支持一些对于复现或者压力测试非常有用的控制参数,例如:
iface = "eth0" |
强制从接口 eth0 来发包 |
count = 6 |
连续发 6 个包 |
loop = 1 |
如果没有指定 count 参数,则非 0 值将启用无限循环发包功能 |
inter = 1 |
连续或循环发包的间隔(以秒为单位,可使用小数) |
下面是一些示例:
无限发包模式,每秒从 eth0 发一个包(注意这里不能有 count 参数)
sendp(
Ether(
dst = ETHER_BROADCAST,
src = my_mac
),
iface = "eth0",
loop = 1,
inter = 1
)
定量发包模式,总共发 9 个包,每个包间隔 300ms
sendp(
Ether(
dst = ETHER_BROADCAST,
src = my_mac
),
count = 9,
inter = 0.3
)
更复杂的重复发包策略可以配合 Python 语言本身的循环控制语句来实现。
棋逢对手:模拟 TCP 握手
对于 TCP 连接的应用,我们可能会经常遇到诸如握手不成功或者连接不稳定(频繁重传)、数据收发异常的问题,在排查过程中可能会怀疑是否与某个 TCP 参数有关(比如 MSS)。这个时候就需要通过手工构建相应的 TCP 参数(甚至是整个 TCP 包)来复现特定的数据/流量并观察和验证这种的假设。比如,假设本机的 IP 地址是 172.17.2.107,将使用 33333 到 44444 范围内的某个随机端口发起 TCP 连接,对端服务器的 IP 地址是 172.17.64.30,应用程序侦听在 TCP 端口 80。该 TCP 连接需要将 MSS 协商为 1368,并且启用 Timestamp。示例脚本如下:
import sys
import time
from scapy.all import *
src_ip = "172.17.2.107"
src_port = random.randint(33333, 44444)
dst_ip = "172.17.64.30"
dst_port = 80
seq_ini = random.randint(1000000000, 3000000000) # ISN
ts_val = int(time.time()) # Local timestamp
pkt_ip = IP(
flags = "DF",
proto = "tcp",
src = src_ip,
dst = dst_ip
)
pkt_tcp = TCP(
sport = src_port,
dport = dst_port,
seq = seq_ini,
ack = 0,
flags = "S",
options = [ # 1:1 duplicate real traffic
('MSS', 1368),
('SAckOK', '') ,
('Timestamp', (ts_val, 0)),
('NOP', None),
('WScale', 128)
]
)
pkt_syn = pkt_ip / pkt_tcp
pkt_sa = sr1(pkt_syn)
ts_val = int(time.time())
opts = pkt_sa.getlayer('TCP').options
for o in opts:
if o[0] == "Timestamp":
ts_ecr = o[1][0]
pkt_tcp = TCP(
sport = src_port,
dport = dst_port,
seq = seq_ini + 1,
ack = pkt_sa.seq + 1,
flags = "A",
options = [
('NOP', None),
('NOP', None),
('Timestamp', (ts_val, ts_ecr)),
]
)
pkt_ack = pkt_ip / pkt_tcp
send(pkt_ack)
这里我们用随机整数生成器 randint() 构造 TCP 源端口和初始序列号(Initial Sequence Number,ISN),时间采集器 time() 则用来获取当前的系统时间。
此外,我们还用到了 getlayer(),它可以在一个包内区分不同的层级。大家可能会注意到,像 options、flags 以及 chksum 等属性会同时在 IP 层和 TCP 层出现。如果没有为它们指定某个具体的层级,Scapy 默认会去引用第三层(即 IP 层)的对象,这就容易引起混淆。有了显式的 getlayer() 后,就可以非常清晰地指定需要操作的对象,就不会再有数据取不到或者对象不存在等脚本问题了。
这段示例代码(或者说整个 Scapy)中最核心的部分就是包收发器 sr1()。和上一节我们讨论的包发送器 send() 不同,sr1() 可以同时发送并接收数据包,这对于在脚本中交互式地处理网络流量极其重要。其中的 1 代表只接收第一个响应包(例如针对简单的握手通信),而不带 1 的 sr() 则代表接收所有的包(例如大规模数据传输需要分片时)。
该脚本执行后可以得到下面的抓包:
我们还可以对这段代码加以扩展,以便构造更丰富的场景。比如想要模拟校验和数据错误是否会带来问题,可以在 IP(或 TCP)层“制造”一个故意写错的校验和 chksum = 0x9876 来实现:
pkt_ip = IP(
flags = "DF",
proto = "tcp",
chksum = 0x9876,
src = src_ip,
dst = dst_ip
)
此外,对于网络故障中常见的 TCP Zero Window 状态,可以在 TCP 层用 window = 0 来“模拟”:
pkt_tcp = TCP(
sport = src_port,
dport = dst_port,
flags = "A",
window = 0
)
请注意,在构建数据包并复现网络问题时,需要尽可能地将对应的数据与网络抓包中观察到的内容保持一致(包括其顺序),这样才能最大限度地提高复现的成功率。例如在上述示例中,我们只是假设 TCP 的 MSS 协商可能导致问题,但为了遵循 1:1 复现的原则,在构建 TCP 包的 option 部分时,我们完全参照了在出现故障时网络抓包中所观察到的所有 TCP 参数以及它们的顺序。
一旦握手成功后,我们就可以继续使用 send() 或 sr() 来发送应用层的数据了。
pkt_app_response = sr1(pkt_ip/pkt_tcp/"GET / HTTP/1.0\r\n\r\n")
print pkt_app_response.show()
print 语句可以显示包的内容,这样就能方便地检查数据或调试 Scapy 脚本。例如,执行完上述脚本后,print 会将应用程序服务器返回的 pkt_app_response 包显示如下:
Received 4 packets, got 1 answers, remaining 0 packets
###[ IP ]###version = 4ihl = 5tos = 0x0len = 559id = 58764flags = DFfrag = 0ttl = 255proto = tcpchksum = 0xf98fsrc = 172.17.64.30dst = 172.17.2.107\options \###[ TCP ]###sport = httpdport = 35074seq = 904524288ack = 1216565010dataofs = 8reserved = 0flags = PAwindow = 4122chksum = 0xd362urgptr = 0options = [('NOP', None), ('NOP', None), ('Timestamp', (560506553, 1533863831))]###[ Raw ]###load = 'HTTP/1.1 200 OK\r\nDate: Fri, 10 Aug 2018 01:17:11 GMT\r\nServer: Apache/2.2.15 (CentOS)\r\nLast-Modified: Thu, 22 Feb 2018 07:22:05 GMT\r\nETag: "45879-77-565c7e6956140"\r\nAccept-Ranges: bytes\r\nContent-Length: 119\r\nConnection: close\r\nContent-Type: text/html; charset=utf-8\r\n\r\n<html>\n<head>\n<title>Main Page</title>\n<script src="test.js"></script>\n</head>\n<body>\n<p>Main Page</p>\n</body>\n</html>\n'
它会打印出关于这个包的所有详细内容,并且可以像 WireShark 那样对能够识别的协议进行数据的属性整理,可读性很好,在使用中十分方便。
Scapy 如此强大,不亏为网络问题验证和复现的神器。本期通过几个典型案例,为大家打开了这扇沉重的大门,在学习和使用中可以帮助大家尽快上手,让 Scapy 不再遥不可及。
发布评论 加入社群
相关文章

F5 BIG-IP系统的CPU
欧阳晨曦
2020-07-07 21:38:01 2979

回复评论
发布评论