☰
自研微型TEE:基于TrustZone的底层安全系统设计与实践
2026/10/7 14:35:40 网站建设 项目流程

做移动端安全的朋友,应该都有过一种无力感:你花了大半精力去给客户端做加固、混淆、反调试,结果攻击者拿到设备后直接刷一个带 root 的系统镜像,把上层所有防护全部瓦解。更难受的是,用户指纹、支付密钥、聊天记录这些敏感数据,其实都保存在同一个操作系统里,一旦系统级服务被 hook 或者内核被提权,整个应用层就是一层纸。这也是我决定把安全重心从“应用层对抗”往下探到“底层隔离”的根本原因。

MiTEE 就是我们自研的一套轻量级底层安全系统,从名字上也能看出来,它的定位是微型可信执行环境(Micro Trusted Execution Environment)。它的核心思路并不复杂:在 ARM TrustZone 提供的硬件隔离基础上,专门划分出一个与主系统完全隔离的安全世界,把所有敏感操作放进这个独立世界里运行。这样即使 Android/Linux 系统被完全攻破,攻击者也拿不到安全世界里受保护的密钥和认证材料。

这篇文章我会从方案选型、整体架构、核心实现、安全测试到实际踩坑,完整复盘一遍。适合正在做移动安全、嵌入式安全、可信计算相关工作的工程师,也适合那些想弄懂“系统底层是怎么保护用户信息安全”的同学。如果你正在准备信息安全工程师相关认证,或者打算参加全国大学生信息安全类竞赛,理解 TEE 这类底层机制绝对是个加分项。

MiTEE 从一开始就不是奔着“做一个演示 Demo”去的,而是作为终端产品的安全底座来设计。下面先聊清楚一个关键问题:为什么在硬件隔离这件事上,我们不直接拿现成的通用 TEE 方案,而是要自己动手。

1. 为什么我会决定自研一套底层安全系统

1.1 传统安全方案的“天花板”在哪里

在引入 MiTEE 之前,我们团队也走过一段“常规路线”。客户端做代码加固、通信做加密、服务端做风控,这套组合拳在绝大多数业务场景里是够用的。但有个前提假设:操作系统本身是可信的。一旦攻击者拿到了内核执行权限,应用层的加密算法、密钥存储、完整性校验,全都变成了“门锁装在纸墙上”的把戏。

举一个真实场景:某次我们收到反馈,用户设备上的支付应用被植入了恶意模块,每次付款时都会把订单金额篡改掉。我们排查后发现,攻击者通过系统漏洞拿到了 root 权限,直接在内存里改了应用数据。应用层自己也做了防护,但面对 root 后的任意读写,根本挡不住。后来我们想尽办法把敏感数据往系统更深处藏,但只要是同一个内核里跑着的进程,终归都在同一个信任域里,靠权限分离只能提高门槛,不能彻底切断暴露面。

这类问题的本质,是没有一个独立于主系统的“可信锚点”。应用层再怎么加固,也只是在同一个不安全环境里做防御。真正需要的是把安全边界从逻辑层面下沉到物理层面,让即便整个主系统沦陷,核心资产依然不在攻击者手里。

1.2 为什么选择 TrustZone 而不是内置一套完整 TEE

业界成熟的 TEE 方案其实不少,比如基于 TrustZone 的 OP-TEE,或者谷歌的 Trusty,甚至高通/qcom 也有自己的一套 TrustZone 实现。那为什么还要自研?

第一次评估时,我们对比了几条路线。通用 TEE 方案功能很全,驱动、存储、认证模块一应俱全,但对我们这种资源受限的嵌入式终端来说,有两个比较现实的问题:一是镜像体积和内存占用太大,为了跑一个“保险箱”功能,要付出几 MB 甚至几十 MB 的代码和运行时成本,在 256MB 内存的设备上不太划算;二是黑盒化程度高,出了问题很难从上到下排查,尤其是想针对业务做定制裁剪时,改动闭源组件非常痛苦。

