F5社区-F5技术交流中心

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,将使用 3333344444 范围内的某个随机端口发起 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(),它可以在一个包内区分不同的层级。大家可能会注意到,像 optionsflags 以及 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 = 4
ihl = 5
tos = 0x0
len = 559
id = 58764
flags = DF
frag = 0
ttl = 255
proto = tcp
chksum = 0xf98f
src = 172.17.64.30
dst = 172.17.2.107
\options \
###[ TCP ]###
sport = http
dport = 35074
seq = 904524288
ack = 1216565010
dataofs = 8
reserved = 0
flags = PA
window = 4122
chksum = 0xd362
urgptr = 0
options = [('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

Login

手机号
验证码
© 2019 F5 Networks, Inc. 版权所有。京ICP备16013763号-1