Files
CCSDS_study/test/run_toy_protocol.md
2026-05-05 21:54:35 +08:00

10 KiB
Raw Blame History

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:命令或响应名称,例如 CMDidentifyRESencrypt
  • #:固定分隔符,用来区分命令名和后续载荷部分。
  • 4-byte little-endian length4 字节小端长度字段。
  • [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.pcap
  • target_src_v1_session2.pcap
  • target_src_v1_session3.pcap

其中 session1session2 被合并为训练消息集合:

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 消息会进入另一个符号。
  • CMDencryptRESencryptCMDdecrypt 等也会分别形成自己的符号。

这样做的原因是,不同命令或响应虽然共享大体格式,但其载荷语义可能不同。按命令名聚类后,后续字段对齐和长度关系推断会更准确。

这一阶段会打印:

  • 聚类后符号数量。
  • 每个符号的名称。

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) 会把原始消息序列映射为符号序列,例如把某条具体消息抽象成 CMDidentifyRESidentify

generateChainedStatesAutomata 会按 session1 的顺序生成链式状态机。它更接近“这次会话实际发生了什么”,每一步消息转换通常对应一段状态迁移。

脚本用:

automata_chained.generateDotCode()

输出 Graphviz DOT 格式文本。

Step 6b单状态自动机

automata_one = Automata.generateOneStateAutomata(abstract1, sym_list)

单状态自动机把消息符号都挂在一个状态周围,结构更粗略。它强调“这些消息都可能出现”,但弱化了严格顺序关系。

这种形式适合做简单协议建模或快速查看符号集合,但表达会话流程的能力不如链式状态机。

Step 6cPTA 自动机

session3 = Session(messages_session3)
abstract3 = session3.abstract(sym_list)
automata_pta = Automata.generatePTAAutomata([abstract1, abstract3], sym_list)

PTA 是 Prefix Tree Acceptor即前缀树接收器。这里它基于 session1session3 两条抽象会话生成。

与只使用一条 session 的链式自动机相比PTA 可以表达多条会话之间的共同前缀和分支路径。脚本中特意用 session3 参与 PTA 生成,是为了展示不同会话路径如何合并进同一个状态模型。

最后同样输出 DOT 代码,可用 Graphviz 转成图片:

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 只包含 session1session2,因此字段模型主要由这两个会话推断出来。
  • session3 只在 PTA 自动机阶段使用,用于引入另一条会话路径。
  • rels[0] 只应用每个符号找到的第一个关系,如果一个符号存在多个候选关系,脚本不会逐一验证。
  • DOT 输出和普通日志混在一起,直接管道到 dot 可能不能得到有效图像,需要提取 DOT 段或调整脚本输出。

这个文件的定位

这个脚本不是面向生产环境的协议解析器,而是一个学习和演示脚本。它展示了 Netzob 如何从示例流量中推断协议结构,并进一步生成协议状态机。对理解协议逆向、字段抽象、长度关系建模和会话自动机生成很有帮助。