4. TEE内存隔离机制:TZASC、TZMA与安全非安全内存隔离
内存隔离,说白了就是给安全世界和非安全世界之间砌一堵墙。这堵墙砌得牢不牢,直接决定了TEE的根基稳不稳。我早年做第一块TEE芯片时,就因为在内存隔离上踩了个坑,导致安全应用的数据能被普通世界读出来——嗯,那场面,至今想起来后背发凉。
ARM TrustZone的内存隔离,主要靠两个硬件模块:TZASC和TZMA。它们分工不同,但目标一致——确保安全内存只有安全世界能碰,非安全世界连看都别想看。
核心原则:安全世界可以访问所有内存(安全+非安全),非安全世界只能访问非安全内存。这个单向可见性,是TEE内存模型的基础。
4.1 TZMA:简单粗暴的片上内存隔离
TZMA(TrustZone Memory Adapter)负责隔离片上SRAM。它做的事情很直接:把一块连续的SRAM从中间切开,前半段给安全世界,后半段给非安全世界。
你想想看,芯片内部的那点SRAM,通常也就几百KB。TZMA通过一个简单的大小寄存器来定义分界线。比如总共有256KB SRAM,你把寄存器设为128KB,那0x0000-0x7FFF就是安全区,0x8000-0xFFFF就是非安全区。
我个人的习惯:在项目初期,我会把TZMA的安全区设得大一些,比如总SRAM的75%。等安全应用稳定了,再慢慢调小,把更多资源让给非安全世界。别一开始就抠门,否则调试时你会发现安全堆栈溢出了,那才叫头疼。
TZMA的配置是静态的,通常在系统启动时由安全固件设置好,之后就不再改动。为什么?因为它的分界线是物理硬连线的,改一次就要重新初始化整个内存系统。
| 特性 | TZMA |
|---|---|
| 管控对象 | 片上SRAM |
| 隔离粒度 | 整块区域(通常KB级) |
| 配置方式 | 启动时静态配置 |
| 灵活性 | 低,改分界线需要复位 |
| 典型大小 | 几十KB到几MB |
4.2 TZASC:灵活强大的片外内存隔离
TZASC(TrustZone Address Space Controller)就比TZMA灵活多了。它管控的是片外DRAM,也就是我们常说的DDR内存。DDR动辄几百MB甚至几GB,不可能像SRAM那样一刀切。
TZASC的核心思想是区域划分。它把DDR地址空间分成若干个区域,每个区域可以独立配置安全属性。我记得在某个项目中,我们用了8个区域,每个区域可以设置起始地址、结束地址、以及访问权限。
每个区域的配置项包括:
- 基地址和大小:定义这个区域在物理内存中的位置
- 安全属性:安全、非安全、或者安全/非安全都可访问
- 可缓存性:是否允许CPU缓存
- 执行权限:是否允许执行代码
我曾经踩过的坑:有一次配置TZASC时,我把安全世界的堆区域设成了"安全+可缓存",但忘了配置相应的缓存清理逻辑。结果安全世界和非安全世界之间切换时,缓存里的脏数据被写回了错误的内存区域。调试了整整三天,最后发现是缓存一致性的问题。所以记住:安全内存的缓存策略一定要和TEE的切换流程匹配。
4.3 TZASC的编程模型
TZASC的寄存器通常通过APB(高级外设总线)访问,而且这些寄存器本身也是安全敏感的——只有安全世界才能写它们。配置流程大致如下:
// 伪代码:配置TZASC区域
void tzasc_configure_region(uint32_t region_id,
uint64_t base,
uint64_t size,
uint32_t attributes) {
// 1. 先禁用该区域(防止配置过程中出现竞态)
write_register(TZASC_REGION_CTRL(region_id), 0);
// 2. 设置基地址(通常需要64位对齐)
write_register(TZASC_REGION_BASE_LOW(region_id), base & 0xFFFFFFFF);
write_register(TZASC_REGION_BASE_HIGH(region_id), (base >> 32) & 0xFFFFFFFF);
// 3. 设置大小(必须是2的幂次,且对齐)
write_register(TZASC_REGION_SIZE(region_id), size);
// 4. 设置属性:安全/非安全、读/写、可执行等
write_register(TZASC_REGION_ATTR(region_id), attributes);
// 5. 使能该区域
write_register(TZASC_REGION_CTRL(region_id), 1);
}
这里有个细节:TZASC的区域大小必须是2的幂次,而且基地址必须对齐到大小边界。比如你要配置一个4MB的安全区域,基地址必须是4MB的整数倍。这个限制在早期规划内存布局时就要考虑进去。
4.4 安全内存与非安全内存的交互
隔离归隔离,两个世界总得交换数据吧?比如安全应用要解密一个文件,然后把明文传给普通世界。这时候就需要共享内存机制。
共享内存的实现方式有两种:
- 非安全缓冲区:安全世界主动把数据写到非安全内存区域,然后通知非安全世界来取。这种方式安全世界需要做一次内存拷贝。
- 安全缓冲区:在TZASC中配置一个"安全+非安全可读"的区域。双方都能访问,但只有安全世界能写。这种方式减少了拷贝,但配置要格外小心。
我建议:除非性能要求极高,否则优先用非安全缓冲区方案。安全缓冲区虽然省了一次拷贝,但配置不当很容易变成安全漏洞——万一非安全世界找到了写这个区域的方法,那安全数据就全暴露了。
4.5 内存隔离的完整视图
下面这张图展示了TZASC和TZMA在整个系统中的位置和协作关系:
4.6 实际项目中的避坑指南
做TEE内存隔离,有几个地方特别容易出问题:
- DMA绕过问题:有些外设支持DMA(直接内存访问),它们可以直接读写内存,不经过CPU的MMU。如果DMA控制器没有被纳入TZASC的保护范围,它就能绕过安全隔离。我见过一个案例,某个网卡DMA直接把数据写到了安全内存区域,导致安全密钥泄露。
- 大小核架构:现在的SoC通常有多个CPU簇,每个簇有自己的L2缓存。如果不同簇的缓存策略不一致,安全世界在一个簇上运行,非安全世界在另一个簇上运行,缓存一致性协议可能会把安全数据同步到非安全缓存中。
- 休眠唤醒:系统进入深度休眠后,TZASC的寄存器状态可能会丢失。唤醒后如果恢复顺序不对,安全内存会在短暂时间内处于未保护状态。我建议在休眠前保存TZASC的配置快照,唤醒后第一时间恢复。
特别提醒:永远不要假设非安全世界的软件是"友好"的。攻击者可能已经控制了整个非安全操作系统。你的TZASC配置必须假设非安全世界会尝试所有非法访问——包括越界读、越界写、以及尝试修改TZASC寄存器本身。
嗯,内存隔离这块,说复杂也复杂,说简单也简单。核心就是记住:TZMA管片上,TZASC管片外,两者配合把安全世界和非安全世界的内存彻底隔开。配置的时候多想想边界情况,多想想攻击面,基本上就不会出大问题。