摘要: 安全启动解决了"固件被篡改"的问题,但没有解决"固件被读出和逆向"。攻击者拿到设备后,只需要一个 J-Link,就能读出完整固件、反汇编、找到算法和密钥。本文系统拆解量产固件的反逆向体系------从 RDP 级别选择、调试接口关闭、Flash 完整性自检,到代码混淆、字符串加密、反调试陷阱。特别指出一个常见误区:很多人以为设置了 RDP Level 2 就安全了,但实际上仍有 5 种绕过方式。包含可移植代码和渗透测试验证方法,建议收藏。
一、引子:RDP Level 2 也挡不住的攻击
去年做了一款安全审计工具,客户是某医疗器械公司。他们产品的固件已经设置了 RDP Level 2(STM32 的最高读保护),按理说攻击者无法读出 Flash。
但客户发现,市面上出现了功能几乎完全一样的仿制品。
这意味着固件被逆向的可能性极高。我们被请去做渗透测试,看看怎么突破 RDP Level 2。
测试过程:
第一步,接上 J-Link,确认 RDP Level 2 已生效------读出全 0,正常。
第二步,检查 PCB,寻找调试接口以外的攻击面:
-
BOOT0 引脚:可以通过拉高进入 Bootloader 模式
-
SWD 引脚:物理上仍在,只是逻辑上被禁用
-
电源引脚:可以测量功耗
-
时钟引脚:可以注入异常时钟
第三步,尝试几种绕过方式:
方式一:故障注入(Fault Injection)
在芯片启动过程中,用电磁脉冲干扰 RDP 检查逻辑,让它误判为"未启用"。
方式二:电压毛刺(Voltage Glitching)
在特定时刻拉低 VDD,让 RDP 检查代码跳过或执行错误。
方式三:时钟毛刺(Clock Glitching)
注入异常时钟,让 CPU 执行意外指令。
方式四:Bootloader 漏洞
ST 的 System Memory Bootloader 有已知 CVE,某些版本在特定条件下会泄露 Flash 内容。
方式五:侧信道攻击(Side Channel)
通过测量功耗或电磁辐射,反推 Flash 内容。
结果:用方式一和方式二,我们在 3 天内成功绕过了 RDP Level 2,读出了部分固件。
结论:RDP Level 2 不是终点,只是起点。 真正的反逆向需要多层防御。本文完整还原这次渗透测试的发现和加固方案。
二、RDP 的真相
2.1 RDP 的级别与含义
STM32 的 RDP(Read Protection)有三个级别:

Level 1 的实际效果:
-
SWD 可以连接,但读 Flash 时返回全 0 或全 1
-
试图读取会触发 Flash 擦除(连 Bootloader 一起擦)
-
但设备可以继续正常运行
Level 2 的实际效果:
-
SWD 完全禁用(连不上)
-
JTAG 完全禁用
-
BOOT0 拉高也无法进入 System Bootloader
-
芯片变成"黑盒"
常见误区: 很多人以为 Level 2 就万无一失。实际上 Level 2 只是提高了门槛,不能完全阻止物理攻击。
2.2 RDP Level 2 的五个绕过方式
绕过方式一:故障注入(Fault Injection)
原理: 在芯片启动时,RDP 检查逻辑会执行一段代码判断是否启用保护。用电磁脉冲或激光在特定时刻干扰这段代码,让它跳过检查。
需要的设备:
-
ChipWhisperer(约 2000 美元)
-
或自制电磁脉冲发生器(约 500 美元)
难度: 中等。需要精确控制干扰时间和位置。
成功概率: 30-60%(取决于芯片型号和防护措施)。
绕过方式二:电压/时钟毛刺(Glitching)
原理: 在 RDP 检查的瞬间,让 VDD 短暂跌落(如从 3.3V 跌到 2.0V),让 CPU 执行错误指令或跳过检查。
需要的设备:
-
ChipWhisperer + 电压毛刺扩展
-
或 FPGA 自制毛刺发生器
难度: 中等偏高。需要精确时序。
成功概率: 20-50%。
绕过方式三:Bootloader 漏洞
原理: ST 的 System Memory Bootloader 在不同版本中有已知漏洞:
-
CVE-2020-xxxx:某些版本的 bootloader 在特定命令下会泄露 Flash 内容
-
通过 UART/USB 发送特制命令,绕过 RDP 读取 Flash
需要的设备: 一台 PC + UART 转接板。
难度: 低(有现成 PoC)。
成功概率: 取决于 bootloader 版本,已知漏洞版本 100% 成功。
绕过方式四:侧信道攻击(Side Channel)
原理: 通过测量芯片的功耗或电磁辐射,反推执行指令和访问的数据。
-
SPA(Simple Power Analysis):观察单次操作的功耗曲线
-
DPA(Differential Power Analysis):统计多次操作的功耗差异
-
CPA(Correlation Power Analysis):相关性能量分析
需要的设备:
-
示波器(高采样率)
-
电磁探头
-
分析软件(如 Riscure Inspector)
难度: 高。需要专业知识。
成功概率: 对无防护的实现 80%+。
绕过方式五:物理开盖 + 光学读取
原理: 用化学方法腐蚀芯片封装,用显微镜直接读取 Flash 存储单元。
需要的设备:
-
化学实验室(腐蚀封装)
-
电子显微镜
-
探针台
难度: 极高。费用 10 万美元以上。
成功概率: 100%(但代价高)。
结论: 对于消费级产品,RDP Level 2 已经足够;对于安全关键产品,需要多层防御。
2.3 防御策略:提高攻击成本
反逆向的核心不是"不可攻破",而是"攻击成本 > 收益"。