我们想要的东西其实很明确:一个代码量足够小、行为足够可预测、内部实现完全可控的微型 TEE。TrustZone 硬件本身就提供了 Normal World 和 Secure World 的物理隔离,只要基于这份硬件能力去写一个微内核,把必要的安全服务放进去就可以了。这就好比你要给重要文件盖一间独立仓库,通用方案是直接拉来一栋大型安保大楼,而我们想要的是一个门锁简单、只有自己手里有钥匙、但结构完全清楚的小房间。MiTEE 就是这个小房间。

1.3 MiTEE 的目标定位与使用场景

MiTEE 初期的目标很简单:在 ARM 架构的终端设备上,提供三个核心能力:密钥管理、完整度量、远程证明。密钥管理负责安全生成、存储和使用密钥;完整度量负责在启动过程中记录系统各阶段镜像的哈希,并在运行时动态校验;远程证明则是向服务端证明“这台设备当前运行的是可信且未被篡改的系统状态”。

这套东西能用在很多场景:移动支付终端的交易签名、门禁设备的身份认证、智能硬件的 OTA 固件验签、甚至车联网节点的安全凭证管理,都可以把核心逻辑下沉到 MiTEE 里。因为它不依赖具体业务,天然就是一个可复用的底层安全基础设施。

思路定下来之后,接下来的问题就是:整个系统在结构上应该怎么分层,TrustZone 的隔离能力要如何充分利用而不透支?这部分是决定项目成败的关键。

2. MiTEE 的整体架构与可信边界设计

2.1 双世界模型:从硬件层面切开信任域

ARM TrustZone 是一种系统级的安全扩展,它把处理器和内存系统分成了两个世界:普通世界(Normal World)和安全世界(Secure World)。普通世界跑的是 Android/Linux 这类功能丰富的富操作系统,安全世界跑的则是我们自己的安全固件。两个世界之间的切换,只能通过一条特权指令 SMC 进入 monitor 层完成,普通世界的软件没有直接进入安全世界的通道。

MiTEE 把自己放在 Secure World 里,跑在 secure EL1 上,而 monitor 只保留在最精简的 EL3 层。这样设计的一个重要原则是:安全世界的代码越少越好,越少越容易审计,越少越难出漏洞。我们刻意避免把大量业务逻辑写进安全世界里,安全世界里的模块只保留密钥操作、度量校验、认证应答这几个核心功能,其他所有逻辑一律留在普通世界。

有一个概念需要重点理解:传统操作系统里的“内核态和用户态”隔离,只是同一台机器里的几扇门,攻击者拿到一个内核漏洞就能到处串门。而 TrustZone 的双世界模型,相当于两栋独立的楼,楼之间唯一的通道是一个有哨兵把守的安检口。即便你在普通世界的楼里放火烧了整层,也影响不到安全世界里的保险柜。

2.2 可信启动链:决定“第一口”是干净可信的

隔离做得再好,如果启动过程不可信,后续一切都白搭。MiTEE 采用标准的信任根链启动(Root of Trust Chain):设备上电后,首先执行固化在芯片内部的 BootROM,BootROM 校验引导加载器 Bootloader 的签名,引导加载器再校验 MiTEE 镜像,最后 MiTEE 校验 Rich OS 的启动镜像。

这套链条听起来简单,但工程实现里有几个分水岭式的细节。最容易被忽略的一点是:BootROM 到 Bootloader 这一步,校验失败后的“失败路径”必须明确。我们见过不少嵌入式平台,设备校验失败后直接进入一个功能完整的厂商下载模式,攻击者就可以利用这个模式刷入自定义 Bootloader,等于信任链末端的这把锁形同虚设。后来我们统一改成:校验失败后进入一个只允许正式签名包恢复的受限模式,其余接口全部关闭,这才把攻击面收住。

可信启动链的另一头,是普通世界的镜像度量。MiTEE 在启动阶段会计算 Android/Linux 内核镜像、设备树、关键驱动模块的哈希,并将这些度量值记录到一个不可篡改的寄存器区域。后续远程证明时,这些度量值会作为“系统可信状态”的证据一起签名发送给验证端。验证端只要拿到证明书,就能判断被验证的设备当前到底跑没跑预期的系统版本,有没有被注入恶意模块。

