Skip to content

NRF52 电压故障注入绕过 APPROTECT 复现(CVE-2020-27211)

公司里有产品用 nRF52 做信任根,关键密钥存放在芯片内部。业务侧开启了 APPROTECT(AP PROTECT) 后,外部无法再通过 SWD 直接读取 Flash/RAM,从而在设备被物理拆解时仍能保护密钥。

公开研究表明:在 上电早期 对核心电源施加电压毛刺,可能扰乱 APPROTECT 的初始化锁定流程,使调试访问临时恢复,完成 OpenOCD/GDB 连接与固件 dump。本文用于回答:该绕过是否可行、复现难度/成本如何、对产品风险意味着什么

目标:在 APPROTECT 开启(SWD 读保护)的前提下,通过对 nRF52 核心电源(DEC1/VCORE)进行电压故障注入,在上电早期扰乱 AHB-AP 锁定流程,使调试访问在本次上电周期内临时恢复,从而实现 OpenOCD/GDB 连接与固件 dump

演示视频: https://b23.tv/A95MaCd


背景:APPROTECT 与读保护

nRF52 为避免 nRF51 上出现的调试口相关漏洞,引入 APPROTECT:当保护开启时,芯片通过硬件方式阻断调试模块与 CPU 的通信,从而拒绝 SWD 调试访问。

从 ARM CoreSight / DAP(Debug Access Port)的视角看,调试访问通常分两类端口:

  • CTRL-AP(Control Access Port):更“管理向”,不依赖 AHB-AP,常用于执行 recover/eraseall 之类的操作。
  • AHB-AP(AHB Access Port):真正用于调试与内存访问的端口,通过 SWD 进行 Flash/RAM/寄存器读写、halt/run、断点等。

APPROTECT 主要目的就是让 AHB-AP 在受保护状态下不可用,因此 OpenOCD 常见报错是“找不到 MEM-AP / 无法控制 core”。

debug_and_trace_overview

开启 APPROTECT

nRF52 的调试口读保护由 UICR.APPROTECT 控制,寄存器地址为 0x10001208。其语义非常简单:默认值 0xFFFFFFFF 表示未启用;写入 0xFFFFFF00(低 8 位为 0x00)表示启用 APPROTECT。该配置在 首次写入后需要复位/重新上电 才会被硬件初始化流程应用。

enable_approtect

开启方式有三类:其一是在固件中通过 NVMC 写入 UICR,然后触发系统复位;其二是通过烧录工具写包含 UICR 段的镜像(注意很多 IDE 产物 HEX 默认不包含 UICR 段,直接改 Keil 输出往往无效)或者是直接用调试工具对该地址写一个 word,例如 OpenOCD flash fillw 0x10001208 0xFFFFFF001nrfjprog --memwr 0x10001208 --val 0xFFFFFF00

第一种方法:在固件中开启示例(首次写 UICR 后重启生效):

c
#include "nrf_nvmc.h"

void SWD_protect(void)
{
    if (NRF_UICR->APPROTECT == 0xFFFFFFFF)
    {
        nrf_nvmc_write_word((uint32_t)&(NRF_UICR->APPROTECT), 0xFFFFFF00);
        nrf_delay_ms(1000);
        NVIC_SystemReset();
    }
}

关闭 APPROTECT 的条件

与之相对,官方“解除 APPROTECT”路径通常等价于 recover/eraseall:通过 CTRL-AP 触发 ERASEALL,或直接使用 nrfjprog -f NRF52 --recover。这一步会擦除 Flash + RAM。本文后续电压故障注入要证明的是:在不执行 recover/擦除、且 0x10001208 仍保持 0xFFFFFF00 的前提下,仍可在本次上电周期内 临时恢复 AHB-AP(MEM-AP)访问

再关闭 APPROTECT 的情况下,你可以直接读取固件和 RAM,我建议使用 j-flash 烧录器和 segger 开发环境,使用很方便。

jmem_read

漏洞概述

