第34章:网络协议逆向(三):自定义二进制协议分析、协议字段提取
说实话,做逆向这么多年,我最怕遇到的就是自定义二进制协议。
HTTP、DNS 这些公开协议,文档一抓一大把,Wireshark 还能帮你自动解析。但自定义协议?嗯,那就是另一回事了。你面对的往往是一堆十六进制数据,没有文档,没有注释,甚至连协议设计者都离职了。
这一章,我们就来啃这块硬骨头。
34.1 自定义二进制协议长什么样?
先看一个我实际项目中遇到的例子。某个 IoT 设备的上报数据包,抓包后长这样:
02 01 00 1E 00 0A 01 02 03 04 05 06 07 08 09 0A
00 00 00 64 00 00 01 2C 00 00 00 0F 00 00 00 1E
如果你直接把它当字符串读,那肯定一头雾水。但如果你知道它的结构,一切就清晰了:
| 偏移 | 长度(字节) | 字段名 | 说明 |
|---|---|---|---|
| 0 | 1 | Magic | 固定 0x02,协议标识 |
| 1 | 1 | Version | 协议版本号 |
| 2 | 2 | Length | 整个包长度(大端) |
| 4 | 2 | Sequence | 序列号 |
| 6 | 10 | DeviceID | 设备标识 |
| 16 | 4 | Temperature | 温度值,int32,单位 0.1°C |
| 20 | 4 | Humidity | 湿度值,int32,单位 0.1% |
| 24 | 4 | Battery | 电量百分比,int32 |
| 28 | 4 | Checksum | CRC32 校验 |
你看,一旦字段对齐,数据就变得可读了。温度 0x64 就是 100,对应 10.0°C。湿度 0x012C 是 300,对应 30.0%。
核心思路:自定义二进制协议的本质就是「字节布局」。你只要搞清楚每个字节或字节段代表什么,协议就破了。
34.2 协议逆向的通用方法论
我个人习惯把协议逆向分成三步走。别嫌啰嗦,这三步我用了十年,从来没翻过车。
第一步:找边界
拿到一堆数据,先别急着分析字段。先问自己几个问题:
- 包从哪里开始?有没有固定的魔数(Magic Number)?
- 包到哪里结束?长度字段在哪?
- 包和包之间怎么区分?是固定长度还是变长?
我曾经遇到一个协议,它的魔数居然是 0xDEADBEEF。嗯,设计者挺幽默的。但不管魔数是什么,只要它固定,就是你的突破口。
第二步:找规律
多抓几个包,对比着看。同一个操作,数据包之间哪些字节在变?哪些不变?
- 不变的字节:可能是协议标识、版本号、固定标志位。
- 规律变化的字节:可能是序列号、时间戳、计数器。
- 大范围变化的字节:可能是数据负载、校验值。
举个例子,如果你连续发 10 次请求,发现第 4-5 字节每次加 1,那基本可以断定这是序列号。
第三步:猜字段
有了规律,就可以开始猜了。这里我分享几个常用的「猜法」:
- 长度字段:通常出现在包头的固定位置,值等于整个包长度或负载长度。你算一下实际长度,跟某个字段的值对比,吻合就中了。
- 校验字段:通常在包尾。你改一个字节,校验值就变了。用 CRC 计算器算一下,看是不是标准 CRC。
- 数值字段:温度、湿度、电压这些,通常用 int16 或 int32 表示。你换算一下,看是不是合理的物理值。
小技巧:用 010 Editor 的模板功能,可以快速定义二进制结构。我习惯先写一个 .bt 模板,边分析边调整,效率比纯手工高很多。
34.3 实战:提取协议字段
光说不练假把式。我们拿上面那个 IoT 协议来实战提取字段。
假设我们抓到了三个包:
包1: 02 01 00 1E 00 0A 01 02 03 04 05 06 07 08 09 0A 00 00 00 64 00 00 01 2C 00 00 00 0F 00 00 00 1E
包2: 02 01 00 1E 00 0B 01 02 03 04 05 06 07 08 09 0A 00 00 00 68 00 00 01 30 00 00 00 0F 00 00 00 1E
包3: 02 01 00 1E 00 0C 01 02 03 04 05 06 07 08 09 0A 00 00 00 6C 00 00 01 34 00 00 00 0F 00 00 00 1E
我们来逐字段提取:
- Magic (0-1字节): 三个包都是 0x02,固定。确认是协议标识。
- Version (1-2字节): 都是 0x01,版本号。
- Length (2-4字节): 都是 0x001E = 30。整个包正好 30 字节,吻合。
- Sequence (4-6字节): 0x000A、0x000B、0x000C,每次加 1。序列号确认。
- DeviceID (6-16字节): 三个包完全一样。设备标识确认。
- Temperature (16-20字节): 0x64=100、0x68=104、0x6C=108。换算成温度就是 10.0°C、10.4°C、10.8°C。合理。
- Humidity (20-24字节): 0x012C=300、0x0130=304、0x0134=308。湿度 30.0%、30.4%、30.8%。合理。
- Battery (24-28字节): 都是 0x0F=15。电量 15%,没变。
- Checksum (28-32字节): 都是 0x0000001E。嗯,这里要注意,三个包的校验值一样?不太对劲。我怀疑这个协议根本没做真正的 CRC,只是随便填了个固定值。
避坑指南:我曾经在一个工控协议里遇到过「假校验」。设计者把校验字段固定为 0x00000000,根本没做任何计算。如果你按标准 CRC 去算,永远对不上。遇到这种情况,别钻牛角尖,先假设它没校验。
34.4 用代码实现字段提取
分析完了,就该写代码了。我个人喜欢用 Python,因为 struct 模块处理二进制数据太方便了。
import struct
def parse_iot_packet(data):
# 解包:格式说明
# B: unsigned char (1字节)
# B: unsigned char (1字节)
# H: unsigned short (2字节, 大端)
# H: unsigned short (2字节, 大端)
# 10s: 10字节字符串
# i: int (4字节, 小端)
# i: int (4字节, 小端)
# i: int (4字节, 小端)
# I: unsigned int (4字节, 小端)
magic, version, length, seq, dev_id, temp, hum, batt, cksum = \
struct.unpack('!BBHH10siiiI', data)
return {
'magic': hex(magic),
'version': version,
'length': length,
'sequence': seq,
'device_id': dev_id.hex(),
'temperature': temp / 10.0,
'humidity': hum / 10.0,
'battery': batt,
'checksum': hex(cksum)
}
# 测试
packet = bytes.fromhex(
'0201001E000A0102030405060708090A'
'000000640000012C0000000F0000001E'
)
result = parse_iot_packet(packet)
print(result)
输出结果:
{
'magic': '0x2',
'version': 1,
'length': 30,
'sequence': 10,
'device_id': '0102030405060708090a',
'temperature': 10.0,
'humidity': 30.0,
'battery': 15,
'checksum': '0x1e'
}
你看,代码写起来其实不复杂。关键是你得先分析清楚结构,否则 struct 的格式字符串写错了,解出来的数据全是乱的。
34.5 协议逆向的进阶技巧
上面讲的是最基础的情况。实际项目中,你还会遇到更「恶心」的设计。我列几个常见的:
- 变长字段:前面有个长度字段告诉你后面跟了多少字节。比如 TLV(Type-Length-Value)格式。
- 位域:一个字节里塞了好几个标志位。比如 bit0 表示开关状态,bit1-3 表示模式。
- 加密字段:数据负载被加密了。这时候你得先逆向加密算法,或者找密钥。
- 乱序字段:字段不是按顺序排列的,而是打乱的。嗯,这种设计我见过,设计者说是为了「安全性」。
我的建议:遇到变长字段,先找长度指示器。遇到位域,用二进制编辑器按位看。遇到加密字段,先找有没有密钥交换过程。遇到乱序字段,多抓几个包做差分分析。
34.6 本章知识体系
下面这张图,是我对自定义二进制协议逆向的总结。你看一遍,应该能对整个流程有个全局认识。
这张图把整个流程串起来了。从抓包开始,到找边界、找规律、猜字段、验证,最后写代码实现。如果验证不通过,就回溯到猜字段那一步重新分析。说白了,这就是一个不断试错、不断修正的过程。
最后说一句:自定义二进制协议逆向,七分靠分析,三分靠工具。别一上来就想着写自动化脚本。先把数据看懂了,工具只是帮你省力气的。
公众号:蓝海资料掘金营,微信deep3321