2.3 安全服务分区:把“保险柜”再分成“保险格”

MiTEE 内部并不是一个大而全的“铁盒子”,我们也对安全世界内的模块做了细分。每个安全服务运行在独立的调度单元里,服务之间通过 TEE 内部的消息传递机制通信,不允许直接共享可写内存。

为什么内部还要再做一次隔离?原因很实际:如果某一条远程证明服务有漏洞,攻击者打穿了安全世界里这个模块,那么密钥服务、存储服务的数据就也可能被牵连。为了避免“一穿全穿”,MiTEE 在安全世界内部也尽量遵循最小权限原则,每个模块只映射自己需要的内存页,其他页的访问直接通过 MMU 丢掉。

这个设计和微内核操作系统的思路是一致的。系统调用、模块调度、内存分配是内核里唯一可信的部分,业务逻辑全部外置。这样即便某个安全服务被攻破,攻击者拿到的只是这一个模块的资源,而不是整个安全世界的钥匙。

3. 核心实现:内存隔离、中断路由与世界切换

3.1 内存隔离:不只靠 MMU,还要靠总线控制器

很多人一听到 TEE 就以为“只要切到安全世界,普通世界就读不到安全内存了”,这个理解只对了一半。软件层面上的 MMU 页表确实可以隐藏地址映射,但真正从物理总线上挡住普通世界访问的,是 ARM TrustZone 配套的 TZASC(TrustZone Address Space Controller)。

TZASC 的作用相当于在内存控制器前面装了一个岗亭,每个物理内存区域都被标记成 Secure 或 Non-Secure。普通世界的读写请求发到总线上时,TZASC 会在硬件层面直接拦截掉对 Secure 区域的访问,根本不给你往内存控制器方向走的机会。这个保护不依赖任何软件配置,所以就算普通世界的攻击者把页表、MMU 全部改掉,也无法直接读取 Secure 内存里的数据。

MiTEE 初始化时会把物理内存分成几个明确区域:Secure RAM 固定给 TEE 内核使用,普通系统无法访问;共享区域只保留消息缓冲区,用来承接普通世界传来的请求数据。初始化完成后,我们会对共享区域的映射做严格检查,禁止安全世界内的代码把它当作可信输入源。所有进入安全世界的输入,都必须经过参数校验和复制,不能直接在共享内存上做业务逻辑。

3.2 中断与异常:为什么安全世界要用 FIQ

双世界模型下,中断处理是一个很容易踩坑的点。ARM 架构里普通中断叫 IRQ,快速中断叫 FIQ。TrustZone 世界里,中断会被路由到当前正在运行的世界,但系统可以通过配置把特定中断标记成 Secure,让它们即使发生在普通世界也能被转发到安全世界。

MiTEE 的策略很直接:安全世界自己的定时器中断、服务请求中断,全部配置成 Secure FIQ。这样普通世界的恶意外设请求,无论怎么触发中断,都进不来安全世界的关键路径。普通世界的 IRQ 在 TEE 内执行期间会被屏蔽,直到回到普通世界才重新打开。

这里有个性能和安全之间的权衡。如果 TEE 内执行任务时全程禁掉普通世界的 IRQ,那么设备上所有普通中断都会被延迟,卡顿会非常明显。我们后来做了一个折中:MiTEE 的内核任务都设计成快速完成,不阻塞等待外部事件;切换回普通世界前统一打开 IRQ。说白了就是让安全世界的服务“短平快”,避免长时间霸占 CPU 导致系统整体响应变差。

3.3 世界切换的完整流程与其代价

一次从普通世界进入 MiTEE 的系统调用,完整路径是这样的:

  1. 普通世界应用调用客户端接口,把请求参数填入共享内存。
  2. 应用调用安全驱动,安全驱动利用 SMC 指令触发世界切换。
  3. 处理器进入 monitor 层,保存普通世界的通用寄存器、系统状态到安全内存中的栈区。
  4. monitor 将处理器切到 Secure 状态,跳转进入 MiTEE 内核入口。
  5. MiTEE 内核验证请求参数、拷贝输入数据、执行对应安全服务。
  6. 返回前将结果写入共享缓冲区,并整理要恢复的普通世界上下文。
  7. 再次通过 SMC 回到 monitor,恢复普通世界状态,继续执行一行代码。