CVE-2020-27211 漏洞核心是利用 nRF52840 上电复位早期的硬件初始化特性:该阶段由硬件状态机读取 UICR.APPROTECT 配置并锁定 AHB-AP,无 BootROM 参与,若在此时对 DEC1 施加 VCORE 电压毛刺,可能导致 Flash 读取异常或状态机锁存错误,使本次上电周期内 AHB-AP 未被正确锁定,从而恢复调试访问;实操中可通过观测 DEC1/DEC4 的功耗波形,将毛刺对齐到 Flash 读取的时间窗口,能显著提升攻击成功率,需注意毛刺强度需适中,过轻无效果、过重会导致芯片异常。


硬件分析

毛刺注入点

电压毛刺注入点:DEC1(VCORE)

nRF52840 的核心电源(VCORE)依赖外部去耦电容稳压,该电容两端电压直接决定核心供电稳定性,因此在 DEC1 注入对地短路毛刺的干扰效果最优。根据数据手册,芯片采用 REG0/REG1 两级稳压,DEC1~DEC6 对应不同内部电源域,其中 DEC1 直接影响 CPU 与调试初始化,是攻击的首选注入点。实操中需同时观测 DEC1 与 DEC4 的功耗波形,以 DEC4 反映的系统功耗模式定位 NVMC 访问 Flash/UICR 的时间窗口,再将毛刺精准对齐;为提升毛刺敏感度,需先移除 DEC1 对应的去耦电容(官方 DK 板常见标号为 C5)。

soc_datasheet

板级差异:你的板子可能“C5 不是 C5”

我最初使用的是一块国产 nRF52840-DK(疑似仿板),其丝印和电容编号与官方板存在差异:官方文档中标注的 DEC1 去耦电容 C5,在我的板子上实际对应为 C7,这导致我一开始定位注入点时出现了错误。这里有一个关键误区:不能仅凭 “短接电容到 GND 后芯片重启 / 闪烁” 就判定注入点正确,这也可能只是短路了其他供电线路。正确的做法是结合 PCB 走线、丝印对照,并用示波器观察 VCORE 上电曲线来验证。

由于芯片是 BGA 封装,无法直接通过引脚定位 DEC1 的去耦电容。在真实攻击场景下,可能需要逐一测试每个电容,但我希望找到更高效的方法,于是通过检索大量公开实验案例,找到官方 DK 的照片进行对照。我发现,尽管仿板的丝印不同,但电容的排布顺序与官方板高度相似(典型的抄板特征)。通过比对官方案例中 C5 的位置,我在自己的板子上找到了对应的 C7,并使用红色杜邦线连接到该点作为注入点。

C5_examplemy_board

实操建议:使用焊笔或热风枪拆除 DEC1 对应的去耦电容。如果不去除该电容,注入的毛刺往往无法有效拉低 VCORE,示波器上也看不到明显的电压下挫。拆除后,从该焊盘引出导线作为 VCORE 注入点,焊接时需特别注意避免导线分叉碰到相邻电容的引脚,以免造成短路。

dongledongle2

nrf52840 dongle 也是同理,经过测试引脚定义如下:

  • 白色:DEC1(与 ChipWhisperer glitch 接口连接)
  • 红色:VCC 供电(与 ChipWhisperer power 接口连接)
  • 黑色:接地(与示波器、ChipWhisperer、JLINK 共地)
  • 橙色、棕色:SWD 的 IO 和 CLK(链接到 J-LINK 用于读写)
  • 紫色:reset(用于自动化重置芯片,与 ChipWhisperer tio3 接口链接)
connect_liveconnect_live2

物理接线拓扑

实验室里有 ChipWhisperer Pro 套件,我用 CW506 分线板把目标板、示波器、J-LINK 和 ChipWhisperer 连接起来,形成了完整的故障注入与调试链路。这样可以通过电脑自动化断电、重置、注入、验证,跑一晚第二天看结果。

