深入解析Arm TrustZone安全三剑客:SAU、IDAU与MSAU内存隔离实战
1. 项目概述:从硬件层面构建嵌入式系统的“安全围栏”
在嵌入式系统,尤其是物联网、汽车电子和工业控制领域,设备的安全边界正从网络层、应用层,不断下沉到最底层的硬件和固件。想象一下,你的智能门锁、汽车ECU或者工厂里的PLC控制器,其内部同时运行着来自不同供应商的代码——有负责关键逻辑的安全固件,也有处理用户交互或网络通信的普通应用。如何确保一个存在潜在漏洞的应用程序,绝不会越界去篡改负责指纹验证或刹车控制的核心代码和敏感密钥?这就是硬件强制内存隔离技术要解决的根本问题。
Arm TrustZone for Armv8-M 架构,正是为此而生的“硬件安全围栏”。它不像纯软件方案那样依赖操作系统的正确性,而是从CPU和总线架构的根源上,将一颗物理芯片在逻辑上划分为两个隔离的世界: 安全(Secure)世界 和 非安全(Non-secure)世界 。安全世界运行可信代码(如加密服务、密钥管理、安全启动),非安全世界运行普通应用。两者之间的切换受到严格管控,非安全世界的代码无法直接窥探或干扰安全世界的运作。
而实现这种隔离的核心“裁判”与“地图绘制者”,就是三个关键硬件单元: SAU(Secure Attribution Unit, 安全属性单元)、IDAU(Implementation Defined Attribution Unit, 实现定义属性单元) 和 MSAU(Master Security Attribution Unit, 主控安全属性单元) 。它们协同工作,为每一次内存或外设访问贴上“安全”或“非安全”的标签,并由总线上的“哨兵”——TrustZone Filter(TZF)——来执行访问规则。理解这三者的分工与协作,是设计任何基于Cortex-M33/M23等芯片的安全系统的基石。本文将结合具体芯片(如瑞萨RA8T2)的实践,深入拆解其原理、配置方法和避坑指南。
2. 安全架构核心三单元:SAU、IDAU与MSAU的分工与协作
要理解内存隔离,首先要明白一次访问的“安全属性”是如何被决定的。当CPU或DMA控制器(统称为Master,主控)发起一次读/写请求时,这个请求会携带一个目标地址。这个地址最终被判定为“安全访问”还是“非安全访问”,需要经过一套裁决流程。SAU、IDAU和MSAU就是这个流程中的关键裁决器。
2.1 IDAU:硬件固定的“基础地图”
IDAU是芯片设计时就在硬件中固化好的安全属性映射规则。它通常采用一种简单而高效的方式: 使用地址的某一位(通常是bit 28)来区分安全别名地址和非安全别名地址 。
工作原理 :
- 当主控(如CPU)访问一个地址时,IDAU会检查该地址的特定高位(例如bit 28)。
- 如果该位为0,IDAU将此访问标记为指向 安全别名区域 。
- 如果该位为1,IDAU则将此访问标记为指向 非安全别名区域 。
这种设计非常巧妙,它将4GB的线性地址空间在逻辑上“折叠”了。例如,物理地址 0x0000_0000 (bit28=0)和 0x1000_0000 (bit28=1)可能指向同一块物理内存,但前者被视为安全访问,后者被视为非安全访问。这为软件提供了极大的灵活性:安全世界的代码可以通过安全别名地址访问自己的数据,而非安全世界的代码通过非安全别名地址访问其被授权访问的部分,两者在物理上可能共享内存,但在逻辑和访问权限上完全隔离。
更重要的是,IDAU不仅定义S(Secure)和NS(Non-secure)区域,还定义了一种特殊的 NSC(Non-secure Callable)区域 。这是安全世界中一块特殊的“门铃”区域。非安全世界的代码只能通过调用位于NSC区域内的特定指令(SG指令),才能合法地进入安全世界。这确保了世界切换的唯一入口受控。
关键点 :IDAU的映射是硬件固定的,软件无法更改。它为系统提供了最基础、不可篡改的安全基线。在RA8T2中,IDAU将代码存储区(如Flash)和SRAM的前半部分(bit28=0)默认映射为安全(或NSC)别名,后半部分(bit28=1)映射为非安全别名。
2.2 MSAU:为非CPU主控定制的“专用地图”
在复杂的SoC中,除了CPU,还有其他总线主控,最典型的就是 DMA(直接内存访问)控制器 。DMA可以不经过CPU,直接在内存与外设间搬运数据。如果DMA可以随意发起安全或非安全访问,那么隔离机制将形同虚设。
MSAU就是为CPU之外的其他总线主控准备的“安全属性裁决器”。你可以把它理解为这些主控专用的IDAU。它的核心作用是: 确保非安全主控(例如,由非安全世界软件配置和启动的DMA通道)无法发起安全事务 。
工作机制 :
- 对于安全主控 :可以访问安全别名和非安全别名地址,发起对应安全属性的访问。
- 对于非安全主控 :只能访问非安全别名地址。即使它错误地尝试访问一个安全别名地址(bit28=0),MSAU也会强制将该访问的属性标记为“非安全”。当这个非安全属性的访问请求到达一个被配置为仅安全可访问的资源(如安全外设)时,会被TrustZone Filter拦截,产生访问错误。
实操心得 :在配置DMA时,务必要注意DMA控制器本身作为一个主控,其发起的传输事务的安全属性,是由 配置DMA的那个CPU所处的世界(安全/非安全) 以及 MSAU的映射规则 共同决定的。一个常见的坑是,在安全世界配置了DMA去搬运非安全世界的数据,如果源/目标地址使用了错误的安全别名,会导致传输失败。通常的规则是,安全世界代码应使用非安全别名地址(bit28=1)来访问与非安全世界共享的数据缓冲区。
2.3 SAU:软件可编程的“动态管控层”
如果说IDAU和MSAU提供了静态的、硬件预定义的“地图”,那么SAU就是软件可以动态绘制的“管制区域图”。SAU是一个可编程单元,其编程模型与大家熟悉的MPU(内存保护单元)非常相似。
核心功能 : SAU允许安全世界的软件(且通常是特权级代码)动态地定义多达8个(具体数量依芯片而定)内存区域,并为每个区域设置安全属性(Secure, Non-secure Callable, 或 Non-secure)。当一次内存访问发生时,最终的裁决逻辑如下:
- 地址匹配 :系统会同时检查IDAU和SAU,看目标地址落在哪个定义的区域内。
- 属性裁决 :如果地址同时匹配IDAU和SAU定义的区域,则遵循 “最高安全等级优先” 原则。安全等级从高到低为:Secure > NSC > Non-secure。例如,如果IDAU将某区域定义为NS,但SAU将其定义为S,则最终该区域被视为S。
- 最终判定 :SAU的设定可以覆盖IDAU的默认设定,但只能向更严格(更安全)的方向覆盖。软件无法用SAU将一个IDAU定义为S的区域降级为NS。
典型配置流程(以RA8T2为例) : 芯片手册会明确给出IDAU的固定映射。例如:
0x0000_0000到0x0FFF_FFFF:被IDAU定义为 NSC 区域。0x1000_0000到0x1FFF_FFFF:被IDAU定义为 NS 区域。
那么,在启用TrustZone时,SAU 必须 进行如下配置:
- 必须(Mandatory)配置 :将所有IDAU定义为NS的区域,在SAU中也明确配置为NS属性。这是为了确保硬件默认的非安全区域在软件层面得到确认。
- 至少一个NSC :在IDAU定义为NSC的区域内(如上述
0x0000_0000开始的区域), 必须 在SAU中创建至少一个使能的、属性为NSC的区域。这是为了给非安全世界提供一个合法的入口点(SG指令存放处)。 - 可选配置 :可以在IDAU定义为Secure或NSC的区域内,用SAU进一步划分出更小的Secure或NSC区域,实现更精细的隔离。
如果不希望使用TrustZone功能,只需保持SAU的全局控制寄存器 SAU_CTRL 为默认值( ENABLE=0 , ALLNS=0 )即可。
3. 内存与外设的安全属性实战配置
理解了原理,我们进入实战环节。配置安全属性不仅仅是设置几个寄存器,更关乎整个系统软件架构的设计。
3.1 内存区域的安全划分
在RA8T2这类芯片中,不同类型的内存(Code Flash, SRAM, TCM)其安全属性的配置方式也不同,主要分为两类:
1. 非易失性内存(如Code MRAM, SiP Flash) : 这类内存的安全属性划分(例如,从物理地址 0x0 开始,多大的区域是安全的)是在芯片的 OEM生命周期状态 下,通过 Boot Firmware命令 一次性烧写的。一旦设定,应用程序无法更改,只能读取。这保证了即使安全世界的应用程序被攻破,也无法缩小安全内存的范围,从而保护了最底层的安全监控代码或密钥。
2. 易失性内存(如SRAM, TCM) : 这类内存的安全属性通过专用的 安全属性边界地址寄存器 来设置。该寄存器通常只能由安全访问写入。例如,可以将SRAM0的低8KB设为安全区域(地址 0x2200_0000 至 0x2200_1FFF ),剩下的部分设为非安全区域(通过非安全别名地址 0x3200_0000 + 8KB 开始访问)。
配置示例与访问规则 : 假设我们配置了SRAM0的低8KB为安全区域。
- 安全世界代码 :可以使用安全别名地址(如
0x2200_1000)直接读写这块内存。 - 非安全世界代码 :
- 如果它尝试使用非安全别名地址(如
0x3200_1000)访问这块物理内存,由于该地址被映射到了安全区域,TrustZone Filter会阻止访问,产生总线错误(BusFault)。 - 如果它尝试使用安全别名地址(
0x2200_1000)访问,则CPU在发出请求时就会因权限不足而触发安全错误(SecureFault)。
- 如果它尝试使用非安全别名地址(如
这种硬件级的拦截是即时且无法绕过的,为敏感数据(如加解密过程中的中间密钥)提供了坚实的保护。
3.2 外设的安全与特权属性
外设的安全配置比内存更灵活,主要分为两种类型:
Type1 外设 :整个外设模块作为一个整体,被赋予统一的安全属性和特权属性。例如,一个SPI控制器要么整个是安全且特权的,要么整个是非安全且非特权的。配置通过集中的安全属性寄存器(如 PSARx )完成。
Type2 外设 :外设内部每个寄存器,甚至每个位字段,都可以独立配置其安全属性和特权属性。这提供了极细的粒度控制。例如,系统控制模块(包含时钟、复位等关键寄存器)通常是Type2,可以将核心的PLL配置寄存器设为安全+特权,而将一些状态寄存器设为非安全+非特权,供应用层查询。
访问权限矩阵 : 外设的访问权限由“安全属性”和“特权属性”共同决定,形成一个清晰的矩阵:
| 访问者属性 \ 外设配置 | 安全+特权 (S+P) | 安全+非特权 (S+U) | 非安全+特权 (NS+P) | 非安全+非特权 (NS+U) |
|---|---|---|---|---|
| 安全+特权访问 | 允许 | 允许 | 拒绝 | 拒绝 |
| 安全+非特权访问 | 允许 | 允许 | 拒绝 | 拒绝 |
| 非安全+特权访问 | 拒绝 | 拒绝 | 允许 | 拒绝 |
| 非安全+非特权访问 | 拒绝 | 拒绝 | 允许 | 允许 |
从上表可以得出几个关键结论:
- 安全高于一切 :安全世界的代码(无论特权与否) 永远不能 访问任何被标记为非安全(NS)的外设。这是硬性隔离。
- 特权在安全世界内部有效 :在安全世界内,特权代码可以访问所有安全外设,而非特权代码只能访问那些被特意开放(配置为S+U)的安全外设。这实现了安全世界内部的权限分离。
- 非安全世界的特权 :在非安全世界,特权模式主要用于访问MPU保护的内核资源,对于外设,只有当外设本身被配置为NS+P时,非安全特权代码才能访问;如果外设是NS+U,则非安全世界的特权和非特权代码都能访问。
避坑指南 :在项目初期进行系统架构设计时,就必须规划好每个外设的归属。一个常见的错误是将一个通信外设(如UART)配置为安全外设,但用于调试打印的非安全世界应用却需要访问它,导致无法输出日志。通常的做法是,将系统关键外设(如加解密引擎、真随机数发生器、安全存储控制器)设为安全外设;将通用通信外设(UART, SPI, I2C)和用户接口相关外设设为非安全外设。如果非安全世界确实需要某种安全服务(如加密),必须通过合法的NSC入口调用安全世界提供的服务API,而不是直接访问安全外设。
4. 系统级安全:生命周期、密钥注入与安全启动
内存和外设的隔离是基础,但要构建一个完整的可信系统,还需要系统级的安保措施,防止设备被非法调试、克隆或运行未经授权的固件。
4.1 设备生命周期管理
DLM定义了设备从出厂到报废整个生命周期的状态,不同状态解锁不同的能力。
- OEM状态 :设备归属客户(产品制造商)。在此状态下,可以执行所有开发活动,包括调试、编程和安全配置。这是主要的开发和配置阶段。
- LCK_BOOT状态 :锁定状态。调试接口和串行编程接口被永久禁用。设备将无法再通过SWD/JTAG进行调试或烧录。这是产品量产发货前的最终状态。
- RMA状态 :返修分析状态。当设备需要返回原厂(如瑞萨)进行故障分析时,需先过渡到RMA_REQ状态。此状态下会擦除用户代码和敏感数据(除永久锁定的区域),然后由原厂分析。
状态转换是单向的,且通常需要密钥认证。例如,从OEM转换到RMA_REQ,需要使用客户预先注入的 RMA_KEY 进行认证。这确保了只有设备所有者才能授权将设备送修,防止中间人恶意获取设备进行分析。
4.2 保护等级与认证等级
在OEM状态下,还有两个重要的子状态:
- 保护等级 :定义了设备能力的基线,分为PL0/PL1/PL2。PL2能力最强(开放安全/非安全调试),PL0最弱(无调试功能)。PL只能降级,不能升级(除非执行初始化擦除)。
- 认证等级 :定义了当前会话的临时权限,也分为AL0/AL1/AL2。AL在每次上电复位后初始化为PL的值。通过使用
AL2_KEY或AL1_KEY进行认证,可以临时将AL提升到更高等级,以执行特定操作(如注入密钥)。
这种设计实现了灵活的权限管理。例如,安全固件开发商可以在AL2下工作,注入根密钥并配置内存划分;然后将PL降为PL1,交给应用开发商在AL1下开发非安全应用;最后在产品出厂前,将PL降为PL0并锁定(LCK_BOOT),彻底关闭调试接口。
4.3 安全密钥注入
所有高级安全功能都依赖于密钥。TrustZone架构支持将用户密钥安全地注入到芯片中。这个过程的核心是 密钥包装 ,确保密钥在传输和存储过程中永不暴露。
标准流程 :
- 生成安装密钥 :用户生成一个256位的UFPK。
- 服务端包装 :将UFPK发送给芯片厂商(如瑞萨)的密钥包装服务,获得包装后的W-UFPK。此步骤利用厂商的根密钥进行保护。
- 本地包装用户密钥 :用户使用UFPK包装自己的实际应用密钥(如AES密钥、RSA私钥),得到“包装后的用户密钥”。
- 芯片端注入 :通过Boot Firmware命令,将W-UFPK和“包装后的用户密钥”发送给MCU。
- 芯片端解包与重包 :MCU内部使用其唯一的硬件密钥解包W-UFPK得到UFPK,再用UFPK解包得到用户密钥,最后用自身的硬件唯一密钥重新包装并存储用户密钥。
核心价值 :在整个过程中,真正的用户密钥明文只出现在用户自己的安全环境中和MCU内部的安全硬件中,从未在通信链路或MCU的非易失存储器中以明文形式出现。即使有人窃取了Flash中的密钥数据,没有MCU的硬件唯一密钥也无法解密。
4.4 安全启动链
安全启动是确保系统从第一行代码开始就可信的根本。RA8T2的安全启动流程是一个典型的基于证书链的验证过程:
- 根密钥注入 :在安全开发阶段,将OEM_ROOT_PK(公钥)的哈希值通过安全密钥注入流程,烧写到芯片的OTP或受保护区域。这是信任的起点。
- OEM引导加载程序验证 : a. 编程时验证 :当通过串行编程器烧写OEM_BL时,Boot Firmware会进行验证。它使用与OEM_BL一同烧写的“密钥证书”(由OEM_ROOT_SK签名)来验证OEM_BL_PK(公钥)的合法性。然后再用“代码证书”(由OEM_BL_SK签名)来验证OEM_BL本身的完整性和真实性。 b. 验证通过后 ,会计算OEM_BL和代码证书的HMAC值(使用基于HUK派生的密钥),并将这个唯一的
OEM_BL_digest存储起来。 - 上电执行 :芯片上电后,固化的第一级引导加载程序会重新计算HMAC,并与存储的
OEM_BL_digest比对。一致则跳转到OEM_BL执行;不一致则触发安全错误(如拉高某个GPIO并进入睡眠模式)。 - 防回滚 :证书中包含镜像版本号。FSBL会检查当前版本不低于已存储的版本,防止被替换成有已知漏洞的旧版本固件。
更新机制 :OEM_BL本身也可以被应用程序更新。更新流程必须重复上述的验证、签名和生成新 OEM_BL_digest 的过程。为了防止更新过程中断电导致系统变砖,强烈建议在代码中实现“更新完成标志”和“增量完成标志”等状态机机制,使新旧两个版本的OEM_BL都能在启动时检测到更新中断并尝试恢复。
5. 常见问题、调试技巧与实战心得
在实际开发和调试基于TrustZone的项目时,会遇到各种问题。以下是一些常见问题的排查思路和经验总结。
5.1 常见问题速查表
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 非安全世界应用访问某内存地址时触发HardFault(SecureFault) | 1. 试图访问安全区域。 2. SAU配置错误,将IDAU的NS区域误配为S。 3. 使用了错误的安全别名地址(应用代码用了bit28=0的地址)。 |
1. 检查触发的是SecureFault还是BusFault。 2. 在安全世界初始化代码中,检查SAU区域配置,确认目标地址所在区域的最终属性是否为NS。 3. 确认应用代码使用的地址其bit28是否为1(非安全别名)。 |
| 安全世界服务函数(位于NSC区域)无法被非安全世界调用 | 1. NSC区域未在SAU中正确启用。 2. 跳转指令不是SG指令。 3. 函数原型未使用 __attribute__((cmse_nonsecure_entry)) 修饰(对于ARMCLANG/GCC)。 4. 函数地址未正确对齐(通常需要至少bit0为1,表示Thumb状态)。 |
1. 确认SAU中至少有一个NSC区域被启用且范围包含SG指令地址。 2. 反汇编查看NSC区域的代码,确认入口是 SG 指令。 3. 检查编译器生成的代码,确保函数开头有清除寄存器等清理操作。 4. 检查链接脚本,确保NSC区域属性正确。 |
| DMA传输失败,或传输到了错误地址 | 1. DMA控制器作为主控,其发起的事务安全属性错误。 2. 源/目标地址的安全别名使用错误。 3. 目标内存/外设的安全属性配置不允许该DMA访问。 |
1. 确认配置DMA的CPU代码运行在哪个世界,这决定了DMA主控的初始安全状态。 2. 安全世界配置DMA访问非安全世界缓冲区时,务必使用非安全别名地址(bit28=1)。 3. 检查目标外设的PSARx寄存器或目标内存的SAU/IDAU属性,是否允许来自该DMA主控的访问。 |
| 调试器连接后,无法访问安全世界的内存或外设 | 1. 芯片的认证等级(AL)不足。 2. 调试接口在安全世界被禁用。 |
1. 检查当前AL。若为AL1,调试器只能访问非安全调试允许的区域;若为AL0,则完全无法调试。 2. 确认是否已通过Boot Firmware命令或选项字节,禁用了安全调试功能。 |
| 安全世界的中断(如SysTick)不触发 | 安全世界的中断向量表(VTOR)地址设置错误,或未在安全世界初始化中正确配置NVIC。 | 1. 确保在安全世界初始化代码中,设置了安全世界的VTOR寄存器,指向安全世界的中断向量表。 2. 确保在跳转到非安全世界之前,已正确配置安全世界需要的中断优先级和使能位。 |
5.2 调试技巧与心得
-
善用安全错误和总线错误 :SecureFault和BusFault是你的朋友。在开发初期,使能这些错误异常,并在其处理函数中打印或记录错误地址、错误来源(IDAU/SAU/MSAU)等信息,能快速定位隔离配置问题。
-
分步验证,循序渐进 :
- 第一步,先不启用SAU :让系统仅依靠IDAU的固定映射运行,确保基础的世界切换和NSC调用流程正确。
- 第二步,配置简单的SAU区域 :例如,只配置一个小的NSC区域和一个NS区域,验证SAU覆盖IDAU的规则是否生效。
- 第三步,细化内存和外设划分 :在基础通信(如串口打印)正常后,再逐步添加更复杂的安全内存区域和外设配置。
-
链接脚本是关键 :链接脚本需要明确划分安全世界、非安全世界以及NSC区域的物理地址和别名地址。务必确保安全世界的代码和数据链接到安全别名地址(如
0x0xxxxxxx),非安全世界的代码和数据链接到非安全别名地址(如0x1xxxxxxx)。编译器/链接器通常提供特定的属性和选项来支持TrustZone(如ARMCLANG的--cmse-implib, GCC的-mcmse)。 -
世界切换的上下文保存 :当从非安全世界通过SG指令调用安全世界函数时,CPU会自动清除通用寄存器中可能包含的非安全世界信息。但是,安全世界函数在返回前,必须使用
BXNS指令,并且需要手动清理可能包含敏感信息的寄存器(R0-R3, R12)以及PSR的某些位。编译器提供的cmse_nonsecure_entry属性会自动在函数入口和出口生成这些清理代码。 -
DMA与共享缓冲区 :安全世界和非安全世界之间传递数据,通常需要一块“共享缓冲区”。这块缓冲区必须在 内存映射上同时存在于两个世界 。实现方法是:在物理内存中划出一块区域,在SAU中将其属性配置为 Non-secure 。这样,安全世界通过非安全别名地址(
0x1...)访问它,非安全世界也通过非安全别名地址访问它,两者看到的是同一块物理内存。切勿将其配置为Secure,否则非安全世界无法访问。 -
启动顺序的重要性 :系统的启动必须是“先安全,后非安全”。安全世界的启动代码负责初始化SAU、配置外设安全属性、初始化自己的运行时环境,然后才跳转到非安全世界的复位向量。这个跳转通常是通过修改非安全世界的VTOR和MSP/PSP,然后执行一个特殊的返回指令(如
BXNS)来完成。
内存隔离是嵌入式安全的基石,而SAU、IDAU、MSAU是实现这基石的核心硬件机制。它要求开发者从硬件视角思考软件架构,将“隔离”作为设计的第一性原则。虽然初期的学习和调试成本较高,但一旦掌握,你将能构建出真正具备硬件级抗攻击能力的嵌入式产品。在物联网设备成为攻击新目标的今天,这项技能的价值不言而喻。
更多推荐
所有评论(0)