1、TrustZone-M 基础概念:ARMv8-M 架构概述、安全状态与非安全状态、SAU/IDAU 配置

各位同学,咱们今天正式开课。先聊聊 TrustZone-M 到底是个什么东西。

很多人一听到 TrustZone,第一反应是「哦,那个手机上的安全方案」。其实 ARM 的 TrustZone 分两条线:一条是 Cortex-A 系列上的 TrustZone-A,跑 Linux/Android 那种;另一条就是我们今天要讲的 TrustZone-M,专门给 Cortex-M 系列微控制器用的。

我最早接触 TrustZone-M 是在一个 IoT 安全网关项目里。当时客户要求固件必须防篡改,密钥必须硬件隔离。我一开始想用传统 MPU 做隔离,后来发现根本不够用——MPU 只能防「意外访问」,防不了「恶意攻击」。TrustZone-M 才是正解。

核心要点:TrustZone-M 不是软件方案,是 ARMv8-M 架构的硬件安全扩展。它在 CPU 内部实现了物理级别的隔离,比任何软件沙箱都可靠。

1.1 ARMv8-M 架构概述

ARMv8-M 是 ARM 针对微控制器推出的最新架构版本。它最大的变化就是引入了 TrustZone 安全扩展。说白了,以前 Cortex-M3/M4 只有一套运行环境,所有代码都在同一个世界里跑;现在 ARMv8-M 把世界一分为二——安全世界和非安全世界。

我建议你记住一个关键点:ARMv8-M 不是全新的指令集,它兼容 ARMv7-M 的 Thumb 指令集。这意味着你现有的 Cortex-M4 代码,基本可以直接移植到 ARMv8-M 上。但 TrustZone 相关的配置寄存器、异常模型、内存保护机制,全是新的。

ARMv8-M 有几个关键特性:

  • 向后兼容:支持 ARMv7-M 的 Thumb/Thumb-2 指令集
  • TrustZone 扩展:硬件级安全隔离
  • 增强的异常处理:安全/非安全两套异常向量表
  • 新的内存保护单元:SAU + IDAU 双保险
  • 低延迟中断:安全中断可以抢占非安全任务

嗯,这里要注意:ARMv8-M 有两个子版本——ARMv8-M Baseline 和 ARMv8-M Mainline。Baseline 对应 Cortex-M23,Mainline 对应 Cortex-M33。M23 不支持 TrustZone,M33 才完整支持。所以你要做 TrustZone 开发,至少得用 Cortex-M33。

1.2 安全状态与非安全状态

TrustZone-M 的核心思想就一句话:把系统分成两个世界。

安全状态(Secure State)和非安全状态(Non-Secure State)。这两个状态在 CPU 内部是硬件区分的,不是靠软件标记。CPU 内部有一个安全位(NS bit),这个位决定了当前执行环境是安全还是非安全。

你想想看,这意味着什么?意味着非安全世界的代码,永远无法直接访问安全世界的资源。即使非安全代码跑在最高特权级(Handler Mode),它也碰不到安全内存、安全外设、安全中断。

我的经验:很多初学者会混淆「特权级」和「安全状态」。特权级(Privileged/User)控制的是对系统寄存器的访问权限;安全状态控制的是对物理内存和外设的访问权限。两者是正交的。我曾经在项目里见过有人把非安全特权级代码当成安全代码用,结果密钥被读走了——血的教训。

安全状态和非安全状态之间的切换,只能通过特定的指令和异常机制:

  • SG 指令:安全门指令,从非安全调用安全函数的唯一入口
  • 异常返回:安全异常返回时,可以切换到非安全状态
  • 中断:安全中断可以打断非安全代码,但非安全中断不能打断安全代码

这里有个关键概念叫「安全调用」(Secure Call)。非安全代码想调用安全世界的函数,必须通过 SG 指令进入一个「安全门」(Secure Gateway)。安全门会检查调用地址是否合法,然后才允许切换状态。这个机制防止了非安全代码随意跳转到安全世界的任意位置。

避坑指南:我曾经在调试一个 Secure Boot 方案时,发现非安全代码可以通过修改函数指针,绕过 SG 指令直接跳进安全内存。后来查了半天,发现是 SAU 配置漏了一个区域——非安全代码能访问到安全代码的入口表。SAU 配置必须覆盖所有可能的入口点,一个都不能漏。

1.3 SAU/IDAU 配置

好,现在问题来了:CPU 怎么知道哪块内存是安全的,哪块是非安全的?

答案是:靠两个硬件单元——SAU(Security Attribution Unit)和 IDAU(Implementation Defined Attribution Unit)。

SAU 是架构定义的,所有 ARMv8-M 芯片都必须有。IDAU 是芯片厂商自定义的,不同厂商实现不同。两者共同决定每个内存地址的安全属性。

