光子发射引导的激光故障注入实现RP2350安全调试
差分光子发射显微镜定位了调试使能寄存器活动,并在SWD之前缩小了激光搜索范围
中文
复制

摘要
— 光子发射显微镜让我们定位到一个寄存器,它负责在 Raspberry Pi 微控制器上启用调试功能。 — 随后,我们在两个相邻位置施加激光脉冲,恢复了芯片 Secure world 的调试器访问权限,尽管调试功能已被永久禁用。 — 在一次救援复位后利用该访问权限,我们从一次性可编程存储器中恢复了一个秘密。复位在固件来得及施加运行时锁之前就中止了芯片,因此该页始终保持 Secure 可读。 — 该攻击需要物理接触、破坏性准备,以及大约 25 万美元的实验设备。
RP2350 的安全模型
RP2350 是 Raspberry Pi 的双核微控制器:每个处理器插槽在启动时可以选择 Arm Cortex-M33 或 RISC-V Hazard3 核心。其硬件安全特性包括:
- 安全启动,根据烧录在一次性可编程存储器(OTP)中的公钥指纹对签名固件进行认证
- Armv8-M TrustZone,将 Secure 与非 Secure 执行状态隔离开
- 永久禁用调试的设置
- 毛刺检测器,用于检测时钟或电源操纵引起的时序扰动
Raspberry Pi 通过其 RP2350 Hacking Challenge 积极邀请研究人员评估这些防护措施。第一轮挑战于 2024 年 8 月至 12 月针对原始芯片展开。在若干发现被修复后,Raspberry Pi 发布了 A4 修订版——也就是我们测试的版本。
永久安全配置和启动公钥指纹存储在一次性可编程(OTP)存储器中:每个位只能从 0 翻转为 1 一次,且不可逆转,因此写入其中的内容在芯片的整个生命周期内都保持不变。
OTP 被组织为 128 字节的页,由两个持久性(即硬)锁行保护:对于第 n 页,PAGEn_LOCK0 配置可选的读密钥和写密钥,以及未输入密钥时的行为,而 PAGEn_LOCK1 包含硬件强制执行的 LOCK_S 和 LOCK_NS 权限。这些状态可以从读写推进到只读或不可访问,但不能变得更宽松。
OTP 子系统对安全相关字段使用冗余编码:根据 RP2350 数据手册,关键标志“在八个连续 OTP 行上以八选三投票方式编码”,OTP 锁位则“以多数投票方式三重冗余”。
在 OTP 复位时,持久的 LOCK_S 和 LOCK_NS 值会初始化每页的运行时锁,也称为软锁。固件可以在下一次 OTP 复位之前收紧该锁,但不能放松它。运行时更改不会在该复位后保留。
外部调试器通过 Arm 的 Serial Wire Debug(SWD) 接口与 RP2350 通信。请求首先到达 Serial Wire Debug Port(SW-DP),然后被路由到访问端口。在此处使用的 Cortex-M33 配置中,每个核心都有一个连接到其系统总线的内存访问端口(Mem-AP);启用后的 Mem-AP 允许调试器读写被允许的内存和外设。一个单独的常开访问端口 RP-AP 则暴露了一小组复位和恢复控制。
Secure debug 指的是带有 Secure 属性的 Mem-AP 访问。调试器随后可以与访问控制逻辑允许的 Secure 内存映射资源进行事务,并暂停或检查运行在 Secure 状态下的核心。
永久的 CRIT1.DEBUG_DISABLE 标志旨在关闭这条路径。当它被置位时,会将两个核心 Mem-AP 的使能信号驱动为零,从而“阻止 AP 执行任何总线访问”,并禁用工厂测试 JTAG 接口和 RISC-V 调试模块的访问端口。SW-DP 和 RP-AP 仍然响应,但两个核心 Mem-AP 都无法访问系统总线。
然而,存在一个覆盖机制:内存映射的 DEBUGEN 寄存器允许 Secure 软件重新启用每个核心的 Mem-AP,并单独重新启用通过它的 Secure 访问。数据手册指出,DEBUG_DISABLE “可以通过设置该寄存器的所有位来完全覆盖”。
这条执行链上的关键覆盖机制,正是我们把调试接口作为目标的原因。拿到 Mem-AP 上的 Secure debug 访问权限,就等于获得了一个通用原语:读写 Secure 内存、暂停内核并单步执行、查看其寄存器。这个寄存器能否被故障置位,就是本文余下部分要回答的问题。
实验设置
目标配置
Raspberry Pi 的 RP2350 Hacking Challenge 要求参赛者提取存储在 OTP1 中的 128 位密钥。启动时,经过签名的挑战固件会先确认第 48 页具有预期的持久锁,然后施加运行时锁,在下次 OTP 复位之前,拒绝安全与非安全两种访问。
我们在自己的 A4 版本芯片上复现了这套厂商定义的配置:
- 将公钥的 SHA-256 指纹写入
BOOTKEY0 - 将
BOOT_FLAGS1.KEY_VALID设为0x1,将BOOT_FLAGS1.KEY_INVALID设为0xe - 启用安全启动(
CRIT1.SECURE_BOOT_ENABLE = 1) - 永久禁用调试(
CRIT1.DEBUG_DISABLE = 1) - 以最高灵敏度启用毛刺检测器(
CRIT1.GLITCH_DETECTOR_ENABLE = 1、CRIT1.GLITCH_DETECTOR_SENS = 3) - 按照挑战配置设置第 1 页和第 2 页的持久锁
- 将第 48 页的持久锁设为
PAGE48_LOCK1 = 0x3c3c3c,拒绝非安全访问(LOCK_NS = INACCESSIBLE),同时保留安全读写访问(LOCK_S = READ_WRITE)
启用安全启动后只允许 Cortex-M33 核心运行,因此这些实验中两个处理器插槽使用的都是 Arm。
样品制备与实验台
芯片经过背面开盖,红外光得以穿过硅衬底到达晶体管,而不会被正面的金属层挡住。随后芯片被焊回一块子板,子板连接到 Scaffold——Ledger Donjon 用于驱动和监控被测设备的开源平台。去掉芯片背面的引线框架会切断 GND 连接,因此用一根铜线将其恢复2。

