私有通讯协议帧逆向:字节序、CRC 变体与变长字段完全指南

· 约 6 分钟 🧩 HEX 协议帧编解码

抓到一段串口或 TCP 的十六进制报文,对着设备文档却怎么也对不上——这是嵌入式、工业控制、物联网调试里的日常。问题几乎总是收敛到三件事:字节怎么排(字节序)、校验怎么算(CRC 变体)、长度藏在哪(变长字段)。这篇把这三座大山讲透,最后手撕一帧真实的 Modbus-RTU。

配合 HEX 协议帧编解码 工具边读边试效果最好——它用一行一字段的模板描述协议,粘进 HEX 就按字段拆解,也能反过来填值生成报文。

为什么私有协议这么难读

JSON、XML 这类格式自带字段名,拿到就能读。而私有二进制协议为了省带宽和算力,把所有结构约定都写在文档里、不写进数据。wire 上只有一串裸字节:

01 03 04 00 64 00 C8 BA 7A

它可能是地址+功能码+数据+校验,也可能是别的——没有文档或模板,这串字节没有唯一解。所谓”解析私有协议”,本质就是把文档里的字段约定翻译成一份模板,让工具照着切。

第一步:把帧切成字段

模板的最小单位是一行 名称 长度 类型

addr   1    uint              # 从站地址
func   1    uint              # 功能码
count  1    uint              # 后续字节数
data   count hex              # 变长数据,长度 = count
crc    2    crc16/modbus le   # 校验,低字节在前
  • 长度:字节数 / 引用前面某字段名(变长)/ *(吃掉剩余全部)
  • 类型uint int hex ascii utf8 float double bcd bool bits skip + 一组校验类型
  • # 之后是注释

切好之后真正的坑才开始——同样的字节,类型和字节序不同,读出来天差地别。

字节序:90% 的”乱码”根源

整数的大端与小端

多字节整数有两种排法。以 00 64(十进制 100)为例:

字节序含义00 64 读作64 00 读作
大端 BE高位字节在前10025600
小端 LE低位字节在前25600100

Modbus、多数网络协议用大端;x86、多数 MCU 内部用小端。读出的数大得离谱或小得离谱,先怀疑字节序反了

浮点的四种字序(Modbus 重灾区)

32 位浮点占 4 字节,工业设备里这 4 字节有四种主流排法。以 1.0(IEEE754 标准大端为 3F 80 00 00)为例:

修饰符字节排列常见于
abcd3F 80 00 00标准大端
dcba00 00 80 3F标准小端
badc80 3F 00 00字内字节交换
cdab00 00 3F 80字交换,大量 Modbus PLC

根源:Modbus 一个寄存器是 16 位,一个浮点要拆进两个寄存器,两个寄存器谁在前(字交换)和寄存器内两字节谁在前(字节交换)两两组合,就是这四种。读出来不是合理量程内的数,就逐个换字序试。doublebe/le 即可,把字交换修饰符用在整数或 double 上工具会直接报错提醒。

有符号数与二补码

int 类型按二补码解释。单字节 0x9C

  • uint 读:156
  • int 读:156 ≥ 128,减 256 = -100

温度、利润、坐标偏移这类可能为负的字段必须声明成 int,否则负值会被读成一个很大的正数。

变长字段:长度藏在报文里

私有协议里 payload 长度经常写在报文中间的某个字节。处理方法是让变长字段引用那个长度字段的名字

len    1    uint     # 后续数据长度
data   len  hex      # 长度 = len 字段的值

解析时先读出 len,再据此截取 data反向编码时是甜点:你只填 data 内容,len 会自动算成 data 的字节数回填,不用手数;被引用为长度的字段在表里显示”自动”,无需手填。

位域:一个字节塞多个状态

状态寄存器、报警字常把多个 bit flag 压进一个字节。用 bits 拆:

status  1   bits
  > online   1
  > alarm    1
  > mode     2
  > reserved 4

子项从最高有效位到最低位排列,位数合计须等于字段字节数×8。字节 0xA0(二进制 1010 0000)拆出:online=1、alarm=0、mode=2(10)、reserved=0。

