MCP协议数据包结构:帧头、负载与校验和
好,咱们今天来聊聊MCP协议的数据包结构。说实话,搞协议逆向这么多年,我见过太多人一上来就盯着二进制流猛看,结果越看越晕。其实啊,只要把数据包拆成三块——帧头、负载、校验和,事情就清晰多了。
我个人习惯把MCP数据包比作一个快递包裹。帧头就是快递单,告诉你从哪来、到哪去、包裹多大;负载就是里面的货物;校验和呢,就是封箱胶带上的防拆标签,确保货物没被调包。
MCP帧头格式
帧头是数据包的“身份证”。我当年逆向第一个MCP设备时,就是靠帧头里的魔数(Magic Number)确认了协议版本。来看一下标准帧头的结构:
| 偏移量 | 字段名 | 长度(字节) | 说明 |
|---|---|---|---|
| 0 | Magic | 2 | 固定值 0x4D43("MC") |
| 2 | Version | 1 | 协议版本号,当前为0x01 |
| 3 | Flags | 1 | 标志位:0x01=请求,0x02=响应,0x04=错误 |
| 4 | Payload Length | 2 | 负载长度(小端序) |
| 6 | Sequence | 2 | 序列号,用于去重和重传 |
| 8 | Checksum | 2 | 帧头校验和(CRC-16) |
嗯,这里要注意:帧头总共10个字节。我见过有人把Checksum算到负载里,结果怎么都对不上。记住,帧头校验和只覆盖前8个字节。
关键点:Magic字段是协议识别的第一道关卡。我在项目中遇到过某厂商魔改了Magic值,导致标准解析器全部失效。所以做逆向时,第一步就是抓包看Magic。
MCP负载格式
负载部分就灵活多了。它由TLV(Type-Length-Value)结构组成。说白了,就是每个数据块都带标签、长度和内容。为什么用TLV?因为扩展性好,你想想看,加一个新功能只需要定义一个新的Type值就行。
负载的通用格式如下:
+--------+--------+--------+--------+--------+--------+
| Type | Length | Value (可变长度) |
| (1字节)| (2字节)| |
+--------+--------+--------+--------+--------+--------+
常见的Type值:
- 0x01:设备信息查询
- 0x02:传感器数据上报
- 0x03:控制指令下发
- 0x04:心跳包
- 0xFF:自定义扩展
我曾经逆向过一个智能电表,它的负载里嵌套了三层TLV。一开始我以为是协议设计有问题,后来才发现是为了兼容不同厂商的扩展功能。所以啊,解析负载时一定要递归处理。
实战技巧:写解析器时,先按TLV拆开,再根据Type分发处理。别想着一次性解析完,容易漏字段。
MCP校验和计算
校验和这块,我踩过不少坑。MCP用的是CRC-16/IBM算法,多项式是0x8005。但有些设备实现时用了不同的初始值,导致校验总是不通过。
标准计算步骤:
- 初始化CRC寄存器为0x0000
- 对每个字节进行异或和移位操作
- 最终结果取反(有些实现不取反)
来看一个C语言实现:
uint16_t mcp_crc16(const uint8_t *data, size_t len) {
uint16_t crc = 0x0000;
for (size_t i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0x8005;
} else {
crc >>= 1;
}
}
}
return crc;
}
避坑指南:我曾经因为校验和初始值没对上,排查了整整两天。后来发现某厂商的设备用了0xFFFF作为初始值。所以做模拟测试时,一定要先验证校验和算法是否和官方一致。
完整数据包示例
咱们来看一个实际抓包的数据:
帧头: 4D 43 01 02 00 14 00 01 12 34
负载: 01 00 05 48 65 6C 6C 6F
校验和: A5 B6
解析一下:
- Magic: 0x4D43("MC")
- Version: 0x01
- Flags: 0x02(响应包)
- Payload Length: 0x0014(20字节)
- Sequence: 0x0001
- 帧头校验和: 0x1234
- 负载: Type=0x01, Length=0x0005, Value="Hello"
- 负载校验和: 0xA5B6
嗯,这里要注意:负载长度是20字节,但实际负载只有7字节(1+2+5)。剩下的13字节是填充数据,用于对齐。我刚开始做逆向时,被这种填充搞晕过好几次。
核心总结:MCP数据包结构其实不复杂,但细节决定成败。帧头校验和只覆盖前8字节,负载用TLV组织,校验和算法要确认初始值。把这些搞清楚了,逆向和模拟测试就成功了一半。
个人经验:做模拟测试时,我习惯先写一个校验和验证工具。把抓到的包丢进去,能快速确认协议实现是否正确。省得后面调试时到处找bug。