第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

我们来逐字段提取:

  1. Magic (0-1字节): 三个包都是 0x02,固定。确认是协议标识。
  2. Version (1-2字节): 都是 0x01,版本号。
  3. Length (2-4字节): 都是 0x001E = 30。整个包正好 30 字节,吻合。
  4. Sequence (4-6字节): 0x000A、0x000B、0x000C,每次加 1。序列号确认。
  5. DeviceID (6-16字节): 三个包完全一样。设备标识确认。
  6. Temperature (16-20字节): 0x64=100、0x68=104、0x6C=108。换算成温度就是 10.0°C、10.4°C、10.8°C。合理。
  7. Humidity (20-24字节): 0x012C=300、0x0130=304、0x0134=308。湿度 30.0%、30.4%、30.8%。合理。
  8. Battery (24-28字节): 都是 0x0F=15。电量 15%,没变。
  9. 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 本章知识体系

下面这张图,是我对自定义二进制协议逆向的总结。你看一遍,应该能对整个流程有个全局认识。

自定义二进制协议逆向流程 1. 抓包与采集 2. 找包边界 3. 找变化规律 4. 猜测字段含义 5. 验证与修正 6. 代码实现提取 常见字段类型 魔数 (Magic) 长度字段 (Length) 序列号 (Sequence) 校验值 (Checksum) 设备标识 (Device ID) 数值字段 (温度/湿度等) 标志位/位域 (Flags) 时间戳 (Timestamp) TLV 变长字段 加密负载 填充字节 (Padding) 协议版本号 不匹配则回溯

这张图把整个流程串起来了。从抓包开始,到找边界、找规律、猜字段、验证,最后写代码实现。如果验证不通过,就回溯到猜字段那一步重新分析。说白了,这就是一个不断试错、不断修正的过程。

最后说一句:自定义二进制协议逆向,七分靠分析,三分靠工具。别一上来就想着写自动化脚本。先把数据看懂了,工具只是帮你省力气的。


公众号:蓝海资料掘金营,微信deep3321