接线策略:

  • 目标板供电ChipWhisperer target_pwr → 目标板 VCC(我用 3.3V 供电,避免 J-Link 同时供电)
  • 故障注入ChipWhisperer glitch → 目标板 VCORE/DEC1(与示波器 CH1 同点测量)
  • 触发ChipWhisperer tio3 ← 目标板 RESET(以上电复位作为攻击触发源)
  • SWD 调试J-Link SWDIO/SWDCLK/GND → 目标板 SWDIO/SWDCLK/GND

接线示意:

text
nrf52 5v vcc <--> cw vcc (3.3v)
nrf52 swd(io/clk/gnd) <--> jlink swd(io/clk/gnd)
nrf52 gnd <--> cw gnd <--> 示波器 gnd
nrf52 vcore <--> cw glitch <--> 示波器 ch1
nrf52 reset <--> cw tio3 (reset)
connect

共地(简单却重要)

Joe Grand 曾通过对 Trezor 的 STM32 进行电压注入恢复了数百万美元,他是我的偶像。我也踩了同样的坑:一开始忘了给 ChipWhisperer 共地,导致毛刺要么没有效果,要么太重直接打挂芯片。下面是没接地时的毛刺示意:

fail_injection

接地后,增加 repeat 次数电源也不会轻易崩掉,毛刺更清晰。

注入时间与参数选择

注入时间判断的物理基础:上电时 AHB-AP 会读取 UICR 中的值决定是否打开 APPROTECT。读取 UICR 会带来功耗纹理变化,可用示波器辅助定位,把毛刺对齐到这段窗口能大幅减少盲扫时间。

AHB_AP_start

只靠 ext_offset 盲扫会很慢,最好先把启动过程的时间基准找出来。经验上能看到:

  • Flash activity 段:启动早期“读 Flash/UICR”的功耗纹理
  • CPU execute 段:CPU 开始取指执行后,DEC1 上的功耗纹理会明显变化

把毛刺对齐到 Flash/NVMC 初始化附近,成功率明显高于完全随机扫。

最佳注入时间点

示波器观察 DEC1 电压波形(黄色),芯片上电(蓝色)大概 1s 后 DEC1 处电压会有一个 100mV 以上的下降,最佳注入点就是这个下降沿。

injection_point

遍历注入点

ChipWhisperer 的关键参数:

  • ext_offset:触发后到毛刺输出的延时(决定打在启动流程的哪个阶段)
  • repeat:毛刺持续/重复次数(相当于“对地短路”的持续强度)
  • width:毛刺宽度(影响单次毛刺的“力度/形态”)

用 RESET 触发 + ext_offset 遍历:以目标板 RESET 边沿作为触发源,仅调整 ext_offset 就能扫不同时间点。

换算延时:时钟 100MHz → 周期 10ns,ext_offset = 169500 时延时约 1.695ms

注意 RESET 触发会有两次边沿(断电、上电),需要用示波器确认哪一个是有效基准,否则 ext_offset 会漂移。

毛刺宽度(width)与重复次数(repeat)

width 默认 10.15625,最大 49;repeat 决定持续程度。实测 width=40repeat=100 时波形基本看不清,但仍有效。

Repeat=1 与 repeat=49 对比:

repeat1repeat2

自动化测试

为高效定位有效的故障注入参数,我设计了基于 ChipWhisperer 的自动化扫描方案:遍历 ext_offset、组合 repeat/width,每次注入后通过 OpenOCD 小范围读取验证成功与否,成功即打印参数并终止。

python
import chipwhisperer as cw
import os, time

scope = cw.scope()

scope.clock.clkgen_freq = 100E6
scope.glitch.clk_src = "clkgen"
scope.glitch.resetDCMs()
scope.glitch.output = "enable_only"

scope.io.tio3 = 'high_z'
scope.trigger.triggers = 'tio3'
scope.glitch.trigger_src = "ext_continuous"

scope.glitch.ext_offset = 366500
scope.glitch.repeat = 100
scope.io.glitch_lp = False
scope.io.glitch_hp = True

print("初始毛刺配置:", scope.glitch)