BCD:给人看的十六进制

BCD 让每个十进制数字单独占 4 位,所以字节 0x25 在 BCD 里读作 25,而按 uint 读是 37。仪表的时间、表号、金额常用 BCD——看到像日期/计数器、但按 uint 读数值偏大的字段,多半是 BCD

校验:CRC 不是只有一种

Rocksoft 参数模型

CRC 是一族算法,由五个参数完全确定:

参数含义
poly生成多项式
init寄存器初值
refin是否对每个输入字节按位反转
refout是否对最终结果按位反转
xorout结果再异或的值

光”CRC16”就有几十个变体。多项式相同、初值不同也是两种算法——比如 CCITT-FALSE 和 XMODEM 都用多项式 0x1021,但初值一个 0xFFFF 一个 0x0000,结果完全不同。

一个常见困惑:Modbus 的多项式文档里写 0x8005,代码里却是 0xA001——0xA0010x8005 的位反转形式,因为 Modbus 是 refin/refout 都为真的”反射式”CRC,按低位优先实现时用反转后的多项式。所以下面两种写法结果相同(对 "123456789" 都得 0x4B37):

crc  2  crc16/modbus
crc  2  crc(poly=0x8005,init=0xFFFF,refin,refout)

内置变体对照表

类型多项式初值反转备注
crc16/modbus0x80050xFFFF串口仪表标配,校验低字节在前,记得加 le
crc16/ccitt0x10210xFFFFCCITT-FALSE
crc16/xmodem0x10210x0000多项式同上、初值不同
crc80x070x00CRC-8/SMBus
sum累加和,取低 8/16 位
xor逐字节异或(LRC)
crc(...)任意任意任意非标准变体照文档填,位宽由字段字节数决定

校验范围

默认校验从帧首到该校验字段之前的所有字节。如果协议只校验中间一段(比如不含帧头),用 range(起字段..止字段) 指定,含两端:

sum  1  sum range(len..data)   # 只校验 len 到 data

完整实战:手撕一帧 Modbus-RTU

读保持寄存器的响应帧:

01 03 04 00 64 00 C8 BA 7A

模板:

addr   1     uint                  # 从站地址
func   1     uint {3:读保持寄存器}   # 功能码
count  1     uint                   # 后续字节数
data   count hex                    # 寄存器数据
crc    2     crc16/modbus le        # CRC 低字节在前

逐字节分析:

  • 01 → addr = 从站 1
  • 03 → func = 3,枚举显示”读保持寄存器(3)”
  • 04 → count = 后面有 4 字节数据
  • 00 64 00 C8 → data,两个 16 位寄存器:0x0064=100、0x00C8=200
  • BA 7A → CRC。因为是 Modbus,低字节在前,实际 CRC 值是 0x7ABA

工具对前 7 字节(01 03 04 00 64 00 C8)算 CRC16/Modbus 得 0x7ABA,与帧里一致 → 显示 ✓。忘记加 le 是这里最常见的错——会把 CRC 读成 0xBA7A 而判定校验失败。

反向构造:填值生成报文

结构确认后,调试时常要手动发指令。切到编码:在字段表”值”列填入各字段内容点「编码」,CRC 和长度字段自动算好回填,复制 HEX 丢给串口助手即可。填超出字段宽度的值(如 1 字节填 300)会给出截断告警,避免静默出错。

逆向未知协议的实用顺序

完全不知道结构时:

  1. 多帧对比找锚点——不变字节多半是帧头、地址、功能码;跟着变的是 payload。
  2. 末尾试校验——拿整帧(或除校验外的部分)试 Modbus / CCITT / 累加和 / 异或,对上就锁定算法和范围。
  3. 找长度字段——某字节值恰好等于其后剩余字节数,它就是 len,把变长字段挂上去。
  4. 定类型与字序——浮点读不出合理值就换 abcd/dcba/badc/cdab;数值偏大怀疑 BCD 或字节序。