背面开盖的 RP2350 安装在分析子板上

攻击所用的实验台
DEBUGEN:绕过永久调试禁用
DEBUGEN 有五个功能位:
| 位 | 名称 | 作用 |
|---|---|---|
| 0 | PROC0 | 启用核心 0 的内存访问端口 |
| 1 | PROC0_SECURE | 允许通过核心 0 的内存访问端口进行安全访问 |
| 2 | PROC1 | 启用核心 1 的内存访问端口 |
| 3 | PROC1_SECURE | 允许通过核心 1 的内存访问端口进行安全访问 |
| 8 | MISC | 启用额外的调试组件,包括交叉触发接口和 RISC-V 调试访问端口 |
在核心上启用 Secure debug 需要两个位同时到位:一个启用 Mem-AP,另一个允许通过它进行 Secure 访问。
与 OTP 安全字段采用冗余编码不同,数据手册中没有记载 DEBUGEN 的位冗余、奇偶校验或多数表决机制。
因此我们测试了激光脉冲能否在上述已锁定设备上置位 DEBUGEN 的位。
光子发射引导定位
这项测试首先需要知道该往哪里打。置位单个 DEBUGEN 位意味着要命中某一个寄存器位的存储单元,如同大海捞针。这比激光故障注入中常见的指令跳过故障更难定位:在指令跳过中,扰乱核心流水线中众多触发器中的任何一个都能产生相同的跳过效果,敏感区域因此足够大,随机扫描就能找到。盲扫单个 DEBUGEN 位则不现实。
开关晶体管会发射微弱的近红外光子,且与自身活动相关,因此在反复执行过程中收集这些发射,就能揭示某个选定控制信号在何处发生状态变化。这使得光子发射显微镜(PEM)非常适合 DEBUGEN:作为内存映射寄存器,Secure 软件可以在循环中精确翻转各个位,产生测量所需的反复状态变化。我们将其用作第一阶段的定位手段,得到的图谱把后续激光扫描限制在了几微米的范围内。
我们比较了反复翻转选定 DEBUGEN 位的循环,这些循环除目标位不同外完全一致。寄存器的光子发射相对于相机自身噪声十分微弱,且对温度等缓慢漂移的环境条件敏感,因此单帧图像什么都看不出来。对每个循环的多帧取平均抑制了随机传感器噪声,再将两组均值堆栈相减,抵消了循环共有的全部成分:静态背景、传感器偏置、热发射,以及与选定位无关的开关活动。采集过程中对两组数值交替采样,避免缓慢漂移对相减结果产生偏置。剩下的就是跟随选定位变化的发射。