三、反逆向七层防御体系
3.1 第一层:RDP Level 2 + 禁用 System Bootloader
这是最基础也是最重要的一层。
// 生产固件中的 RDP 设置
void Secure_EnableRDP(void)
{
HAL_FLASH_Unlock();
HAL_FLASH_OB_Unlock();
// 设置 RDP Level 2(不可逆)
FLASH->OPTSR_PRG = (FLASH->OPTSR_PRG & ~FLASH_OPTSR_RDP_Msk)
| (0xCC << FLASH_OPTSR_RDP_Pos); // 0xCC = Level 2
// 禁用 System Bootloader(防止通过 BOOT0 进入)
FLASH->OPTSR_PRG |= FLASH_OPTSR_nSWBOOT0; // 忽略 BOOT0
FLASH->OPTSR_PRG |= FLASH_OPTSR_nBOOT0; // 强制从主 Flash 启动
HAL_FLASH_OB_Launch(); // 生效(可能触发复位)
}
禁用 System Bootloader 的效果:
-
攻击者无法通过拉高 BOOT0 进入 ST 的 Bootloader
-
彻底堵住 Bootloader 漏洞这条路
-
代价:量产设备无法通过 UART/USB 升级固件,必须通过应用层 IAP
注意: 这一步应该在产品量产固件里设置,开发阶段保持可调试。
3.2 第二层:Flash 完整性自检
目的: 如果攻击者通过故障注入让 RDP 检查失效,Flash 自检能发现 Flash 被修改。
// flash_integrity.c
// Flash 完整性检查:启动时校验关键区域的 CRC32
#include <stdint.h>
#define FLASH_BASE 0x08000000
#define BOOTLOADER_SIZE (32 * 1024) // 32 KB
#define APP_BASE (FLASH_BASE + BOOTLOADER_SIZE)
// 存储的 CRC 值(编译时嵌入)
extern const uint32_t g_bootloader_crc;
extern const uint32_t g_app_crc;
int Flash_VerifyIntegrity(void)
{
// 检查 Bootloader 自身
uint32_t crc = CRC32_Calculate((uint8_t *)FLASH_BASE, BOOTLOADER_SIZE);
if (crc != g_bootloader_crc) {
return -1; // Bootloader 被篡改
}
// 检查应用程序
// 应用大小从元数据读取(见第 14 篇)
uint32_t app_size = *(uint32_t *)(APP_BASE + 8);
crc = CRC32_Calculate((uint8_t *)(APP_BASE + 64), app_size);
if (crc != g_app_crc) {
return -2; // 应用被篡改
}
return 0;
}
// 在 main 的最开始调用
int main(void)
{
if (Flash_VerifyIntegrity() != 0) {
// 完整性失败:不执行任何用户代码
// 进入安全模式(如闪烁 LED 后复位)
Safety_Lockdown();
}
// 正常启动
// ...
}
关键点:
-
CRC 值在编译时嵌入,攻击者改代码后 CRC 不匹配
-
冗余检查:Bootloader 自身也校验
-
失败后不跳转:进入死循环或复位,绝不执行被篡改的代码
3.3 第三层:调试接口的物理加固
物理层面:
// 在硬件设计上:
// 1. SWD 引脚在量产固件中配置为 GPIO 或模拟输入
// 2. 焊盘在 PCB 上隐藏或故意破坏(激光切割、覆铜屏蔽)
// 3. 使用 BGA 封装(引脚难以接触)
void Secure_DisableDebugPins(void)
{
// 将 SWDIO 和 SWCLK 配置为普通 GPIO 或模拟输入
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_13 | GPIO_PIN_14; // SWDIO, SWCLK
gpio.Mode = GPIO_MODE_ANALOG; // 模拟输入,不消耗电流
gpio.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &gpio);
// 关闭调试接口时钟
__HAL_RCC_DBGMCU_CLK_DISABLE();
}
但注意: RDP Level 2 一旦启用,SWD 引脚实际上已经被硬件禁用,上面的代码是"双保险"。
3.4 第四层:关键代码和数据加密
目的: 即使固件被读出,也无法直接使用。
方案一:关键函数运行时解密
// 关键算法在 Flash 中加密存储,运行时解密到 RAM 执行
__attribute__((section(".encrypted_code")))
static const uint8_t s_encrypted_algo[] = { 0x3A, 0x7F, ... };
void RunCriticalAlgo(void)
{
static uint8_t decrypted[512];
// 用 OTP 中的 AES 密钥解密
AES_Decrypt(s_encrypted_algo, decrypted, sizeof(s_encrypted_algo));
// 跳转到解密后的代码
typedef void (*algo_t)(void);
algo_t algo = (algo_t)decrypted;
algo();
// 立即清零
memset(decrypted, 0, sizeof(decrypted));
}
方案二:字符串加密
固件中的敏感字符串(如密钥、协议命令、错误信息)应该加密存储:
// ❌ 明文:攻击者可以直接 strings 提取
const char *api_key = "sk-1234567890abcdef";
// ✅ 加密:运行时解密
// 工具:xor_encrypt.py 生成加密后的字节
static const uint8_t s_api_key_enc[] = { 0x3A, 0x7F, 0x21, ... };
static const uint8_t s_key_xor[] = { 0xAA, 0x55, ... };
void GetApiKey(char *out, size_t len)
{
for (size_t i = 0; i < len; i++) {
out[i] = s_api_key_enc[i] ^ s_key_xor[i % sizeof(s_key_xor)];
}
out[len] = '\0';
}
方案三:常量表的运行时计算
// ❌ 明文表:攻击者可以 extract
const uint8_t s_crc_table[256] = { 0x00, 0x07, 0x0E, ... };
// ✅ 运行时计算:只存多项式,运行时生成表
void BuildCRCTable(uint8_t *table, uint8_t poly)
{
for (int i = 0; i < 256; i++) {
uint8_t crc = i;
for (int j = 0; j < 8; j++) {
crc = (crc & 1) ? ((crc >> 1) ^ poly) : (crc >> 1);
}
table[i] = crc;
}
}
3.5 第五层:反调试陷阱
反调试的核心: 在固件中埋下"陷阱",当检测到异常调试行为时执行自我保护。
陷阱一:时间检测
// 如果某个关键操作耗时异常,说明可能被调试器暂停
int TimeSanityCheck(void)
{
uint32_t start = DWT_GetCycle();
// 执行一个已知耗时的操作(约 1000 周期)
for (volatile int i = 0; i < 100; i++);
uint32_t elapsed = DWT_GetCycle() - start;
if (elapsed > 5000) { // 正常应该 < 2000 周期
return -1; // 检测到调试暂停
}
return 0;
}
陷阱二:看门狗保护
// 关键操作前喂狗,如果操作耗时过长(被调试),看门狗复位
IWDG_Refresh();
CriticalOperation();
IWDG_Refresh();
陷阱三:异常行为自毁
// 检测到异常时,清除关键密钥/配置
void Anti_DebugResponse(void)
{
// 清除 RAM 中的密钥
memset(s_crypto_keys, 0, sizeof(s_crypto_keys));
// 擦除 Flash 中的配置
Flash_EraseConfig();
// 复位
NVIC_SystemReset();
}
陷阱四:检测调试寄存器
// 某些调试状态可以通过寄存器读取
int IsDebuggerAttached(void)
{
// CoreDebug->DHCSR 的 C_DEBUGEN 位
if (CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk) {
return 1;
}
return 0;
}
注意: 反调试手段如果误触发,可能让正常固件无法启动。必须充分测试。
3.6 第六层:代码混淆
目的: 让反汇编后的代码难以理解。
方法一:控制流平坦化
把顺序执行改成状态机,跳转关系难以静态分析:
// 原始代码
void Original(void) {
a();
b();
c();
}
// 混淆后
void Obfuscated(void) {
int state = 0;
while (1) {
switch (state) {
case 0: a(); state = 1; break;
case 1: b(); state = 2; break;
case 2: c(); state = 3; break;
case 3: return;
}
}
}
方法二:不透明谓词
插入恒真或恒假的复杂判断,干扰反编译器:
// 这个条件恒为真,但反编译器难以判断
if (((x * x) % 4) != 0 || x < 0) {
// 正常执行
}
方法三:指令替换
用等价但更难理解的指令序列替换原指令:
// 原始
a = b + 1;
// 混淆
a = b - (-1);
// 或
a = (b ^ 0xFF) - 0xFE;
推荐工具:
-
Obfuscator-LLVM:基于 LLVM 的混淆框架
-
Tigress:C 代码混淆工具
-
VMProtect:商业级虚拟化保护(嵌入式不常用)
3.7 第七层:冗余校验与主动防护
冗余校验: 关键检查做多次,用不同的实现方式:
// RDP 检查做三次,用不同方法
int CheckRDP_Redundant(void)
{
int result1 = (FLASH->OPTSR_CUR & FLASH_OPTSR_RDP) == (0xCC << FLASH_OPTSR_RDP_Pos);
int result2 = ReadRDPFromBackupRegister();
int result3 = (uint32_t)(*(uint16_t *)OTP_RDP_FLAG) == EXPECTED_MAGIC;
// 三个结果必须全部一致
if (result1 == result2 && result2 == result3) {
return result1;
}
return -1; // 检测到不一致,可能被攻击
}
主动防护: 检测到攻击时执行对抗措施:
void Anti_TamperResponse(void)
{
// 1. 清除 RAM 中的关键数据
memset(g_secrets, 0, sizeof(g_secrets));
// 2. 擦除部分 Flash(让固件不可用)
// 注意:这会触发 OTP 保护,谨慎使用
// 3. 让芯片进入死循环,但不响应调试器
while (1) {
__WFI(); // 进入低功耗模式
}
}
四、渗透测试验证
反逆向措施的效果必须通过渗透测试验证。 推荐的验证流程:
4.1 测试清单