每锁定一段就在模板里固化,逐步逼近完整结构。全程在浏览器本地运行,不上传,处理内部设备协议数据也放心。

配套工具

把字节序、校验、变长这三件事吃透,私有协议逆向就从”碰运气”变成”按部就班”——剩下的只是照着设备文档把字段一行行填进模板。

❓ 常见问题

同一段 HEX 在不同工具里解出来的浮点数为什么不一样?

字节序不同。一个 32 位浮点占 4 字节,这 4 字节怎么排有四种主流方式:abcd(标准大端)、dcba(标准小端)、badc(字内字节交换)、cdab(字交换,很多 Modbus PLC 用)。同样的 00 00 3F 80,按大端读是个极小的非规格化数(≈4.6e-41),按 cdab 读才是 1.0工具默认猜的字序和设备实际用的不一致,就会读出乱七八糟的数。解决办法:在 float 字段后逐个试这四种修饰符,哪种读出来是合理量程内的数值,就是设备实际用的。double 一般只有 be/le 两种。

CRC 算出来总是对不上,是工具算错了吗?

几乎都是参数没对齐,不是算法错。CRC 不是单一算法,而是一族——由五个参数定义(Rocksoft 模型):多项式 poly、初值 init、输入反转 refin、输出反转 refout、结果异或 xorout。光"CRC16"就有 Modbus、CCITT-FALSE、XMODEM、ARC、USB 等几十种变体,多项式相同初值不同就是两种算法。先确认设备文档写的是哪一种:Modbus 用 crc16/modbus,注意它的校验字节在帧里是低字节在前(加 le 修饰符)。文档给了非标准参数就用通用形式 crc(poly=…,init=…,refin,refout,xorout=…) 照抄。还要核对校验范围——是从帧首算还是只算中间一段。

报文长度不固定(中间有个长度字段)怎么解?

让变长字段的长度引用前面那个长度字段的名字。比如 len 1 uint 后面跟 data len ascii——解析时先读出 len 的数值,再据此截取 data 的字节数。反向编码时更省事:你只填 data 的内容,len 会自动算成 data 的实际字节数回填,不用手数。最后一个吃掉剩余全部字节的字段,长度写 * 即可。

一个字节里塞了好几个状态位(bit flag)怎么拆?

bits 类型,下面用 > 名称 位数 缩进逐位声明,从最高有效位到最低位排列。例如 status 1 bits 下接 > online 1> alarm 1> mode 2> reserved 4——子项位数合计必须等于字段字节数×8。0xA0(二进制 1010 0000)就会拆成 online=1、alarm=0、mode=2、reserved=0。这是工业协议里状态寄存器、报警字最常见的结构。

BCD 码和普通十六进制有什么区别?

BCD 是"给人看"的编码:每个十进制数字单独占 4 位(半字节),所以字节 0x25 在 BCD 里就读作十进制 25,而按普通 uint 读是 37(0x25=37)。仪表的时间、表号、金额经常用 BCD,因为它和十进制显示一一对应、不用换算。陷阱:0x25 当成 uint 解会得到 37,量纲全错——看到"日期/时间/计数器但数值莫名偏大"就该怀疑是 BCD。

怎么从零逆向一个完全不知道结构的报文?

先找锚点再逐段试。(1) 收集多帧同类报文对比,不变的字节多半是帧头、地址、功能码、固定长度字段;跟着数据变的是 payload。(2) 末尾 1-2 字节通常是校验,用整帧去试 CRC16/Modbus、CCITT、累加和、异或,能对上就锁定算法和范围。(3) 找长度字段——某个字节的值恰好等于后面剩余字节数(或减去校验位),它就是 len。(4) 剩下的按文档或经验猜类型,浮点读不出合理值就换字序。每锁定一段就在模板里固化,逐步逼近完整结构。

🧩 打开 HEX 协议帧编解码 自定义模板解析/反解析私有通讯协议·uint/int/float/ascii/bcd/位域·CRC16-Modbus/CCITT/累加和/异或·变长长度联动·大小端字交换·本地不上传