这套流程每次都要做两次世界切换,上下文保存恢复加上运行环境切换,通常会有微秒到几十微秒级别的开销。如果业务代码高频调用安全服务,这个性能损耗会被放大。所以我们后来在架构上做了一个“批量操作”优化:把多个安全操作打包成一次请求,减少切换次数。比如服务端签发一个批量订单签名时,一次世界切换可以连续签多笔,而不是每签一笔就来回跳两次。

写到这里要澄清一个概念:世界切换本身不是漏洞,但每次切换都是新的攻击面。monitor 层的代码如果处理异常、非法 SMC 参数时有逻辑缺陷,攻击者就可能构造恶意 SMC 触发状态混淆,最终导致安全数据被普通世界访问。这也是我们坚持把 monitor 层代码压到极致、并做形式化验证的原因。

4. 密钥管理、完整度量与远程证明的实现细节

4.1 硬件信任根与密钥分层

任何安全系统的根基,最终要落到一个物理不可导出的秘密上。MiTEE 信任根的来源是每个设备芯片在出厂时写入的一根唯一设备密钥,它被放在芯片内部的 OTP 区域,连我们自己的固件都无法通过普通 API 把这份密钥原样读出来。

密钥管理采用分层结构:设备密钥负责加密和包装“次级密钥”,次级密钥再用于具体业务的签名、解密、衍生会话密钥。这样即使某个业务密钥泄露,攻击者也无法拿它去解开设备里所有其他业务的数据。通常我们会使用 Secure Key、SSK、Storage Key 这三层,层级之间互不暴露底层原始密钥。

存储侧的实现参考了类似 OP-TEE 的 Secure Storage 思路:密钥对象不会以明文直接写在一般文件里,而是用设备密钥加密后再落盘。每次使用时,由 TEE 内部完成解密,普通世界只拿到一个临时句柄,用完即释放。应用层的 Java/Kotlin 或 C++ 代码永远不会接触真正的密钥明文,即便逆向到崩溃也只能看到一堆密文。

4.2 远程证明:如何让服务端相信一台设备是干净的

远程证明可以理解成“给我一份能证明我就是我的证书”。设备上的 MiTEE 会生成一个证明报告,内容包含设备 ID、当前关键软件组件的度量哈希、启动日志、证书有效期等字段,用设备私钥签名后返还给服务端。

服务端验证时要做三件事:验签名、验证书链、验证度量值是否匹配预期镜像列表。任何一步不通过,服务端就可以拒绝请求。这个过程还需要考虑时间戳防重放,避免攻击者录下一个合法证明,之后再反复重放。

有一类容易被忽略的攻击是“重放旧版本镜像”:攻击者把设备降级到早期固件,早期固件的漏洞已经被公开,于是设备验证时返回旧版本的度量值。这就要求远程证明系统带上防回滚计数器,每次升级安全固件时,计数器只增不减。MiTEE 把回滚计数器放在持久化存储区,当系统检测到新镜像不存在或不合法时,拒绝写入,防止“刷回老版本再利用”的操作。

4.3 安全存储与完整性度量:数据落地前的最后一道防线

安全存储方面,我们不仅加密数据内容,还对数据的读取加一层访问控制。每个存储在 TEE 内部的数据对象都带一个安全属性,像“只能由某个指定服务读取”、“写入前必须是经过校验的镜像”等。这样即使普通世界发来伪造请求,试图从安全存储里读取别的业务数据,接口层也会直接拒绝。

完整度量方面,MiTEE 会维护一块“度量日志”,记录启动以来一系列关键代码段和配置文件的哈希值。服务端远程验证时可以直接读取度量日志,梳理整个系统从开机到当前状态的完整链路,而不是只知道“当前哈希对不对”。这相当于从一张照片升级成了一段录像,排查问题时定位更准确。

