英特尔® Software Guard Extensions(英特尔® SGX )教程系列第三部分将介绍如何将英特尔 SGX 融入应用设计之中。 我们将运用第一部分中回顾的部分概念,以对第二部分的示例应用 Tutorial Password Manager 进行高级设计。 我们将描述该应用的整体结构以及英特尔 SGX 对它的影响,并创建一个类模型来帮助我们设计和集成安全区。
文章英特尔® Software Guard Extensions 教程系列简介列举了所有已经发布的教程。
尽管我们不对安全区或安全区界面进行编码,但该系列的安装部分仍然会提供源代码。 非英特尔 SGX 版本的应用内核(不包含用户界面)可供下载。 它包含一个小型测试程序,一个用 C# 编写的控制台应用,以及一个示例密码仓库文件。
专为安全区设计
以下是我们设计面向英特尔 SGX 的 Tutorial Password Manager 时所采用的通用方法:
- 识别应用机密。
- 识别这些机密的提供者和使用者。
- 确定安全区边界。
- 定制安全区的应用组件。
识别应用机密
设计英特尔 SGX 应用的第一步是识别应用机密。
机密表示不应被其他人知晓或看到的事情。 因此,机密只供其用户或目标应用访问,绝对不能展示给其他用户或应用,无论其拥有多高的权限级别。 机密包含财务信息、病历、个人可识别信息、身份数据、授权的媒体内容、密码和加密密钥。
Tutorial Password Manager 中包含多个可即刻识别为机密的条目,如表 1 所示。
秘密
用户帐号密码
用户帐号登录信息
用户的主密码或口令
密码仓库的主密钥
帐号数据库的加密密钥
表 1:初步的应用机密列表。
这些选项比较明显,但我们可以将所有用户帐号信息(不仅仅是登录信息)都包含在内,以扩展该列表。 修改后的列表如表 2 所示。
秘密
用户帐号密码
用户帐号登录信息
用户的主密码或口令
密码仓库的主密钥
帐号数据库的加密密钥
表 2: 修改后的应用机密列表。
即使没有显示密码,帐号信息(比如服务名称和 URL)对攻击者来说也非常重要。 在密码管理器中显示此类数据会为不怀好意的攻击者提供重要提示。 有了此类数据,他们可以选择通过社交工程或密码重置攻击来对服务本身发动攻击,以访问所有者的帐号,因为他们完全知道谁是他们的攻击目标。
识别这些机密的提供者和使用者。
识别应用机密后,接下来是确定它们的来源和目的地。
在当前的英特尔 SGX 版本中,安全区代码未经加密,任何能够访问该应用文件的人都能对其进行拆卸和观察。 根据定义,可供观察的事情不能成为机密,即机密不能静态地编译成安全区代码。 应用机密必须来源于安全区外部,在运行时加载至安全区内。 在英特尔 SGX 术语表中,这称之为将机密供应至安全区。
当机密来源于可信计算基 (TCB) 外部的组件时,必须最大限度地降低暴露给非信任代码的几率。 (远程验证之所以成为英特尔 SGX 的重要组件,主要是因为它支持服务提供者建立与英特尔 SGX 应用的可信关系,然后衍生出加密密钥用于将加密机密供应至仅能通过客户端系统上的可信安全区解密的应用。) 从安全区导出机密时同样需要谨慎小心。 一般来说,如果没有事先在安全区内进行加密,应用机密不能发送至非信任代码。
对 Tutorial Password Manager 应用而言遗憾的是,我们确实需要将机密发送至安全区以及从安全区发出机密,而且这些机密必须在某个点以明文形式存在。 最终用户通过键盘或触摸屏输入帐号信息和密码,并在将来根据需要重新调用。 他们的帐号密码需要显示在屏幕上,甚至根据请求拷贝至 Windows* 剪切板。 只有满足这些核心要求,密码管理器应用才能发挥作用。
这意味着我们无法完全消除攻击面:只能最大限度地缩小攻击面,并且需要实施规避策略来处理机密以纯本文形式位于安全区外部的情况。
机密 | 来源 | 目的地 |
用户帐号密码 | 用户输入* 密码仓库文件 | 用户界面* 剪切板* 密码仓库文件 |
用户帐号信息 | 用户输入* 密码仓库文件 | 用户界面* 密码仓库文件 |
用户的主密码或口令 | 用户输入 | 密钥衍生功能 |
密码仓库的主密钥 | 密钥衍生功能 | 数据密钥加密 |
帐号数据库的加密密钥 | 随机生成 密码仓库文件 | 密码仓库加密 密码仓库文件 |
表 3: 应用机密及其来源与目的地。 星号 (*) 表示可能存在安全风险。
表 3 新增了 Tutorial Password Manager 中机密的来源和目的地。 星号 (*) 表示可能存在的问题 — 机密可能会暴露给非信任代码。
确定安全区边界
识别机密后,接下来应该确定安全区的边界。 首先通过应用的核心组件查看机密的数据流。 安全区边界应该:
- 包含少量对应用机密起作用的关键组件。
- 如果可行,完全包含多个机密。
- 最大限度地减少与非信任代码的交互以及对它的依赖。
Tutorial Password Manager 应用的数据流和所选安全区边界如图 1 所示。
图 1:面向 Tutorial Password Manager 中的机密的数据流。
此处用圆圈表示应用机密:蓝色圆圈表示应用执行期间在某个点以(未加密)纯文本形式存在的机密,绿色圆圈表示经过应用加密的机密。 安全区边界围绕加密和解密路径、密钥衍生功能 (KDF) 和随机数生成器绘制。 此举可为我们执行以下几项任务:
- 在安全区内生成数据库/仓库密钥,用于加密部分应用机密(帐号信息和密码),而且在安全区外部不以明文形式发送该密钥。
- 通过用户口令在安全区内衍生出主密钥,并将其用于加/解密数据库/仓库密钥进行。 主密钥是临时的,不会以任何形式在安全区外部发送。
- 数据库/仓库密钥、帐号信息和帐号密码在安全区内通过非信任代码(见 #1 和 #2)不可见的加密密钥完成加密。
遗憾的是,通过安全区边界的未加密机密存在一些我们无法避免的问题。 在 Tutorial Password Manager 执行过程中的某个点,用户需要通过键盘输入密码,或将密码拷贝至 Windows 剪切板。 这些非安全通道无法放在安全区内部,而且是执行应用所必需的操作。 如果决定在托管代码库顶端构建应用,该问题会变得非常严重。
保护安全区外部的机密
我们没有完备的解决方案来保护安全区外部的未加密机密,只能采取规避策略来缩小攻击面。 最好地方法是最大限度地缩短信息以易受影响的形式所存在的时间。
以下一般性建议可帮助处理非信任代码中的敏感数据:
- 使用完敏感数据后零填充数据缓冲区。 务必使用确保不会由编译器进行优化的函数,比如 SecureZeroMemory (Windows) 和 memzero_explicit (Linux)。
- 不要使用 C++ 标准模板库 (STL) 容器保存敏感数据。 STL 容器有自己的内存管理方法,对象删除后,难以确保分配给对象的内存经过安全擦除。 (使用自定义分配程序,可解决部分容器所存在的这种问题。)
- 处理 .NET 等托管代码或具有自动内存分配功能的语言时,使用专为保留安全数据所设计的存储类型。 其他存储类型依赖垃圾回收器和即时编译,可能无法按照需求进行清空和释放。
- 如果必须将数据放在剪切板上,请务必尽快清空。 尤其是应用退出后,不要让数据留在剪切板上。
对 Tutorial Password Manager 项目而言,我们必须使用原生代码和托管代码。 在原生代码中,我们将分配 wchar_t和 char 缓冲区,并在释放之前,使用 SecureZeroMemory 将其擦除干净。 在托管代码空间中,我们将使用 .NET’s SecureString 类。
将 SecureString 发送至未托管代码时,我们将使用 System::Runtime::InteropServices 的帮助程序函数封送数据。
using namespace System::Runtime::InteropServices; LPWSTR PasswordManagerCore::M_SecureString_to_LPWSTR(SecureString ^ss) { IntPtr wsp= IntPtr::Zero; if (!ss) return NULL; wsp = Marshal::SecureStringToGlobalAllocUnicode(ss); return (wchar_t *) wsp.ToPointer(); }
可以使用两种方法将数据封送至其他方向,即从原生代码封送至托管代码。 如果 SecureString 对象已经存在,我们将使用 Clear 和 AppendChar 方法通过 wchar_t字符串设置新的数值。
password->Clear(); for (int i = 0; i < wpass_len; ++i) password->AppendChar(wpass[i]);
如果创建新的 SecureString 对象,我们将使用构建程序通过现有 wchar_t字符串创建 SecureString。
try { name = gcnew SecureString(wname, (int) wcslen(wname)); login = gcnew SecureString(wlogin, (int) wcslen(wlogin)); url = gcnew SecureString(wurl, (int) wcslen(wurl)); } catch (...) { rv = NL_STATUS_ALLOC; }
密码管理器还支持将密码传输至 Windows 剪切板。 剪切板是不安全的存储空间,其他用户都能够访问该剪切板,因此 Microsoft 建议不要将敏感数据放在剪切板上。 不过密码管理器旨在支持用户创建不需要记住,且安全性较强的密码。 它还支持创建长密码,其中包含随机生成,难以手动输入的字符。 尽管存在风险,但剪切板能够提供诸多便利。
为缓解风险,我们需要采取其他防范措施。 首先确保应用退出时已清空剪切板。 原生对象中的析构函数可帮助完成清空。
PasswordManagerCoreNative::~PasswordManagerCoreNative(void) { if (!OpenClipboard(NULL)) return; EmptyClipboard(); CloseClipboard(); }
我们还可设置剪切板计时器。 密码拷贝至剪切板时,设置 15 秒的计时器,到时间后执行相关函数清空剪切板。 如果计时器已经运行,表示旧密码到期之前剪切板放置了新密码,该计时器将取消并由新计时器取代。
void PasswordManagerCoreNative::start_clipboard_timer() { // Use the default Timer Queue // Stop any existing timer if (timer != NULL) DeleteTimerQueueTimer(NULL, timer, NULL); // Start a new timer if (!CreateTimerQueueTimer(&timer, NULL, (WAITORTIMERCALLBACK)clear_clipboard_proc, NULL, CLIPBOARD_CLEAR_SECS * 1000, 0, 0)) return; } static void CALLBACK clear_clipboard_proc(PVOID param, BOOLEAN fired) { if (!OpenClipboard(NULL)) return; EmptyClipboard(); CloseClipboard(); }
定制安全区的应用组件
识别机密并绘制完安全区边界后,现在可以构建应用,同时将安全区纳入帐之号。 安全区内的操作内容存在诸多限制,而且这些限制条件将规定放在安全区内的组件,安全区外的组件,以及移植现有应用时需要一分为二的组件。
影响 Tutorial Password Manager 最大的限制条件是安全区不能执行任何 I/O 操作。 安全区不能通过键盘读取,也不能写入至显示器,因此所有机密(密码和账号信息)必须封送至安全区内,或者从安全区封送出来。 而且,它不能读取或写入仓库文件:解析仓库文件的组件必须与执行物理 I/O 操作的组件分开。 这意味着我们穿过安全区边界所封送的内容除了机密外,还有文件内容。
图 2:Tutorial Password Manager 类图。
图 2 显示了应用内核(包括用户界面)的基本类图,包括用作机密来源和目的地的类。 请注意,在该图中,为简单起见,PasswordManagerCore 类被视作必须与 GUI 交互的机密的来源和目的地。 表 4 对各个类及其用途进行了简要介绍。
类 | 类型 | 功能 |
PasswordManagerCore | 托管 | 与 C# 图形用户界面 (GUI) 交互,并将数据封送至本地层。 |
PasswordManagerCoreNative | 原生,非信任 | 与托管 PasswordManagerCore 类交互。 同时负责在 Unicode 和多字节字符数据之间进行转换。 |
VaultFile | 托管 | 读写仓库文件。 |
Vault | 原生,安全区 | 将密码仓库数据保存在 AccountRecord成员中。 在读取过程中对仓库文件进行反序列化处理,并重新实现其序列化以供写入。 |
AccountRecord | 原生,安全区 | 将各个账号的账号信息和密码保存在用户的密码仓库中。 |
Crypto | 原生,安全区 | 执行加密功能。 |
DRNG | 原生,安全区 | 随机数生成器接口。 |
表 4:类描述。
请注意,我们需要将仓库文件处理分成两部分:一部分执行物理 I/O 操作,另一部分保存读取并解析后的内容。 还需新增序列化和反序列化方法来处理作为机密中间来源和目的地的 Vault 对象。 所有这些操作都必不可少,因为 VaultFile 类完全不了解仓库文件的结构,且要求访问安全区内部的加密函数。
连接 PasswordManagerCoreNative 类和 Vault 类时我们还绘制了一条虚线。 大家可能还记得我们在第二部分说过,安全区只能链接至 C 函数。 有两种 C++ 类相互之间不能通信:必须使用表示为 Bridge Functions 框的中介。
非英特尔® Software Guard Extensions 代码路径
英特尔 SGX 代码路径如图 2 中的示意图所示。 PasswordManagerCoreNative 类不能直接链接至 Vault 类,因为后者位于安全区内。 不过非英特尔 SGX 代码路径中不存在这种限制: PasswordManagerCoreNative 可直接包含 Vault类的成员。 这是我们面向非英特尔 SGX 代码路径进行应用设计时所能走的唯一捷径。为了简化安全区集成,非安全区代码路径仍然会将仓库处理隔离在 Vault 和 VaultFile 类中。
两种路径之间的另外一个不同点是英特尔 SGX 路径中的加密函数将来源于英特尔 SGX SDK。 非英特尔 SGX 代码路径不能使用这些函数,因为它们将利用 Microsoft 的 Cryptography API: Next Generation* (CNG)。 这意味着我们需要保持 Crypto 类有两个不同的副本:一个在安全区内使用,一个在非信任空间中使用。 (其他的类也是如此;第 5 部分将予以介绍。)
示例代码
如简介部分所述,本部分将提供示例代码以供下载。 随附档案包含进行安全区集成之前,用于 Tutorial Password Manager 核心 DLL 的源代码。 换言之,它是非英特尔 SGX 版本的应用内核。 这里不提供用户界面,但包含用 C# 编写,并通过一系列测试操作运行的基础测试应用。 它执行两个测试套件,一个创建新仓库文件并在此基础上执行各种操作,另一个处理通过源分发模式所包含的参考仓库文件。 编写时,测试应用预计测试仓库位于 Documents 文件夹中,不过您可以根据需要在 TestSetup 类中进行更改。
按照本教程简介部分所述要求,源代码在 Microsoft Visual Studio* Professional 2013 中开发。 此时不需要英特尔 SGX SDK,但需要系统支持英特尔® 数据保护技术(基于安全密钥)。
即将推出
教程第四部分将介绍如何开发安全区和桥接函数。 敬请关注!
在文章英特尔® Software Guard Extensions 教程系列简介中查找已发布的教程列表。