4. TEE中的权限模型:安全世界与普通世界的隔离

说到TEE的权限模型,我第一个想到的就是当年做第一个TEE项目时踩的那个坑。那时候我刚接触这个领域,以为安全世界和普通世界就是简单的「开关切换」——嗯,后来被现实狠狠教育了一顿。

其实TEE的权限模型,说白了就是解决一个问题:怎么让两个世界共存,又不让它们互相捣乱。你想想看,普通世界跑着你的Android或Linux,安全世界跑着支付、人脸识别这些敏感业务。如果权限没管好,普通世界的一个野指针就能把安全世界的内存给踩了,那还谈什么安全?

4.1 两个世界的硬件隔离机制

ARM TrustZone 是当前最主流的TEE实现方案。它的核心思路很简单——在硬件层面把SoC的所有资源分成两个「世界」:普通世界(Normal World)和安全世界(Secure World)。

怎么分的?靠一根物理信号线——NS位(Non-Secure bit)。这根线挂在总线上,所有访问内存和外设的请求都得过它这一关。

维度 普通世界(Normal World) 安全世界(Secure World)
操作系统 Android / Linux / RTOS 可信操作系统(如OP-TEE、QSEE)
内存访问 只能访问非安全内存区域 可访问安全和非安全内存(需配置)
外设访问 非安全外设 安全外设(如指纹传感器、安全键盘)
中断处理 普通中断(IRQ) 安全中断(FIQ)
特权级别 EL0/EL1(用户态/内核态) EL0/EL1 + Monitor模式(EL3)

我在项目中遇到过一个问题:某个外设的驱动在普通世界也能访问安全内存区域。查了半天,发现是硬件设计时NS位没连对。这种问题在芯片设计阶段就要卡住,不然后面软件怎么补都补不牢。

⚠️ 特别注意: 两个世界的隔离不是「软件约定」,而是硬件强制。普通世界的代码无论如何都访问不了安全内存——即使它跑在EL1(内核态)也不行。这是TrustZone的底线。

4.2 世界切换:Monitor模式的「看门人」角色

两个世界怎么切换?靠一个特殊的异常级别——Monitor模式(EL3)。你可以把它理解成一个「看门人」,所有跨世界的调用都得经过它。

切换流程大概是这样的:

  1. 普通世界发起调用:通过SMC(Secure Monitor Call)指令触发安全监控调用。
  2. 陷入Monitor模式:CPU保存普通世界的上下文,切换到Monitor模式。
  3. 安全检查:Monitor检查调用是否合法(比如参数范围、调用来源)。
  4. 切换到安全世界:恢复安全世界的上下文,开始执行安全逻辑。
  5. 返回:安全逻辑执行完毕,再通过Monitor切回普通世界。

这里有个关键点:Monitor模式的代码是唯一能直接操作两个世界上下文的。所以Monitor的代码必须极其精简、极其安全。我记得有一次做安全审计,发现Monitor里有个缓冲区溢出漏洞——那感觉就像发现自家大门的锁芯被人锯了一半。

💡 个人建议: 写Monitor代码时,尽量用汇编,少用C语言。汇编的每一行指令都是可控的,C语言的编译器优化有时候会引入意想不到的安全问题。我习惯在Monitor里只做三件事:保存上下文、检查参数、切换世界。多一行都不写。

4.3 TEE中的安全域(SP)与可信应用(TA)

安全世界内部也不是「铁板一块」。它里面还分了多个安全域(Secure Partition,SP),每个SP运行一个或多个可信应用(Trusted Application,TA)。

你可以这么理解:

  • 安全世界 = 一个安保严密的园区
  • 安全域(SP) = 园区里的独立办公楼
  • 可信应用(TA) = 办公楼里的各个办公室

每个SP有自己的内存空间、堆栈和资源。SP之间默认是隔离的——一个SP的TA不能直接访问另一个SP的内存。这种设计是为了防止「一颗老鼠屎坏了一锅粥」:就算某个TA被攻破了,攻击者也拿不到其他TA的数据。

🔑 核心原则:最小权限
每个TA只拥有完成其功能所需的最小权限。比如一个支付TA,它只需要访问密钥存储区和网络接口,就不应该给它访问指纹传感器的权限。

4.4 TA的权限管理机制

TA的权限管理,我把它总结成三个层面:

4.4.1 静态权限:编译时声明

每个TA在编译时就要声明自己需要哪些权限。这有点像Android的Manifest文件。OP-TEE里用manifest.xml来声明:

<?xml version="1.0" encoding="utf-8"?>
<ta>
    <uuid>12345678-9abc-def0-1234-56789abcdef0</uuid>
    <name>payment_ta</name>
    <version>1.0.0</version>
    <permissions>
        <permission>GP_IS_GET_KEY_STORAGE</permission>
        <permission>GP_IS_CRYPTO_OPERATION</permission>
        <permission>GP_IS_NETWORK_ACCESS</permission>
    </permissions>
    <memory>
        <heap_size>0x10000</heap_size>
        <stack_size>0x4000</stack_size>
    </memory>
</ta>