5. 安全测试、形式化验证与性能调优

5.1 为什么拿形式化验证“死磕”世界切换逻辑

传统安全测试是“喂输入、看输出”的黑盒思路,对于移动终端这种攻击面相对可控的场景,黑盒测试可以覆盖大部分问题,但没法穷举所有边界。MiTEE 的定位不同,它是底层安全底座,一旦出漏洞就是灾难性的,所以我们引入了形式化验证,把关键逻辑的“状态机”用数学工具表达出来,再证明它的行为符合预期。

我们重点验证的是世界切换流程。切换的本质是一个状态机:普通世界运行态、进入中断态、monitor 保存上下文、secure 状态执行、返回保存等。每一步的触发条件和转移关系都写成形式化规范,然后基于模型检测工具确认“任何一个合法输入序列,都不会让安全世界的状态被普通世界篡改”。

验证过程中最有价值的一次发现,是一个看似无害的“寄存器恢复顺序”问题。因为代码路径里先恢复了普通世界寄存器,再清理安全上下文,导致存在一个极短的时间窗口,攻击者如果抓住这个窗口触发异常,就可能让普通世界拿到一个安全世界上下文中尚未清空的临时密钥区。这个窗口极难靠人肉审出来,靠黑盒测试也难以稳定复现,但形式化验证能直接把它揪出来。自研 TEE 之所以要死磕这条路,为的就是把这一类“瞬间漏洞”消灭在代码上线之前。

5.2 硬件侧信道与故障注入的防御实践

隔离边界就算在逻辑上无懈可击,也挡不住攻击者从物理层面找突破口。侧信道攻击是个绕不开的话题。比如通过测量设备执行某个密钥操作时的功耗曲线,攻击者可以推测密钥里某些比特的值;又或者向设备注入时钟毛刺,试图让某条跳转指令失效,从而绕过认证检查。

MiTEE 的做法分两路。一路是在关键路径上做“常数时间执行”,尽量让分支判断不依赖秘密数据;另一路是在认证、验签这类高风险操作前插入随机延时和冗余计算,增加侧信道分析的成本。这类物理层面的攻击防御在通用产品里确实不是主流需求,但在做安全终端这一类需要对抗真实世界攻击者的项目里,必须提前做预案。

5.3 性能数据与优化经验

加了一层硬件隔离之后,大家最担心的就是性能。我们做性能调优时重点盯三个指标:单次世界切换延迟、安全接口吞吐量、长时间运行后的稳定性。初期版本里,每次切换的延迟大约是几十微秒级别,对于低频的密钥操作完全够用。但应用层一做“循环验签”这类操作时,延迟会被放大到用户可感知的程度。

我们做的第一个优化是减少不必要的 SMC 调用次数,把多个操作合并成一条命令。第二个优化是精简上下文保存区,把一些不需要在安全世界切换间保留的寄存器省掉。第三个优化是预热缓存,把安全世界核心代码和数据尽量锁在 L1/L2 缓存里,避免每次切入时都经历一次冷缓存的高延迟访问。做完这三步后,我们测到的平均切换延迟下降到了原来的三分之一左右,业务层基本无感。

性能和安全经常是矛盾体,但我个人的原则是:安全世界的核心操作,宁可慢一点,也不能为了快而牺牲正确性。性能优化可以循序渐进,安全漏洞却可能一次性摧毁全部信任。

6. 实际落地过程中踩过的坑与排查记录

6.1 缓存一致性问题:普通世界改数据,安全世界读不到“新数据”

某次联调时我们发现,普通世界往共享内存里写入了一组签名参数,但 TEE 内的服务读取到的却是旧数据。排查半天才发现是缓存一致性问题:普通世界写入了某个地址,由于 CPU 缓存还没回写,安全世界切换过来后访问同一地址,读到的却是缓存里的旧副本。

这个问题的解决方案比较暴力但有效:在发起世界切换之前,驱动层强制对共享内存区域执行 cache clean,切回普通世界后再执行 cache invalidate。这里要特别交代的是,共享缓冲区的地址必须对齐到 cacheline,否则刷缓存时容易误伤相邻数据。