4.2 实测结果
针对我们加固后的固件:

结论:多层防御显著提高了攻击成本。
五、生产流程与自检
5.1 量产固件的检查清单
编译时:
- □ 开启
-Os优化(减少可分析的代码) - □ 关闭调试符号(
--strip-debug) - □ 启用 LTO(跨模块优化,更难分析)
- □ 关闭 assert(避免泄露内部信息)
烧录时:
- □ 设置 RDP Level 2
- □ 禁用 System Bootloader
- □ 禁用 SWD/JTAG
- □ 关闭调试时钟(DBGMCU)
运行时:
- □ Flash 完整性自检
- □ 时间检测(反调试)
- □ 看门狗保护
- □ 关键字符串加密
- □ 关键函数运行时解密
PCB 设计:
- □ SWD 焊盘隐藏或破坏
- □ 使用 BGA 封装
- □ 增加金属屏蔽层
5.2 自检测试
烧录后运行自检程序:
// selftest.c
void Security_SelfTest(void)
{
// 1. 验证 RDP 已启用
if ((FLASH->OPTSR_CUR & FLASH_OPTSR_RDP) != (0xCC << FLASH_OPTSR_RDP_Pos)) {
ReportError("RDP not enabled");
}
// 2. 验证 System Bootloader 已禁用
if (!(FLASH->OPTSR_CUR & FLASH_OPTSR_nSWBOOT0)) {
ReportError("System bootloader not disabled");
}
// 3. 验证调试时钟已关闭
if (DBGMCU->CR != 0) {
ReportError("Debug clock still enabled");
}
// 4. 验证 Flash 完整性
if (Flash_VerifyIntegrity() != 0) {
ReportError("Flash integrity check failed");
}
// 5. 验证关键函数解密正常
if (TestDecryption() != 0) {
ReportError("Decryption test failed");
}
}
每个产品在出厂前跑一遍自检,确保所有保护生效。
六、避坑清单

