一、总论
TEE(Trusted Excution Enviroment)我一直没有学明白过,也是借着 recycle 学长的论文的机会,重新学习了一下。
我觉得我一直没有学明白的原因是,TEE 涉及的技术太繁多了(内存加密,安全世界,加密计数器,可信报告,……),而又缺少核心定义,以至于我对于到底啥是 TEE 一直没有具体的概念,反而被上面描述的技术或者概念牵着鼻子走。而实际上这些技术与概念,往往是为了应对某种具体的攻击场景,并不本质。以内存加密为例,难道它只能用在 TEE 中吗?显然不是的,这套加密算法可以被用于任何容易遭受物理攻击或者重放(replay)攻击的地方;同样的,也不是说 TEE 就一定需要内存加密,如果没有物理攻击(比如说手机,或者 HBM 这种天生对物理攻击防御能力强的硬件),也可以不需要内存加密。
二、硬件信任根
那么 TEE 的核心到底是什么?我觉得是它把信任根(Root of Trust)明确放在了硬件上(粗浅说应该就是 CPU 上),而且只有硬件上。这是什么意思呢?就是我们其实之前的信任根,往往是某个软件。比如说我们在跑一个进程的时候,不担心另一个进程看到这个进程内部的数据,这是因为我们相信 OS 会正确配置我们的页表,确保两个进程的虚拟地址空间的隔离性。
在这个过程中,其实同样也是有硬件参与的,比如说 MMU 进行地址翻译,也是硬件过程。但是我们依然说,我们信任的是 OS 而非硬件。这是因为比较常见的情境下,硬件一般就是“乙方”,而软件才是“甲方”,软件负责按照一定规则使用硬件,硬件基本上也不能抗拒。而这种方式下,如果软件是恶意的,就算硬件有心反抗,也是没有办法的事情。所以我们才说,一般情形下,我们其实信任的是一个高特权级的软件。
那么为什么我们无法再信任软件了呢?我觉得其实有两方面原因,一方面比较传统,那就是高特权级的软件可能被黑客攻击了,它自然可以窃取低特权级的信息了,比如说黑客拿到了 Root 权限,查看某个进程的虚拟地址空间的机密数据,这就非常容易了。但是我觉得这种场景,其实不太能够完全催生 TEE 的诞生,因为最直白的方法,是通过修复 bug 来阻止黑客拿到 Root 权限,而不是构建一个连 OS 都控制不了的 TEE,通过牺牲便捷性,来保证安全。另一个方面则是云服务时代的到来,用户开始向云厂商租赁服务器等资源,此时的 OS 或者 VM 这种高特权级软件,往往是云厂商提供的。都不需要黑客,只需要云厂商有一点恶意的念头,就可以把租户的数据窃取出来。这种不信任所涵盖的范围(OS,VM,Hypervisor)是之前的单点攻击所不具备的(Root 的危害性跟它们比起来弱爆了)。这个时候租户迫切需要一种完全不涉及任何高特权级软件的信任根,也就是由硬件去保证的信任根。当然了,第二点在智能手机来临以后,又变成了手机的生产厂商,对于手机用户的不信任了,手机用户可以获得手机的 Root 权限(故意的,或者无意的),进而将手机厂商或者用户的敏感数据泄露。
那是不是意味着我们只能信任硬件呢,顶多再加上上面的固件呢?并不是,因为信任根所能提供的功能实在是太有限了(无法管理设备,无法提供充足的编程抽象,……)。所以我们其实会使用一些软件的。但是我们该如何保证这些软件的可信性呢?我们使用信任链来保证,也就是如果我们信任这个硬件,而硬件信任软件,则我们信任软件。通过这种方式,我们从信任根(也就是信任链的头部)开始,构建出一整个信任基(Trusted Compute Base,TCB)。
当然这个时候就有一个疑问了,那就是如果我们可以使用信任链,那为什么我们不能把云厂商的 VM 也加入信任链中,这样不就可以避免云厂商的恶意了吗?我觉得有两个方面的原因:一方面是信任的成本是非常高的,我们为了验证软件是可信任的,通常需要用各种方式检验它是否被篡改,即使未被篡改,我们也需要保证它之前也是不含恶意或者 bug 的(这点就非常难了)。另一方面是,信任链越长,可信基越大,那么攻击面也就越广,反而不利于保证安全。
但是 TCB 如果较小,那么往往就对应着 TEE 较为受限的功能。那么 TEE 如何与普通计算环境的协作,如何在功能性和安全性上做 tradeoff,就成了 TEE 的一个重要的设计课题。
最后总结一下,为了建立信任,我们需要一个被信任的“上帝”,也就是信任根。而传统扮演信任根的高特权级软件,失去了我们的信任,因此我们选择信任硬件作为信任根。
三、组件
3.1 总论
依然先强调,这里出现的相关的组件,并不是一定会出现在每一个 TEE 机制中的。有些 TEE 是缺少部分组件的。
3.2 硬件隔离
这说的是,CPU 会提前准备一片物理内存区域,这片内存区域不会被 OS 看到,只有在 CPU 进入到特定模式的时候才可以看见。而 CPU 是否进入这个特定模式,也不由 OS 控制。
总的来说,这其实是相当于构建了一层更高级的特权级,这个特权级比 OS 或者 Hypervisor 的权限还要高。它的工程实现,就是某个寄存器上有个 bit 来指定它是否存在于这个特定模式(在 ARM 中就是是否在 EL3),然后 CPU 在访问这个隔离内存的时候,都会检查一下这个 bit,如果没有置位,就会触发异常。
硬件隔离机制确实是所有 TEE 必备的要素。因为隔离就是安全的最基础的机制。
3.3 远程校验
一般的 TEE 是信任硬件的,也就是信任硬件和其上的 TCB 是未被篡改的。但是能不能更进一步的,就是并不是无条件信任硬件,而是我们真的可以校验一下我们使用的硬件,这个硬件是否是可信的,这就是远程校验(remote attestation)的思路。
具体而言,就是硬件会在启动后生成一份安全报告,来表明目前自己的状况(应该是哈希吧)。我们只需要拿着这份报告去与官方的报告去比对是否相同,就可以确认 TCB 是否被篡改。
3.4 机密性保护
如果只有硬件隔离机制,那么那片隔离内存中的数据,其实是明文存储的。但是这也没有关系,进程隔离的时候,虚拟地址空间的数据也是明文存储的,也没有见到有什么需要防范的必要。
但是如果我们考虑物理攻击手段,那就不一定了。也就是攻击者直接把内存拆下来,然后读取其中的内容。这个时候仅依靠硬件隔离就不行了。
所以我们可以用一个密钥把内存加密了,然后把密钥放在 CPU 上,进出 CPU 的都是密文,解密和重加密过程都发生在 CPU 的内部。有表示:
ciphertext = encryp(plaintext, key)
plaintext = decryp(ciphertext, key)
3.5 完整性保护
即使完全看不见机密的数据,攻击者依然可以展开一些攻击,比如说篡改攻击。篡改攻击指的是不直接破解密文,而是将密文给篡改掉,这样同样可以展开攻击。
为了防止这种攻击,我们很容易就想到,我们可以使用 hash 函数对明文进行处理,生成摘要。我们称这种摘要为 MAC(Message Authentication Code) 。在使用的时候,我们只需要比较一下我们之前保留的 MAC 和我们对着明文重新 hash 的 MAC 是否一致,就可以确定数据是否被更改了,也就是:
MAC = hash(plaintext)
需要注意到是,并不是整个内存我们只哈希一次,而是按照 cacheline 的粒度去 hash ,也就是数据有多少个 cacheline ,就有多少个 MAC 。这是因为如果我们只哈希一次,相当于这个数据有一点点的改动,我们都需要重新 hash ,这种开销是高额的,而以 cacheline 为粒度刚刚好,因为无论是只改 1 个 bit 还是改 10byte ,最终都是以 cacheline 为粒度写回的,不会太大也不会太小。
但是很快我们就发现了不对,毕竟 MAC 不能保存在 CPU 上(MAC 的数量太多了),所以 MAC 也是在片下的。如果攻击者知道了具体的 hash 是什么,然后就可以伪造 MAC 了。那我们的校验依然失效了。
因此我们必须让 MAC 无法被伪造。方法也很简单,就是在 hash 到时候加入密钥。因为密钥是片上到,所以是不可伪造的。攻击者就算篡改了 MAC ,也没有办法将其篡改成我们用密钥和明文 hash 出来到内容。因为攻击者并不知道密钥是什么。此时我们的 MAC 形式如下:
MAC = hash(plaintext, key)
但是攻击者依然有办法。因为 MAC 的粒度问题,我们只能保证一个 cacheline 块内到数据不被篡改。而如果一个数据占两个 cacheline ,攻击者可以交换这两个数据和 MAC ,同样可以展开攻击而不被发现,这种攻击被称作重排攻击。
为了解决这个问题,我们可以在 MAC 的生成中再引入数据的地址,这样如果交换数据,那么地址也会发生改变,导致 MAC 的差异。所以最终我们的 MAC 形式如下:
MAC = hash(plaintext, key, addr)
3.6 新鲜性保护
在上文中,我们的 MAC 已经可以阻止篡改攻击和重排攻击了。但是还有一种重放攻击(replay)它无法阻止。
重放攻击指的是,攻击者将旧的数据和 MAC 保存下来,用它们替换掉新的数据和 MAC 。因为旧的数据和 MAC 都是用正常的密钥,地址生成的,所以依然能通过校验。
那有什么办法呢?我们可以给每个 cacheline 引入一个计数器(counter),或者说版本号。每次我们写这个 cacheline 的时候,都会自增这个 counter 。然后我们再将 counter 融入到 MAC 的计算中。这样我们在校验的时候,发现 counter 是旧的,那么就会被发现,也就是:
MAC = hash(plaintext, key, addr, counter)
另外其实有个问题,那就是为什么 counter 一定要放到 MAC 的生成中,它孤零零的悬在外面不好吗?不好。这是因为 counter 同样可以被猜出来。所以我们需要用 key 来确保 counter 不是伪造的。
但是还有一个问题,那就是 counter 本身也是可以被重放的,毕竟不能所有的 counter 都在片上。那攻击者完全可以在重放数据和 MAC 的同时,顺带手重放 counter 就好了。
那能不能整个数据使用一个 counter ,然后把这个 counter 存在片上呢?不可以,还是性能问题,只要这个 global counter 一自增,那么所有的数据计算出来的 MAC 都过期了,我们都需要重新生成。
所以最终我们的解决方案是,用一个树形结构来描述 counter 。所有数据块的 counter 组成了叶节点,然后相邻数据块的 counter 做 hash (当然也要加入密钥,毕竟哈希的结果也在片外)形成最低层的中间节点。然后相邻中间节点的哈希结果,再次进行哈希,形成更上层的节点。直到生成根节点。然后这个根节点(根哈希)会被存在片上。
当我们读数据的时候,我们会从 cacheline 自底向上计算,直到计算出一个新的根哈希,然后和片上的比较。当我们写数据的时候,也是自底向上,更新 counter 和涉及到的中间节点,直至更新片上的根节点。这样我们需要更新的 MAC 也局限在了可控范围内。这种数据结构被叫做 Merkle Tree。
此外,counter 的 size 也是一个问题,如果太大了,那么内存开销很大;而如果太小了,那么很容易冲突。所以我们一般会采用一个较大的 counter ,但是把它的高位提取出来共用,反正这个部分也不常更新,这个部分就叫做 major counter size ,而剩下的低位,就叫做 minor counter size 。
四、案例
4.1 ARM TrustZone
这个案例中,就是只有硬件隔离的 TEE,并没有内存加密等组件。相当于引入了 EL3, 通过设置 NS bit,来区分 Normal World 和 Secure World。在 Normal World 里跑 Normal OS,在 Secure World 里跑 Secure OS。总的来说 TCB 还是挺大的。
之所以只有一个硬件隔离机制,是因为 TrustZone 并不是专门为了云服务器开发的,而是为了端侧手机开发的。所以并不会有一个黑客采用物理攻击手段。主要用于防止软件攻击。
4.2 Intel SGX / AMD SEV
更小的可信基,只有一段代码,被成为 enclave,OS / hypervisor 都不可信。首次引入了远程验证的机制。在使用的时候,普通程序代码通过 icall 来调用 enclave 的代码,就像调用函数一样。enclave 的代码,用 ecall 调用普通函数。
虽然这样可信基确实小了,但是相当于要改写程序,专门为 enclave 写代码,所以并不是很方便。而且 enclave 可以使用的隔离内存的大小是有限的。
这种最细粒度的机制,同样不是为了云厂商开发的,而是为了保护敏感数据,进行机密计算,比如说进行视频解密,防盗版等。
4.3 Intel TDX / AMD SEV-SNP
将整个 VM 作为 TEE,也就是说,恶意的 Hypervisor 是看不到 VM 里的内容的,Hypervisor 只能调度,而不能修改。这种技术基本上就是为了云服务开发的。
相当于租户可以完全透明的使用 VM,不用担心云厂商的窃取,也不用引入额外的修改。
4.4 ARM CCA
也是为云场景开发的。我理解就是有多个 Realm,但是我也不知道 Realm 是什么,大概就是 VM 吧。不过它有一个自己运行在 EL2 的 Hypervisor,被叫作 RMM(Realm Management Monitor)。