363 lines
8.7 KiB
Markdown
363 lines
8.7 KiB
Markdown
# Tianwen-1-parse-netzob.py 教学说明
|
||
|
||
这个脚本是一个 **Netzob 入门教学脚本**。它使用天问一号原始二进制帧数据做例子,但故意不使用 `Tianwen.ccsds` 里已经写好的协议解析器。
|
||
|
||
它的核心假设是:
|
||
|
||
> 我们只拿到一个 `.u8` 原始二进制文件,不知道 Tianwen-1 的空间帧具体协议,想先用 Netzob 和基础统计方法观察数据结构。
|
||
|
||
脚本位置:
|
||
|
||
```text
|
||
/home/zjz/CCSDS_study/test/Tianwen-1-parse-netzob.py
|
||
```
|
||
|
||
默认输入数据:
|
||
|
||
```text
|
||
/home/zjz/CCSDS_study/Tianwen/tianwen1_frames_20200730.u8
|
||
```
|
||
|
||
## 这个脚本不做什么
|
||
|
||
它不做已知协议解析。
|
||
|
||
具体来说,它不会:
|
||
|
||
- `import Tianwen.ccsds`
|
||
- 调用 `ccsds.AOSFrame.parse(...)`
|
||
- 直接使用已知的 AOS 帧字段定义
|
||
- 直接解释 spacecraft ID、virtual channel、APID 的真实协议含义
|
||
|
||
这样做是为了模拟未知协议逆向分析的第一步。
|
||
|
||
## 这个脚本主要做什么
|
||
|
||
它主要演示:
|
||
|
||
1. 如何把原始 bytes 包装成 Netzob `RawMessage`。
|
||
2. 如何把一组消息放入 Netzob `Symbol`。
|
||
3. 如何用 `Format.splitStatic` 自动观察固定字段和动态字段。
|
||
4. 如何用 `Format.clusterByKeyField` 按候选字段聚类。
|
||
5. 如何用 `Field` 和 `Raw` 手工写一个候选帧结构。
|
||
6. 如何用简单统计辅助 Netzob 分析,比如熵、唯一值数量、最常见字节值。
|
||
|
||
## 运行方式
|
||
|
||
推荐在项目根目录运行:
|
||
|
||
```bash
|
||
/home/zjz/CCSDS_study/ccsds/bin/python /home/zjz/CCSDS_study/test/Tianwen-1-parse-netzob.py
|
||
```
|
||
|
||
查看帮助:
|
||
|
||
```bash
|
||
/home/zjz/CCSDS_study/ccsds/bin/python /home/zjz/CCSDS_study/test/Tianwen-1-parse-netzob.py --help
|
||
```
|
||
|
||
常用参数:
|
||
|
||
```bash
|
||
--frame-size 220
|
||
```
|
||
|
||
如果你已经知道候选帧长,可以直接指定,跳过帧长估计。
|
||
|
||
```bash
|
||
--sample-size 32
|
||
```
|
||
|
||
控制参与统计和 Netzob 分析的帧数。数值越大,结果越稳定,但运行更慢。
|
||
|
||
```bash
|
||
--cluster-sample-size 8
|
||
```
|
||
|
||
只控制 `clusterByKeyField` 演示使用多少帧。Netzob 聚类比较耗时,所以默认较小。
|
||
|
||
## Step 1:读取未知二进制数据
|
||
|
||
脚本首先读取整个 `.u8` 文件:
|
||
|
||
```python
|
||
raw = load_raw_bytes(args.input)
|
||
```
|
||
|
||
此时 `raw` 只是一个很长的 `bytes`,没有帧边界,也没有字段含义。
|
||
|
||
新手可以理解为:
|
||
|
||
```text
|
||
raw = 一大串连续字节
|
||
```
|
||
|
||
## Step 2:估计固定帧长
|
||
|
||
如果不知道每帧多长,脚本会尝试多个候选帧长,例如默认的 180 到 260。
|
||
|
||
核心思路:
|
||
|
||
1. 用候选帧长把字节流切成多行。
|
||
2. 看每一行开头若干字节的统计特征。
|
||
3. 如果帧长猜对了,帧头字段会对齐。
|
||
4. 对齐后的帧头通常比随机载荷更稳定。
|
||
5. 稳定字段的平均熵更低。
|
||
|
||
因此脚本会打印类似:
|
||
|
||
```text
|
||
Top frame-size candidates by low average header entropy:
|
||
size=220 avg_entropy=...
|
||
```
|
||
|
||
这里 `220` 通常会排在最前面。
|
||
|
||
注意:这不是数学证明,只是未知协议分析中的启发式方法。
|
||
|
||
## Step 3:创建 Netzob RawMessage 和 Symbol
|
||
|
||
脚本把切好的每一帧包装成 Netzob `RawMessage`:
|
||
|
||
```python
|
||
messages = [RawMessage(data=frame) for frame in frames]
|
||
```
|
||
|
||
然后把这些消息放进一个 `Symbol`:
|
||
|
||
```python
|
||
symbol = Symbol(messages=messages, name=name)
|
||
```
|
||
|
||
新手可以这样理解:
|
||
|
||
- `RawMessage`:一条原始消息。
|
||
- `Symbol`:一组格式相似的消息。
|
||
|
||
刚开始 Netzob 不知道字段边界,所以一个 Symbol 通常只有一个大字段。
|
||
|
||
## Step 4:字节位置统计
|
||
|
||
在使用 Netzob 自动推断前,脚本先做基础统计。
|
||
|
||
每个字节位置会统计:
|
||
|
||
- `offset`:字节偏移。
|
||
- `unique`:这个位置出现过多少种不同字节值。
|
||
- `entropy`:这个位置的熵。
|
||
- `most common byte values`:最常见的几个字节值。
|
||
|
||
例如:
|
||
|
||
```text
|
||
offset unique entropy most common byte values
|
||
0 2 0.811 0x7d:72, 0x54:24
|
||
```
|
||
|
||
可以粗略理解为:
|
||
|
||
- `unique` 很小:可能是版本、类型、ID、固定标记。
|
||
- `entropy` 很小:字段比较稳定。
|
||
- `unique` 很大:可能是计数器、时间戳、载荷或校验。
|
||
|
||
脚本还会把连续位置合并成候选区间:
|
||
|
||
```text
|
||
bytes 000-003 low-var
|
||
bytes 004-004 dynamic
|
||
bytes 005-007 low-var
|
||
```
|
||
|
||
这一步不是 Netzob 必需的,但很适合新手理解数据形态。
|
||
|
||
## Step 5:使用 Format.splitStatic
|
||
|
||
这一段是 Netzob 的核心教学点之一。
|
||
|
||
脚本调用:
|
||
|
||
```python
|
||
Format.splitStatic(...)
|
||
```
|
||
|
||
它会比较同一个 Symbol 中所有消息的同一字节位置:
|
||
|
||
- 如果所有样本都一样,可能是 static field。
|
||
- 如果样本之间不同,可能是 dynamic field。
|
||
|
||
脚本演示两种模式。
|
||
|
||
### 合并相邻字段
|
||
|
||
```python
|
||
mergeAdjacentStaticFields=True
|
||
mergeAdjacentDynamicFields=True
|
||
```
|
||
|
||
这种模式会把连续的静态字段或动态字段合并。
|
||
|
||
如果样本中几乎每个字节位置都变化过,那么可能会合并成一个很大的 dynamic field。
|
||
|
||
### 不合并相邻字段
|
||
|
||
```python
|
||
mergeAdjacentStaticFields=False
|
||
mergeAdjacentDynamicFields=False
|
||
```
|
||
|
||
这种模式会让每个字节都变成独立字段。
|
||
|
||
这样做的好处是:
|
||
|
||
```text
|
||
fields[0] 对应 byte 0
|
||
fields[1] 对应 byte 1
|
||
fields[2] 对应 byte 2
|
||
```
|
||
|
||
后面做 `clusterByKeyField` 时就可以直接选择某个 byte offset。
|
||
|
||
## Step 6:使用 clusterByKeyField 聚类
|
||
|
||
未知协议分析中,经常需要找一个“分类字段”。
|
||
|
||
例如真实协议里可能有:
|
||
|
||
- 版本字段
|
||
- 消息类型字段
|
||
- spacecraft ID
|
||
- virtual channel ID
|
||
- 命令类型
|
||
|
||
脚本先用统计方法找出低变化候选 offset,然后选择一个字段调用:
|
||
|
||
```python
|
||
Format.clusterByKeyField(bytewise_symbol, bytewise_symbol.fields[key_offset])
|
||
```
|
||
|
||
这会把 key 字段值相同的消息放到同一组。
|
||
|
||
输出类似:
|
||
|
||
```text
|
||
clusters created: 2
|
||
key=0x7d messages=6
|
||
key=0x54 messages=2
|
||
```
|
||
|
||
新手可以理解为:
|
||
|
||
```text
|
||
按某个字节值把帧分组
|
||
```
|
||
|
||
这个过程不能直接证明字段含义,但可以帮助我们发现数据中是否存在不同类别。
|
||
|
||
## Step 7:手工建立候选模型
|
||
|
||
脚本最后演示如何用 Netzob 手工定义一个候选结构:
|
||
|
||
```python
|
||
candidate_header = Field(Raw(nbBytes=6), name="candidate_header_0_5")
|
||
candidate_insert_or_secondary = Field(Raw(nbBytes=8), ...)
|
||
candidate_payload = Field(Raw(nbBytes=...), ...)
|
||
candidate_tail = Field(Raw(nbBytes=4), ...)
|
||
```
|
||
|
||
这里的字段名都带 `candidate_`,意思是:
|
||
|
||
> 这只是猜测,不是已经确认的协议规范。
|
||
|
||
然后脚本创建:
|
||
|
||
```python
|
||
Symbol(name="manual_candidate_tianwen_like_frame", fields=[...])
|
||
```
|
||
|
||
并打印结构树。
|
||
|
||
这一段的重点是学习 Netzob 如何表达协议结构,而不是确认 Tianwen-1 的真实字段。
|
||
|
||
## 主要函数说明
|
||
|
||
### `estimate_frame_size(...)`
|
||
|
||
用于估计固定帧长。
|
||
|
||
它会遍历候选帧长,计算帧头前若干字节的平均熵,并按熵排序。
|
||
|
||
### `slice_frames(...)`
|
||
|
||
把连续 bytes 切成固定长度帧。
|
||
|
||
### `build_symbol(...)`
|
||
|
||
把 Python `bytes` 转成 Netzob `RawMessage`,再放入 `Symbol`。
|
||
|
||
### `byte_statistics(...)`
|
||
|
||
统计每个字节偏移的唯一值数量、熵、最常见字节。
|
||
|
||
### `demonstrate_split_static(...)`
|
||
|
||
演示 Netzob `Format.splitStatic`。
|
||
|
||
### `demonstrate_cluster_by_key_field(...)`
|
||
|
||
演示 Netzob `Format.clusterByKeyField`。
|
||
|
||
### `demonstrate_manual_candidate_model(...)`
|
||
|
||
演示如何用 Netzob `Field` 和 `Raw` 写候选协议结构。
|
||
|
||
## 和 Tianwen-1-frame-analysis-v2.py 的区别
|
||
|
||
`Tianwen-1-frame-analysis-v2.py` 是在已知 Tianwen-1 / CCSDS 解析结构的基础上分析数据。
|
||
|
||
它会使用:
|
||
|
||
```python
|
||
Tianwen.ccsds
|
||
Tianwen.tianwen1_tm
|
||
```
|
||
|
||
而 `Tianwen-1-parse-netzob.py` 是未知协议探索教学脚本。
|
||
|
||
它关注的是:
|
||
|
||
```text
|
||
不知道协议时,如何开始观察数据?
|
||
如何用 Netzob 包装消息?
|
||
如何用 Netzob 初步切字段?
|
||
如何用候选字段聚类?
|
||
如何写候选模型?
|
||
```
|
||
|
||
## 新手总结
|
||
|
||
这个脚本可以理解成一个“协议逆向入门练习”。
|
||
|
||
它的工作流是:
|
||
|
||
```text
|
||
原始二进制文件
|
||
-> 估计帧长
|
||
-> 切成一帧一帧
|
||
-> 包装成 Netzob RawMessage
|
||
-> 放入 Netzob Symbol
|
||
-> 用 splitStatic 看固定/变化字段
|
||
-> 用 clusterByKeyField 按候选字段分组
|
||
-> 用 Field/Raw 写候选协议模型
|
||
```
|
||
|
||
如果你是新手,建议按这个顺序理解:
|
||
|
||
1. 先看 `raw`:它只是原始字节。
|
||
2. 再看 `frames`:它是按候选帧长切出来的帧列表。
|
||
3. 再看 `RawMessage`:每帧被包装成 Netzob 消息。
|
||
4. 再看 `Symbol`:一组消息被认为属于同一类格式。
|
||
5. 再看 `splitStatic`:Netzob 开始帮你找字段变化规律。
|
||
6. 再看 `clusterByKeyField`:Netzob 按候选字段值分组。
|
||
7. 最后看 `Field(Raw(...))`:你开始把观察结果写成候选协议结构。
|
||
|
||
这就是这个文件最重要的教学目的。 |