我画了一张图,帮你理解 SAU 和 IDAU 的关系:

SAU / IDAU 内存安全属性决策流程 CPU 核心 IDAU 优先判断 IDAU 已定义? 是 使用 IDAU 结果 否 查询 SAU 配置 输出最终安全属性 (NS bit) 说明 IDAU 优先级高于 SAU IDAU 未定义时回退到 SAU SAU 默认所有地址为非安全

这张图展示了 CPU 访问内存时的决策流程。每次内存访问,CPU 都会先问 IDAU:这个地址你定义了吗?如果 IDAU 说定义了,就用 IDAU 的结果;如果 IDAU 说没定义,再去查 SAU 的配置表。

SAU 的配置其实很简单。它有一组寄存器,每个寄存器定义一个内存区域的安全属性。你可以配置最多 8 个区域(ARMv8-M Mainline 支持 8 个,Baseline 只支持 4 个)。每个区域由起始地址、结束地址和安全属性组成。

SAU 寄存器配置示例:

// SAU 区域配置结构
typedef struct {
    uint32_t RBAR;  // 区域基地址寄存器
    uint32_t RLAR;  // 区域限制地址寄存器
} SAU_Region_Type;

// 配置 SAU 区域 0:将 0x10000000 - 0x1000FFFF 设为安全区域
SAU->RNR = 0;                    // 选择区域 0
SAU->RBAR = 0x10000000;          // 基地址
SAU->RLAR = (0x1000FFFF & ~0x1F) | 1;  // 限制地址 + 安全属性(bit0=1 表示安全)

// 配置 SAU 区域 1:将 0x20000000 - 0x2000FFFF 设为非安全区域
SAU->RNR = 1;
SAU->RBAR = 0x20000000;
SAU->RLAR = (0x2000FFFF & ~0x1F) | 0;  // bit0=0 表示非安全

这里有个细节要注意:RLAR 寄存器的 bit0 是安全属性位。1 表示安全,0 表示非安全。而且地址必须 32 字节对齐,因为 RLAR 的低 5 位被用作属性位了。

我的习惯:配置 SAU 时,我建议先把所有区域默认设为非安全,然后只把需要保护的内存区域设为安全。这样即使你漏配了一个区域,也不会意外暴露安全资源。反过来做的话,漏配一个区域就等于把安全内存拱手送人了。

IDAU 的配置就复杂多了,因为它跟具体芯片相关。不同厂商的 IDAU 实现完全不同。比如 NXP 的 LPC55xx 系列,IDAU 是硬连线的,你改不了;而有些芯片的 IDAU 可以通过系统控制寄存器配置。

我列个表格,帮你对比 SAU 和 IDAU 的区别:

特性 SAU IDAU
定义者 ARM 架构定义 芯片厂商自定义
可配置性 软件可编程(最多 8 个区域) 通常硬连线或有限配置
优先级 低(IDAU 未定义时才生效) 高(优先于 SAU)
默认行为 所有地址默认为非安全 取决于芯片设计
典型用途 运行时动态调整安全边界 芯片出厂时固定安全分区

嗯,这里要特别提醒你:IDAU 的优先级高于 SAU。这意味着即使你在 SAU 里把一个区域配成了安全,如果 IDAU 说它是非安全,那它还是非安全。反过来也一样——IDAU 说它是安全的,你 SAU 改不了。

我在一个项目中就踩过这个坑。当时想把某段 SRAM 从非安全改成安全,SAU 配了半天没效果。后来查手册才发现,那家芯片厂商的 IDAU 把那块 SRAM 硬连线成了非安全,SAU 根本覆盖不了。最后只能换方案,用另一块 IDAU 没定义的 SRAM。

避坑指南:拿到一块新芯片,第一件事就是看它的 IDAU 映射表。搞清楚哪些地址是 IDAU 固定的,哪些是 SAU 可配的。不要想当然地认为 SAU 能覆盖一切。我曾经见过一个团队,花了两周调试 SAU 配置,最后发现是 IDAU 在「捣乱」。

最后总结一下本章的核心要点:

  • ARMv8-M 是 Cortex-M 系列的安全升级版,Cortex-M33 是主力选手
  • 安全状态和非安全状态是硬件隔离的,不是软件标记
  • 安全调用必须通过 SG 指令,不能直接跳转
  • SAU 和 IDAU 共同决定内存安全属性,IDAU 优先级更高
  • SAU 可配置最多 8 个安全区域,地址必须 32 字节对齐

这些基础概念是后面所有章节的基石。TrustZone-M 的固件逆向分析,说白了就是分析安全世界和非安全世界之间的交互。不理解这两个世界的边界在哪里,你就没法分析攻击面,也没法找到漏洞。