掩码 0x3 与掩码 0xc 全部 200 次采集的均值叠加图,以及两者的带符号差值。红色为正,表示 0x3 的发射更强;蓝色为负,表示 0xc 的发射更强。下方的定位图在合并配对差值之前,还额外对采集顺序做了平衡。
在不同位掩码之间反复比对后,我们在相机视场的三个区域内发现了与 DEBUGEN 位 0–3 相关的紧凑位点。

相机视场的红外概览图,标出三个区域。彩色像素标记与 DEBUGEN 位 0–3 相关的位点。
这些区域显示出与每个 DEBUGEN 位相关的开关活动,但它们并不直接指向存储单元。每个位观察到多个热点,可能来自存储元件,也可能来自相关逻辑。没有版图数据,我们无法区分这两种情况。不过,这些区域仍然大幅缩小了搜索范围。
发现 1 —— 让 DEBUGEN 出错即可获得 Secure debug
激光故障注入(LFI)使用 980 nm 脉冲激光,最大光功率 2.97 W,实际工作在约 40%(约 1.2 W),脉冲宽度 100 ns,经 50x 物镜聚焦。每次脉冲之后,我们通过 SWD 探测调试访问端口。
在 PEM 找到的区域内,我们跑了一遍 LFI 扫描,并利用 SWD 的反馈校准出两个相距几微米、均有响应的位置。在其中一个位置,脉冲打开了通过 core 1 Mem-AP 的总线访问,说明 PROC1 已被置位。在另一个位置,Mem-AP 的 Control/Status Word 报告了 SDeviceEn = 1,这个状态引导信号表明 PROC1_SECURE 很可能已被置位。每次脉冲后我们都会检查这两个指示。

