VirtDisk.dll的api创建CreateVirtualDisk创建vhdx虚拟磁盘存在4个版本的不同类型,v1和v2是微软文档化的,v1仅支持固定大小的虚拟磁盘,v2支持基于父虚拟磁盘的差异磁盘,从v3开始支持基于差异磁盘的追踪控制ResiliencySourceLimit查询功能的磁盘创建,这种类型并未微软文档化但是结构体已经公开,笔者通过逆向找到了成功创建的方法.
[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);
}
1、创建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 \youripvhdx /TRANSPORT:QUIC /SKIPCERTCHECK这样的命令创建一个smb挂载路径,然后从这个挂载路径创建虚拟磁盘对象,笔者开源了smb quic模式文件存储后端实现工程,使用这个工具为我们提供了一个新的攻击面,在不需要借助远程计算机创建smb共享的方式下就可以实现本地重放所有vhdx虚拟磁盘操作的所有数据包,并控制内容.
2、第二个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链接结构体,并判断是否为有效的内存引用取消链接这个链接结构体,如果超出边界的条目的内存池分配无效,或者不包含符合条件的链接结构体,则产生内存破坏导致操作系统内核崩溃,调试过程中复现以下分析结果。如果超出边界位置这个链接结构体使用堆喷由用户控制,则链接结构体值可能会被取消链接,从而导致可能的特权提升漏洞。但实际利用条件比较苛刻.
char __fastcall Rct::SequenceTable::IsUpdatePresentInOffsetTable(Rct::SequenceTable *this, unsigned __int64 offset, __int64 len, int seq)
{
v4 = offset + len;
idxlow = offset / 0x3FD;
idxhigh = (offset + len - 1) / 0x3FD + 1;
while ( idxlow < idxhigh )
{
fid = *&this->OffsetTableBuf_40[2 * idxlow].value_0;
if ( __CFSHR__(fid, 2) )
fidcfshr = fid >> 32;
else
LODWORD(fidcfshr) = -HIDWORD(fid[1].Blink);
if ( fidcfshr != seq )
{
v11 = 0i64;
LODWORD(v12) = 1021;
if ( __CFSHR__(fid, 2) )
return 0;
if ( !fid )
return 0;
v13 = fid->Flink;
// out of out of boundary write link entry
if ( v13->Blink != fid
|| (v14 = fid->Blink, v14->Flink != fid)
|| (v14->Flink = v13,
v13->Blink = v14,
v15 = this->PageCachelinklist_80,
v15->Flink != &this->PageCachelinklist_78) )
{
__fastfail(3u);
}
}
__int64 __fastcall Rct::SequenceTable::ConsumeOffsetTable(Rct::SequenceTable *this, __int64 OffsetTableize, struct Rct::Format::SequenceOffsetTablePage *rawbuf, ULONG a4, unsigned int ActiveSequenceNumber)
{
//OffsetTableize in testchild.vhdx.rct with offset 0x1028, is very small
int filesizetrim =OffsetTableize / 0x3FD
int filesizecalc = 8i64 * filesizetrim;
// OffsetTableBuf_40 will be very small tampered by smb
this->OffsetTableBuf_40 =ExAllocatePool2(0x40i64, filesizecalc, 'ms2V');
}
void __fastcall Rct::SequenceTable::ContinueQuery(Rct::SequenceTable *this, RctQueryRecursiveOperation *op, struct AE_TODO *a3)
{
__int64 startoffset = op->startoffsetshr4page_68;
cacheidx = startoffset / 0x3FD;
cahcheentry = *&this->OffsetTableBuf_40[2 * cacheidx].value_0;
if ( (cahcheentry & 1) != 0 )
{
v12 = this->pagerawbuf_20;
v13 = 0x3FDi64;
ACTSEQ = HIDWORD(cahcheentry);
v16 = v12 + 3;
while ( v13 )
{
*v16++ = ActiveSequenceNumber;
--v13;
}
*v12 = cacheidx;
op->completerouting_10 = Rct::SequenceTable::ReadCompleteForQuery;
void __fastcall Rct::SequenceTable::ReadCompleteForQuery(struct AE_ENVIRON *a1, RctQueryRecursiveOperation *opref, struct AE_TODO *a3)
{
AeUseAndPushCompletion(this->UseCompletiontimer_0, a3, op);
//call VhdmpiRctQueryForCopyCallback
if ( !(opref->RctQueryRecursiveCallback_78)(
opref,
startidx + *opref->startoffsetshr4page_68,
fidlen,
ActiveSequenceNumberref) )
}
char __fastcall VhdmpiRctQueryForCopyCallback(struct VHD_RCT_STATE *a1, struct RctQueryRecursiveOperation *a2, __int64 nextoffset, unsigned int len, unsigned int actseq)
{
struct Rct::SequenceTable::Range *rng;
rng->rangebuf[idx].offset_8 = nextoffset;
rng->rangebuf[idx].len_10 = fidlen;
}
v12 = Rct::SequenceTable::Update(&mmiorrct->seqtbl_a88, v25, rng, opcount, 0, v22, v16);
Rct::SequenceTable::IsUpdatePresentInOffsetTable(
this,
rng[idxfrom0].offset_8,
rng[idxfrom0].len_10,
rng[idxfrom0].rctidseq_0)
}
}
原文始发于微信公众号(富贵安全):cve原创漏洞挖掘
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论