这些权限在TA加载时由TEE内核检查。如果TA运行时试图调用一个未声明的服务,TEE内核会直接拒绝——我曾经见过一个开发团队在manifest里漏写了加密权限,结果TA在运行时调用加密接口直接返回错误,排查了两天才找到原因。

4.4.2 动态权限:运行时检查

静态权限只是「准入资格」,真正干活时还要过动态检查。TEE内核维护了一个权限表,每次TA调用系统服务时都会查表:

TA UUID 服务ID 允许的操作 调用次数限制
payment_ta 0x0001(密钥存储) 读、写 100次/分钟
payment_ta 0x0002(加密运算) 加密、解密 无限制
face_auth_ta 0x0003(人脸比对) 比对 10次/分钟

为什么要加调用次数限制?防止暴力破解。比如人脸比对接口,如果攻击者能无限次调用,就可以用穷举法试出匹配的人脸。我在一个金融项目中就遇到过这种攻击场景——虽然最终没成功,但那次之后我坚持所有敏感接口都要加频率限制。

4.4.3 安全上下文:调用链追踪

TA的权限还跟调用来源有关。一个TA被谁调用的?是普通世界的CA(Client Application)调用的,还是另一个TA调用的?这决定了TA是否信任这个请求。

OP-TEE里用tee_ta_session结构体来追踪调用链:

struct tee_ta_session {
    TEE_UUID            uuid;           // TA的UUID
    struct tee_ta_ctx   *ctx;           // TA上下文
    uint32_t            session_id;     // 会话ID
    uint32_t            caller_id;      // 调用者ID
    uint32_t            login_method;   // 登录方式
    void                *user_data;     // 用户数据
};

这里login_method很关键。它表示调用者是怎么登录的:

  • TEE_LOGIN_PUBLIC:公开调用,任何人都可以发起
  • TEE_LOGIN_USER:用户身份调用,需要用户认证
  • TEE_LOGIN_GROUP:组身份调用,需要组成员认证
  • TEE_LOGIN_APPLICATION:特定应用调用,需要应用签名验证

我习惯在TA的入口函数里先检查login_method。比如一个管理密钥的TA,只允许TEE_LOGIN_APPLICATION方式调用——也就是说,只有特定签名的CA才能访问。这样就算普通世界有恶意应用,它连TA的门都进不来。

4.5 安全世界内部的隔离:SP之间的防火墙

前面说了SP之间默认隔离,但有时候SP之间确实需要通信。比如支付TA需要从密钥管理TA那里获取加密密钥。这时候怎么办?

TEE规范定义了SP之间的通信机制,但通信必须经过TEE内核的「防火墙」:

  1. SP A发起请求:调用sp_request(),指定目标SP的ID和请求数据。
  2. TEE内核拦截:检查SP A是否有权限访问SP B的服务。
  3. 权限验证:查权限表,确认SP A在SP B的「白名单」里。
  4. 数据传递:TEE内核把数据从SP A的内存拷贝到SP B的内存(注意是拷贝,不是共享指针)。
  5. 返回结果:SP B处理完,TEE内核再把结果拷贝回去。

这里有个设计细节:数据必须拷贝,不能共享指针。为什么?因为如果共享指针,SP A就可以在SP B处理数据时偷偷修改它——这就是TOCTOU(Time-of-Check Time-of-Use)攻击。我见过一个安全评审报告,里面专门强调了这个问题,后来所有SP间通信都改成了深拷贝。

⚠️ 避坑指南: 我曾经在SP间通信里用过共享内存来提升性能,结果安全审计时被打了回来。审计师说:「共享内存是性能的蜜糖,安全的毒药。」从那以后,我宁愿多花点CPU周期做拷贝,也不碰共享内存。

4.6 权限模型的整体架构

说了这么多,我们来画一张图把整个权限模型串起来:

TEE权限模型架构图 普通世界(Normal World) CA应用(支付App) CA应用(人脸识别) 普通世界内核(Linux/Android) SMC指令 Monitor模式(EL3) 上下文切换 安全世界(Secure World) 安全域 SP1 TA:支付服务 TA:密钥管理 安全域 SP2 TA:人脸比对 TA:特征提取 权限控制要点 1. 硬件隔离(NS位) 普通世界无法访问安全内存 硬件强制,软件无法绕过 2. Monitor模式检查 SMC调用必须经过EL3验证 参数合法性、调用来源检查 3. 静态权限声明 TA在manifest中声明权限 加载时TEE内核验证 4. 动态权限检查 运行时查权限表 服务ID + 操作类型 + 频率限制 5. 安全上下文追踪 login_method决定信任级别 调用链可追溯 6. SP间防火墙 SP间通信必须经过内核 数据深拷贝,防TOCTOU攻击 7. 最小权限原则 每个TA只拥有必要权限 权限越少,攻击面越小

这张图把整个权限模型串起来了。从普通世界的CA发起SMC调用,经过Monitor模式的检查,进入安全世界,再到SP内部的TA权限管理——每一层都在做权限控制。

最后说一句:权限模型不是「配好了就完事」。我在项目里见过太多「权限配好了就不管了」的案例,结果后来业务变了,权限没跟着更新,留下了安全隐患。建议每个迭代都做一次权限审计,看看有没有「僵尸权限」或者「过度授权」的情况。