Driverlet 笔记
TrustZone
当前的一些安全技术:
- 特权级:每个进程在特定的特权级上执行,例如 RISC-V 将用户进程放置在 U 模式下运行,将操作系统放置在 S 模式下运行。这样可以隔离操作系统和用户进程的代码和数据。
- 虚拟内存:向进程提供一段连续的逻辑空间,从逻辑上可以扩展有限的物理内存空间的大小。从安全的角度来讲,它可以实现每个用户进程都有独立的内存空间,从而实现多个进程之间代码和数据的隔离。
除此之外,很多消费的电子产品都使用扩展的安全模块确保数据安全:
- 外部硬件安全模块:数据的处理交由外部的安全模块实现,这些模块能够保护自己的资源和密钥等数据的安全。
- 内部集成硬件安全模块:一个芯片有两个核,一个普通核和一个安全核。
TrustZone 是 ARM 为其 Armv8-M 系列核所建立的可选的安全扩展,其目的是为广泛的嵌入式应用构建安全框架来抵御各种可能的攻击。在该设计中,处理器有安全和非安全两种模式,不安全的应用软件只能访问不安全的内存。不同于 Cortex-A 处理器的实现,内存映射方式是区分安全和常规模式的依据,并且地址翻译是在地址转换中自动完成的。
此外,还有以下不同点:
Armv8-M | Cortex-A | |
---|---|---|
安全功能入口点 | 支持多个入口点 | 仅支持单个入口点 |
非安全中断 | 支持 | 不支持 |
下面这张图展示了 Armv8-M 的 TrustZone 是如何在处理器上添加安全和非安全操作的:
我们将以下重要模块放置在安全模式中:
- 安全启动加载器。
- 安全密钥
- 闪存编程支持
- 高价值资
Driverlet
TrustZone 可以很好的隔离 IO 硬件,但是缺少对现代 IO 设备的驱动支持。为了解决这个问题,论文提出一种新颖的驱动程序 Driverlet。该驱动是各个小驱动的派生版本,在受信任的执行环境中运行,监视着驱动和设备之间的交互。
Driverlet 驱动具有以下的优点:构建简单,易于使用,安全。而且性能在一个可接受的范围内,开销大约是本地驱动的 1.4 倍到 2.7 倍。
介绍
Secure IO 目前存在一个很大的问题,就是驱动软件有很大的空白。即使是 OPTEE / Trusty 开发了近十年,还是缺少对一些 IO 的支持。实现困难是一大原因,因为开发者必须了解各个设备的细节,比如命令/传输寄存器是如何工作的?DMA 是怎么交互的?等等。
相比于开发新的驱动,现有的驱动已经非常的成熟,非常的完善。如果可以直接使用这些已有的驱动就好了。当然,也确实有人做出了这方面的尝试。主要有以下两种方式:
- 大量移植现有的驱动代码。这样做工作量特别的大,代码规模可以达到 K SLoC。
- 剥离 trustlet 不需要的代码部分。代码量确实可以减少,但是开发者仍然需要了解驱动和设备如何工作的细节。
既然移植工作如此的繁杂,我们就换个方向解决这个问题。我们仍然保留现有的驱动,在 TrustZone 中实现一个 Driverlet,由 Driverlet 完成安全相关的操作。Driverlet 会完成以下几个事情:
- 执行已有的成熟的设备驱动。
- 记录 driver 和 device 之间的交互事件。
- 当 trustlet 触发驱动接口的时候,TEE 就会重现记录下的交互事件。
过程描述图如下:
这样的作法我们会遇到两种挑战。第一个就是正确性,怎么保证复现的和原本的行为是一致的?第二个挑战就是可表达性。我们复现的输入可以表达出所有的驱动和设备的交互事件吗?
为了解决第一个挑战,driverlet 的复现行为必须和原本的交互行为对设备的状态改变相同。基于这么一个点,我们可以将交互行为分成两种,一种是对设备状态影响较小的交互,一种是对设备状态影响较大的交互。对于前者,我们就可以不需要提供精确的复现行为,允许有一定的偏差;对于后者,我们就需要高精度的复现。为了解决第二个挑战,这里实现了一个 interaction template。该模板记录了从 trustlet、设备、TEE 环境输入所期待的事件,也记录了复现模块可能的输出事件。
通过这些做法,开发者不需要做特别多的工作;driverlet 也有 3~10 个交互模板以及 50~1500 个交互事件;不需要特别地依赖 TEE 环境;代码规模也仅有 1000 SLoC;性能也只有较小的性能损失。
动机
目前,Secure IO 有着丰富的应用,例如安全存储、可信感知、可信 UI 等。而且,有一些不错的机遇:
- Trustlet 对 IO 的性能并不敏感。
- Trustlet 可以提供简单的 IO 设备功能。
- Trustlet 可以在粗粒度的时间内共享 IO 设备。
这样我们可以已 IO 性能和细粒度共享为代价,实现简单的驱动程序以及对其安全性做出保证。
除了上述以外,也有了很多现有技术的支持:
- 成熟的设备驱动:现有的设备驱动有着良好的性能,完整的设备支持以及细粒度的共享。
- 已有的解决方式(Trim down):全部移植或者是部分移植。
- 不太可行的重构方案。
- 分区:可以对驱动进行分区,只对安全敏感的分区在安全模式下完成相关操作。但是呢,也会遇到两种阻碍。一是我们的解决方式也是基于 Trim down,需要重新开发和解决内核依赖。其次是要仔细考虑安全环境和非安全环境之间的接口。
解决方法
系统环境
- SoC 硬件
- 目标 IO 设备
- 设备状态机
- 驱动和设备之间的交互
- 状态改变事件:一种是全部的输出事件;另一种是输入事件的子集,包括中断、服务响应、影响至少下一个接下来的输出事件的寄存器或者贡献内存的读事件。这样定义状态改变事件非常的宽泛,可能会把成功的事件被判定为错误的事件,但是保证了错误的事件不会被误判为成功的事件。
我们的方式
选择性重用状态改变所诱导的驱动和设备之间的交互。核心的机制有以下几点:
- 设计前提:配置设备使其禁止中断、并发作业以及电量管理。
- 记录事件
- 复现事件
可行的原因
- 状态改变的相关的事件是有序的。
- 可以改变对设备的激励方式,同时也保证在同一条状态路径上。如图 2 的 b,即使是输出事件和原本正常的驱动的输出事件不相同(参数等),但是最后设备返回的状态相同。
- 通过重置的方式来解决状态异常的问题。
局限性
第一,driverlet 依赖于设备状态机之间的数据独立性;第二,driverlet 依赖正确的完整的原有驱动,不排除原有驱动有 bug 的情况。
记录
开发者运行多个驱动程序,在每个运行当中,使用一个特殊的请求。当记录器(recorder识别到这个特殊的请求之后,记录事件并生成一个交互模板。一旦记录成功,记录器就会对这个模板进行签名使之不可以再被修改,并且反馈一个输入空间覆盖率的信息。如果开发者认为没有达到自己所期望的覆盖率,可以重复进行记录,直到满足为止。
交互模板
交互模板会涉及到下表所涉及到的事件:
- 输入事件 $V = <I, C, A>$,表示从接口 $I$ 输入的值 $V$,约束 $C$,以及输入属性 $A$。
- 输出事件 $<I, V>$,表示将值 $V$ 写入到接口 $I$当中。
- 元事件,delay 或者是 poll $<I, E, Cond>$。后者表示接口 $I$ 在终止条件为 $Cond$ 的情况下,循环执行 $E$ 循环体。
挑战
- 如何发现输入和输出事件之间存在的因果关系?
我们不能自然地使用输入输出的数据的不同来发现因果关系。因为两者之间没有必然的联系。解决方式是选择性标志执行。当发现有约束条件的时候,记录器会复制一个新进程,让新进程执行这个约束条件不满足的情况。然后比较这两个进程接下来的状态转变序列。如果比较发现不同,我们可以总结出,这是一个状态改变事件,就必须在一个相同的设备状态改变相同的路径上。例子如下图所示:
- 如何发现输入和输出事件之间的数据依赖?
通过动态的掩码追踪来实现。例如图 3 所示。我们对 blkid 打上掩码,然后在 write 的时候,发现了 SDARG 以及打过掩码的 blk,于是就触发一个包含 blkid 以及相应操作的输出事件。
然而这个掩码有一个问题,最多只能识别出 8 (0x7 的二进制为 b111)个有关系的 id。对于 DMA 操作,如果生成了 8 个以上的描述符怎么办呢?这里就简单的解决了这个问题:要求模板必须在记录运行的时候必须生成相同数量的描述符。
- 如何记录一个轮询事件?
通过静态循环分析。静态分析代码,通常这些代码逻辑很简单。
复现
复现(replay)的方式:TEE 的 trustlet 简单地链接复现器(replayer)和压缩交互模板作为库,这个最后被作为了 driverlet。复现器会完成以下的工作:
- 选择一个交互模版:首先我们需要使用 TEE 来解压缩模板,从已有的模版中选择满足 trustlet 输入约束的模版。同时,我们也只能选择一个模版以避免状态路径被打乱。如果此时没有模版可供选择,我们就反馈一个错误。
- 实例化模版:保留模板中链接的输入输出值,并将他们独立命名。在实例化过程中,如果相关的地址是物理设备的地址,需要将其重新映射的 TEE 的虚拟地址上。
- 执行事件:复现器单线程、顺序执行这些事件。同时也要维护两个上下文,一个原始驱动器所产生的上下文,一个中断处理函数的上下文。
- 重置设备:当遇到以下两种情况的时候会软重置设备。一是在相邻两个交互模板的执行之间,另一个是设备状态发散。如果软重置之后仍然没有解决错误现象,则需要中断执行并转储调用栈信息。
- 自我安全强化:有以下四种方式,检查输入的模版签名;只接受 trustlet 的输入;广泛的边界检查;减少并发执行。