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