scan_running = True
while scan_running:
    scope.glitch.ext_offset += 100
    for i in range(1, 100, 10):
        scope.glitch.repeat = i * 10
        print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 扫描参数:offset={scope.glitch.ext_offset}, repeat={scope.glitch.repeat}")

        scope.glitch.width = 40
        scope.io.glitch_lp = False
        scope.io.glitch_hp = True

        scope.io.target_pwr = True
        time.sleep(2)

        exit_status = os.system(
            'openocd -s /usr/local/share/openocd/scripts '
            '-f ./interface/jlink.cfg '
            '-c "transport select swd" '
            '-f ./target/nrf52.cfg '
            '-c "init;dump_image nrf52_dumped.bin 0x0 0x100;exit"'
        )

        if exit_status == 0:
            print("✅ 攻击成功!当前有效参数:")
            print(scope.glitch)
            scan_running = False
            raise SystemExit(0)

        time.sleep(2)
        scope.io.target_pwr = False
        time.sleep(0.3)

        if scope.glitch.ext_offset > 380000:
            print("❌ 扫描范围结束,未找到有效参数")
            scan_running = False
            break

快速验证(扫描阶段):仅读取 0x0~0x100,避免全量读取耗时过长:

bash
openocd \
  -s /usr/local/share/openocd/scripts \
  -f ./interface/jlink.cfg \
  -c "transport select swd" \
  -f ./target/nrf52.cfg \
  -c "init;dump_image nrf52_dumped.bin 0x0 0x100;exit"

全量验证(成功后):按芯片 Flash 大小读取完整固件(nRF52840 为 0x80000/0x100000):

bash
openocd \
  -s /usr/local/share/openocd/scripts \
  -f ./interface/jlink.cfg \
  -c "transport select swd" \
  -f ./target/nrf52.cfg \
  -c "init;dump_image nrf52_dumped.bin 0x0 0x80000;exit"
start_injection

自动化扫描通常需要数小时,我遇到的坑:

  1. “假成功”:示波器显示电压无下降沿,但 OpenOCD 仍无法读 —— 毛刺太大导致芯片短暂崩溃。
  2. 设备差异:nRF52840-DK 始终无法复现,更换 J-Link、调参无效;换成 nRF52840 Dongle 后成功,参数差异显著(上电时序不同)。
  3. 焊接问题:注入引脚焊接时若误触相邻引脚,毛刺路径异常,导致失败。
fail1fail111

成功迹象与参数示例

电压波形:VCORE 端可观测到精准的注入毛刺,且毛刺后电压无明显下降(推测:上电初始化阶段因故障干扰,未执行 APPROTECT 锁定操作)。

功能验证:可通过 OpenOCD dump 完整固件、读取全部 RAM,GDB 可正常挂载调试。

success3success4

这是我测试成功的有效参数(不同硬件差异很大,仅供参考):

  • ext_offset = 169500(100MHz 时钟下约 1.695ms)
  • repeat = 11
  • width = 10
successsuccess1

成功后读取 UICR.APPROTECT(0x10001208)验证,其值仍为 0xFFFFFF00(保护使能状态),说明该攻击仅在 本次上电周期 临时绕过调试保护,并未擦除或修改保护位。

success2

漏洞危害

这类漏洞可被用于:

  • 固件提取与逆向(也可能被用于非法抄板)
  • 钱包/安全设备:若密钥或中间值落在 RAM,可进一步读 RAM 或配合其他攻击
  • 扩展:故障注入也可用于 DFA 攻击、绕过 secure boot、获取串口 shell 等

修复情况

Nordic 在新版芯片中提供 improved APPROTECT,修复了该类问题。仅通过电压波形很难判断目标是否为 improved 版本,更靠谱的是按芯片型号/批次对照 Nordic 最新 PS 文档(如 nRF52840 PS v1.7)和官方说明:https://devzone.nordicsemi.com/nordic/nordic-blog/b/blog/posts/working-with-the-nrf52-series-improved-approtect


参考文献和致谢

特别致谢 @yichen from @OSR 的帮助。

参考文献: