防逆向基础:在量产固件中关闭调试接口并设置读保护(RDP)的正确姿势

摘要: 安全启动解决了"固件被篡改"的问题,但没有解决"固件被读出和逆向"。攻击者拿到设备后,只需要一个 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%。

三个核心要点:

  1. RDP Level 2 是基础,不是终点,物理攻击依然有效

  2. 反逆向的核心是提高攻击成本,不是追求"不可攻破"

  3. 多层防御 + 冗余校验才能真正保护固件

下篇预告:第 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:渗透测试验证指南

相关推荐
打工仔折腾 AI2 小时前
在 Ubuntu ARM64 部署 Q1 V3.0 MuJoCo 模型:加载、单步推理与执行器响应解析
linux·运维·人工智能·python·深度学习·ubuntu·性能优化
zhangrelay5 小时前
单片机原理与接口技术-2026融合版-从MEGA2560-ESP32与Wokwi到ROS2-Lyrical与具身智能栈
单片机·嵌入式硬件
lhbweilai5 小时前
分析FreeRTOS内核对象API的源码实现(八)
c语言·开发语言·stm32·单片机
艾络迅Acceleronix6 小时前
环境监测终端:STM32+EG800Z AT+ 艾络迅平台实战|多传感器采集联动全流程
c语言·stm32·单片机·嵌入式硬件·物联网·1024程序员节
涂山苏苏⁠6 小时前
stm32复习总结
stm32·单片机·嵌入式硬件
Zwawa6 小时前
一只按键不只“按下”:短按、长按和连按如何共存
c语言·单片机·嵌入式
苏格拉真没有底7 小时前
ESP32 AI MCP调试器
stm32·单片机·嵌入式硬件
深圳老胡7 小时前
压力检测空吸功能硬件方案全解析:主流压力传感器品牌与实现路径
笔记·单片机·代码规范
硅基造物主8 小时前
STM32 从 0 到 1 做智能家居(二):把开发环境一次配好
stm32·mqtt·智能家居·hal库·esp-12f