10 KiB
run_toy_protocol.py 功能讲解
run_toy_protocol.py 是一个基于 Netzob 的玩具协议推断示例脚本。它参考 discover_features.rst 教程,把 PCAP 中捕获到的消息导入后,逐步完成字段切分、按命令聚类、字段对齐、长度关系推断,以及协议状态机生成。
脚本建模的协议格式如下:
<CMD/RES name> '#' <4-byte little-endian length> [<data>]
也就是说,每条消息由三类内容组成:
CMD/RES name:命令或响应名称,例如CMDidentify、RESencrypt。#:固定分隔符,用来区分命令名和后续载荷部分。4-byte little-endian length:4 字节小端长度字段。[data]:可选数据字段,长度通常由前面的长度字段描述。
依赖与输入数据
脚本开头通过 sys.path.insert 把本地 Netzob 测试源码目录加入 Python 模块搜索路径:
sys.path.insert(0, '/home/zjz/CCSDS_study/netzob-030/test/src')
随后用:
from netzob.all import *
导入 Netzob 的主要 API。该脚本依赖本地目录 /home/zjz/CCSDS_study/netzob-030/,而不是项目 pyproject.toml 中声明的普通包依赖。
PCAP 文件目录固定为:
PCAP_DIR = "/home/zjz/CCSDS_study/netzob-030/test/resources/pcaps"
脚本读取三个会话文件:
target_src_v1_session1.pcaptarget_src_v1_session2.pcaptarget_src_v1_session3.pcap
其中 session1 和 session2 被合并为训练消息集合:
messages = messages_session1 + messages_session2
session3 没有参与字段推断和聚类,主要用于最后生成 PTA 自动机时展示另一条会话路径。
Step 1:从 PCAP 导入消息
脚本使用 Netzob 的 PCAPImporter.readFile 读取 PCAP:
messages_session1 = list(PCAPImporter.readFile(...).values())
PCAPImporter.readFile 返回的是一个类似字典的对象,脚本只取其中的 .values(),并转成列表。三个列表分别代表三个会话中的原始消息。
这一阶段会打印:
- 每个 session 中的消息数量。
session1 + session2中所有消息的内容。
这一步的作用是把网络抓包中的实际报文转成 Netzob 可以分析的消息对象。
Step 2:按 # 分隔符切分字段
脚本先把所有训练消息放入一个通用 Symbol:
symbol = Symbol(messages=messages)
在 Netzob 中,Symbol 可以理解为一组具有相似格式的消息抽象。初始时,所有消息都被放在同一个符号中,字段结构还没有细分。
随后调用:
Format.splitDelimiter(symbol, String("#"))
这会根据字符 # 把消息切成多个字段。对于本协议,# 前面是命令或响应名称,# 后面是长度字段和可选数据。
切分后,典型字段大致可以理解为:
field[0] = CMD/RES name
field[1] = '#'
field[2] = length + data
脚本会打印切分后的 symbol,用于观察 Netzob 推断出的字段结构。
Step 3:按命令名字段聚类
切分出第一个字段后,脚本使用它作为 key field:
symbols = Format.clusterByKeyField(symbol, symbol.fields[0])
这一步会根据 field[0] 的取值把消息拆成多个更具体的 Symbol。例如:
CMDidentify消息会进入一个单独符号。RESidentify消息会进入另一个符号。CMDencrypt、RESencrypt、CMDdecrypt等也会分别形成自己的符号。
这样做的原因是,不同命令或响应虽然共享大体格式,但其载荷语义可能不同。按命令名聚类后,后续字段对齐和长度关系推断会更准确。
这一阶段会打印:
- 聚类后符号数量。
- 每个符号的名称。
Step 4:对载荷字段做序列对齐
脚本遍历每个聚类后的符号:
for name, sym in symbols.items():
if len(sym.fields) >= 3:
Format.splitAligned(sym.fields[2], doInternalSlick=True)
这里重点处理 sym.fields[2],也就是 # 后面的部分。根据协议格式,这个字段内部通常包含:
4 字节长度字段 + 可选数据字段
Format.splitAligned 会对同一符号下的多条消息做序列对齐,尝试把稳定部分、变化部分、长度字段、数据字段进一步拆开。
例如对于不同长度的 CMDencrypt 消息:
CMDencrypt#\x06\x00\x00\x00abcdef
CMDencrypt#\x0a\x00\x00\x00123456test
序列对齐有机会把 \x06\x00\x00\x00 / \x0a\x00\x00\x00 识别为一个长度相关字段,把后面的 abcdef / 123456test 识别为数据字段。
doInternalSlick=True 表示启用内部清理或简化逻辑,让字段切分结果更规整。
这一阶段会打印每个符号对齐后的结构。
Step 5:查找并应用长度关系
脚本使用:
rels = RelationFinder.findOnSymbol(sym)
对每个符号查找字段间关系。这里最重要的是长度关系,也就是某个字段的数值描述另一个字段的长度。
当找到关系时,脚本会打印类似信息:
<relation_type>: '<x_attribute>' <-> '<y_attribute>'
随后脚本应用找到的第一个关系:
rels[0]["x_fields"][0].domain = Size(rels[0]["y_fields"], factor=1/8.0)
这行代码的含义是:把关系中的 x_fields[0] 设置为一个 Size 域,使它表示 y_fields 的大小。
factor=1/8.0 的作用是把位数换算成字节数。Netzob 内部很多大小计算以 bit 为单位,而协议里的长度字段表示的是 byte 数,因此需要除以 8。
应用关系后,脚本专门打印 CMDencrypt 的调试结构:
print(symbols["CMDencrypt"]._str_debug())
这是为了展示长度关系被写回符号模型之后,字段域发生了什么变化。
Step 6:生成协议自动机
完成消息格式推断后,脚本把所有符号转成列表:
sym_list = list(symbols.values())
然后基于会话序列生成三类自动机。
Step 6a:链式状态自动机
session1 = Session(messages_session1)
abstract1 = session1.abstract(sym_list)
automata_chained = Automata.generateChainedStatesAutomata(abstract1, sym_list)
Session(messages_session1) 表示一条实际通信会话。session1.abstract(sym_list) 会把原始消息序列映射为符号序列,例如把某条具体消息抽象成 CMDidentify 或 RESidentify。
generateChainedStatesAutomata 会按 session1 的顺序生成链式状态机。它更接近“这次会话实际发生了什么”,每一步消息转换通常对应一段状态迁移。
脚本用:
automata_chained.generateDotCode()
输出 Graphviz DOT 格式文本。
Step 6b:单状态自动机
automata_one = Automata.generateOneStateAutomata(abstract1, sym_list)
单状态自动机把消息符号都挂在一个状态周围,结构更粗略。它强调“这些消息都可能出现”,但弱化了严格顺序关系。
这种形式适合做简单协议建模或快速查看符号集合,但表达会话流程的能力不如链式状态机。
Step 6c:PTA 自动机
session3 = Session(messages_session3)
abstract3 = session3.abstract(sym_list)
automata_pta = Automata.generatePTAAutomata([abstract1, abstract3], sym_list)
PTA 是 Prefix Tree Acceptor,即前缀树接收器。这里它基于 session1 和 session3 两条抽象会话生成。
与只使用一条 session 的链式自动机相比,PTA 可以表达多条会话之间的共同前缀和分支路径。脚本中特意用 session3 参与 PTA 生成,是为了展示不同会话路径如何合并进同一个状态模型。
最后同样输出 DOT 代码,可用 Graphviz 转成图片:
python test/run_toy_protocol.py | dot -Tsvg -o out.svg
不过脚本的标准输出不只包含 DOT,也包含大量说明文字和中间结构。因此如果要直接交给 dot,通常需要先单独提取某一段 DOT 输出,或者修改脚本只输出目标自动机的 DOT 内容。
整体执行流程
完整流程可以概括为:
- 从三个 PCAP 文件导入原始消息。
- 用
session1 + session2作为训练数据。 - 把所有训练消息先放进一个通用
Symbol。 - 用
#分隔符切出命令名和载荷部分。 - 根据命令名字段把消息聚类成多个协议符号。
- 对每类消息的载荷字段做序列对齐,进一步拆分长度和数据。
- 自动查找长度字段与数据字段之间的关系。
- 把找到的长度关系写回符号模型。
- 把 session 中的具体消息抽象为符号序列。
- 生成链式、单状态和 PTA 三种协议自动机。
脚本输出内容
运行脚本时,输出主要分为几类:
- PCAP 导入结果:每个 session 的消息数量和训练消息列表。
- 字段切分结果:
splitDelimiter('#')后的Symbol。 - 聚类结果:按命令名拆出来的符号列表。
- 序列对齐结果:每个符号内部的字段结构。
- 关系推断结果:字段间的长度关系。
CMDencrypt调试结构:展示应用Size关系后的模型。- 三段 DOT 代码:分别对应链式状态自动机、单状态自动机和 PTA 自动机。
注意事项
- 路径是硬编码的,脚本只能在当前仓库布局下直接运行。
- 依赖的
netzob来自本地netzob-030/test/src,不是标准安装路径。 messages只包含session1和session2,因此字段模型主要由这两个会话推断出来。session3只在 PTA 自动机阶段使用,用于引入另一条会话路径。rels[0]只应用每个符号找到的第一个关系,如果一个符号存在多个候选关系,脚本不会逐一验证。- DOT 输出和普通日志混在一起,直接管道到
dot可能不能得到有效图像,需要提取 DOT 段或调整脚本输出。
这个文件的定位
这个脚本不是面向生产环境的协议解析器,而是一个学习和演示脚本。它展示了 Netzob 如何从示例流量中推断协议结构,并进一步生成协议状态机。对理解协议逆向、字段抽象、长度关系建模和会话自动机生成很有帮助。