6.2 中断风暴导致的安全世界卡死

还有一次线上设备偶发死机,经过日志采集发现是中断风暴打满了安全世界。安全世界里运行的定时器中断没有做好节流控制,外部设备高频触发 FIQ,导致 TEE 内核几乎把所有 CPU 时间都花在处理中断上,业务安全任务一直得不到执行。

后来我们在中断入口处加了“中断合并与延迟处理”机制:短时间内连续到达的相同中断会进行去重,只处理最后一条有效事件,其他被丢弃;对于安全世界外部触发的中断,则增加了频次阈值,超过阈值直接屏蔽并记录日志。这之后设备再也没有出现过同类死机。

6.3 共享内存参数校验的缺漏

第三个坑是共享内存里的参数指针。最初版本的客户端接口只校验了请求参数里的长度字段,没有校验指针指向的位置。攻击者可以构造一个恶意请求,让 TEE 去访问一个它本来无权访问的地址,最终造成信息泄露。

这类问题没有捷径,唯一的办法是:对每一份进入安全世界的共享内存请求,都做完整的“地址范围+权限+长度”三元校验,而且校验逻辑必须写在安全世界内部,不能信任普通世界传入的任何值。我们把这条原则写进了开发规范,后续所有新模块的评审都会逐行检查参数校验路径。

下面把这三个问题整理成一张速查表,方便对照:

问题现象根本原因排查思路最终解法
TEE 读到旧数据缓存未回写检查共享内存 cache 操作切换前 clean,切换后 invalidate,并对齐 cacheline
设备偶发死机中断风暴占满 TEE统计 FIQ 中断频率中断合并、频次阈值、丢弃重复事件
信息泄露风险共享指针未校验审计所有客户端 API 入口安全世界内部做地址+权限+长度三元校验

6.4 调试与部署:日志、看门狗与升级策略

底层安全系统的部署要比普通 App 谨慎得多。普通世界崩溃了大不了重启系统,安全世界里的错误一旦影响信任链,设备可能直接变砖。我们的原则是:安全世界里尽量不输出冗余日志,只保留必要的运行状态和错误码,并把这部分日志放在只能由特定调试镜像读取的受限区域,防止攻击者从日志中分析出密钥相关线索。

升级策略上,MiTEE 固件不能像普通应用一样随意热升级。每次升级必须重新走一遍签名验签流程,还要检查防回滚计数器是否符合升级要求。如果设备上运行的旧版本固件存在已知漏洞,服务端会强制下发新版本,同时拒绝接受旧版本生成的远程证明报告,避免“带漏洞版本继续工作”的情况。

7. 一些个人经验与后续扩展方向

如果读完以上部分你也有想法去做类似的底层安全底座,我个人最想分享的有三点。

第一,不要把“安全”做成事后补丁。MiTEE 之所以能快速投入实际使用,是因为架构层面从一开始就把隔离边界、信任根、最小权限这几个原则定死了,后面所有功能都是在这些约束里生长出来的,而不是遇到一个洞补一个洞。

第二,要考虑你的终端用户到底是谁。如果只是给普通手机应用做加密代理,通用 TEE 方案足够可靠。但如果是做小型 IoT 设备、支付终端、或者需要严格审计的系统,那么一个轻量、可控、可验证的微型 TEE 带来的定制价值,远远大于直接引入一套重型方案的省事成本。

第三,不断更新威胁模型。安全系统不是静态的,攻防技术在演进,硬件平台也在变化。MiTEE 现在的代码量和模块划分是基于当前威胁模型设计的,下一个阶段我们已经在考虑加入更多硬件辅助隔离能力,并继续扩展远程证明在端管云协同场景里的应用。

回到开头那个问题:为什么明明有那么多现成的安全方案,还要自研一套 MiTEE?答案其实就是一句话——你要保护的东西太重要了,重要到你不能把安全性的终极置信度完全交给一个自己看不透的黑盒。自己做底层安全系统确实慢,也确实难,但每一步验证都清清楚楚,每一次输出都经得起审计,这份底气和确定性,是其他方案很难替代的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询