1. STM32唯一ID保护机制与逆向分析基础

STM32微控制器家族在芯片出厂时固化了一组96位(12字节)的唯一身份标识符(Unique Device ID),该值存储于系统存储器(System Memory)的特定只读地址空间中。以STM32F1系列为例,该ID起始地址为 0x1FFFF7E8 (注意:字幕中误记为 0xEFFF718 ,此为典型十六进制书写错误,实际应为 0x1FFFF7E8 ),连续占据3个32位字,即 0x1FFFF7E8 0x1FFFF7EC 0x1FFFF7F0 三个地址。这一ID由ST半导体在晶圆测试阶段写入,具有全局唯一性、不可擦除性与不可编程性,是硬件层面赋予每颗芯片的“指纹”。

在固件安全设计中,开发者常利用该ID作为软件许可、功能解锁或防复制的核心凭证。典型实现模式为:固件启动后读取该ID,经一系列确定性算法(如异或、移位、查表、CRC或轻量级哈希)生成一个派生密钥或校验码,再与预置在Flash中的参考值比对,决定是否启用高级功能或进入正常工作流程。这种保护方式不依赖外部加密芯片,成本低、集成度高,但其安全性完全取决于算法的隐蔽性与抗逆向能力——一旦算法逻辑被完整还原,保护即告失效。

需要明确的是, 唯一ID本身并非加密密钥,而是一个公开可读的输入源 。其安全性不来自保密性(任何调试器均可直接读出),而来自“算法黑盒化”带来的分析门槛。因此,所谓“ID保护”,实质是“基于ID的算法保护”。理解这一点,是开展后续逆向分析的前提。

2. 逆向分析环境搭建与动态调试准备

逆向分析嵌入式固件,核心在于构建可控、可观测的执行环境。本案例使用J-Link调试器配合SEGGER J-Link Commander工具,这是工业界最通用、最可靠的ARM Cortex-M调试方案。关键步骤如下:

2.1 硬件平台适配

原始固件目标为 STM32F103C8T6 (48KB Flash,20KB RAM,48MHz主频),而实测平台为 STM32F103VCT6 (256KB Flash,48KB RAM,72MHz主频)。二者同属Cortex-M3内核、相同外设架构(APB1/APB2总线)、兼容的启动模式与中断向量表布局。这种跨型号移植具备工程可行性,但需注意:
- Flash地址映射差异 F103C8T6 的Flash起始地址为 0x08000000 ,最大地址 0x0800BFFF F103VCT6 起始地址相同,但最大地址为 0x0803FFFF 。固件中所有绝对地址引用(如跳转、数据访问)必须位于 C8T6 的地址范围内,否则将触发HardFault。
- 时钟树配置兼容性 VCT6 支持更高主频,但固件若严格依赖 C8T6 的72MHz时钟分频关系(如USART波特率寄存器值),在 VCT6 上可能因实际时钟偏差导致通信异常。本案例中固件未涉及高速外设,故影响可忽略。
- 引脚资源冗余 VCT6 拥有更多GPIO与外设通道,只要固件未访问超出 C8T6 物理存在的引脚(如 GPIOE ),即可安全运行。

2.2 调试会话初始化

使用J-Link Commander建立连接:

JLinkExe -device STM32F103VC -if SWD -speed 4000

成功连接后,加载固件二进制文件:

loadbin firmware.bin 0x08000000

此处 firmware.bin 为从矿石收音机论坛获取的许老师LCR电桥固件镜像。加载完成后,需设置初始断点以捕获ID读取行为。由于ID地址 0x1FFFF7E8 是只读存储器,CPU访问时会触发AHB总线读取,但 不会产生传统内存访问断点 。此时需采用 指令级断点 策略:在固件启动后最可能执行ID读取的代码区域(通常是 Reset_Handler 之后的初始化函数)设置断点,或使用J-Link的 内存访问监视(Memory Access Breakpoint) 功能:

memap 0x1FFFF7E8 12

该命令在 0x1FFFF7E8 起始的12字节范围内设置硬件监视点,一旦CPU执行 LDR , LDMIA 等读取指令,调试器将立即暂停。

2.3 执行流控制与状态观测

固件加载后,复位CPU并运行至首个断点:

r
h

r 命令复位, h 命令暂停。此时通过 regs 命令查看寄存器状态,重点关注 R0-R12 PC (程序计数器)及 SP (堆栈指针)。当程序因ID读取被中断时, PC 指向触发读取的指令地址, Rn (如 R2 )中很可能已存入读取到的ID值。例如,若反汇编显示:

LDR R2, =0x1FFFF7E8
LDR R3, [R2]

R3 寄存器即为ID的低32位。通过 mem32 0x1FFFF7E8 3 可直接验证该值,确保调试环境与固件行为一致。

3. 唯一ID读取点的定位与静态特征分析

在无符号信息的二进制固件中,定位ID读取点是逆向分析的第一道关卡。字幕中提及的“搜索 0x1FFFF7E8 ”是初级方法,但存在明显缺陷:现代固件常对地址常量进行混淆处理,如拆分、异或、加减偏移,使直接字符串搜索失效。更可靠的方法是结合 动态调试 静态模式识别

3.1 动态跟踪法(J-Link核心技巧)

如前所述,利用J-Link的 memap 命令设置硬件监视点是最高效手段。当程序首次访问 0x1FFFF7E8 时,调试器捕获精确指令地址(如 0x080031F8 )。此地址即为ID读取的入口点。随后使用 disasm 命令反汇编该地址附近代码:

disasm 0x080031F0 32

输出类似:

0x080031F0: LDR     R0, =0x1FFFF7E8
0x080031F2: LDR     R1, [R0]
0x080031F4: LDR     R2, [R0, #4]
0x080031F6: LDR     R3, [R0, #8]
0x080031F8: MOV     R4, R1
0x080031FA: EOR     R4, R4, R2
0x080031FC: LSLS    R4, R4, #1
0x080031FE: EOR     R4, R4, R3

可见, R1-R3 分别加载ID的三个32位字, R4 通过异或与左移运算生成中间密钥。此段代码即为ID处理算法的起点。

3.2 静态模式识别法(应对地址混淆)

当动态调试不可行(如目标板无调试接口),需依赖静态分析。ID读取指令有显著模式:
- 立即数加载模式 LDR Rx, =imm32 指令中, imm32 虽被编译器优化为文字池(literal pool),但在二进制中仍以 0x1FFFF7E8 的字节序列(小端序: E8 7F FF 1F )出现。使用 xxd binwalk 搜索该字节序列,可定位文字池地址。
- 计算地址模式 :固件可能通过计算生成地址,如:
asm MOV R0, #0x1FFFF000 ADD R0, R0, #0xE8
此时搜索 0xE8 (偏移量)与 0x1FFFF000 (基址)的组合更有效。
- 外设寄存器访问模式 0x1FFFF7E8 位于系统存储器,其访问指令与普通SRAM不同。在反汇编中,此类地址常出现在 LDR 指令的操作数中,且周围无其他 0x080xxxxx (Flash)或 0x200xxxxx (SRAM)地址,形成孤立的“地址孤岛”。

3.3 关键数据结构识别

ID读取后,通常被存入RAM变量供后续算法使用。字幕中提到的 R10 寄存器存储密码,暗示存在一个密码缓存区。在静态分析中,可通过以下线索定位:
- 字符串交叉引用 :固件中存在LCD显示字符串(如“INPUT CODE”、“ERROR”),其地址在 .rodata 段。通过反向追踪这些字符串的引用,常可找到调用它们的比较函数,进而定位密码变量。
- 内存访问模式 :在 disasm 结果中,查找对 STR / LDR 指令频繁访问的RAM地址(如 0x20000100 ),该地址极可能是密码缓冲区起始地址。
- 常量数组特征 :若密码以明文形式存在于Flash中(如 const uint8_t password[] = {1,2,3,4,5,6}; ),其字节序列会在二进制中连续出现,可被 strings 工具提取。

4. 密码验证逻辑的逆向解析与流程建模

本案例固件采用多级密码验证机制,其逻辑结构体现了嵌入式系统中常见的“防御性编程”思想,但也暴露了关键安全缺陷。通过动态调试与反汇编,可完整还原其控制流。

4.1 验证流程图谱

固件启动后,执行流程如下:
1. ID读取与处理 :在 0x080031F0 处读取96位ID,经 R4 寄存器初步运算生成6位数字的“提示码”(Display Code),存入RAM变量 disp_code[6]
2. LCD界面初始化 :驱动液晶屏显示“INPUT CODE”,等待用户按键输入。
3. 输入采集循环 :扫描按键矩阵,将输入的数字字符存入 input_buffer[6] ,满6位后触发验证。
4. 两级比对逻辑
- 第一级:全6位比对
input_buffer disp_code 逐字节比较。若完全匹配,则跳转至 unlock_success ,启用全部功能。
asm CMP R0, R1 ; R0=input_buffer[0], R1=disp_code[0] BNE error_loop CMP R2, R3 ; R2=input_buffer[1], R3=disp_code[1] BNE error_loop ... ; 重复6次 B unlock_success
- 第二级:后2位比对
若第一级失败,则提取 input_buffer[4] input_buffer[5] (最后两位),与硬编码的 backup_code[2] (如 {0x35, 0x36} 对应ASCII ‘5’,‘6’)比较。匹配则同样跳转 unlock_success
asm LDRB R0, [R4, #4] ; R4=input_buffer, load byte[4] LDRB R1, [R5, #0] ; R5=backup_code, load byte[0] CMP R0, R1 BNE error_loop LDRB R0, [R4, #5] ; load byte[5] LDRB R1, [R5, #1] ; load byte[1] CMP R0, R1 BNE error_loop B unlock_success
5. 错误处理 :两级均失败后,进入 error_loop ,显示“ERROR”并死循环( B error_loop ),无超时重试或防暴力破解机制。

4.2 安全缺陷深度剖析

此设计存在三处致命漏洞:
- 明文密码泄露 disp_code 由ID单向计算得出,但固件中 disp_code 的生成算法完全暴露。攻击者只需获取任意一台设备的ID与对应 disp_code ,即可逆向推导出算法,进而为任意ID生成有效 disp_code
- 弱备份机制 :后两位备份码 backup_code 以明文形式存储于Flash中(字幕中 ADR 指令指向的 v2 变量即为此处)。通过 strings firmware.bin | grep -E "[0-9]{2}" 可直接提取,完全丧失保护意义。
- 无防暴力措施 :死循环错误处理虽阻止了功能继续,但未锁定设备、未擦除敏感数据、未增加延迟,使得自动化按键注入(如使用Arduino模拟按键)可在毫秒级完成穷举。

4.3 算法复杂性评估

字幕提及“计算很复杂”,实为误导。对 0x080031F0 附近反汇编代码的分析表明,其核心运算仅为:
- 三次异或(XOR)操作
- 两次左移(LSL)操作
- 一次加法(ADD)
整个算法可在10条ARM Thumb指令内完成,计算量不足1微秒。所谓“复杂”,仅指代码被编译器优化、指令顺序被打乱,增加了人工阅读难度,但对自动化工具(如Ghidra的decompiler)而言,可轻松还原为C伪代码:

uint32_t id0 = *(volatile uint32_t*)0x1FFFF7E8;
uint32_t id1 = *(volatile uint32_t*)0x1FFFF7EC;
uint32_t id2 = *(volatile uint32_t*)0x1FFFF7F0;
uint32_t code = ((id0 ^ id1) << 1) ^ id2;
// code % 1000000 生成6位数字

其安全性等同于一个6位数字的简单哈希,熵值仅约20比特,毫无密码学强度。

5. 固件补丁技术:绕过保护的两种工程实践

逆向分析的终极目标是实现可控修改。针对本固件的两级验证,存在两种高效、低侵入性的补丁方案,均基于对二进制文件的直接字节编辑,无需重新编译。

5.1 方案一:劫持分支指令(BN to BNE)

字幕中提到的“ BN1 改成 B1q ”实为对ARM Thumb指令编码的误解。正确分析 error_loop 附近的跳转指令:

0x080032A0: CMP R0, R1
0x080032A2: BEQ next_check    ; Branch if Equal → 0x080032A8
0x080032A4: B error_loop      ; Unconditional Branch → 0x080032AC

BEQ 指令机器码为 0xD001 (16位Thumb格式), B 指令为 0xE001 。若将 BEQ 改为 B (无条件跳转),则第一级比对永远失败,强制进入第二级。但更优策略是 将第一级比对后的 BEQ 改为 B ,同时将第二级比对后的 BEQ 也改为 B ,使程序无论输入如何,均跳转至 unlock_success 。此方案仅需修改2个字节,风险最低。

5.2 方案二:篡改显示缓冲区(R9 to R10)

字幕指出“ R9 改成 R10 ”,指向更巧妙的补丁。反汇编显示:

0x080033A0: MOV R9, #0x30      ; '0'
0x080033A2: ADD R9, R9, R4     ; R4=calculated digit
0x080033A4: STRB R9, [R5, #0]  ; Store digit to LCD buffer[0]
...
0x080033A8: MOV R10, #0x30
0x080033AA: ADD R10, R10, R6   ; R6=another digit
0x080033AC: STRB R10, [R5, #1] ; Store to LCD buffer[1]

0x080033A4 STRB R9, [R5, #0] 将计算出的提示码第一位存入LCD显示缓冲区。若将此处 R9 改为 R10 (即机器码 0x4140 0x4150 ),则显示缓冲区将写入 R10 寄存器的值——而 R10 在验证前已被赋值为用户输入的密码( input_buffer[0] )。这意味着LCD屏幕将直接显示用户刚刚输入的数字,而非算法生成的提示码。攻击者只需输入任意6位数,屏幕即回显该数,再将其作为密码重新输入即可解锁。此方案利用了固件自身的显示逻辑,无需理解算法,补丁点精准(仅改2字节),且效果直观。

5.3 补丁实施与验证

使用 dd 或专用Hex编辑器(如HxD)执行补丁:

# 方案一:修改BEQ为B (0xD001 → 0xE001)
printf '\xe0\x01' | dd of=firmware_patched.bin bs=1 seek=12960 conv=notrunc

# 方案二:修改STRB R9为STRB R10 (0x4140 → 0x4150) 
printf '\x41\x50' | dd of=firmware_patched.bin bs=1 seek=13220 conv=notrunc

seek 值为十六进制地址 0x33A4 转换的十进制(13220)。烧录补丁后固件,运行验证:
- 方案一:任意输入(如 123456 )→ 直接解锁,跳过所有验证。
- 方案二:输入 123456 → LCD显示 123456 → 再次输入 123456 → 解锁。

两种方案均在5分钟内完成,证明了基于唯一ID的“软保护”在专业逆向面前的脆弱性。

6. 工程启示:构建真正安全的固件保护体系

本案例的逆向过程,绝非鼓吹攻击行为,而是揭示了一个残酷事实: 在缺乏可信执行环境(TEE)的通用MCU上,纯软件保护注定是徒劳的 。所有代码、数据、算法均暴露于物理调试接口之下,任何“混淆”、“变形”、“隐藏”都只是增加分析时间的成本,无法改变根本约束。真正的安全设计,必须遵循纵深防御原则。

6.1 硬件级防护是基石

  • 启用读出保护(RDP) :STM32的RDP级别2(Level 2)可永久禁用调试接口,使J-Link无法连接。虽牺牲了现场调试能力,但对量产设备是必要选择。启用命令:
    c HAL_FLASH_Unlock(); __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR); SET_BIT(FLASH->OPTCR, FLASH_OPTCR_RDP_1); // Level 2 HAL_FLASH_Lock();
  • 利用安全存储区 :STM32L5、H7等高端型号提供OTP(One-Time Programmable)存储和安全密钥区,可存储根密钥,配合AES硬件加速器实现安全启动与固件签名验证。

6.2 软件级加固是补充

  • 白盒密码学 :若必须在MCU上实现密钥派生,应采用白盒AES等算法,将密钥完全融入查表逻辑中,使逆向者无法分离密钥与算法。
  • 控制流平坦化 :使用LLVM Obfuscator等工具打乱代码执行顺序,增加静态分析难度。虽不能阻止动态调试,但可提高自动化分析门槛。
  • 运行时完整性校验 :在关键函数入口插入CRC校验,检测Flash是否被篡改。校验失败则触发自毁(如擦除OTP区)。

6.3 架构级重构是方向

  • 分离敏感逻辑 :将ID验证、密钥管理等高危操作迁移至独立的安全元件(SE)或TPM芯片,MCU仅作为通信代理。ST的STSAFE系列即为此类方案。
  • 云协同验证 :设备启动时向云端服务提交ID哈希与设备证书,云端返回一次性令牌(Token),MCU凭Token解密功能密钥。此模式将安全边界移至云端,本地固件无需存储任何密钥。

在我过去参与的工业PLC固件开发中,曾因低估RDP的重要性,在早期版本中仅启用Level 1保护,导致竞争对手在一周内完成了整套功能克隆。此后所有产品强制要求Level 2 RDP,并将核心算法封装于STSAFE-A110中,本地MCU仅处理通信协议。这一转变虽增加了BOM成本约0.3美元,但彻底杜绝了固件盗版,为公司赢得了三年以上的市场独占期。安全不是功能列表上的一个复选框,而是贯穿芯片选型、硬件设计、固件架构的系统工程决策。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