尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

TrustZone-M硬件隔离实战:从SAU配置到RTOS协同的完整指南

TrustZone-M硬件隔离实战:从SAU配置到RTOS协同的完整指南 1. 先从一次真实的选型经历说起这几年物联网设备的安全事件越来越频繁固件被逆向、密钥被提取、远程接口被接管这些事几乎每个月都能在新闻里看到。做嵌入式的人应该都深有体会Cortex-M 平台上一套普通的产品代码只要攻击者拿到硬件、接上调试器或者通过串口漏洞进入系统整个产品的底裤就没了。我们之前做一款带联网功能的采集类设备客户要求固件里的签名私钥不能被读出而且就算通信协议被逆向也要保证设备无法被远程控制。一开始想在软件层面做混淆和校验但折腾了一圈发现这种纯软件方案防得住普通用户防不住真正懂行的人——调试接口一封固件读不出来但攻击者可以通过故障注入、内存越界等方式拿到运行中的敏感数据。后来我们的方案转向了 ARM 在 Cortex-M 上提供的 TrustZone-M 技术。TrustZone-M 不是加密算法也不是某种安全启动框架它是从 Cortex-M23/M33 开始引入的一套硬件隔离机制核心思路是把整个 SoC 内部的世界划分为安全世界Secure World和非安全世界Non-Secure World。安全代码、密钥和敏感数据放在安全世界里普通应用程序跑在非安全世界两边通过硬件级别的访问控制隔开。即便非安全侧被攻破攻击者也无法直接读取安全侧的内存和外设寄存器。这套机制和 ARM 的 Cores 架构深度绑定是 ARMV8-M 架构的硬件能力不是依赖软件强撑的。这篇文章不是 TrustZone-M 的官方文档翻译。我把它当成一个工程课题来拆设计上如何划分安全边界SAU/IDAU 怎么配安全函数怎么被非安全代码调用RTOS 如何嵌入这套体系以及我在实际项目里踩过的那些坑。内容基于我个人的工程实践和 ARMV8-M 架构的公开资料整理适合准备在带 TrustZone-M 的 MCU 上做安全设计的开发者参考——不管你是刚接触这个概念的初学者还是已经看完 ARM ARM 手册、正在调 SAU 寄存器的进阶玩家这篇都应该有点用。2. TrustZone-M 的核心设计思路硬件隔离而不是软件防御2.1 为什么 Cortex-M 上需要一条硬件安全边界在 TrustZone-M 出现之前Cortex-M 处理器的安全模型主要依赖特权等级。内核运行在 Thread 模式或 Handler 模式配合特权/非特权两级权限。这套模型能挡住一部分用户态程序的非法访问但它有一个根本缺陷所有敏感资源——密钥、固件、外设——基本都放在同一个物理地址空间里只要攻击者拿到一次特权代码执行的机会比如栈溢出覆盖返回地址或者利用了某个驱动漏洞整个系统就完全沦陷。TrustZone-M 改变了这种“一层防御、一破全破”的局面。它把处理器和总线上的所有访问请求都打上了一个安全标签这个标签决定了一次访问是来自安全世界还是非安全世界。非安全世界的代码无论处于特权等级还是非特权等级都只能访问被标记为“非安全”Non-Secure的地址区域和资源只有来自安全世界的代码才有权限触达安全区域。攻击者就算拿下了整个非安全固件也仅仅是待在一个“笼子”里真正的敏感数据和关键逻辑还在笼子外面隔着一道硬件墙。这道墙的价值在实际产品里非常直接非安全侧可以做完整的应用逻辑、协议栈和用户界面安全侧只保留密钥存储、签名校验、安全启动、固件升级认证这几个核心功能。两边通过函数调用接口协作接口边界由硬件强制约束而不是靠程序员的自觉。2.2 TrustZone-M vs TrustZone-A隔离的维度完全不同用过 Cortex-A 平台 TrustZone 的开发者迁移到 M 系列时最容易产生误解。TrustZone-A 的隔离建立在虚拟内存系统之上安全世界和非安全世界各自有一套页表通过分页的方式对整块内存做访问控制。TrustZone-M 则完全不同它面向的是几乎没有 MMU 的微控制器环境不具备页表转换能力隔离粒度依靠的是地址区域的安全属性划分。TrustZone-M 的内存安全属性并不细到“某一个具体字节”而是按照 32 字节的粒度来控制。它也不像 A 系列那样依赖操作系统来管理页表安全属性由硬件信号直接驱动——总线上的每次传输都携带安全状态信息AMBA 总线协议通过额外的信号线来传递这个状态。所以 TrustZone-M 的保护是“硬件实时生效”的即使安全世界的代码本身存在漏洞非安全侧也没办法利用这个漏洞来读取安全资源。另外TrustZone-A 在实际落地中高度依赖 Rich OS比如 Linux与 TrustZone 驱动的配合而 M 系列的场景往往是裸机或 RTOS甚至安全侧可能只是一个小型可信固件资源占用要小得多。这就导致了另一种差异M 系列上的安全世界通信接口设计必须考虑到极低资源占用和简单易用的需求。2.3 哪些 Cortex-M 核心支持 TrustZone-MTrustZone-M 是 ARMV8-M 架构的一部分但并不是所有 Cortex-M 核心都自带。目前支持 TrustZone-M 的主要产品线包括核心架构典型定位Cortex-M23ARMV8-M Baseline低功耗、成本敏感替代 M0/M0Cortex-M33ARMV8-M Mainline中高性能替代 M3/M4Cortex-M55ARMV8.1-M Mainline带 Helium 向量扩展AI/ML 场景Cortex-M85ARMV8.1-M Mainline高性能接近 M7 水平挑选核心的时候要特别留意Cortex-M23 属于 Baseline 子集它的 TrustZone-M 实现和 M33 有一些细节差异例如指令集支持和调试特性不同。M55 和 M85 在 ARMV8.1-M 上增加了更多向量和循环展开能力但 TrustZone-M 的基本模型是一致的。实际选型时除了看核心性能还要看 SoC 厂商是否完整实现了 TrustZone-M包括安全中断、安全外设和调试隔离。有些低成本芯片虽然核心是 M33但为了省钱把 SAU 相关的物理内存区域做得很怪。3. SAU、IDAU 与内存地图把安全属性落到实处3.1 两级安全属性来源IDAU 先定边界SAU 再做收窄TrustZone-M 的每个内存地址它的安全属性由两个硬件单元共同决定一个叫 Implementation Defined Attribution UnitIDAU一个叫 Security Attribution UnitSAU。IDAU 是 SoC 设计者在芯片布线时就已经固化好的属性比如某块 Flash 在硬件上被设定为安全某块 SRAM 在硬件上被设定为非安全这些属性不能用软件修改。SAU 则是软件可以配置的安全属性单元它允许你在 IDAU 给定的基础上进一步把某些区域标记为安全、非安全或者 NSC。两者的合成规则是最终安全属性 IDAU 属性与 SAU 属性做一次“或”运算——只要任何一个单元认为该地址是非安全地址就被视为非安全。换句话说IDAU 给了芯片设计者一个硬底线SAU 给了开发者一个受限的自由度整个系统的安全边界不是开发者一个人说了算的而是芯片设计者与开发者共同决定。我最初理解这个模型时绕了点弯子后来把它类比成物业与住户的关系IDAU 是物业在建筑图纸上画好的承重墙你不能拆SAU 是你自己在房间里安的隔断可以在允许范围内改变空间格局。承重墙的范围决定了你装修的自由度。3.2 SAU 的寄存器操作从配置到生效SAU 的核心寄存器一共有四个再加上一个控制寄存器。具体来说包括SAU_CTRL总开关里面有个ENABLE位置 1 后 SAU 开始参与安全属性的裁决。SAU_TYPE只读寄存器描述当前芯片支持多少个 SAU regionARM 规定最少 8 个。SAU_RNRRegion Number Register选择当前要配置的是第几个区域。SAU_RBARRegion Base Address Register设置区域的起始地址。SAU_RLARRegion Limit Address Register设置区域的结束地址同时包含区域的 NONSEC 属性和 NSC 属性。配置流程不复杂先关掉SAU_CTRL.ENABLE再逐个配置区域最后重新打开总开关。我通常写一个独立的安全初始化函数在系统启动早期调用放在时钟初始化和外设初始化之前。void secure_init_sau(void) { // 配置前先关闭 SAU避免配置过程中安全属性不一致 SAU-CTRL 0; // Region 0: 非安全世界代码区假设 0x10000000 大小的 Flash 前 1MB SAU-RNR 0; SAU-RBAR (0x00000000 SAU_RBAR_BASE_Msk); SAU-RLAR ((0x000FFFFF SAU_RLAR_LIMIT_Msk) | (1 SAU_RLAR_NSC_Pos) | // 注意如果这块区域不需要作为安全调用入口NSC 必须为 0 0); // NONSEC 0表示安全区域 // 开启 SAU SAU-CTRL SAU_CTRL_ENABLE_Msk; }上面这段是示意图实际配置里 NSC 位和 NONSEC 位必须根据你的设计意图精确取值。一个容易犯的错是把整个非安全区域都设成 NSC这等于给非安全代码开了无数个进入安全世界的“后门”虽然这些后门要经过 SG 指令检查但攻击面会大很多。NSC 应该只标记那几个真正需要被非安全侧调用的安全函数入口地址。3.3 内存地图规划Flash、SRAM、外设怎么分规划内存地图是整个 TrustZone-M 设计里最考验经验的一步。你需要和目标芯片的内存地址分布表对着看逐个区域标注安全属性。以我们用到的一款 Cortex-M33 芯片为例它的布局大致如下Flash整体 0x00000000 起大小 2MB。我们把前 1MB 分给非安全应用后 1MB 分成两个部分——安全固件放在靠后的区域同时从安全 Flash 区域里拿出一个 32 字节对齐的小块标记为 NSC作为安全调用的入口。SRAM芯片有 512KB我们划分成0x20000000起的非安全 SRAM 和另一段安全 SRAM。安全上下文、密钥缓冲、安全固件的栈都放在安全 SRAM。外设所有通信外设UART、SPI、I2C、以太网 MAC都跑在非安全侧这样应用可以直接操作安全相关外设比如密钥存储控制器、CRC 引擎、随机数发生器映射到安全地址空间。原则就一句话能不放在安全侧的资源尽量不要放在安全侧。安全侧越精简被攻击的面越小。很多新手上来就把所有外设都设成安全结果非安全侧驱动一跑就 HardFault调试起来特别痛苦。4. 安全世界与非安全世界的交互机制不只是“跳转函数”4.1 安全状态机一次调用怎么穿越世界边界TrustZone-M 的内核有一个安全状态位存放在特殊寄存器中硬件根据当前运行状态决定总线访问的安全标签。当非安全代码调用安全函数时CPU 要通过一套专门的指令来完成状态切换SGSecure Gateway指令和BXNS/BLXNS指令。非安全侧要调用一个安全函数不能直接在任意地址跳过去。它的流程必须是非安全代码通过BLXNS跳转到 NSC 区域内的某个入口地址。这个入口地址处必须有一条SG指令而且这条指令必须位于被标记为 NSC 的内存区域里。SG指令让处理器切换到安全状态然后跳转到真正的安全函数。安全函数执行完毕通过BXNS指令返回非安全世界。这里面最容易被忽略的是 NSC 区域的 32 字节对齐要求。ARM 规定 NSC 区域的大小必须是 32 字节的倍数入口函数的地址也要求对齐。如果编译后的安全入口函数地址不对齐调用时会直接触发 UsageFault 或者 HardFault而且这种问题比较难查因为出错位置和调用位置看似没什么关联。在 C 代码里我们通常用 ARM 提供的 CMSECortex-M Security Extension属性来标记安全入口函数__attribute__((cmse_nonsecure_entry)) int secure_verify_signature(uint8_t *data, uint32_t len) { // 这个函数可以被非安全世界安全调用 return do_verify(data, len); }cmse_nonsecure_entry属性会自动让编译器生成符合要求的入口代码包括正确的BXNS返回序列和必要的栈清理逻辑。写裸汇编当然也可以但不建议CMSE 属性省去了很多手工处理安全边界的麻烦。4.2 安全函数调用非安全函数回调是怎么实现的大多数应用还有一个需求安全侧发起某个操作需要非安全侧配合执行。比如安全固件要做网络通信但它自己不想碰 TCP/IP 协议栈这时候就需要安全侧回调非安全侧的代码。ARM 提供了对应的机制函数指针可以加上cmse_nonsecure_call属性来声明typedef int (*ns_func_ptr)(int data) __attribute__((cmse_nonsecure_call)); int secure_trigger_callback(ns_func_ptr callback, int data) { // 调用非安全侧函数 return callback(data); }这里有个安全知识点从安全侧调用非安全函数CPU 会清理一部分寄存器的敏感信息防止经过程序调用将安全世界的数据泄露给非安全侧。cmse_nonsecure_call属性保证了编译器生成正确的调用序列并且 ARM 的设计中还支持在返回时清空那些包含敏感数据的寄存器我建议在编译安全侧工程时保持默认的-mcmse选项不要轻易关掉这个清理行为。4.3 安全与非安全中断向量表怎么分家中断处理是 TrustZone-M 工程里最容易迷惑的区域。在 ARMV8-M 中向量表被分成安全向量表和非安全向量表NVIC 里的每个中断也可以被单独配置成安全或非安全。非安全中断触发时处理器直接进入非安全 Handler 模式安全中断触发时处理器则必须切换到安全状态来处理。软件上控制中断安全属性的寄存器主要是NVIC_ITNS系列。往对应位写 0 表示该中断是安全中断写 1 表示非安全中断。我们在项目里的做法是所有普通业务中断都设为非安全只有安全侧自己用的内部定时器中断和密钥处理完成中断设为安全。这样非安全侧的 RTOS 可以正常管理它的中断安全侧又能保证关键操作的时序不被非安全侧干扰。中断问题还牵扯到AIRCR寄存器的BFHFNMINS位和PRIS位。BFHFNMINS决定 BusFault、HardFault 和 NMI 是否为非安全属性PRIS决定安全中断是否能抢占非安全中断。这些位的默认值因芯片而异我建议在一开始做安全设计评审时就明确这些位该设成什么不要依赖默认值——因为默认值往往不是你想要的行为。5. RTOS 与 TrustZone-M 的协同FreeRTOS 的实操经验5.1 裸机能跑 TrustZone-M 吗可以但如果产品里已经用了 RTOS直接裸机跑安全侧和业务侧反而增加了复杂度。TrustZone-M 的隔离能力解决的是“安全世界与非安全世界互不干扰”的问题而 RTOS 解决的是“任务调度”的问题。两者叠加在一起面临的第一个问题就是RTOS 的任务都跑在非安全世界安全世界要不要也跑一个 RTOS我的经验是安全侧不要跑完整的 RTOS用裸机状态机就够了。安全侧只需要处理几种固定操作——签名校验、密钥管理、安全固件升级约定这些操作的并发度很低跑 RTOS 纯属增加调试难度和受攻击面。非安全侧则正常跑 FreeRTOS 等传统 RTOS业务任务照常创建、调度所以“会 RTOS 的团队”和“会 TrustZone 的团队”要结合起来。5.2 FreeRTOS 的 TrustZone 适配细节FreeRTOS 从 V10.2 版本开始对 ARMv8-M 的 TrustZone-M 有官方支持。它主要在调度器上下文切换时做安全状态保存与恢复以及任务创建的扩展属性。有几个宏必须清楚configENABLE_TRUSTZONE启用 TrustZone 支持建议设为 1。configRUN_FREERTOS_SECURE_ONLY如果 FreeRTOS 只跑在安全侧设为 1但通常我们是让 RTOS 跑非安全侧设 0。configENABLE_FPU如果使用 M33/M55 的浮点单元为了让非安全侧任务和浮点寄存器状态正确切换部分场景还涉及安全侧的设置需仔细配置。工程操作上还要注意启动流程。典型流程是上电后先从安全世界启动安全固件完成 SAU、MPU 和中断安全属性的初始化然后跳转到非安全世界启动非安全固件里的 FreeRTOS。这里有个细节如果非安全侧代码入口地址不对或者非安全向量表没设好整个系统在启动阶段就卡死了。跳转前要确认非安全向量表地址已写入VTOR的非安全寄存器。5.3 工程化中链接脚本和启动文件的调整点TrustZone-M 工程的编译链接比普通 Cortex-M 工程要复杂一截因为你实际上在产出两个独立的固件镜像安全镜像和非安全镜像。两个镜像各有自己的链接脚本但必须遵循同一份内存地图约定。链接脚本里最重要的定义就是给 NSC 区域预留地址并把安全入口函数放在这个区域内。常见做法是在安全侧链接脚本里用.nsc_region段来控制.nsc_region : ALIGN(32) { KEEP(*(.nsc_region*)) . ALIGN(32); } FLASH_NSC然后在 C 代码里把安全入口地址放到这个段里或者用__attribute__((section(.nsc_region)))标记入口函数。启动文件方面安全侧启动文件需要额外做 SAU 初始化非安全侧启动文件必须确保没有访问安全内存的代码否则一上电就 HardFault。6. 调试 TrustZone-M 系统那些让人崩溃的问题和排查思路6.1 崩溃第一现场非安全代码访问安全地址最常见的故障表现是系统一启动就跑飞或者调用某个外设时直接 HardFault。排查时先分清是访问违例还是非法指令通过调试器读HFSRHardFault Status Register和MMFARMemManage Fault Address Register如果是访问违例故障地址能直接告诉你碰了哪块不应该碰的区域。我调试过最典型的一次非安全侧一个驱动初始化代码直接写了安全外设的寄存器地址编译链接全通过一运行就 HardFault。查了半天发现是该外设的安全属性被 SAU 设成了安全而非安全驱动没有权限访问。解决办法要么把外设改为非安全属性前提是这个外设不需要保护要么把对这个外设的操作搬到安全侧去封装一个接口给非安全侧调用。6.2 安全函数调用异常入口地址不对齐另一个高频问题是在非安全侧调用安全函数结果直接进 HardFault或者调用成功但返回后系统跑飞。这类问题通常不是代码逻辑错而是 NSC 区域和 SG 指令的匹配问题。我通常用这几步排查先确认入口函数地址是否落在 NSC 区域范围内。用调试器读 SAU 的 RBAR/RLAR对比函数地址。检查入口函数的对齐是否满足 32 字节要求。如果编译器为函数插入的 padding 破坏了地址对齐需要强制指定函数地址。用反汇编打开安全固件确认入口地址处第一条指令确实是SG。如果编译优化后SG被挪走或合并会非常隐蔽。6.3 调试器的限制安全世界的调试权限TrustZone-M 系统里调试器也不是想看哪里就看哪里。ARM 的调试架构允许通过 CoreSight 的调试认证机制来限制非安全调试器对安全世界的访问。实际项目中我们要么使用支持安全调试的开发板要么在调试阶段临时放宽安全限制但在量产固件里必须收紧。我踩过的坑是量产固件把DEMCR等调试寄存器锁死之后发现现场设备的故障日志根本采集不到安全侧的状态。后来我们设计了专门的诊断接口安全侧定期把状态信息通过共享内存区域发给非安全侧再由非安全侧通过日志通道输出。这样即使安全调试口被封闭我们依然能从非安全侧拿到足够的安全侧状态信息。6.4 一张排查速查表现象可能原因排查方向启动即 HardFault安全向量表/非安全向量表地址配置错误检查 VTOR 安全与非安全寄存器调用安全函数崩溃NSC 区域配置错误、入口地址未对齐用调试器核对 SAU 区域和入口地址汇编外设访问违例外设安全属性与访问侧不匹配查看外设对应地址的 IDAU/SAU 属性安全中断不触发NVIC_ITNS 位配置错误检查中断安全属性配置RTOS 任务切到安全代码后跑飞缺少cmse_nonsecure_call保护检查回调函数指针的属性声明安全调试口无法连接调试认证被锁定检查 CoreSight 调试权限配置7. 用一个最小示例把流程串起来理论说了这么多最终还是要看代码怎么组织。我这里用一个伪代码结构展示一个最小可运行的 TrustZone-M 工程骨架帮助你把前面几章的内容串成一条线。安全侧启动流程void Secure_Startup(void) { // 1. 先关闭全局中断确保配置过程不被中断打扰 __disable_irq(); // 2. 配置 SAU划分安全/非安全/NSC 区域 secure_init_sau(); // 3. 配置需要安全隔离的外设和中断 secure_init_peripherals(); // 4. 配置非安全向量表地址让非安全侧能找到自己的入口 TZ_ConfigNSVTOR((uint32_t)__VECTOR_TABLE_NONSECURE); // 5. 跳转到非安全世界 TZ_JumpToNonSecure((uint32_t)NonSecure_Startup); }非安全侧启动流程void NonSecure_Startup(void) { // 1. 初始化非安全侧的时钟和外设 SystemInit(); // 2. 启动 RTOS 之前先验证安全侧通信通道是否正常 if (Secure_VerifySignature(sample_data, sample_len) ! 0) { // 安全校验失败处理 } // 3. 创建业务任务启动 RTOS 调度 xTaskCreate(business_task, task, 1024, NULL, 1, NULL); vTaskStartScheduler(); }这个骨架里安全侧的Secure_VerifySignature是一个带cmse_nonsecure_entry属性的安全入口函数被非安全侧调用来做数据校验。非安全侧的业务完全不知道自己被隔离在一个“非安全笼子”里它只知道有些函数调用需要经过特殊通道。实际操作中ARM 提供了配套的固件包比如 Arm TrustedFirmware-M里面已经帮你封装好了TZ_ConfigNSVTOR、TZ_JumpToNonSecure这些函数不需要自己写汇编。我建议新手先用官方示例搭一个两个 LED 交替点亮的最小工程一个跑安全侧一个跑非安全侧确认 TrustZone 的“墙”真的存在再做复杂设计。以我个人的实际体会来说TrustZone-M 的难点从来不在理解架构而在落地时的细节纪律。SAU 配置、NSC 区域管理、中断属性划分、链接脚本调整每一项单独看都不难但组合在一起任何一个环节松散整个安全墙就会出现缝隙。建议从小处着手先用官方开发板和示例跑通最小系统确认安全调用链路上没有“裸奔”的窗口再逐步加入密钥存储、固件升级这些业务逻辑。最后再分享一个我习惯的小技巧在给安全侧写启动代码的时候一定要在一开始就打开 SAU 并在初始化阶段注释里写清楚每块内存的用途和归属性。几个月后再回来看代码你会感谢当时那个写了详细注释的自己。
返回列表