背景:手头一个 351,232 字节的
.bin文件,文件名是 OTA 服务器下发的随机 ID。已知信息只有一句话——“一款蓝牙热敏打印机的固件”,其余全靠纯逆向。这是一个学习/练手项目,不打算烧录设备,但每一步都按”真要烧回设备需要什么”的标准来做。最终在不接触实机的前提下,把这个黑盒还原成了一张完整的软件栈全景图。本文记录这套方法论、一个完整的签名逆向案例,以及四条可迁移的教训。
目录
- 起点与原则
- 第一步:固件指纹
- 案例:8 字节 footer 签名逆向
- 安全面扫描
- 逆向成果:完整软件栈
- 四条教训
- 工具选择与项目边界
- 总结
1. 起点与原则
起点:
- 输入:
0VyHq2zPM5I6hkc2.bin,351,232 B(0x55C00) - 已知:“一款蓝牙热敏打印机的固件”
- 没有:芯片型号、厂商资料、文档、原理图、实机
核心原则只有一条:信号 vs 噪声判别。不依赖文件名、不依赖厂商提示,所有结论都要在 bin 本身验证。任何”这看起来像 X”必须给出可证伪的检查。这条原则后面多次救场——固件里充满了诱人的假线索。
目标分两层:
- 理解一台便携蓝牙热敏打印机的完整软件栈
- 建立”未知 MCU 固件 → 可改造”的标准流程
2. 第一步:固件指纹
不急着反汇编。先跑 5 个 pass 的指纹流程,全部用 Python 完成:
- 尺寸 + 整体熵 + 4KB 块熵图 → 分段地图
- 向量表 → 架构 + Flash base + Reset + IRQ 数
- 字符串过滤 → 厂商/协议/RTOS/模块指纹
- 已知 magic 扫描 → 压缩/文件系统块
- footer + padding 边界 → 签名候选位置
2.1 整体熵先行
整体熵 7.105 bits/byte——典型的 MCU 固件(代码 + 字库高熵块混合)。如果熵接近 8,说明要么加密要么压缩,纯静态逆向基本判死刑;7.1 说明明文代码占主体,有得玩。
2.2 向量表读出芯片平台
文件开头 256 字节是标准 Cortex-M 向量表:
| 索引 | 地址 | 含义 |
|---|---|---|
| 0 | 0x20013E78 | Initial SP → RAM ≥ 80KB |
| 1 | 0x080041B1 | Reset_Handler(Thumb)→ Flash base = 0x08000000 |
| 11/14/15 | 实装 | SVCall / PendSV / SysTick 全部存在 → 跑着 RTOS |
| 16+ | … | 48 个外设 IRQ 全部填实 |
最关键的一击来自字符串:gd32e50x interrupt controller——芯片平台直接锁定 GigaDevice GD32E50x(Cortex-M33)。IRQ 数量与异常处理布局也对得上 GigaDevice 的标准 HAL 启动模板,交叉印证。
2.3 字符串里的厂商与协议
HPRT→ 厂商汉印,便携票据打印机大厂COMMENT:Impact Printer;ACTIVE COMMAND:CPCL;→ 打印协议是 CPCL(Zebra Comtec Programming Language)- 完整 AT 指令集(
AT+BAUD=921600、AT+CLASS=100680、AT+OTA=1……)→ 蓝牙是独立串口模组,MCU 通过 UART 发 AT 指令与其通信,而非内置协议栈 - 自检页字段
WeChat App、Easy Pairing、Multiconn→ 中国大陆移动票据/微信小票场景 - 拼写错误
update succes、Simplied Chinese→ HPRT 固件的长期特征,再次交叉印证厂商判断
2.4 熵图分段 = 内存地图
4KB 块熵图把文件切成清晰的段:
| 范围 | 大小 | 内容 |
|---|---|---|
0x00000–0x00100 | 256 B | 向量表 |
0x00100–0x41000 | ~260 KB | 主程序代码(HAL + RTOS + CPCL 解释器 + AT 解析) |
0x44000–0x49000 | 20 KB | GB18030 中文字库(80.7% 字节 ≥ 0x80) |
0x4B000–0x4F000 | 16 KB | 位图字模 / Logo |
0x55BC4–0x55BF8 | 52 B | 零填充 |
0x55BF8–0x55C00 | 8 B | 签名 footer |
几小时前它还是黑盒;现在有了芯片、厂商、协议、RTOS、内存图,以及下一个目标——末尾那 8 个字节。
3. 案例:8 字节 footer 签名逆向
3.1 结构
0x55BC4..0x55BF8 00 ... 00 零填充(52B)
0x55BF8..0x55BFC 9D 57 73 5A sum_le = 0x5A73579D
0x55BFC..0x55C00 85 AC 65 23 xor_le = 0x2365AC85
文件 4 字节对齐,footer 占最末 8 字节。
3.2 先跑便宜假设
看到 8 字节 footer,第一反应不该是 CRC64,而是 (u32 sum, u32 xor) 组合——这是国产便携设备厂商的”标配偷懒”,5 行 Python 就能验证:
import struct
def reseal(fw: bytes) -> bytes:
"""重算 8 字节 footer,返回新 bytes。"""
assert len(fw) % 4 == 0
s = x = 0
for i in range(0, len(fw) - 8, 4):
w = struct.unpack_from("<I", fw, i)[0]
s = (s + w) & 0xFFFFFFFF
x ^= w
return fw[:-8] + struct.pack("<II", s, x)
一次命中。
诚实交代:实际过程是先跑了完整的 CRC 全家桶(各种多项式、反射、初值排列),全部落空后才回头试这个组合,白白浪费一小时。教训直接沉淀成流程:cheap sweep 必须排在 CRC sweep 前面。
3.3 多边界扫描定位签名范围
假设命中后还要确定精确的签名范围。对多个候选边界分别检验 sum/xor:
| 范围 | sum | xor |
|---|---|---|
[0, size-8) | ✓ | ✓ |
[0, size-16) | ✓ | ✓(尾部 8 字节全零,不改变 sum/xor) |
[0, 0x55BC4) | ✓ | ✓(零填充同理) |
[0, 0x55BC0) | ✗ | ✗ |
[0x100, size-8)(跳过向量表) | ✗ | ✗ |
结论:签名范围必须从 offset 0 开始,到 size-8 结束(含零填充)。
3.4 Round-trip 自验是金标准
算法还原的最后一步也是最重要的一步——round-trip 自验:
reseal(original) == original ✓ byte-perfect
mod[0x10000] ^= 0xAB; reseal(mod) → footer 变为 36 57 73 5A 2E AC 65 23
对扰动后的 body 重算 sum/xor → 与新 footer 完全匹配 ✓
正向能复现原签名(证明算法对),扰动能正确重封(证明理解完整)。两条都过,签名算法才算 100% 拿下。
最终结论:无加密、无密钥、无硬件绑定。任意修改固件主体后都能重新封装——这台设备的固件防篡改强度约等于零。
4. 安全面扫描
签名拿下后,对整个安全面做了系统扫描,三个结论都出人意料。
零活跃加密。 固件里确实存在完整的 MD5 常量和一份 256 项全对的 zlib CRC32 反射表——但它们全是 dead code。以 CRC 表为例:全文件搜它的地址作为 32-bit literal,0 引用;搜 movw/movt 加载该地址,0 匹配;搜 ±4KB 内 PC-relative ldr,0 引用。它是从 SDK/libc 链接进来但从未被调用的死代码。存在不等于使用,引用计数才算数。
MCU 不能自写 flash。 全文件找不到 FMC 解锁魔数,app 没有擦写自身 flash 的能力——即使破解了签名,也没法通过 app 自身把改后的固件刷回去。
OTA 与 MCU 无关。 MCU 端的全部 OTA 逻辑就是发一条 AT+OTA=1\r 给蓝牙模组,之后模组独立完成一切。蓝牙 OTA 更新的是模组固件,不是 MCU app。
三条拼起来就是实战约束:改固件容易(重封即可),烧回去难——需要 SWD/JTAG 物理接触,且 bootloader 是否做外层校验未知(本 bin 是 app-only,bootloader 不在其中)。
5. 逆向成果:完整软件栈
后续用 IDA Pro 做了系统性反编译增强(约 250 个函数重命名、120 个变量重命名、200 条注释、76 篇分析笔记),最终把这台打印机还原成了一张完整的全景图:
GD32E50x (Cortex-M33) Flash 343KB @ 0x08000000 SRAM 80KB
PSRAM 2MB @ 0xB0000000 (SQPI)
启动: Reset_Handler → SystemInit → main()
→ GPIO 初始化 → 自研 RTOS 初始化 → 分配 400KB+40KB 打印缓冲
→ BT 模组初始化 → 6 个子系统注册 → LCD 驱动 IC 检测
→ CPCL 任务初始化 → 调度器启动
数据流:
BT 模组 ──EXTI17──→ USART3 (921600) → 环形缓冲
→ bt_data_comm_task → 协议分流
├─ [GS,'J',SOH] 前缀 → 专有帧协议
└─ 其余字节 → CPCL 解释器(vtable 分派,92 处 memcmp 关键字匹配)
→ TEXT / BARCODE / QR / GRAPHICS 渲染到位图
→ 400KB 缓冲 + 2MB PSRAM
→ SPI1 移位 → TPH 打印头(576 dots)+ TIMER0 加热 PWM
→ TIMER2/3 步进电机走纸 → 切刀 GPIO
外设: USART3=BT USART1=Debug SPI1=TPH SPI0=PSRAM SPI2=外部Flash
TIMER0=加热PWM TIMER2/3=电机 ADC0/1=电压/温度/纸张 EXTI17=BT就绪
几个值得一提的发现:
- 自研 RTOS:7 级优先级、8 个软件定时器、TCB 链表调度——不是 FreeRTOS 也不是 RT-Thread
- CPCL 解释器:逐字节状态机 + vtable 分派,35 条命令完整分派表,支持 30+ 代码页(从 PC437 到 GB2312/BIG5,双 14×16 点阵中文字库内嵌)
- JSON 字段描述符表 @
0x080409A8:36 字节 stride 的{char name[32]; void *handler}结构表,把字段名映射到 handler——而 handler 指针其实是被编译器合并进巨型 dispatcher 函数内部的 case label 地址 - LCD 驱动 IC 自适应:从 NVRAM 读配置,IST3931 / ST7567A / NV3023 三选一
理解度也分了三档:启动序列、RTOS 结构、BT 数据管线、协议分层、PSRAM、footer 签名等已完全理解(可直接重写);CPCL handler 细节、TPH 确切 GPIO 映射、电机相序、NVRAM 地址映射部分理解;bootloader、蓝牙模组固件、确切 IRQ 编号则需要硬件访问才能继续。
6. 四条教训
这个项目最值钱的可能不是结论,而是踩出来的四条可迁移教训。
教训 1:字符串名字反映业务概念,不反映实现位置
firmware_crc 这个字符串非常诱人。顺着它反汇编对应 handler(0x080226EF),结果发现代码只是把已存储的 CRC 值格式化成字符串——连续 subs #0x30、除以 10 的魔数——并不计算 CRC。真正的算法在别处。字符串说的是”我是谁”(API 字段名),不是”我在哪被算出来”。
教训 2:dead-code 表会骗人
即第 4 节 CRC 表的故事:一份 256 项全对的标准 CRC32 反射表,引用计数为零。存在 ≠ 使用。
教训 3:内循环形状只是必要条件,常数才是充分条件
找到过一个”教科书式”的 table-driven CRC32 内循环:
loop:
ldrb r3, [r1], #1
uxtb r0, lr
eors r0, r3
ldr.w r0, [ip, r0, lsl #2]
eor.w lr, r0, lr, lsr #8
形状完全正确。但它加载的表熵只有 2.5 bits/byte——CRC 表应该接近 8。那其实是位图/像素展开 LUT。形状像不等于就是,验证表的常数内容才是判决性证据。
教训 4:弱校验试在前
8 字节 footer 优先试 (sum, xor),5 行代码;CRC 全家桶放后面。顺序错了就是一小时。
以及一条贯穿始终的元教训:round-trip 自验是金标准——正向复现 + 扰动重封,缺一不可。
7. 工具选择与项目边界
前期故意不用 Ghidra/IDA,全程 Python + capstone:
- 351KB 在脚本可控范围内
- 流程要固化成可复用的 skill,命令行/Python-only 比 GUI 更容易沉淀
- 不依赖大工具,强迫每个发现都被精确表达——熵值、引用计数、边界扫描表,而不是”Ghidra 说是这样”
进入大规模反编译阶段后才引入 IDA Pro 做系统性增强。
项目边界也要说清楚:本 bin 是 app-only,bootloader 未拿到(可能存在第二层校验);蓝牙模组固件独立,不在分析范围内;全部结论未经实机验证——这是纯静态分析能做到的极限,也是它诚实的样子。
8. 总结
从一个 351KB 的随机命名 bin 出发:指纹五 pass 锁定 GD32E50x + HPRT + CPCL + 自研 RTOS;cheap-sweep 优先策略 5 行 Python 破解 footer 签名;安全面扫描确认零活跃加密、无 flash 自写能力、OTA 与 MCU 无关;最终还原出从蓝牙字节流到打印头加热 PWM 的完整数据通路。
全套流程(指纹、footer 逆向、安全扫描、外设映射)已沉淀为可复用的方法论。下一个未知固件,起点会高得多。
本文整理自 2026-06 固件逆向项目笔记(AI 辅助静态分析),经人工审校。项目为纯学习目的,未对任何实机做修改。