这篇文章的目的是介绍Hyper-V平台TPM组件服务进程vmsp.exe相关的通用TPM漏洞CVE-2025-2884分析与复现.
文章结合了笔者魔改的用户模式隔离进程调试工具和逆向代码及调试结果分析了通用TPM漏洞CVE-2025-2884的利用过程和漏洞成因.
Windows 11 24h2 canary preview 启用嵌套虚拟化模式
Hyper-V 的 虚拟信任级别(VTL)是Windows的Hyper-V 虚拟化技术中用于隔离不同安全域的安全机制.VTL1 是其中较高的特权级别,其特性如下 VTL1 的特权高于 VTL0(最低权限级别),在虚拟安全模式(Virtual Secure Mode)中,VTL1 用于运行安全内核和隔离用户模式(IUM isolated user mode process)的代码,这些程序通过系统调用与 VTL0 中的内核交互.常见IUM进程有LsaIso.exe,vmsp.exe等. 开启虚拟安全模式可在 Windows 安全中心>设备安全>核心隔离详细信息>中找到内存完整性启用后重启计算机就能开启. 在VTL0 域中即使获取了内核级的范围权限 ,也无法对VTL1中内存进行操作.这种设计可抵御内核级攻击,保护用户密码哈希、BitLocker加密密钥等机密信息.VTL1 的安全内核模式由securekernel.exe承载用于调度用户IUM模式系统调用. 笔者使用最新版的LiveCloudKd 工具找到了一种可以嵌套虚拟化模式下调试虚拟机内用户模式隔离进程方法,原理在于区别虚拟机分区中虚拟安全模式隔离了跨VTL内存的访问权限, 对于虚拟机的父分区仍然可以调用内核模式hypercall或winhv.sys驱动api直接操作虚拟机线性物理内存,LiveCloudKd工具为我提供了一个签名的驱动hvmm.sys通过应用层api就能实现这些操作,即使在虚拟机中这片保护的物理内存被划分给VTL1级别,仍然可以在父分区使用物理地址对这片虚拟机内的内存进行访问.如果要调试用户IUM模式进程就需要patch一下 securekernel!SkpsIsProcessDebuggingEnabled函数,在新版中这个函数被内联在IumInvokeSecureService系统调用实现中,简单描述下特征
securekernel默认的调试策略被保存在其镜像策略配置中,直接修改配置或patch文件二进制会导致securekernel签名验证失败,重启虚拟机蓝屏,所以这种方式是不可行.但是对于运行时的代码只需要匹配包含特征的代码patch掉SkpsEnableDebugging条件始终为true就可以. 在这里我们需要得到3个关键的数据:
1.搜索到这片代码所在的物理页面地址的映射
2.找到一个立即触发的securekernel函数地址HvGetVpRegisters通过这个hypercall得到虚拟地址RIP和cr3
3.找到securekernel进程虚拟机地址和物理页面地址的映射
一个最常见的可以稳定触发的securekernel函数笔者找到的是IumInvokeSecureService这个函数,通过SharpDisasm这个工具反编译目标得到函数返回指令ret地址,然后不断搜索匹配的个函数所在页面的二进制数据,直到匹配到一个合适的物理页面,然后把指令ret改成如下无限循环汇编代码
修改内存后并不会引起虚拟机蓝屏,查询虚拟机所有vcpu的RIP通过HvGetVpRegisters这个hypercall,如果rip的低位与这个指令的低16位匹配,这样我们得到了一个符合条件的虚拟地址gva和匹配的物理地址gpa,就可以计算出页表页目录的基址cr3(也是通过hypercall)等.也同样通过SharpDisasm汇编指令匹配SkpsIsProcessDebuggingEnabled字符串, 得到要patch代码的地址根据securekernel计算得出的基址,加上偏移量转为物理地址后patch目标汇编代码,这样我们就成功绕过并启用调试用户IUM模式进程.
1.下载最新版的LiveCloudKd
2.复制所有文件到:"C:\Program Files\Windows Kits\10\Debuggers\x64"
3.修改"C:\Program Files\Windows Kits\10\Debuggers\x64\cfg\RegParam.reg" 其中WinDbgPath改成"C:\Program Files\Windows Kits\10\Debuggers\x64\"
4.安装vc运行库x64版
5.regsvr32 "C:\Program Files\Windows Kits\10\Debuggers\x64\ExdiHvSrv.dll"
6."C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -exec bypass -Command "Set-VMProcessor -VMName YourVMName -ExposeVirtualizationExtensions $true"其中YourVMName是你的虚拟机名字,启用嵌套虚拟化模式
7.复制虚拟机内C:\Windows\System32\securekernel.exe文件到:"C:\Program Files\Windows Kits\10\Debuggers\x64"和笔者工具同目录
8.虚拟机运行后运行笔者工具,看到打印出securekernel.exe基址即为成功
笔者工具使采用.net程序实现,笔者工具无需禁用安全启动或者修改bcdedit配置,下图是笔者在的Win11 24h2物理机上嵌套虚拟化模式下调试用户模式隔离进程vmsp.exe效果
TPM模块实现2.0(Trusted Platform Module)是可信平台模块的第二代版本,是一种安全密码处理器的国际标准,TPM模块实现可以是cpu上的加密芯片固件程序或者Hyper-V等虚拟机平台TPM组件模拟服务进程如vmsp.exe.笔者poc采用uefi程序实现,初始化TPM客户端通过与Hyper-V服务的共享物理页面 PcdTpmBaseAddress = 0xFED40000获取适配的TPM版本模式, 使用字段的结构地址进行所有的TPM通信数据包交互,有固定的输入输出缓冲区大小设置和操作返回等读写功能. 通用TPM漏洞CVE-2025-2884补丁发布与今年6月所影响包含所有共享tpm组件代码的固件及服务端应用程序,基于笔者的IUM模式进程调试工具我们得到了一种可以调试vmsp.exe进程的方法,下面我我们就通过代码结合调试分析下这个漏洞. 该漏洞存在于CryptHmacSign函数中,补丁前后的代码分析如下:
到达目标的漏洞代码需要调用Tpm2_CreatePrimary函数创建一个signKey,具体方法需要预定义一个key参数模板publicArea, 成功后在返回结果可以得到它的句柄,根据tpm文档构造参数传入调用TPM2_Sign,预先验证参数的signScheme和signKey的相关字段匹配成功后, 该函数对于传入的signKey->publicArea.type如果hash类型为TPM_ALG_KEYEDHASH会调用CryptHmacSign进行对传入的数据进行hash也就是签名, 根据其中的signature->signature.any.hashAlg算法可以是TPM_ALG_SHA1到TPM_ALG_SHA512等,hash数据的签名结果大小在0x10-0x30之间,而且可以通过传入的hashData控制, 这个函数默认是给HMAC算法使用,但实际类型可以由用户控制,导致签名的结果数据反序列化时存在对signature->sigAlg类型的混肴,进行另外一种类型对其反序列化,可能允许攻击者读取该签名的结果缓冲区末尾后最多65535个字节.
使用正常TPM_ALG_HMAC类型反序列化长度是对称的最多0x30,导致这个缓冲区之后的数据被泄露字节是没有问题的不存在泄露,但如果比如使用一种不匹配的反序列化类型比如TPM_ALG_EDDSA,由于输出缓冲区是共用的,会将CryptHmacSign结果signature->signature.hmac.digest前4个字节后的2个字节(UINT16可以由hash结果控制)作为反序列作为下个输出反序列化字节流的长度,这个长度可以是UINT16最大值65535,所以就导致了可能的越界读取信息泄露或者内存破坏等拒绝服务情况.但实际上在输出缓冲区反序列化时候会对输出字节流的最大长度判断即输出最大数据大小Sign_Out减去当前长度,如果这个长度很大比如65535,会导致结果是负数,进入不可预知的分支崩溃断言,导致vmsp.exe进程崩溃(int 3),如下显示是崩溃时的复现结果.但如果这个值未超过输出缓冲区的最大大小,比如果小于65535会根据这个值累加返回缓冲区大小长度,当然这个值可能导致最后的缓冲区UINT16大小溢出,则可能导致越过TPM_ALG_HMAC哈希结果的最大0x30大小导致这个缓冲区之后的数据被泄露,但是实际上这些数据都是0,只有溢出的数据才是泄露的部分,这个结果证明信息泄露的可能性和客户端可以读取实际上的越界信息泄露.CVE-2025-2884通过bindiff分析可以找到相同的补丁修复位置,手动在windbg把验证的代码绕过就可以触发到达漏洞的复现位置,复现效果为和未打补丁前相同,其他tpm通用平台漏洞复现也可以使用类似poc代码实现.
笔者漏洞poc采用uefi程序复现,出于安全原因笔者不能提供完整的poc代码,下图是笔者在的Win11 24h2物理机上使用嵌套虚拟化模式成功复现了CVE的利用效果
CVE-2025-2884
TPM源码
补丁
笔者工具
作者来自ZheJiang Guoli Security Technology,邮箱cbwang505@hotmail.com
int SkpsIsProcessDebuggingEnabled()
{ int editto1=0;
int Policy = SkpspFindPolicy((__int64)v560, 2, 7, (__int64)&BugCheckParameter2_4, (__int64)v554);
if ( Policy < 0 || BugCheckParameter2_4 >= 4 )
{
DbgPrint(
"%hs: SkpspFindPolicy returned 0x%x and DebugEnable is %d.\n",
"SkpsIsProcessDebuggingEnabled",
Policy,
BugCheckParameter2_4);
}
else if ( BugCheckParameter2_4 )
{
if ( BugCheckParameter2_4 == 1 )
{
editto1 = 1;
}
else if ( BugCheckParameter2_4 == 2 )
{
editto1 = (unsigned int)SkIsSecureBootEnabled() == 0x80430006;
}
else
{
editto1 = BYTE1(*v560) & 1;
}
}
if ( editto1 )
{
//editto1改成1开启调试
SkpsEnableDebugging((unsigned __int64)v560, a1[16]);
}
int SkpsIsProcessDebuggingEnabled()
{ int editto1=0;
int Policy = SkpspFindPolicy((__int64)v560, 2, 7, (__int64)&BugCheckParameter2_4, (__int64)v554);
if ( Policy < 0 || BugCheckParameter2_4 >= 4 )
{
DbgPrint(
"%hs: SkpspFindPolicy returned 0x%x and DebugEnable is %d.\n",
"SkpsIsProcessDebuggingEnabled",
Policy,
BugCheckParameter2_4);
}
else if ( BugCheckParameter2_4 )
{
if ( BugCheckParameter2_4 == 1 )
{
editto1 = 1;
}
else if ( BugCheckParameter2_4 == 2 )
{
editto1 = (unsigned int)SkIsSecureBootEnabled() == 0x80430006;
}
else
{
editto1 = BYTE1(*v560) & 1;
}
}
if ( editto1 )
{
//editto1改成1开启调试
SkpsEnableDebugging((unsigned __int64)v560, a1[16]);
}
spin_loop:
pause ; <-- 关键指令
jmp spin_loop
ret
spin_loop:
pause ; <-- 关键指令
jmp spin_loop
ret
static UINT64 PcdTpmBaseAddress = 0xFED40000;
//入口TPM2_Sign
TPM_RC
CryptSign(OBJECT* signKey, // IN: signing key
TPMT_SIG_SCHEME* signScheme, // IN: sign scheme.
TPM2B_DIGEST* digest, // IN: The digest being signed
TPMT_SIGNATURE* signature // OUT: signature
)
{
signature->sigAlg = signScheme->scheme;
signature->signature.any.hashAlg = signScheme->details.any.hashAlg;
if(signKey->publicArea.type==TPM_ALG_KEYEDHASH)
{
static TPM_RC CryptHmacSign(TPMT_SIGNATURE* signature, // OUT: signature
OBJECT* signKey, // IN: HMAC key sign the hash
TPM2B_DIGEST* hashData // IN: hash to be signed
)
{
HMAC_STATE hmacState;
UINT32 digestSize;
//这行是补丁代码
if (signature->sigAlg == TPM_ALG_HMAC)
{
digestSize = CryptHmacStart2B(&hmacState,
signature->signature.any.hashAlg,
&signKey->sensitive.sensitive.bits.b);
CryptDigestUpdate2B(&hmacState.hashState, &hashData->b);
CryptHmacEnd(&hmacState, digestSize, (BYTE*)&signature->signature.hmac.digest);
return TPM_RC_SUCCESS;
}
return TPM_RC_SCHEME;
}
}
static UINT64 PcdTpmBaseAddress = 0xFED40000;
//入口TPM2_Sign
TPM_RC
CryptSign(OBJECT* signKey, // IN: signing key
TPMT_SIG_SCHEME* signScheme, // IN: sign scheme.
TPM2B_DIGEST* digest, // IN: The digest being signed
TPMT_SIGNATURE* signature // OUT: signature
)
{
signature->sigAlg = signScheme->scheme;
signature->signature.any.hashAlg = signScheme->details.any.hashAlg;
if(signKey->publicArea.type==TPM_ALG_KEYEDHASH)
{
static TPM_RC CryptHmacSign(TPMT_SIGNATURE* signature, // OUT: signature
OBJECT* signKey, // IN: HMAC key sign the hash
TPM2B_DIGEST* hashData // IN: hash to be signed
)
{
HMAC_STATE hmacState;
UINT32 digestSize;
//这行是补丁代码
if (signature->sigAlg == TPM_ALG_HMAC)
{
digestSize = CryptHmacStart2B(&hmacState,
signature->signature.any.hashAlg,
&signKey->sensitive.sensitive.bits.b);
CryptDigestUpdate2B(&hmacState.hashState, &hashData->b);
CryptHmacEnd(&hmacState, digestSize, (BYTE*)&signature->signature.hmac.digest);
return TPM_RC_SUCCESS;
}
return TPM_RC_SCHEME;
}
}
UINT16
BYTE_Array_Marshal(BYTE* source, BYTE** buffer, INT32* size, INT32 count)
{
if(buffer != 0)
{
if((size == 0) || ((*size -= count) >= 0))
{
memcpy(*buffer, source, count);
*buffer += count;
}
pAssert((size == 0) || (*size >= 0));
}
if(count >= INT16_MAX){
{
//poc位置
DebugBreak();
}}
return ((UINT16)count);
}
UINT16
TPM2B_ECC_PARAMETER_Marshal(TPM2B_ECC_PARAMETER* source, BYTE** buffer, INT32* size)
{
UINT16 result =
(UINT16)(result + UINT16_Marshal((UINT16*)&(source->t.size), buffer, size));
if(source->t.size == 0)
return result;
return (UINT16)(result
+ BYTE_Array_Marshal((BYTE*)&(source->t.buffer),
buffer,
size,
(INT32)source->t.size));
}
UINT16
TPMS_SIGNATURE_ECC_Marshal(TPMS_SIGNATURE_ECC* source, BYTE** buffer, INT32* size)
{
UINT16 result = (UINT16)(result
+ TPMI_ALG_HASH_Marshal(
(TPMI_ALG_HASH*)&(source->hash), buffer, size));
return = (UINT16)(result
+ TPM2B_ECC_PARAMETER_Marshal(
(TPM2B_ECC_PARAMETER*)&(source->signatureR), buffer, size));
...
}
UINT16
TPMU_SIGNATURE_Marshal(
TPMU_SIGNATURE* source, BYTE** buffer, INT32* size, UINT32 selector)
{s
switch(selector=signature->sigAlg)
{
//HMAC算法使用类型
case TPM_ALG_HMAC:
return TPMT_HA_Marshal((TPMT_HA*)&(source->hmac), buffer, size);
...
//对signature->sigAlg类型的混肴
case TPM_ALG_EDDSA:...
return TPMS_SIGNATURE_ECC_Marshal(
(TPMS_SIGNATURE_EDDSA*)&(source->eddsa), buffer, size);
case ..
}
UINT16
BYTE_Array_Marshal(BYTE* source, BYTE** buffer, INT32* size, INT32 count)
{
if(buffer != 0)
{
if((size == 0) || ((*size -= count) >= 0))
{
memcpy(*buffer, source, count);
*buffer += count;
}
pAssert((size == 0) || (*size >= 0));
}
if(count >= INT16_MAX){
{
//poc位置
DebugBreak();
}}
return ((UINT16)count);
}
[培训]传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2025-10-15 16:25
被王cb编辑
,原因: int3
上传的附件: