Scapy 入门:用 Python 构造、发送与解析数据包
从分层构造报文、收发探测、实时嗅探到读取 pcap 文件,用 Scapy 把协议知识变成可以运行的代码。
- DATE
- LEN
- 437 字
- TTL
- 1 min
- LAYER
- tools · 工具与实践
Wireshark 让我们“看”报文,而 Scapy 让我们“写”报文。学协议时,能自己构造一个报文并观察对方的反应,理解会深刻得多。
警告
本文涉及的发包、探测操作,请只在自己的实验环境或获得授权的网络中进行。
安装
pip install scapy
# 发送原始报文和抓包需要 root 权限
sudo python3 -c "from scapy.all import *; print(conf.iface)"
用 / 叠加协议层
Scapy 最优雅的设计:用 / 把各层协议叠起来,没写的字段自动填默认值。
from scapy.all import *
pkt = IP(dst="192.0.2.1") / ICMP() / b"hello"
pkt.show() # 展开所有字段(包括自动计算的)
hexdump(pkt) # 十六进制视图
print(len(pkt)) # 20 + 8 + 5 = 33
show() 的输出与 Wireshark 的详情树非常像,len、chksum 这类字段会在序列化时自动计算。
想知道某层有哪些字段,可以用 ls():
ls(TCP)
序列化与解析
raw_bytes = raw(pkt) # 序列化成 bytes
parsed = IP(raw_bytes) # 从 bytes 解析回来
print(parsed[ICMP].type) # 8 (echo-request)
print(parsed.summary())
这在分析未知协议时特别有用:拿到一段字节,丢给 Scapy 看它认不认得。
发送与接收
| 函数 | 层 | 是否等待响应 |
|---|---|---|
send() | 三层 | 否 |
sendp() | 二层 | 否 |
sr() | 三层 | 是,返回 (应答, 未应答) |
sr1() | 三层 | 是,只返回第一个应答 |
srp() | 二层 | 是 |
一个最简单的 ping:
ans = sr1(IP(dst="192.0.2.1") / ICMP(), timeout=2, verbose=0)
if ans:
print(ans.summary(), "TTL =", ans[IP].ttl)
例子:手写一次 SYN 探测
结合上一篇的三次握手知识,我们手动发一个 SYN,看看对方怎么回应:
from scapy.all import IP, TCP, sr1, send, RandShort
target, port = "192.0.2.10", 80
syn = IP(dst=target) / TCP(sport=RandShort(), dport=port, flags="S")
resp = sr1(syn, timeout=2, verbose=0)
if resp is None:
print("无响应:端口可能被过滤")
elif resp.haslayer(TCP) and resp[TCP].flags == "SA":
print("收到 SYN/ACK:端口开放")
# 礼貌地发 RST 结束这次半开连接
send(IP(dst=target) / TCP(sport=resp[TCP].dport, dport=port, flags="R", seq=resp[TCP].ack), verbose=0)
elif resp.haslayer(TCP) and resp[TCP].flags.R:
print("收到 RST:端口关闭")
内核为什么会多发一个 RST?
用 Scapy 发出的 SYN 绕过了内核的 TCP 协议栈。当 SYN/ACK 回来时,内核发现自己根本没有这个连接,会自动回一个 RST。抓包时看到两个 RST 不用奇怪。
实时嗅探
from scapy.all import sniff, DNSQR, IP
def handle(pkt):
if pkt.haslayer(DNSQR):
print(f"{pkt[IP].src:15} 查询 {pkt[DNSQR].qname.decode()}")
# filter 使用 BPF 语法,与 tcpdump 相同;store=False 避免占用内存
sniff(filter="udp port 53", prn=handle, store=False, count=20)
读取 pcap 文件
from scapy.all import rdpcap, TCP
pkts = rdpcap("handshake.pcap")
for p in pkts:
if TCP in p:
t = p[TCP]
print(f"{t.sport:>5} -> {t.dport:<5} flags={str(t.flags):<3} seq={t.seq} ack={t.ack}")
用这个脚本打印出三次握手的报文,你会看到原始的(非相对的)序列号,可以亲手验证 ack = seq + 1。
小结
/叠层、show()查看、raw()序列化、Layer(bytes)解析,是 Scapy 的四个基本动作;sr1适合一问一答的探测,sniff适合被动观察;- Scapy 与 Wireshark 配合使用效果最好:用 Scapy 构造,用 Wireshark 验证。