左:与 DEBUGEN 位相关的 PEM 站点。右:LFI 红外视图上的激光故障点。
一个脉冲在置位一个位的同时可能清除另一个位,因此要同时置位两个位,必须采用迭代序列。我们的脚本反复向 PROC1 位置施加脉冲,直到总线访问可用,然后向 PROC1_SECURE 位置施加脉冲,直到 SDeviceEn = 1;一旦总线访问丢失,就回到第一个位置。位置和脉冲参数校准完成后,该序列在几秒内即可启用 Secure debug。有意思的是,我们无法用 20x 物镜复现这一序列。两个位置相距只有几微米,更宽的光斑很可能同时打到了置位区域和清除区域,因此无法得到正确的值。
两个位一旦置位,就保持置位状态,无需继续施加脉冲或进行软件写入。通过 core 1 的 Mem-AP 读取 Secure-only 的 DEBUGEN 寄存器,返回 0xc;由于 DEBUGEN 是 Secure-only,这次读取成功说明该事务被标记为 Secure。
通过 core 1 的 Mem-AP 启用 Secure 访问后,调试器就能读写内存映射资源——只要这些资源的 ACCESSCTRL 权限允许调试器作为总线管理器接入,且其目标特定控制允许 Secure AHB 事务。除了这些直接读取之外,调试器还可以暂停内核、单步执行,并检查或修改其寄存器,从而借助 Secure 内核的中介提取来破坏 TrustZone 的运行时隔离。这并不会让 boot ROM 接受未认证的固件:固件正常启动时,secure boot 仍会对其进行认证,但无法保护验证之后仍对调试器可访问的运行时状态。
应用于 Hacking Challenge 配置
上述 Secure 属性的 Mem-AP 访问会暴露 Secure 运行时状态,但挑战第 48 页的运行时锁仍会在固件运行后阻止对密钥的访问。该页的持久锁 PAGE48_LOCK1 = 0x3c3c3c 拒绝 Non-secure 读取,但让 LOCK_S 保持在 READ_WRITE,因此在运行时锁收紧之前,它仍可通过 Secure 属性的访问读取。
每次启动时,固件都会把限制最严的二进制值 0b1111 写入运行时锁 otp_hw->sw_lock[48]。该寄存器随后使该页对 Secure 和 Non-secure 访问都不可访问,Secure debug 也不例外,从而阻止来自 Mem-AP 的 Secure 事务读取密钥。
如文档所述,软件锁“在复位时从 OTP 锁页初始化”,而写入只会将状态推进“直到下一次复位”。复位 OTP 块会丢弃 0b1111,并恢复由 PAGE48_LOCK1 推导出的值,对此 LOCK_S = READ_WRITE。
剩下的问题是,如何在不允许固件重新施加运行时锁的情况下复位已锁定的芯片。RP-AP 仍然“始终可访问,即使外部调试被禁用”。设置 CTRL.RESCUE_RESTART 会触发 rescue reset:一次完整的系统复位,同时标记 boot ROM 在任何用户软件运行之前暂停。
boot ROM 在 watchdog、flash 或 USB 启动之前检查 POWMAN_CHIP_RESET.RESCUE_FLAG,将其清除,然后让 core 0 停在禁用中断的等待循环中,让 core 1 停在其等待向量路径中。3 数据手册没有记录对 CTRL.RESCUE_RESTART 的任何限制。
我们按以下顺序操作:
- 救援复位。 将
CTRL.RESCUE_RESTART置为1,然后通过 RP-AP 将其清除为0。芯片复位并停留在 boot-ROM 等待路径中。签名固件永远不会运行,因此sw_lock[48]永远不会被收紧,并保持在由PAGE48_LOCK1推导出的宽松值——LOCK_S = READ_WRITE。 - 将
DEBUGEN故障注入为0xc。 在两个核心都处于 boot-ROM 等待路径的情况下,按上述方式设置PROC1和PROC1_SECURE;这两次置位产生值0xc。 - 暂停核心 1,通过其调试暂停控制与状态寄存器(
DHCSR)在现已安全的 Mem-AP 上执行。 - 读取密钥,从 OTP 行
0xc08–0xc0f通过受保护的读取接口。
我们在被测设备上运行了此序列,并恢复了完整的挑战密钥。
DEBUGEN_LOCK 无法阻止激光引起的变化
DEBUGEN_LOCK 阻止软件对相应 DEBUGEN 位的写入:每个锁定位都是“写入 1 以锁定 DEBUGEN 的 […] 位。一旦设置便无法清除”。数据手册将此描述为一种“避免意外写入”的方式。
在目标 DEBUGEN 位为 0 且其锁定位为 1 的试验中,脉冲仍可设置 DEBUGEN,而锁定位保持 1。脉冲也会设置锁定位,无论是否有相应的 DEBUGEN 变化。在成功的序列中,当 PROC1 和 PROC1_SECURE 都被设置时,所有五个功能锁定位都已为 1。我们从未看到锁定位从 1 返回 0,因此一旦故障设置了相应的锁定位,后续写入 DEBUGEN = 0 无法恢复禁用状态。
基于软件的缓解措施的局限性
一旦通过 Mem-AP 启用安全访问,仅凭安全归属便不再能将调试器与安全固件区分开。这种访问不会覆盖硬 OTP 锁或外设特定的控制。ACCESSCTRL 可以阻止调试器管理器对特定目标的直接事务,但它本身并不能阻止控制安全核心的调试器引发核心发起的访问或通过核心寄存器提取已加载的值。在救援复位后,ACCESSCTRL 会在固件重新配置之前恢复到其全开放的复位时默认值。因此,ACCESSCTRL 减少的是直接 Mem-AP 暴露,而非形成独立的机密性边界。
尽管如此,固件可以通过在 ACCESSCTRL 中拒绝调试器访问敏感目标,然后在 ACCESSCTRL.LOCK 中设置调试器位,使调试器事务无法重新打开这些权限,从而减少启动后的暴露。安全固件还可以定期检查 DEBUGEN,并在出现意外值时触发故障安全复位,清除处理器冷复位域。这些措施是尽力而为的运行时缓解措施:已启用的调试器可能会在下一次检查之前暂停核心,而救援复位会在固件配置 ACCESSCTRL 或运行监控程序之前停止。因此,它们无法阻止此处演示的固件前密钥读取。
RP2350 文档中记录的加密启动流程说明了运行时锁在固件前的局限性,以及调试器管理器过滤在启动后的局限性,分别处于两种不同的机器状态。救援复位后,boot ROM 在解密前暂停:此时尚不存在明文负载,但如果 OTP 页的持久权限允许安全访问且没有其他目标控制阻止该事务,解密密钥可能可直接读取。正常加密启动后,SRAM 中存在明文:直接 Mem-AP 读取取决于 ACCESSCTRL 中的调试器管理器权限,而安全核心控制可能允许核心介导的提取,即使直接读取被拒绝。这是架构分析,而非经过测试的加密启动结果;加密启动仍能保护外部闪存免受离线检查。
影响与攻击条件
演示的攻击序列提供了对归因于 Secure 的内存访问、对 Secure 世界执行的控制,以及在重置挑战程序运行时的页锁之后对其挑战密钥的访问。它需要以下资源:
- 破坏性物理接触。 背面开盖会永久改变封装,并使裸片暴露在外。
- 专用实验室设备。 上述完整装置的成本约为 25 万美元。
- 硬件安全专业知识。 该流程需要样品制备、裸片导航、激光参数选择,以及协调激光控制、位移台定位与 SWD 测量。
结论
RP2350 将关键的调试禁用标志以冗余投票的方式编码在 OTP 中,但 DEBUGEN 可以覆盖它们的效果,而数据手册中没有记录任何与之等效的保护。在我们的实验中,激光脉冲在 DEBUGEN_LOCK 的情况下改变了 DEBUGEN,并能设置一个锁定位,使固件无法恢复被禁用的值。另外,RP-AP 救援复位将挑战程序的运行时页锁恢复为其持久值,同时阻止用户固件执行。这些软件可见的机制各自都履行了其文档所述的功能,但它们与激光故障的相互作用却开启了 Secure 调试,并恢复了挑战密钥。差分 PEM 首先定位到与比特相关的 DEBUGEN 活动,而引导式 LFI 将这一空间线索转化为持久的 Secure 调试。系统层面的教训是:安全分析必须覆盖完整的执行路径,从持久的 OTP 配置,到可变的控制寄存器,再到复位行为,因为系统安全取决于这条路径,而不是孤立地取决于各个机制。
披露与致谢
我们于 2026 年 7 月 28 日向 Raspberry Pi 披露了这一故障。我们感谢 Raspberry Pi 团队参与披露讨论,以及他们在安全研究上的透明态度。
Antoine Plin,Ledger Donjon 硬件安全实习生
脚注
- https://github.com/raspberrypi/rp2350_hacking_challenge RP2350 Hacking Challenge 仓库,内含我们复现时参照的 lockdown 配置与固件。 ↩
- Courk,Laser Fault Injection on a Budget: RP2350 Edition。 ↩
- 救援检查位于
src/main/arm/varm_boot_path.c中 core 0 启动路径的第 1 步;在src/main/arm/arm8_bootrom_rt0.S中,varm_wait_rescue进入关中断的varm_dead_quietWFI 循环,而 core 1 仍停留在 boot ROM 的等待向量路径中。 ↩