TPM 2.0 参考实现代码中的漏洞
在这篇博文中,我们将详细讨论在可信平台模块 (TPM) 2.0 参考实现代码中发现的两个漏洞。这两个漏洞,越界写入 (CVE-2023-1017) 和越界读取 (CVE-2023-1018),影响了多个 TPM 2.0 的软件实现(例如虚拟化软件使用的那些)以及多个硬件 TPM。
介绍
2021 年 10 月,微软发布了 Windows 11。其中一个引人注目的安装要求是需要可信平台模块 (TPM) 2.0。此要求的含义是,为了能够在虚拟机中运行 Windows 11,虚拟化软件必须向 VM 提供 TPM,方法是直通主机上的硬件 TPM,或提供虚拟 TPM 给他们。
我们发现这是一个有趣的漏洞研究主题,因为虚拟 TPM 的添加意味着虚拟化软件的攻击面扩大,可以从客户机内部访问,因此可能被用于虚拟机逃逸。作为研究成果,我们发现了两个安全问题:标识为 CVE-2023-1017 的越界写入和标识为 CVE-2023-1018 的越界读取。它们可以通过发送带有加密参数的恶意 TPM 2.0 命令来触发用户模式应用程序。有趣的是,这两个漏洞的影响范围比我们最初想象的要大得多:由于它们起源于可信计算组织(简称 TCG,发布和维护 TPM 规范的非营利组织)发布的参考实现代码,这些安全漏洞不仅影响了我们测试的每一个虚拟化软件,还影响了硬件实现。
请注意,本篇博文中的大部分评估(例如关于可利用性、影响或受影响的平台)都是基于我们对软件虚拟 TPM 的分析,因为我们可以通过一种简单的方式调试它们以执行动态分析(尽管 Hyper-V 的虚拟 TPM 运行为 IUM 进程 ,但那是另一回事)。相反,在没有调试接口的单独芯片中运行 TPM 固件,并了解运行时发生了什么是一个完全不同的问题。即使对硬件 TPM 固件进行静态分析也很困难:我们尝试分析的几个 TPM 固件更新恰好是加密的。因此,缺乏对硬件 TPM 的具体评估并不意味着它们不受影响;这只是由于缺乏可观察性,我们无法评估它们中的大多数如何受到影响。然而,使用这篇博文中发布的概念验证代码,我们已经证实至少有一些单独的 TPM 芯片容易受到攻击。在尝试 OOB 写入之后,芯片将停止响应(即不再识别命令),需要重新启动计算机才能再次正常工作,并确认其漏洞状态。
受影响的平台
这些受影响的软件和硬件平台并非详尽列表。此处列出的产品是我们借助本文中提供的 PoC 证明漏洞存在的产品,但其他 TPM(无论是虚拟的还是物理的)很可能也存在漏洞。
漏洞代码存在于 TPM 2.0 参考实现的最新可用版本(在我们进行研究时):Trusted Platform Module Library Specification, Family “2.0”, Level 00, Revision 01.59 – November 2019;
Windows 10 上的 Microsoft Hyper-V(受影响的模块:TPMEngUM.dll,版本 10.0.19041.1415);
VMware Workstation 版本 16.2.4 build-20089737(受影响的模块:tpm2emu.exe,可执行文件中没有版本信息);
Libtpms/SWTPM,由 Qemu 和 VirtualBox 使用(从 master 分支编译,commit 520a2fa27d27a4ab18f4cf1c597662c6a468565f);
Nuvoton 硬件 TPM(固件版本:1.3.0.1);
通常来说,所有基于可信计算组织提供的参考代码实现的 TPM 2.0 固件都有可能受到影响。
对云计算的威胁
所有主要的云计算提供商都提供带有虚拟 TPM 的实例。这暴露了一个有趣的场景,因为恶意行为者可能会尝试利用虚拟 TPM 中的这些漏洞来逃离虚拟机并危害主机系统。
Amazon AWS 有 NitroTPM;
Microsoft Azure 提供虚拟 TPM 作为 Trusted Launch 的一部分;
Google Cloud 提供虚拟 TPM 作为 Shielded VMs 的一部分;
Oracle Cloud Infrastructure 提供虚拟 TPM 作为 Shielded Instances 的一部分。
那些使用基于 TCG 参考实现的虚拟 TPM 的供应商预计会受到攻击。就 Google Cloud 而言,上面链接的博文提到他们的虚拟 TPM 的核心来自 IBM 发布的代码,它是从 TPM 2.0 规范的完整源代码中自动提取的,我们验证了其中的错误 CryptParameterDecryption 功能都存在于其中。对于 Microsoft Azure,之前链接的文档提到他们的虚拟 TPM“符合 TPM 2.0 规范”,我们已经验证 Windows 10 上可用的 Hyper-V 版本中包含的虚拟 TPM 确实是易受伤害的。这些错误也存在于 Microsoft 的开源参考实现中。
关于 Amazon AWS 和 Oracle Cloud Infrastructure,我们没有太多关于它们使用什么的细节,只是 NitroTPM 文档提到它“符合 TPM 2.0 规范”并带有指向 TCG 网站的链接。
修复
参考实现
Trusted Computing Group 发布了 TCG Trusted Platform Module Library 勘误表 1.4 版,并提出了针对这两个错误的修复建议。
软件产品
微软在 2023 年 3 月的安全更新中修补了 Hyper-V 中的漏洞。他们对 Azure 的 Pluton/HCL/Overlake/Manticore 标准服务器中 TPM 2.0 中 OOB 写入影响的评估很低,因为覆盖只有 2 个字节,他们的团队还没有确定一种一致且易于实现的方法来获得 EoP 或 RCE只有 2 个字节。
微软还通过提交 9bdd9f0aaba5e54b3c314cfff02cf532281a067e 修补了他们的开源参考实现。
VMware 预计将在 2023 年 4 月发布针对这些错误的修复程序。
Libtpms 修补了提交 324dbb4c27ae789c73b69dbf4611242267919dd4 中的错误。
Chromium OS 修补了提交 3b87ed233acb4c76c27872e1ac0b74dc032199f1 中的漏洞。
IBM 在提交 102893a5f45dbb0b0ecc0eb52a8dd4defe559f92 中修补了他们的开源实现。
硬件产品
Nuvoton 为其 NPCT65x TPM 芯片发布了安全公告 SA-003。
Lenovo 针对使用上述 Nuvoton TPM 的受影响产品发布了安全公告 LEN-118320。
请检查计算机制造商的网站以获取 TPM 固件更新。
技术细节
TPM 加密参数入门
如 Trusted Platform Module Library Specification, Family 2.0, Part 1: Architecture 第 21 节 - “Session-based encryption”中所述,一些 TPM 2.0 命令具有可能需要加密的进出 TPM 的参数。基于会话的加密可用于确保这些参数的机密性。引用规范如下所述:
Not all commands support parameter encryption. If session-based encryption is allowed, only the first
parameter in the parameter area of a request or response may be encrypted. That parameter must have
an explicit size field. Only the data portion of the parameter is encrypted. The TPM should support
session-based encryption using XOR obfuscation. Support for a block cipher using CFB mode is platform
specific. These two encryption methods (XOR and CFB) do not require that the data be padded for
encryption, so the encrypted data size and the plain-text data size is the same.
[…]
Session-based encryption uses the algorithm parameters established when the session is started and
values that are derived from the session-specific sessionKey.
[…]
If sessionAttributes.decrypt is SET in a session in a command, and the first parameter of the command is
a sized buffer, then that parameter is encrypted using the encryption parameters of the session.
具有加密参数的 TPM 2.0 命令由基本命令标头组成,后跟一个 handleArea,然后是 sessionArea,最后是加密的 parameterArea。下图说明了上述结构:
±----------------------------+
| |
| |
| Base command header |
| |
| |
±----------------------------+
| |
| handleArea |
| |
±---------------------------+
| |
| sessionArea |
| |
±--------------------------+
| |
| parameterArea |
| |
±-------------------------+
在TPM 2.0参考实现中,ExecCommand.c 文件中的 ExecuteCommand 函数会检查 sessionArea 的 authorizationSize 字段,保证其至少为 9([1])。然后,在[2]处计算 parameterArea(位于 sessionArea 之后)的起始位置,并将其保存到 parmBufferStart 变量中。在[3]处计算 parameterArea 的大小,并将其保存到 parmBufferSize 变量中。然后调用 ParseSessionBuffer()([4]), 并传递 parmBufferStart 和 parmBufferSize 作为参数([5],[6])。
// ExecuteCommand()
//
// The function performs the following steps.
// a) Parses the command header from input buffer.
// b) Calls ParseHandleBuffer() to parse the handle area of the command.
// c) Validates that each of the handles references a loaded entity.
//
// d) Calls ParseSessionBuffer() () to:
// 1) unmarshal and parse the session area;
// 2) check the authorizations; and
// 3) when necessary, decrypt a parameter.
[...]
LIB_EXPORT void
ExecuteCommand(
unsigned int requestSize, // IN: command buffer size
unsigned char *request, // IN: command buffer
unsigned int *responseSize, // OUT: response buffer size
unsigned char **response // OUT: response buffer
)
{
[…]
// Find out session buffer size.
result = UINT32_Unmarshal(&authorizationSize, &buffer, &size);
if(result != TPM_RC_SUCCESS)
goto Cleanup;
// Perform sanity check on the unmarshaled value. If it is smaller than
// the smallest possible session or larger than the remaining size of
// the command, then it is an error. NOTE: This check could pass but the
// session size could still be wrong. That will be determined after the
// sessions are unmarshaled.
[1] if( authorizationSize < 9
|| authorizationSize > (UINT32) size)
{
result = TPM_RC_SIZE;
goto Cleanup;
}
// The sessions, if any, follows authorizationSize.
sessionBufferStart = buffer;
// The parameters follow the session area.
[2] parmBufferStart = sessionBufferStart + authorizationSize;
// Any data left over after removing the authorization sessions is
// parameter data. If the command does not have parameters, then an
// error will be returned if the remaining size is not zero. This is
// checked later.
[3] parmBufferSize = size - authorizationSize;
// The actions of ParseSessionBuffer() are described in the introduction.
[4] result = ParseSessionBuffer(commandCode,
handleNum,
handles,
sessionBufferStart,
authorizationSize,
[5] parmBufferStart,
[6] parmBufferSize);
[…]
SessionProcess.c 文件中的函数 ParseSessionBuffer 将解析命令中的 sessionArea。如果会话设置了 Decrypt 属性([1]),并且命令代码允许参数加密,则 ParseSessionBuffer 会调用 CryptParameterDecryption()([2]),并传递 parmBufferSize([3]) 和 parmBufferStart([4]) 参数:
// ParseSessionBuffer()
//
// This function is the entry function for command session processing. It iterates sessions in session area
// and reports if the required authorization has been properly provided. It also processes audit session and
// passes the information of encryption sessions to parameter encryption module.
//
// Error Returns Meaning
//
// various parsing failure or authorization failure
//
TPM_RC
ParseSessionBuffer(
TPM_CC commandCode, // IN: Command code
UINT32 handleNum, // IN: number of element in handle array
TPM_HANDLE handles[], // IN: array of handle
BYTE *sessionBufferStart, // IN: start of session buffer
UINT32 sessionBufferSize, // IN: size of session buffer
BYTE *parmBufferStart, // IN: start of parameter buffer
UINT32 parmBufferSize // IN: size of parameter buffer
)
{
[…]
// Decrypt the first parameter if applicable. This should be the last operation
// in session processing.
// If the encrypt session is associated with a handle and the handle’s
// authValue is available, then authValue is concatenated with sessionAuth to
// generate encryption key, no matter if the handle is the session bound entity
// or not.
[1] if(s_decryptSessionIndex != UNDEFINED_INDEX)
{
// Get size of the leading size field in decrypt parameter
if( s_associatedHandles[s_decryptSessionIndex] != TPM_RH_UNASSIGNED
&& IsAuthValueAvailable(s_associatedHandles[s_decryptSessionIndex],
commandCode,
s_decryptSessionIndex)
)
{
extraKey.b.size=
EntityGetAuthValue(s_associatedHandles[s_decryptSessionIndex],
&extraKey.t.buffer);
}
else
{
extraKey.b.size = 0;
}
size = DecryptSize(commandCode);
[2] result = CryptParameterDecryption(
s_sessionHandles[s_decryptSessionIndex],
&s_nonceCaller[s_decryptSessionIndex].b,
[3] parmBufferSize, (UINT16)size,
&extraKey,
[4] parmBufferStart);
CryptParameterDecryption 函数中的漏洞
CryptUtil.c 文件中的 CryptParameterDecryption 函数用于将加密的命令参数就地解密。
// 10.2.9.9 CryptParameterDecryption()
//
// This function does in-place decryption of a command parameter.
//
// Error Returns Meaning
//
// TPM_RC_SIZE The number of bytes in the input buffer is less than the number of
// bytes to be decrypted.
//
TPM_RC
CryptParameterDecryption(
TPM_HANDLE handle, // IN: encrypted session handle
TPM2B *nonceCaller, // IN: nonce caller
UINT32 bufferSize, // IN: size of parameter buffer
UINT16 leadingSizeInByte, // IN: the size of the leading size field in
// byte
TPM2B_AUTH *extraKey, // IN: the authValue
BYTE *buffer // IN/OUT: parameter buffer to be decrypted
)
{
SESSION *session = SessionGet(handle); // encrypt session
// The HMAC key is going to be the concatenation of the session key and any
// additional key material (like the authValue). The size of both of these
// is the size of the buffer which can contain a TPMT_HA.
TPM2B_TYPE(HMAC_KEY, ( sizeof(extraKey->t.buffer)
+ sizeof(session->sessionKey.t.buffer)));
TPM2B_HMAC_KEY key; // decryption key
UINT32 cipherSize = 0; // size of cipher text
pAssert(session->sessionKey.t.size + extraKey->t.size <= sizeof(key.t.buffer));
// Retrieve encrypted data size.
if(leadingSizeInByte == 2)
{
// The first two bytes of the buffer are the size of the
// data to be decrypted
[1] cipherSize = (UINT32)BYTE_ARRAY_TO_UINT16(buffer);
[2] buffer = &buffer[2]; // advance the buffer
}
#ifdef TPM4B
else if(leadingSizeInByte == 4)
{
// the leading size is four bytes so get the four byte size field
cipherSize = BYTE_ARRAY_TO_UINT32(buffer);
buffer = &buffer[4]; //advance pointer
}
#endif
else
{
pAssert(FALSE);
}
[3] if(cipherSize > bufferSize)
return TPM_RC_SIZE;
// Compute decryption key by concatenating sessionAuth with extra input key
MemoryCopy2B(&key.b, &session->sessionKey.b, sizeof(key.t.buffer));
MemoryConcat2B(&key.b, &extraKey->b, sizeof(key.t.buffer));
if(session->symmetric.algorithm == TPM_ALG_XOR)
// XOR parameter decryption formulation:
// XOR(parameter, hash, sessionAuth, nonceNewer, nonceOlder)
// Call XOR obfuscation function
[4] CryptXORObfuscation(session->authHashAlg, &key.b, nonceCaller,
&(session->nonceTPM.b), cipherSize, buffer);
else
// Assume that it is one of the symmetric block ciphers.
[5] ParmDecryptSym(session->symmetric.algorithm, session->authHashAlg,
session->symmetric.keyBits.sym,
&key.b, nonceCaller, &session->nonceTPM.b,
cipherSize, buffer);
return TPM_RC_SUCCESS;
}
该函数中出现了两个安全问题:
Bug #1 - OOB 读取 (CVE-2023-1018):在[1]处,该函数使用 BYTE_ARRAY_TO_UINT16 宏从 parmBufferStart 指向的缓冲区中读取一个 16 位的字段(cipherSize),但没有检查会话区域后面是否有任何参数的数据。先前在 ExecuteCommand 函数中执行的长度检查是唯一的一个,但该检查只验证了命令的 sessionArea 至少为 9 字节大小。因此,如果畸形命令不包含超出 sessionArea 的 parameterArea 部分,则会触发越界内存读取,使 TPM 访问命令结尾处的内存。
请注意,BYTE_ARRAY_TO_UINT16 宏不执行任何边界检查:
#define BYTE_ARRAY_TO_UINT16(b) (UINT16)( ((b)[0] << 8)
+ (b)[1])
应该使用 UINT16_Unmarshal 函数,它在从给定缓冲区读取数据之前执行了适当的大小检查。
Bug #2 - OOB 写入 (CVE-2023-1017):如果提供了适当的 parameterArea(避开了Bug #1),则 parameterArea 的前两个字节将被解释为要解密的数据大小(cipherSize 变量位于 [1])。在读取 cipherSize 之后,在 [2] 处,buffer 指针向前移动了 2 字节。在 [3] 处有一个安全检查(如果 cipherSize 值大于实际缓冲区大小,则退出),但这里有个问题:在读取 16 位的 cipherSize 字段,并将 buffer 指针向前移动 2 字节后,该函数没有从 bufferSize 中将其减去(2 字节)。因此,可能会成功通过 [3] 处的安全检查,并得到一个比剩余数据的实际大小大 2 的 cipherSize 值。因此,在调用 CryptXORObfuscation() 或 ParmDecryptSym() 函数(分别位于 [4] 和 [5] 处)来实际解密紧随 cipherSize 后的 parameterArea 中的数据时,TPM 最终写入的数据会超出缓冲区末尾 2 字节,导致越界写。
乍一看,仅 2 个字节的 OOB 写入似乎不是一个特别强大的原语,但是就在去年,我们的同事 Damiano Melotti 和 Maxime Rossi Bellom 设法通过单个字节的 OOB 写入 0x01 在 Google 的 Titan M 芯片上获得了代码执行。
漏洞影响
1)OOB 读取:CryptUtil.c 文件中的 CryptParameterDecryption 函数可以从接收到的 TPM 命令的末尾读取 2 个字节。如果受影响的 TPM 在接收多个命令之间不清零命令缓冲区,则可能导致受影响的函数从先前命令中读取已经存在的任何 16 位值。这取决于具体的实现方式:例如,VMware 在请求之间不清除命令缓冲区,因此 OOB 读取可以访问来自上一个命令的任何值;相反,Hyper-V 的虚拟 TPM 在每次接收请求时都用零填充了未使用的字节,在这种情况下 OOB 访问最终只会读出 0。
2)OOB 写入:CryptUtil.c 文件中的 CryptXORObfuscation/ParmDecryptSym 函数(由 CryptParameterDecryption 调用)可以将数据写入超出指定长度 2 字节的位置,导致内存损坏。
第二个漏洞显然是最有趣的,能否覆盖到有用的数据,取决于每种实现在接收 TPM 命令时如何分配缓冲区。例子如下:
VMware 使用大小为 0x10000 的缓冲区,远远超出 TPM 命令的需要(最大 0x1000 字节);
Hyper-V 使用大小为 0x1000 的静态变量作为命令缓冲区;
SWTPM 使用 malloc() 来分配大小为 0x1008 的缓冲区(其中 8 个字节用于 send command prefix,可用于修改 locality,然后再加上最大 TPM 的长度 0x1000 字节)。
因此,我们可以用 OOB 写入覆盖的数据取决于具体实现。上述三个虚拟 TPM 采用了完全不同的方式来分配命令缓冲区。同样地,在给定的硬件 TPM 固件中,位于命令缓冲区右侧可供覆盖的有用内容,完全取决于特定硬件供应商如何为传入的命令分配缓冲区。
触发漏洞
为了复现上述两个错误,需要向目标 TPM 发送两个命令。第一个命令必须是 TPM2_StartAuthSession 命令,以启动授权会话。为简单起见,我们可以指定 TPM_ALG_XOR 作为要使用的对称算法。然后,我们将得到一个包含会话句柄的 TPM 响应。
之后,我们需要发送支持参数加密的命令。这里使用 TPM2_CreatePrimary,虽然其他几个命令也可能有效。我们将前一步中获得的会话句柄传递给 TPM2_CreatePrimary 命令的 sessionArea,并在 sessionAttributes 字段中设置了 Decrypt 标志。接下来:
要复现 Bug#1(OOB 读取),我们使用最小的有效 sessionArea 发送 TPM2_CreatePrimary 命令,并且后面没有跟随其他数据,即缺少 parameterArea。
要复现 Bug#2(OOB 写入),我们将其总大小设置为 TPM 命令的最大长度(0x1000 字节),并发送 TPM2_CreatePrimary 命令,这里包含了 parameterArea,其中 cipherSize 字段设置为 0xfe5(0x1000-sizeof(command_base_header)-sizeof(handleArea)-sizeof(sessionArea)),后面加上 0xfe3 字节的任意值来填充整个 TPM2_CreatePrimary 命令的 0x1000 字节空间。
PoC
你可以在此处下载 PoC 以复现这两个漏洞。该 .zip 文件包含一个 Python 版本的 PoC,可以在 Linux 系统上运行,以及一个 C 版本,可以在 Windows 系统上运行。
结论
我们在 TPM 2.0 参考实现的代码中发现了两个安全问题:越界读取和越界写入,基于可信计算组发布的参考代码实现的每个 TPM(软件或硬件实现)预计都会受到影响。
有趣的是,尽管所有受影响的 TPM 包含完全相同的易受攻击函数(该函数源于参考实现代码),但能否成功利用取决于命令缓冲区的特定实现方式。就我们看到的情况,每个实现都有不同的方式:有的在收到请求之间会清除命令缓冲区,而其他的则不会;有的通过 malloc() 分配堆中的命令缓冲区 ,而其他的则使用全局变量。
我们能够验证,这些漏洞存在于主流桌面虚拟化解决方案(如 VMware Workstation、Microsoft Hyper-V 和 Qemu)中包含的软件 TPM 中。最大的云计算提供商提供的虚拟 TPM 也可能受到影响。例如,谷歌云使用 IBM 发布的代码,自动从 TCG 参考实现中提取,我们验证了 IBM 提供的代码中存在错误。就微软 Azure 而言,我们提到 Windows 10 上的 Hyper-V 受到影响,并且由于 Azure 管理程序基于 Hyper-V,预计这两个漏洞也存在于微软的云平台上。
最后,我们预计大多数 TPM 硬件供应商也会受到影响。由于缺乏调试设备,难以了解运行时 TPM 固件中发生的事情,这使得确认物理芯片中是否存在漏洞比较困难。静态分析可能是评估硬件 TPM 是否易受攻击的替代方法,但我们设法获得的少数 TPM 固件是加密的,暂时无法评估。