Scapy 入门:用 Python 构造、发送与解析数据包

从分层构造报文、收发探测、实时嗅探到读取 pcap 文件,用 Scapy 把协议知识变成可以运行的代码。

DATE
LEN
437 字
TTL
1 min

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:端口关闭")
SEQSYN 探测的三种结果
Scapy Target SYN SYN, ACK(开放) RST 或者 SYN RST, ACK(关闭) 或者 SYN(被防火墙丢弃)

内核为什么会多发一个 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 验证。
↑↓ 选择 · Enter 打开 · Esc 关闭