3. 数据包构造与解析基础

在Scapy中,数据包不是一段不可拆的字节流,而是一棵由协议层对象逐层嵌套而成的树。理解这棵树的结构,是后续做扫描、嗅探、注入和模糊测试的前提。本节围绕四个协议层(Ether、IP、TCP/UDP、ICMP)展开,讲清楚字段如何读写、层与层如何拼接、如何用show()和summary()观察结构,以及如何用hexdump和原始字节还原真实报文。

一、分层封装:斜杠运算符的本质

Scapy用 / 运算符完成分层封装。它的语义是“把右边的包作为左边包的载荷(payload)”。例如:

from scapy.all import *
pkt = Ether()/IP(dst="192.168.1.10")/TCP(dport=80)/b"GET / HTTP/1.0\r\n\r\n"

这行代码构造了四层:Ether 帧头、IP 头、TCP 头,以及最内层的原始字节载荷。每一层对象都持有对下一层的引用,形成 pkt[Ether].payload 指向 IP、pkt[IP].payload 指向 TCP 的链式结构。需要特别注意的是,/ 只是建立嵌套关系,并不会自动计算校验和、长度等派生字段——这些在发送时才由Scapy根据当前各层内容重新计算。因此,先改字段再发送,顺序不能颠倒。

访问某一层有两种常用写法:按类名索引 pkt[IP],或按层序号索引 pkt[1](Ether为0,IP为1,依此类推)。当同一协议出现多次(如IP-in-IP隧道)时,类名索引返回第一个匹配层,序号索引更精确。

Scapy 分层封装结构与字节视图 对象树(Python 层) Ether dst / src / type IP src / dst / ttl / proto TCP / UDP sport / dport / flags Raw load(原始字节) 封装运算符 / 左层 嵌套 右层 payload 引用链 字节视图(Wire 层) Ether 头 14 字节 dst(6) | src(6) | type(2) IP 头 20 字节(无选项) ver/ihl | tos | len | id | flags/frag | ttl | proto | csum | src | dst TCP 头 20 字节(无选项) sport | dport | seq | ack | offset/flags | win | csum | urg 载荷(Raw) GET / HTTP/1.0\r\n\r\n 关键点: ① 每层对象通过 payload 引用下一层;② 长度、校验和等派生字段在发送时由 Scapy 重算;③ 修改字段后需重新封装或发送,顺序不可颠倒。

二、字段操作:读写与默认值

每个协议层对象都暴露其字段为属性。读取用 pkt[IP].ttl,写入用 pkt[IP].ttl = 64。Scapy为每个字段提供了合理的默认值:Ether 的 dst 默认是广播地址 ff:ff:ff:ff:ff:ff,IP 的 ttl 默认 64、proto 会根据上层自动推导,TCP 的 sport 默认随机、flags 默认 S(SYN)。这些默认值让构造最小包变得简单,但也意味着如果只改 dport 而不改 flags,发出去的可能是 SYN 而非数据包。

字段值支持多种写法。IP 地址可以写字符串 "192.168.1.1",也可以写整数或 Net() 对象;MAC 地址写 "aa:bb:cc:dd:ee:ff";端口写整数。对于需要批量变化的字段,Scapy支持列表和范围:IP(dst=["10.0.0.1","10.0.0.2"]) 会生成两个包,TCP(dport=(80,443)) 会生成端口从80到443的包序列。这种“字段即生成器”的特性,是后续做扫描和模糊测试的基础。

ICMP 层相对简单,核心字段是 type 和 code。type=8 表示 Echo Request,type=0 表示 Echo Reply,code 通常为0。构造一个 ping 包只需 IP(dst="192.168.1.1")/ICMP(),Scapy会自动填充 type=8、code=0 和校验和。

三、show() 与 summary():两种观察粒度

构造完包后,第一件事是确认结构是否符合预期。pkt.show() 输出完整的层树和每个字段的当前值,适合逐字段核对;pkt.summary() 只输出一行摘要,格式类似 Ether / IP / TCP 192.168.1.10:ftp_data > 192.168.1.1:http S,适合在大量包中快速筛选。

两者的分工很明确:show() 用于调试单个包的字段级问题,summary() 用于批量观察和日志记录。在脚本中,常用 for p in pkts: print(p.summary()) 来快速浏览一批包的类型和方向。如果 summary() 显示 Ether / IP / UDP 而不是预期的 TCP,说明封装时写错了层类,应立即回查构造代码。

四、hexdump 与原始字节解析

show() 展示的是Scapy解析后的对象视图,而 hexdump(pkt) 展示的是即将上线传输的真实字节。两者对照,才能确认“对象字段”与“线上字节”是否一致。hexdump 输出每行16字节,左侧是偏移量,中间是十六进制,右侧是ASCII可打印字符。

>>> hexdump(pkt)
0000  FF FF FF FF FF FF 00 0C  29 3A 4B 5C 08 00 45 00  ........).:K\..E.
0010  00 28 00 01 00 00 40 06  00 00 C0 A8 01 0A C0 A8  .(....@.........
0020  01 01 1F 90 00 50 00 00  00 00 00 00 00 00 50 02  .....P........P.
0030  20 00 00 00 00 00                                   .....

逐段解读:偏移 0x00–0x0D 是 Ether 头,前6字节广播 MAC,接着6字节源 MAC,最后 08 00 是 type=IPv4。偏移 0x0E 开始是 IP 头,45 表示版本4、首部长度5(20字节),00 28 是总长度40,40 是 TTL=64,06 是协议号 TCP。偏移 0x22 开始是 TCP 头,1F 90 是源端口8080,00 50 是目的端口80,50 02 中 0x50 是数据偏移5(20字节),0x02 是 flags=SYN。

如果要把原始字节还原成Scapy对象,用 Ether(raw_bytes) 即可。这在处理从pcap文件读取、从socket接收或从十六进制字符串恢复的场景中非常常用。反过来,bytes(pkt) 或 raw(pkt) 可以把对象序列化为字节。注意:序列化时Scapy会重新计算长度和校验和,因此如果手动改过这些字段,序列化后会被覆盖。

实用对照表:调试时按以下顺序排查——先 pkt.summary() 看层结构对不对,再 pkt.show() 看字段值对不对,最后 hexdump(pkt) 看字节布局对不对。三层都一致,包才真正符合预期。
常见陷阱:直接修改 pkt[IP].len 或 pkt[TCP].chksum 后立即发送,Scapy会在发送时重算并覆盖你的值。若确实需要固定这些字段(如构造畸形包做测试),应在发送前用 pkt[IP].len = None 之外的方式,或使用 sendp(pkt, ...) 时确认重算行为,必要时通过 pkt = pkt.__class__(bytes(pkt)) 重新解析来观察实际字节。

掌握以上四点后,你就具备了用Scapy“看见”和“控制”每一个协议字段的能力。后续的ARP扫描、TCP SYN探测、ICMP时间戳请求等任务,本质上都是在这棵分层树上选择正确的层、写入正确的字段、再交给发送函数。字段操作越熟练,构造和排错的速度就越快。