七、小结与预告
本文从一个真实的固件仿制案例出发,系统拆解了量产固件的反逆向七层防御体系------RDP Level 2、Flash 完整性自检、调试接口物理加固、代码加密、反调试陷阱、代码混淆、冗余校验。特别指出 RDP Level 2 的五个绕过方式和对应的防护措施。通过渗透测试验证,多层防御把攻击成功率从 30-100% 降到 < 5%。
三个核心要点:
-
RDP Level 2 是基础,不是终点,物理攻击依然有效
-
反逆向的核心是提高攻击成本,不是追求"不可攻破"
-
多层防御 + 冗余校验才能真正保护固件
下篇预告:第 16 篇《敏感数据存储:不要明文写密钥在代码里,利用芯片唯一 ID(UID)派生加密密钥的轻量级方案》
将拆解嵌入式系统中敏感数据(密钥、证书、校准参数)的安全存储方案。从最不安全的明文硬编码,到 UID 派生密钥、安全芯片(ATECC608A)存储、OTP 存储,逐步演进。包含完整可移植代码、实际攻击演示和性能对比。适合做物联网、安全设备、智能硬件的开发者。
本文是《嵌入式系统调优高手课》的付费内容。 完整专栏收录 20 篇深度长文,涵盖 RTOS 调度器汇编级拆解、内存池设计、Cache 一致性、低功耗陷阱与安全启动。每一篇都包含真实事故复盘、可移植源码和实测数据。如果你不想再靠"重启试试"解决问题,这个专栏就是为你写的。
交付物清单(code/ 目录):
-
rdp_config.c/h:RDP 和调试接口保护配置 -
flash_integrity.c/h:Flash 完整性自检 -
anti_debug.c/h:反调试陷阱实现 -
obfuscation_examples.c:代码混淆示例 -
selftest.c:安全自检程序 -
pentest_guide.md:渗透测试验证指南