AWS Nitro Enclaves 攻击面分析
在保护云应用程序的竞赛中,AWS Nitro Enclaves 已成为隔离敏感工作负载的强大工具。但能力越大,责任越大——潜在的安全陷阱也随之而来。作为机密计算安全领域的先驱,Trail of Bits 团队深入研究了 AWS Nitro Enclaves 的攻击面,发现了可能危及这些强化环境的潜在漏洞。
本文提炼了我们来之不易的见解,为部署 Nitro Enclaves 的开发人员提供可操作的指导。阅读后,您将能够:
- 识别并缓解 enclave 部署中的关键安全风险
- 实施随机性、侧信道保护和时间管理的最佳实践
- 避免虚拟套接字处理和认证中的常见陷阱
我们将涵盖多个主题,包括:
- 虚拟套接字安全
- 随机性和熵源
- 侧信道攻击缓解
- 内存管理
- 时间源考虑
- 认证最佳实践
- NSM 驱动程序安全
无论您是 Nitro Enclaves 的新手,还是希望强化现有部署,本指南都将帮助您驾驭 AWS 上机密计算的独特安全格局。
简要威胁模型
首先,简要威胁模型。Enclaves 可能受到父 Amazon EC2 实例的攻击,这是唯一可以直接访问 enclave 的组件。在 enclave 攻击的背景下,我们应假设父实例的内核(包括其 nitro_enclaves 驱动程序)由攻击者控制。来自实例的 DoS 攻击并不是真正的问题,因为父实例始终可以关闭其 enclaves。
如果 EC2 实例从互联网转发用户流量,那么对其 enclaves 的攻击可能来自该方向,并可能涉及所有常见的攻击向量(业务逻辑、内存损坏、加密等)。而在另一个方向上,用户可能成为恶意 EC2 实例通过冒充攻击的目标。
在信任区域方面,enclave 应被视为单个信任区域。Enclaves 运行正常的 Linux,理论上可以使用其访问控制功能在内部“划分界限”。但这将是徒劳的——对 enclave 内部任何内容的对抗性访问(例如,通过供应链攻击)将削弱其强隔离和认证的好处。因此,单个 enclave 组件的泄露应被视为整个 enclave 的泄露。
最后,hypervisor 是可信的——我们必须假设其行为正确且非恶意。
图 1:AWS Nitro Enclaves 系统的简化模型
Vsocks
Enclave 的主要入口点是本地虚拟套接字(vsock)。只有父 EC2 实例可以使用该套接字。Vsocks 由 hypervisor 管理——hypervisor 为父 EC2 实例和 enclave 的内核提供 /dev/vsock 设备节点。
Vsocks 由上下文标识符(CID)和端口标识。每个 enclave 必须使用唯一的 CID,可以在初始化期间设置,并可以侦听多个端口。有一些预定义的 CID:
- VMADDR_CID_HYPERVISOR = 0
- VMADDR_CID_LOCAL = 1
- VMADDR_CID_HOST = 2
- VMADDR_CID_PARENT = 3(父 EC2 实例)
- VMADDR_CID_ANY = 0xFFFFFFFF = -1U(侦听所有 CID)
Enclaves 通常仅使用 VMADDR_CID_PARENT CID(发送数据)和 VMADDR_CID_ANY CID(侦听数据)。VMADDR_CID_PARENT 的示例用法可以在 AWS 的 enclaves SDK 的 init.c 模块中找到——enclave 在初始化后立即向父 EC2 实例发送“心跳”信号。该信号由 nitro-cli 工具处理。
标准套接字相关问题是 vsocks 的主要担忧。开发 enclave 时,请考虑以下事项以确保此类问题不会启用某些攻击向量:
- Enclave 是否异步接受连接(使用多线程)?如果不是,单个用户可能会长时间阻止其他用户访问 enclave。
- Enclave 是否超时连接?如果不是,单个用户可能会持久占用套接字或打开多个连接到 enclave 并耗尽可用资源(如文件描述符)。
- 如果 enclave 使用多线程,其状态同步是否正确实现?
- Enclave 是否正确处理错误?使用 recv 方法从套接字读取尤其棘手。常见模式是循环 recv 调用直到收到所需字节数,但应仔细实现此模式:
- 如果返回 EINTR 错误,enclave 应重试 recv 调用。否则,enclave 可能会丢弃有效和活动的连接。
- 如果没有错误但返回的长度为 0,enclave 应中断循环。否则,对等方可能在发送预期字节数之前关闭连接,使 enclave 无限循环。
- 如果套接字是非阻塞的,则正确读取数据更加棘手。
这些问题的主要风险是 DoS。父 EC2 实例可以关闭其任何 enclaves,因此实际风险仅存在于外部用户可以触发 DoS 的情况下。及时访问系统是 enclave 和与 enclave 通信的 EC2 实例的共同责任。
涉及 vsocks 的另一类漏洞是 CID 混淆:如果 EC2 实例运行多个 enclaves,它可能将数据发送到错误的 enclave(例如,由于竞争条件问题)。然而,即使存在此类错误,也不应构成太大风险或对 enclave 的攻击面贡献太多,因为用户和 enclave 之间的流量应进行端到端认证。
最后,请注意 enclaves 默认使用 SOCK_STREAM 套接字类型。如果将类型更改为 SOCK_DGRAM,请进行一些研究以了解此通信类型的安全属性。
随机性
Enclaves 必须能够访问安全随机性。此上下文中的“安全”意味着对手不知道或控制用于生成随机数据的所有熵。在 Linux 上,一些熵源由内核混合在一起。其中包括 CPU 提供的 RDRAND/RDSEED 源和平台提供的硬件随机数生成器(RNG)。AWS Nitro 可信平台模块提供自己的硬件 RNG(称为 nsm-hwrng)。
图 2:Linux 内核中的随机性源
最终随机性可以通过 getrandom 系统调用或从(不太可靠的)/dev/{u}random 设备获取。还有 /dev/hwrng 设备,它提供对所选硬件 RNG 的更直接访问。用户空间应用程序不应使用此设备。
当内核注册新的硬件 RNG 时,它会立即用于向系统添加熵。可用硬件 RNG 的列表可以在 /sys/class/misc/hw_random/rng_available 文件中找到。注册的 RNG 之一被自动选择以定期添加熵,并在 /sys/devices/virtual/misc/hw_random/rng_current 文件中指示。
我们建议配置您的 enclaves 以显式检查当前 RNG(rng_current)是否设置为 nsm-hwrng。此检查将确保 AWS Nitro RNG 已成功注册,并且是内核定期用于添加熵的 RNG。
为了进一步增强 enclave 随机性的安全性,只要有方便的源可用,就让它从外部源提取熵。常见的外部源是 AWS Key Management Service,它提供了一个方便的 GenerateRandom 方法,enclaves 可以使用该方法通过加密通道引入熵。
如果您想遵循 NIST/AIS 标准(参见“Linux 随机数生成器文档和分析”中的第 5.3.1 节)或怀疑 RDRAND/RDSEED 指令有问题(另请参阅此 LWNet 文章和此推文),您可以禁用 random.trust_{bootloader,cpu} 内核参数。这将通知内核不包括这些源以估计可用熵。
最后,确保您的 enclaves 使用大于 5.17.12 的内核版本——内核的随机算法引入了重要更改。
侧信道
应用程序级定时侧信道攻击对 enclaves 构成威胁,就像对任何应用程序一样。在 enclaves 内部运行的应用程序必须在恒定时间内处理机密数据。来自父 EC2 实例的攻击可以使用几乎系统时钟精确的时间测量,因此不要依赖网络抖动进行缓解。您可以在我们的博客文章“优化屏障的生命周期”中阅读更多关于定时攻击向量的信息。
此外,尽管这并不真正构成侧信道攻击,但 enclave 返回的错误消息可以被攻击者用来推断 enclave 的状态。考虑诸如填充 oracle 和账户枚举等问题。我们建议尽可能保持 enclaves 返回的错误通用。错误应多通用取决于给定的业务需求,因为任何应用程序的用户都需要某种程度的错误跟踪。
CPU 内存侧信道
要了解的主要侧信道攻击类型涉及 CPU 内存。CPU 共享一些内存——最显著的是缓存行。如果内存同时可被来自不同信任区域的两个组件(如 enclave 及其父 EC2 实例)访问,那么一个组件可能通过测量内存访问模式间接泄漏另一个组件的数据。即使应用程序在恒定时间内处理秘密数据,访问此类侧信道的攻击者也可以利用数据相关的分支。
在典型架构中,CPU 可以分为 NUMA 节点、CPU 核心和 CPU 线程。最小的物理处理单元是 CPU 核心。核心可能有多个逻辑线程(虚拟 CPU)——最小的逻辑处理单元——线程共享 L1 和 L2 缓存行。L3 行(也称为最后一级缓存)在 NUMA 节点中的所有核心之间共享。
图 3:系统 CPU 排列示例,通过 lstopo 命令获得
父 EC2 实例可能仅从 NUMA 节点分配了几个 CPU 核心。因此,它们可能与其他实例共享 L3 缓存。然而,AWS 白皮书“AWS Nitro 系统的安全设计”声称 L3 缓存从未同时共享。不幸的是,关于该主题的信息不多。
图 4:AWS 白皮书摘录,声明具有一半最大 CPU 数量的实例应填充整个 CPU 核心(插槽?)
Enclaves 中的 CPU 呢?CPU 从父 EC2 实例获取并分配给 enclave。根据 AWS 和 nitro-cli 源代码,hypervisor 强制执行以下操作:
- CPU #0 核心(所有其线程)不可分配给 enclaves。
- Enclaves 必须使用完整核心。
- 分配给 enclave 的所有核心必须来自同一 NUMA 节点。
在最坏的情况下,enclave 将与其父 EC2 实例(或其他 enclaves)共享 L3 缓存。然而,L3 缓存是否可用于执行侧信道攻击是有争议的。一方面,AWS 白皮书并未对此攻击向量大惊小怪。另一方面,最近的研究表明此类攻击的实用性(参见“最后一级缓存侧信道攻击在现代公共云中是可行的”)。
如果您非常担心 L3 缓存侧信道攻击,可以在完整的 NUMA 节点上运行 enclave。为此,您必须为父 EC2 实例分配多个完整的 NUMA 节点,以便一个 NUMA 节点可用于 enclave,同时在另一个 NUMA 节点上为父节点保存一些 CPU。请注意,此缓解措施资源效率低下且成本高昂。
或者,您可以尝试使用 Intel 的缓存分配技术(CAT)来隔离 enclave 的 L3 缓存(参见 intel-cmt-cat 软件)与父节点。但请注意,我们不知道 CAT 是否可以为运行的 enclave 动态更改——这将使此解决方案无用。
如果您实施上述任何缓解措施,则必须向认证添加相关信息。否则,用户将无法确保 L3 侧信道攻击向量真正得到缓解。
无论如何,您希望安全关键代码(如密码学)以与秘密无关的内存访问模式实现。硬件和软件级安全控制在这里都很重要。
内存
Enclaves 的内存从父 EC2 实例中划分出来。保护对 enclave 内存的访问并在其返回给父实例后清除它是 hypervisor 的责任。当 enclave 内存作为攻击向量时,开发人员真正需要担心的只是 DoS 攻击。在 enclave 内部运行的应用程序应限制外部用户可以存储的数据量。否则,单个用户可能能够消耗 enclave 的所有可用内存并使 enclave 崩溃(尝试在 enclave 内运行 cat /dev/zero 以查看消耗大量内存时的行为)。
那么您的 enclave 有多少空间?答案有点复杂。首先,enclave 的 init 进程不会挂载新的根文件系统,而是保留初始 initramfs 并 chroot 到目录(尽管有一个待处理的 PR,一旦合并将改变此行为)。这对文件系统的大小施加了一些限制。此外,保存在文件系统中的数据将消耗可用 RAM。
您可以通过在 enclave 内执行 free 命令来检查总可用 RAM 和文件系统空间。文件系统的大小限制应约为总空间的 40-50%。您可以通过填充整个文件系统空间并检查最终存储了多少数据来确认:
|
|
内存的另一个问题是 enclave 没有任何持久存储。一旦关闭,其所有数据都会丢失。此外,AWS Nitro 不提供任何特定的数据密封机制。实现它是您的应用程序的责任。阅读我们的博客文章“翻转位的轨迹”以获取更多信息。
时间
安全问题的较少常见来源是 enclave 的时间源——即 enclave 从哪里获取时间。能够控制 enclave 时间的攻击者可以执行回滚和重放攻击。例如,攻击者可以将 enclave 的时间切换到过去,并使 enclave 接受过期的 TLS 证书。
在机密计算领域,获取可信时间源可能是一个有点复杂的问题。幸运的是,enclaves 可以依赖可信 hypervisor 来提供安全时钟源。从开发人员的角度来看,只有三个操作值得采取以提高 enclave 时间源的安全性和正确性:
- 确保在 enclave 的内核配置中将 current_clocksource 设置为 kvm-clock;甚至考虑添加应用程序级运行时时钟检查(以防在 enclave 引导期间出现问题,最终使用不同的时钟源)。
- 启用精确时间协议以更好地同步 enclave 和 hypervisor 之间的时钟。它类似于网络时间协议(NTP),但通过硬件连接工作。它应该比 NTP 更安全(因为攻击面更小)且更容易设置。
- 对于安全关键功能(如重放保护),使用 Unix 时间。小心 UTC 和时区,因为夏令时和闰秒可能“使时间向后移动”。
为什么是 kvm-clock?
使用 x86 架构的机器可以有几种不同的时间源。我们可以使用以下命令检查 enclaves 可用的源:
|
|
Enclaves 应该有两个源:tsc 和 kvm-clock(如果您运行示例 enclave 并检查其源,可以看到它们);后者默认启用,可以在 current_clocksource 文件中检查。这些源如何工作?
TSC 机制基于时间戳计数器寄存器。它是一个每 CPU 单调计数器,实现为模型特定寄存器(MSR)。每个(虚拟)CPU 都有自己的寄存器。计数器随每个 CPU 周期递增(或多或少)。Linux 基于计数器按 CPU 频率缩放和某个初始日期计算当前时间。
如果我们有 root 权限,可以读取(和写入!)TSC 值。为此,我们需要 TSC 的偏移量(为 16)和其大小(为 8 字节)。MSR 寄存器可以通过 /dev/cpu 设备访问:
|
|
TSC 也可以使用 clock_gettime 方法和 CLOCK_MONOTONIC_RAW 时钟 ID 读取,以及使用 RDTSC 汇编指令读取。
理论上,如果我们更改 TSC,clock_gettime 与 CLOCK_REALTIME 时钟 ID、gettimeofday 函数和 date 命令报告的挂钟时间应该改变。然而,Linux 内核努力使 TSC 行为合理并彼此同步(例如,查看 tsc 看门狗代码和与 MSR_IA32_TSC_ADJUST 寄存器相关的功能)。因此破坏时钟并不那么容易。
TSC 可用于跟踪经过的时间,但 enclaves 从哪里获取“某个初始日期”来计算经过的时间?通常,在其他系统中,该日期使用 NTP 获取。然而,enclaves 没有开箱即用的网络访问权限,也不使用 NTP(参见 AWS 2020 re:Invent 会议此演示文稿的第 26 张幻灯片)。
图 5:Enclave 的可能时间源
使用 tsc 时钟且没有 NTP,初始日期有些随机选择——事实上我们尚未确定它来自哪里。您可以通过传递 no-kvmclock no-kvmclock-vsyscall 内核参数(但请注意这些参数不应在运行时提供)强制 enclave 在没有 kvm-clock 的情况下启动,并自行检查初始日期。在我们的实验中,日期是:
|
|
如您所见,TSC 机制与 enclaves 配合不佳。此外,当机器虚拟化时,它会严重破坏。因此,AWS 引入了 kvm-clock 作为 enclaves 的默认时间源。它是半虚拟时钟驱动程序(pvclock)协议的实现(有关 pvclock 的更多信息,请参见此文章和此博客文章)。使用此协议,主机(在我们的案例中为 AWS Nitro hypervisor)向客户机(enclave)提供 pvclock_vcpu_time_info 结构。该结构包含使客户机能够调整其时间测量的信息——最显著的是主机的挂钟(system_time 字段),用作初始日期。
有趣的是,即使启用了 kvm-clock,客户机的用户态应用程序也可以使用 TSC 机制。这是因为 RDTSC 指令(通常)不被模拟,因此可能提供未调整的 TSC 寄存器读数。
请注意,如果您的 enclaves 使用不同的时钟源或启用 NTP,您应进行一些额外研究以查看是否存在相关的安全问题。
认证
加密认证是最终用户的信任来源。用户正确解析和验证认证至关重要。幸运的是,AWS 提供了关于如何消费认证的良好文档。
最重要的认证数据是协议特定的,但我们有一些普遍适用的提示供开发人员牢记(除了 AWS 文档中编写的内容):
- Enclave 应强制执行最小 nonce 长度。
- 用户应检查认证中提供的时间戳以及 nonces。
- 认证的时间戳不应用于推断 enclave 的时间。此时间戳可能与 enclave 的时间不同,因为前者由 hypervisor 生成,而后者由 enclave 使用的任何时钟源生成。
- 如果可能,不要将 RSA 用于 public_key 功能。
NSM 驱动程序
您的 enclave