【二进制漏洞-Hyper-v虚拟磁盘驱动vhdmp.sys漏洞汇总分析】此文章归类为:二进制漏洞。
这篇文章的目的是介绍hyper-v虚拟磁盘驱动vhdmp.sys相关的漏洞CVE-2023-36408,CVE-2025-24048和CVE-2025-24050汇总分析.
文章结合了逆向代码和调试结果分析了hyper-v虚拟磁盘驱动vhdmp.sys相关的漏洞的利用过程和漏洞成因.
Windows 11 24h2 canary preview Built by: 27764 .1000 支持smb quic模式
Windows 10 22h2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
struct Rct::Header { __int64 OffsetTableize_0;
__int64 field_8;
char NewRctId_10;
char field_11[ 7 ];
Rct:: Format ::BranchHistoryEntry * rawbuf_18;
int headerlen_20;
Rct::RctId rctid_24;
Rct::RctId RctId_3c;
__int64 field_54;
_OWORD rctguid_5c;
int field_6c;
__int64 field_70;
}; void __fastcall Rct::Header::GenerateFormattedHeader(Rct::Header * this, struct Rct:: Format ::Header * a2)
{ / /
memmove((char * )a2 + 0x1000 , (const void * )this - >rawbuf_18, 24i64 * (unsigned int )this - >headerlen_20);
} __int64 __fastcall Rct::Header::InitializeFromFormattedFullHeader(Rct::Header * this, struct Rct:: Format ::FullHeader * a2, enum Rct:: Format ::FailureReason * a3, unsigned int a4)
{ __int64 rawbuf_18 = Rct::AllocateBranchHistory(
(Rct * ) * (unsigned __int16 * )((char * )a2 + v11 + 4164 ),
(char * )a2 + v11 + 0x2000 ,
(struct Rct:: Format ::BranchHistoryEntry * ) * (unsigned __int16 * )((char * )a2 + v11 + 0x1044 );
/ / memory pool allocation is alloced with user controled length Rct::Header - >headerlen_20 from test.vhdx.rct file with offset 0x1044
this - >headerlen_20 = * (unsigned __int16 * )((char * )a2 + v11 + 0x1044 );
this - >rawbuf_18 = (__int64)rawbuf_18;
} |
以上漏洞代码显示,vhdmp.sys驱动在初始化虚拟磁盘相关对象时,会调用为Rct::Head->rawbuf_18内存池分配由test.vhdx.Rct文件定义的长度大小的内存区域,
这个长度由用户通过文件内容控制,rct文件存在2个冗余的Rct::Headr结构体每个长度为0x4000,第一个起始位置在文件偏移量0x1000,每个结构体长度字段所在偏移量为0x1044,
当其中一个验证无效时采用备用结构体替代,这个结构体内存池长度字段可以是用户控制的Uint16任意值,但是最终写入目标缓冲区的也是默认Rct::Headr的长度只有0x4000字节,
存在越界写入,覆盖此缓冲区将导致下一个内存池头损坏,访问此缓冲区时会在后续io操作写入不可预测的位置。
触发漏洞需要使用使用VirtDisk.dll的api创建具有预定义磁盘大小的.vhdx文件调用CreateVirtualDisk函数,并使用SetVirtualDiskInformation和参数SetVirtualDiskInfo设置ChangeTrackingEnabled设置为true,
启用Hyper-V虚拟磁盘的弹性更改跟踪Resilient Change Tracking功能,创建关联的.rct文件.配置完成后,关闭相关虚拟磁盘对象.之后操作打开rct文件修改数据偏移量在0x1044,
将rct::Header->headerlen_20更改为大于默认值的值。
最后,打开虚拟磁盘文件,调用设置较大磁盘大小的ExpandVirtualDisk扩展磁盘函数,将触发Rct::HeaderUpdateOperation::WriteHeader更新写入这个结构体漏洞函数,进行的内存拷贝操作会导致越界写入。
在调试过程中,操作系统内核崩溃显示了以下分析结果。可见篡改后的拷贝内存池长度0xffff大于目标内存池大小.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
8 : kd> bp vhdmp!Rct::Header::GenerateFormattedHeader
4 : kd> r
rax = ffffc986cb6edb30 rbx = 0000000000000001 rcx = ffffc986cb6edb30
rdx = ffffc986cb6f2000 rsi = ffffad867a6af430 rdi = ffffc986cb6eda88
rip = fffff80148642380 rsp = ffffad867a6af2d8 rbp = ffffad867a6af430
r8 = 0000000000000000 r9 = 0000000000000800 r10 = ffffa70000000028
r11 = 0000007ffffffff8 r12 = ffffc986cb6edb30 r13 = 0000000000000000
r14 = ffffad867a6af470 r15 = ffffc986cb6ed790
iopl = 0 nv up ei pl nz na pe nc
cs = 0010 ss = 0018 ds = 002b es = 002b fs = 0053 gs = 002b efl = 00040202
vhdmp!Rct::Header::GenerateFormattedHeader: fffff801` 48642380 48895c2408 mov qword ptr [rsp + 8 ],rbx ss: 0018 :ffffad86` 7a6af2e0 = ffffc98600000000
4 : kd> dq rcx
ffffc986`cb6edb30 00000000 ` 00001900 00000000 ` 00000002
ffffc986`cb6edb40 000038a8 `ffff0001 ffffc986`c08e0000
/ / Rct::Header - >headerlen_20 is set to 0xffff
ffffc986`cb6edb50 a613ae4c` 0000ffff 6d79c69c ` 4c7489a1
ffffc986`cb6edb60 00000000 ` 6feb42b7 64ccccbf ` 00000000
ffffc986`cb6edb70 bed8ec9d` 47f453e2 00000000 ` 8d25f7c2
ffffc986`cb6edb80 00000000 ` 00000000 6d0a30e0 ` 00000000
ffffc986`cb6edb90 10570d84 ` 455d901c 00000001 `dd6d5130
ffffc986`cb6edba0 00000000 ` 00000000 6d0a30e0 ` 00000000
4 : kd> kv
# Child-SP RetAddr : Args to Child : Call Site
00 ffffad86` 7a6af2d8 fffff801` 48642fb9 : ffffc986` 00000000 8a000001 ` 6c6009e3 00000000 ` 00000000 fffff164`c365b7b0 : vhdmp!Rct::Header::GenerateFormattedHeader
01 ffffad86` 7a6af2e0 fffff801` 486421ed : 00000000 ` 00000103 ffffad86` 7a6af3c8 ffffad86` 7a6af3c8 ffffc986`cb6eda88 : vhdmp!Rct::HeaderUpdateOperation::WriteHeader + 0x29
02 ffffad86` 7a6af310 fffff801` 486324e6 : ffffc986`cb6eda88 ffffad86` 7a6af450 ffffc986`cb6eda10 ffffc986`cb6ed0d0 : vhdmp!Rct::HeaderUpdateOperation::Execute + 0xbd
03 ffffad86` 7a6af350 fffff801` 48633d8a : 00000000 ` 00000000 00000000 ` 00001940 ffffc986`cb6ed0d0 ffffc986`cb6edb30 : vhdmp!VhdmpiUpdateRctHeader + 0xb2
04 ffffad86` 7a6af560 fffff801` 486986d6 : 00000202 ` 002bda80 00000000 ` 00000000 ffffc986`cafa88f0 00000000 ` 00000000 : vhdmp!VhdmpiResizeRctFile + 0x106
05 ffffad86` 7a6af750 fffff801` 4869759a : ffffc986`cc061080 ffffc986`cc3d8300 ffffc986`cc061080 ffffb500`b46ce000 : vhdmp!VhdmpiVhd2ResizeBackingStoreInternal + 0xde
06 ffffad86` 7a6afa70 fffff801` 48674249 : ffffc986`cc061080 ffffc986`cc3d8300 ffffc986`cacbfa90 ffffc986`cafa8890 : vhdmp!VhdmpiVhd2ExpandThread + 0x14a
07 ffffad86` 7a6afad0 fffff801` 2a5268f5 : ffffc986`cc061080 00000000 ` 00000080 fffff801` 486741f0 00000000 ` 00000000 : vhdmp!VhdmpiAsyncOpThread + 0x59
08 ffffad86` 7a6afb10 fffff801` 2a604c68 : ffffb500`b46c0180 ffffc986`cc061080 fffff801` 2a5268a0 00000000 ` 00000000 : nt!PspSystemThreadStartup + 0x55
09 ffffad86` 7a6afb60 00000000 ` 00000000 : ffffad86` 7a6b0000 ffffad86` 7a6a9000 00000000 ` 00000000 00000000 ` 00000000 : nt!KiStartSystemThread + 0x28
the write destination buffer is only have 0x4000 bytes legth,overwrite this buf will cause corrupt next pool header
4 : kd> !pool rdx
Pool page ffffc986cb6f2000 region is Nonpaged pool
* ffffc986cb6f2000 : large page allocation, tag is V2lg, size is 0x4000 bytes
Pooltag V2lg : VHD2 core large allocation, Binary : vhdmp.sys
|
补丁后修复代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
__int64 __fastcall Rct::ValidateHeader(_DWORD * a1)
{ / / Rct::Header - >headerlen_20 偏移量为 0x1044 加了验证 0xffff
entrylen = * (buf + 0x44 );
if ( (entrylen - 1 ) < = 0x1FEu
&& (v6 = * (buf + 0x30 ),
LOBYTE(v3) = * (buf + 0x46 ) ! = 0 ,
v7.idxlow_10 = * (buf + 0x40 ),
v7.Low_0 = v6,
Rct::ValidateBranchHistory((buf + 0x1000 ), entrylen, &v7, v3)) )
{
return = 0i64 ;
}
return = 6i64 ;
} |
补丁后修复可见对偏移量为0x1044的Uint16结构体长度多了判断 (entrylen - 1) <= 0x1FE超过目标长度就返回错误从而导致对应的结构体未通过完整性验证,采用备用结构体替代.
VirtDisk.dll的api创建CreateVirtualDisk创建vhdx虚拟磁盘存在4个版本的不同类型,v1和v2是微软文档化的,v1仅支持固定大小的虚拟磁盘,v2支持基于父虚拟磁盘的差异磁盘,
从v3开始支持基于差异磁盘的追踪控制ResiliencySourceLimit查询功能的磁盘创建,这种类型并未微软文档化但是结构体已经公开,笔者通过逆向找到了成功创建的方法.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
[StructLayout(LayoutKind.Sequential, Pack = 1 )]
public struct RCTID { public RCTID( bool newobj, Guid Lowval, uint Highval)
{
Guid Low_0 = Lowval;
uint High_10 = Highval;
uint Limit_14 = 0 ;
}
} public static SafeFileHandle CreateVHDChain(string vhd_path_child, string vhd_path_parent, string vhd_path_source, bool parentdisk, ulong DefaultDiskSizeNext, RCTID newrctid)
{ VIRTUAL_STORAGE_TYPE vhd_type = new VIRTUAL_STORAGE_TYPE();
vhd_type.DeviceId = StorageDeviceType.Vhdx;
vhd_type.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT;
CreateVirtualDiskParameters ps = new CreateVirtualDiskParameters();
ps.SectorSizeInBytes = 512 ;
ps.MaximumSize = DefaultDiskSize;
CreateVirtualDiskFlag Flags = CreateVirtualDiskFlag.UseChangeTrackingSourceLimit;
ps.Version = CreateVirtualDiskVersion.Version3;
ps.ParentPath = vhd_path_parent;
ps.SourcePath = vhd_path_source;
string SourceLimitPath = newrctid.ToString();
ps.SourceLimitPath = SourceLimitPath;
ps.UniqueId = Guid.NewGuid();
int error = CreateVirtualDisk(ref vhd_type, vhd_path_child, VirtualDiskAccessMask. None , null,
Flags, 0 , ref ps, IntPtr.Zero, out IntPtr hDisk);
return new SafeFileHandle(hDisk, true);
} void __fastcall Rct::SequenceTable::ReadCompleteForQuery(struct AE_ENVIRON * a1, RctQueryRecursiveOperation * opref, struct AE_TODO * a3)
{ while ( startidx < diff3fdminuscalc )
{
pagbufvalptr = pagebuf + 4 * startidx + 4 * startoffrefless3FD;
currentoffsetbase0x1 = 1 ;
for ( j = * (pagbufvalptr + 3 ); currentoffsetbase0x1 < diff3fdminuscalc - startidx; + + currentoffsetbase0x1 )
{
if ( * &pagbufvalptr[ 4 * currentoffsetbase0x1 + 0xC ] ! = j )
break ;
}
ActiveSequenceNumberref = seqtbl - >ActiveSequenceNumber_140;
if ( j < = ActiveSequenceNumberref )
ActiveSequenceNumberref = j;
/ / fffff805` 5038cb90 48895c2410 mov qword ptr [rsp + 10h ],rbx
/ / 2 : kd> k
/ / # Child-SP RetAddr Call Site
/ / 00 fffff609`b9e0e878 fffff805` 5039c31a vhdmp!VhdmpiRctQueryRecursiveCallback
/ / 01 fffff609`b9e0e880 fffff805` 5034a6ed vhdmp!Rct::SequenceTable::ReadCompleteForQuery + 0x2ba
/ / 02 fffff609`b9e0e910 fffff805` 5038caee vhdmp!AeProcessTodo + 0x7d
/ / 03 fffff609`b9e0e970 fffff805` 5038e30a vhdmp!VhdmpiRctQueryRecursive + 0x10a
/ / 04 fffff609`b9e0ea50 fffff805` 503ed0ca vhdmp!VhdmpiQueryChangesSinceRctId + 0x1c2
/ / 05 fffff609`b9e0ec40 fffff805` 503ed3bf vhdmp!VhdmpiReadChangedData + 0x8e
/ / 06 fffff609`b9e0ee80 fffff805` 503ed2bb vhdmp!VhdmpiReadFromSource + 0x47
/ / 07 fffff609`b9e0eec0 fffff805` 503fde09 vhdmp!VhdmpiPopulateVirtualDiskFromSource + 0x11b
/ / 08 fffff609`b9e0f060 fffff805` 5042b0ed vhdmp!VhdmpiVhd2FinalizeNewBackingStoreFile + 0xd9
/ / 09 fffff609`b9e0f160 fffff805` 504249fb vhdmp!VhdmpiCreateNewVhd + 0xfc5
/ / 0a fffff609`b9e0f3d0 fffff805` 50426d18 vhdmp!VhdmpiCreateThread + 0x19b
/ / 0b fffff609`b9e0f470 fffff805`a3a5666a vhdmp!VhdmpiAsyncOpThread + 0x58
/ / 0c fffff609`b9e0f4b0 fffff805`a3c74de4 nt!PspSystemThreadStartup + 0x5a
/ / 0d fffff609`b9e0f500 00000000 ` 00000000 nt!KiStartSystemThread + 0x34
if ( !(opref - >RctQueryRecursiveCallback_78)(
opref,
startidx + * startoffsetptr,
currentoffsetbase0x1,
ActiveSequenceNumberref) )
{
break ;
}
}
Rct::SequenceTable::ContinueQuery(seqtbl, opref, a3);
} |
创建v3的虚拟磁盘api需要使用到一个称为RCTID的一共0x18字节大小结构体用于表示虚拟磁盘的差异追踪信息,当子磁盘创建时会去递归查找父虚拟磁盘的差异信息,
需要把在v3结构体定义的字符串化的SourceLimitPath也是RCTID字段作为差异查找结束位置,成功操作后会把找到的匹配差异信息对应的磁盘数据从父磁盘拷贝的子磁盘,
差异查找的具体实现逻辑在VhdmpiRctQueryRecursive函数中,首先比较Rct::Header的RctId是否与SourceLimitPath匹配,而差异的具体信息存在于.rct文件的Rct::SequenceTable结构体中通过一个SequenceNumber表示差异索引,
Rct::SequenceTable在rct文件的偏移量0xa000开始分配,每个页面为0x400大小内容为4个字节的SequenceNumber头部和Rct::Header一样存在crc校验,
且存在一个转换函数Rct::Header::RctIdToSequenceNumber将SequenceNumber转换为RctId,
最终将小于目标SourceLimitPath的RctId值的SequenceNumber对应的实际数据在虚拟磁盘偏移位置和长度作为查找结果,完成后拷贝数据.通过对vhdmp.sys的逆向发现,
这个漏洞的成因来源于一个类似于RctId查找实现MirrorVirtualDisk也是调用VhdmpiRctQueryRecursive查找2个虚拟磁盘相同位置差异追踪信息,用于拷贝镜像磁盘文件.
这个虚拟磁盘api MirrorVirtualDisk需要用于两个相同预定义大小的虚拟磁盘的进行镜像操作,
可以在镜像前使用参数SetVirtualDiskInfo的api创建与虚拟磁盘关联的.rct文件。
镜像操作将同样同步两个相同大小的虚拟磁盘内容和弹性差异追踪信息。
如果.rct文存在镜像操作通过调用VhdmpiPopulateRctFiles函数直接读取主虚拟磁盘rct文件。
但是这里并不使用第二个虚拟磁盘rct信息而是调用truncate重置rct文件并将用主rct文件内容重写为相同内容,
此时两个rct标头结构都是相同的rct::header内容,分配的相关目标结构体缓冲区大小都是相同的,而且第二个虚拟磁盘rct是由内核以独占方式打开的,用户层无法篡改.
但是这里存在一个例外情况,vhdx虚拟磁盘允许以smb共享文件路径创建,而文件的内容是完全由smb共享提供者控制的,即使文件以独占访问打开,smb共享提供者仍然可以篡改文件及其内容.
在win11及以后的操作系统允许以smb quic的协议方式绑定本地smb文件共享端口,可以使用net.exe use \\yourip\vhdx /TRANSPORT:QUIC /SKIPCERTCHECK这样的命令创建一个smb挂载路径,
然后从这个挂载路径创建虚拟磁盘对象,笔者开源了smb quic模式文件存储后端实现工程,使用这个工具为我们提供了一个新的攻击面,在不需要借助远程计算机创建smb共享的方式下就可以实现本地重放所有vhdx虚拟磁盘操作的所有数据包,并控制内容.
第二个rct文件在truncate操作被写入主rct文件相同的数据,但之后的读取操作可以通过smb quic模式进行篡改,一旦对第二个rct文件调用Vhdmp内核函数PopulateRcTFiles,
漏洞代码显示rct:∶SequenceTable->OffsetTableBuf_40=ExAllocatePool2(0x40i64,filesizecalc,'ms2V')内存池分配使用第二个rct文件用户控制的Rct::Header->OffsetTableize_0长度进行分配,
计算公式为rct::header->OffsetTableize_0/0x3FD*8,这个rct::header来自第二个test.rct文件,偏移量为0x1028,原始值与主rct文件中的值相同,
但是这个值可以被篡改为一个非常小的值来创建较小的内存池分配,如果大小与主.rct文件值不匹配,镜像操作中并没有检查。
VhdmpiPopulateRctFiles函数用于从主.Rct文件中读取OffsetTable开始偏移量0x9000后大小为0x1000的每个Rct::OffsetEntry,
如果Rct:∶OffsetEntry标头不为空且有效,则偏移量0xc的值作为4字节长度,bit的31位将确定是否为SequenceTable。
如果该位已设置或为空SequenceTable页,则每个SequenceTable页条目将保存在Rct::SequenceTable->OffsetTableBuf_40缓冲区中,如果该位未设置则不保存。
在之后的镜像操作VhdmpiCopyRctInformation函数将搜索的SequenceTable条目是否为空页,否则同步每个页所在io Segment数据偏移量和大小的数据段,并通过拷贝io镜像磁盘数据。
在这里MirrorVirtualDisk会调用刚才说的VhdmpiCopyRctInformation函数也是从另一种回调实现VhdmpiRctQueryForCopyCallback调用VhdmpiRctQueryRecursive查找2个虚拟磁盘相同位置差异追踪信息.
如果在查找之前目标Rct::SequenceTable时不存在符合验证的页面,调用Rct::SequenceTable::ContinueQuery函数,
将使用默认的rct元数据的Rct::Header->ActiveSequenceNumber作为内容填充整个SequenceTable页面,当然这个SequenceNumber是符合要求的,
这个过程将在回调返回一个Rct::SequenceTable::Range,其中包含io Segment也就是所在磁盘文件偏移量和大小,
以便后续调用Rcts::SequenceTables::Update.这里如果OffsetTableize未被篡改,则Rct:的SequenceTable->OffsetTableBuf_40中的OffsetTable条目中存在符合条件要更新的io Segment,
有足够的条目用于搜索SequenceTable;但是如果内存池分配被篡改的则分配的大小非常小。这将导致完成搜索后在Rct::SequenceTable::IsUpdatePresentInOffsetTable函数中,
在这里引用超出边界的数据尝试获取包含一个LIST_ENTRY链接结构体,并判断是否为有效的内存引用取消链接这个链接结构体,
如果超出边界的条目的内存池分配无效,或者不包含符合条件的链接结构体,则产生内存破坏导致操作系统内核崩溃,
调试过程中复现以下分析结果。如果超出边界位置这个链接结构体使用堆喷由用户控制,则链接结构体值可能会被取消链接,从而导致可能的特权提升漏洞。但实际利用条件比较苛刻.
更多【二进制漏洞-Hyper-v虚拟磁盘驱动vhdmp.sys漏洞汇总分析】相关视频教程:www.yxfzedu.com