TL;DR
我的导师给我分配了这个任务:“CVE-2025-29824已被在野利用来攻击Windows机器 - 找出他们是如何做到的。哦,而且没有公开样本,所以你只能盲打。祝你好运!” 😅
事实证明,这是Windows通用日志文件系统(CLFS)驱动程序中的一个释放后使用漏洞。当日志文件句柄关闭时,FsContext2结构在CClfsRequest::Cleanup()中被错误释放,而另一个IRP请求可能仍在进行中。攻击者将此漏洞与Cisco ASA漏洞利用串联使用。
挑战在于:逆向工程分析其实际工作原理,理解微软的引用计数为何出错,并从头构建一个可工作的PoC。
剧透警告:这涉及比我最初预期的更多的汇编代码分析和崩溃转储分析!🔍💥
技术深度剖析
摘要
| 项目 |
详情 |
| 产品 |
Microsoft Windows |
| 厂商 |
Microsoft |
| 严重性 |
高 |
| 受影响版本 |
Windows 10-11, Windows Server 2008-2025 |
| 测试版本 |
Windows 11 23H2 |
| 影响 |
权限提升 |
| CVE ID |
CVE-2025-29824 |
| CWE |
CWE-416: 释放后使用 |
| PoC可用? |
是 |
| 补丁可用? |
是 |
| 漏洞利用可用? |
否 |
CVSSv3.1评分系统
基础分数:7.8(高)
向量字符串:CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
| 指标 |
值 |
| 攻击向量 (AV) |
本地 |
| 攻击复杂度 (AC) |
低 |
| 所需权限 (PR) |
低 |
| 用户交互 (UI) |
无 |
| 范围 (S) |
未改变 |
| 机密性影响 (C) |
高 |
| 完整性影响 (I) |
高 |
| 可用性影响 (A) |
高 |
微软如何修复该漏洞
补丁分析 - 前后对比
为了分析漏洞修复,我们比较了两个版本的clfs.sys:
补丁前:
- 版本:10.0.22621.5097
- SHA256:3FB7DA92269380F7BC72A1AD1B06D9BF1CB967AC3C5C7010504C841FCCC6108E
补丁后:
- 版本:10.0.22621.5192
- SHA256:969AC3B22CA9A22D73E8CC0BC98D9CF8A969B7867FAF2EE1093B0B0695D3439F
安全修复在CClfsLogCcb::Cleanup()和CClfsRequest::Close()函数中实现。
CClfsRequest::Cleanup()实现
清理过程遵循以下序列:
1
2
3
4
5
6
7
|
CClfsRequest::Cleanup(PIRP Irp){
...
CClfsLogCcb::AddRef(FsContext2);
CClfsLogCcb::Cleanup(FsContext2);
CClfsLogCcb::Release(FsContext2);
...
}
|
CClfsLogCcb::AddRef()对FsContext2引用计数器执行原子递增操作:
1
2
3
|
CClfsLogCcb::AddRef(FsContext2){
return _InterlockedIncrement(FsContext2->reference_count);
}
|
CClfsLogCcb::Release()原子递减FsContext2引用计数器,并在计数达到零时触发清理,调用析构函数CClfsLogCcb::~CClfsLogCcb()进行适当的资源释放:
1
2
3
4
5
6
7
8
|
CClfsLogCcb::Release(FsContext2){
refcount = _InterlockedDecrement(&FsContext2->reference_count);
if ( !refcount && FsContext2 ){
CClfsLogCcb::~CClfsLogCcb(FsContext2);
...
}
return refcount;
}
|
CClfsLogCcb::Cleanup()变化
补丁前实现:
1
2
3
4
5
|
CClfsLogCcb::Cleanup(FsContext2)
{
...
CClfsLogCcb::Release(FsContext2);
}
|
补丁后实现:
1
2
3
4
5
6
7
|
CClfsLogCcb::Cleanup(FsContext2)
{
...
if(!PatchFlag()){
CClfsLogCcb::Release(FsContext2);
}
}
|
此补丁使得CClfsLogCcb::Cleanup()不再调用CClfsLogCcb::Release(FsContext2)。这已被移至CClfsRequest::Close()中。
CClfsRequest::Close()变化
补丁前实现:
1
2
3
4
5
6
7
8
9
10
11
|
CClfsRequest::Close(PIRP Irp){
CClfsLogFcbCommon *ReservedContext;
...
boolean = ExAcquireResourceExclusiveLite(&ReservedContext->m_resLockFcb, 1u);
CClfsLogFcbCommon::Close(ReservedContext);
if (boolean){
ExReleaseResourceForThreadLite(&ReservedContext->m_resLockFcb, (ERESOURCE_THREAD)KeGetCurrentThread());
}
...
return 0;
}
|
补丁后实现:
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
|
CClfsRequest::Close(PIRP Irp){
CClfsLogFcbCommon *ReservedContext;
...
FileObject = CurrentStackLocation->FileObject;
if (PatchFlag()){
FsContext2 = (CClfsLogCcb *)CurrentStackLocation->FileObject->FsContext2;
v10 = FsContext2; //保存FsContext2
if (FsContext2){
CClfsLogCcb::AddRef(FsContext2); //递增FsContext2引用计数
CClfsLogCcb::Close(FsContext2); //递减FsContext2引用计数
}
}
...
boolean = ExAcquireResourceExclusiveLite(&ReservedContext->m_resLockFcb, 1u);
CClfsLogFcbCommon::Close(ReservedContext);
if (boolean){
ExReleaseResourceForThreadLite(&ReservedContext->m_resLockFcb, (ERESOURCE_THREAD)KeGetCurrentThread());
FsContext2 = v10; //加载FsContext2
}
...
if (PatchFlag()){
FileObject->FsContext = 0;
FileObject->FsContext2 = 0;
if (FsContext2){
CClfsLogCcb::Release(FsContext2);
}
}
...
return 0;
}
|
清理VS关闭
补丁修复漏洞的原因是将CClfsLogCcb::Release(FsContext2)从CClfsRequest::Cleanup()移至CClfsRequest::Close(),这两个函数分别通过控制代码IRP_MJ_CLEANUP和IRP_MJ_CLOSE调用。
IRP_MJ_CLEANUP在与目标设备对象关联的文件对象的最后一个用户模式句柄关闭时发送(但由于未完成的I/O请求,可能尚未释放)。
IRP_MJ_CLOSE在文件对象的最后一个引用被释放,且所有未完成的I/O请求已完成或取消时发送。
因此,在补丁后,当FsContext2在CClfsRequest::Close()中被释放时,应该没有其他请求正在使用它。这意味着应该没有利用它的机会。
FsContext2结构深度剖析
根据微软官方文档,FsContext2属于FILE_OBJECT结构,系统使用该结构表示文件对象。FsContext2是指向驱动程序为文件对象维护的任何附加状态的指针,否则为NULL。简而言之,FsContext2根据所表示的文件类型而不同。
通过一些逆向工程和参考他人完成的现有文档,在CLFS的情况下,FsContext2采用以下未记录的结构CClfsLogCcb形式。在可能的情况下,结构的成员已被识别并相应命名。
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
|
00000000 struct __unaligned __declspec(align(8)) CClfsLogCcb // sizeof=0x110
00000000 {
00000000 _BYTE gap0[8];
00000008 _LIST_ENTRY list_entry;
00000018 ULONG reference_count;
0000001C _DWORD flag;
00000020 _BYTE gap20[8];
00000028 unsigned int m_cArchiveRef;
0000002C _BYTE gap2C[4];
00000030 _QWORD m_hPhysicalHandle;
00000038 _QWORD qword38;
00000040 char *field_40;
00000048 PFILE_OBJECT m_foFileObj;
00000050 _QWORD m_foFileObj2;
00000058 CLFS_LSN clfs_lsn58;
00000060 CLFS_LSN clfs_lsn60;
00000068 _QWORD qword68;
00000070 __int64 field_70;
00000078 // 填充字节
00000080 CClfsBaseFileSnapshot *field_80;
00000088 _BYTE gap88[16];
00000098 ERESOURCE m_Resource;
00000100 struct CClfsLogCcb::ILifetimeListener *m_pltlLifeTimeListener;
00000108 _QWORD qword108;
00000110 };
|
这个0x110字节的结构包含CLFS日志文件句柄的所有状态信息。偏移量0x18处的reference_count和偏移量0x98处的m_Resource对于理解漏洞至关重要。
如何触发漏洞
触发漏洞
此处的漏洞在于CLFS驱动程序中清理和控制操作之间的不当同步。具体来说,存在以下竞争条件:
- CloseHandle() - 按顺序调用CClfsRequest::Cleanup()和CClfsRequest::Close(),释放FsContext2。
- DeviceIoControl() - 调用使用FsContext2的特定函数。
当用户以日志文件句柄作为参数调用CloseHandle()函数时,CClfsRequest::Cleanup()和CClfsRequest::Close()按该顺序调用。
DeviceIoControl()允许用户直接向指定的驱动程序发送控制代码,以调用与发送的控制代码对应的函数。在这种情况下,我们希望向CLFS驱动程序发送控制代码来触发漏洞。
寻找正确的函数
要找出我们想要调用的CLFS驱动程序函数,我们必须逆向所有可通过DeviceIoControl()访问的函数。因此,我们转向搜索CClfsRequest::Dispatch(),它负责处理发送到CLFS驱动程序的控制代码并调用相关函数。这里的目标是找到使用FsContext2的CClfsRequest函数。
第一步是理解数据如何在各种函数之间传递。我们发现CClfsRequest::Cleanup()、CClfsRequest::Close()和CClfsRequest::Dispatch()都将Irp数据结构作为参数,该结构表示I/O请求包。通过逆向CClfsRequest::Cleanup(),我们可以看到它们如何访问FsContext2。
1
2
3
4
5
6
7
8
|
CClfsRequest::Cleanup(PIRP Irp){
...
CurrentStackLocation = Irp->Tail.Overlay.CurrentStackLocation; //偏移量0x78h
...
//偏移量:FileObject (0x30h), FsContext2 (0x20h)
FsContext2 = CurrentStackLocation->FileObject->FsContext2;
...
}
|
现在我们查看CClfsRequest::Dispatch()内部,看看Irp是如何传递到这些函数中的。它存储在结构CClfsRequest的偏移量0x30h处,然后传递到这些函数中。
1
2
3
4
5
6
7
8
9
10
11
12
|
CClfsRequest::Dispatch(CClfsRequest *this, PIRP Irp, struct _DEVICE_OBJECT *a3){
this->m_pIrp = Irp; //偏移量0x30h
...
switch ( LowPart ){
case 0x8007A85C:
appended = CClfsRequest::AdvanceLogBase(this);
goto LABEL_9;
case 0x8007A810:
appended = CClfsRequest::SetArchiveTail(this);
goto LABEL_9;
...
}
|
我们正在寻找的函数示例:
1
2
3
4
|
CClfsRequest::UsefulFunction(this){
//偏移量:this->0x30->0x78->0x30->0x20
FsContext2 = this->m_pIrp->Tail.Overlay.CurrentStackLocation->FileObject->FsContext2;
}
|
发现的易受攻击函数
现在我们知道了FsContext2相对于传递到这些函数的参数的偏移量,我们查看每个这些函数以找到访问FsContext2的函数。
所有这些函数在其执行过程中的某个时刻加载并解引用FsContext2:
- CClfsRequest::ReserveAndAppendLog()
- CClfsRequest::WriteRestart()
- CClfsRequest::ReadArchiveMetadata()
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
|
__int64 __fastcall CClfsRequest::ReserveAndAppendLog(CClfsRequest *this){
...
Irp = this->m_pIrp; //偏移量0x30h
CurrentStackLocation = Irp->Tail.Overlay.CurrentStackLocation; //偏移量0x78h
...
FsContext2 = CurrentStackLocation->FileObject->FsContext2;
...
v29 = FsContext2[13]; // 潜在的FsContext2解引用
...
}
__int64 __fastcall CClfsRequest::WriteRestart(CClfsRequest *this){
...
Irp = this->m_pIrp; //偏移量0x30h
CurrentStackLocation = Irp->Tail.Overlay.CurrentStackLocation; //偏移量0x78h
...
this->m_Ccb = CurrentStackLocation->FileObject->FsContext2;
...
if ( this->field_F0 + this->m_Ccb->qword68 >= 0 ){ // 潜在的FsContext2解引用
...
}
__int64 __fastcall CClfsRequest::ReadArchiveMetadata(CClfsRequest *this){
...
Irp = this->m_pIrp; //偏移量0x30h
CurrentStackLocation = Irp->Tail.Overlay.CurrentStackLocation; //偏移量0x78h
...
FsContext2 = CurrentStackLocation->FileObject->FsContext2;
...
CClfsLogCcb::ReadArchiveMetadata(FsContext2, ...);
...
if ( FsContext2 ){
CClfsLogCcb::Release(FsContext2);
}
}
__int64 __fastcall CClfsLogCcb::ReadArchiveMetadata(FsContext2, ...){
...
p_m_Resource = &FsContext2->m_Resource; // 潜在的FsContext2解引用
...
if ( FsContext2->m_cArchiveRef ) // 潜在的FsContext2解引用
...
}
|
攻击向量
这些函数可用于解引用FsContext2并触发漏洞:
- CClfsRequest::ReserveAndAppendLog()可通过调用用户函数ReserveAndAppendLog()或直接通过DeviceIoControl()发送控制代码0x8007A827来访问。
- CClfsRequest::WriteRestart()可通过调用用户函数WriteLogRestartArea()或直接通过DeviceIoControl()发送控制代码0x8007281F来访问。
- CClfsRequest::ReadArchiveMetadata()可通过通过DeviceIoControl()发送控制代码0x80076856来访问。
ReserveAndAppendLog()和WriteLogRestartArea()都需要额外设置一个称为Marshal的单独变量,该变量将作为参数传递到这些函数中,而CClfsRequest::ReadArchiveMetadata()可以通过DeviceIoControl()调用,无需设置任何额外变量。因此,由于简单易用,CClfsRequest::ReadArchiveMetadata()在概念验证中被使用。
逐步利用
为了触发此漏洞,可以执行以下操作:
- 通过调用CreateLogFile()获取日志文件的文件句柄。
- 创建2个线程,将文件句柄传递给这两个线程。
- 线程1将对文件句柄调用CloseHandle(),从而告诉CLFS驱动程序执行CClfsRequest::Cleanup()和CClfsRequest::Close()。
- 线程2将对文件句柄调用ReserveAndAppendLog()、WriteLogRestartArea()或DeviceIoControl()。
- 如果时机恰到好处,FsContext2将在线程1释放后被线程2解引用,导致释放后使用漏洞。
概念验证
概念验证的一般思想是创建2个线程,并尝试引发竞争条件,使CloseHandle()和CClfsRequest::ReadArchiveMetadata()同时执行,从而由于释放后使用漏洞导致崩溃。
概念验证有很高的可能性触发此漏洞并导致崩溃,从而在ntoskrnl.exe中产生IRQL_NOT_LESS_OR_EQUAL蓝屏错误。此错误是内存相关错误,如果系统进程或驱动程序尝试访问不正确或损坏的指针,则会出现此错误。
崩溃上下文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# Child-SP RetAddr Call Site
00 fffffa83`f58c9a38 fffff802`1e9668e2 nt!DbgBreakPointWithStatus
01 fffffa83`f58c9a40 fffff802`1e965fa3 nt!KiBugCheckDebugBreak+0x12
02 fffffa83`f58c9aa0 fffff802`1e816c07 nt!KeBugCheck2+0xba3
03 fffffa83`f58ca210 fffff802`1e82c4e9 nt!KeBugCheckEx+0x107
04 fffffa83`f58ca250 fffff802`1e827a34 nt!KiBugCheckDispatch+0x69
05 fffffa83`f58ca390 fffff802`1e6fc805 nt!KiPageFault+0x474
06 fffffa83`f58ca520 fffff802`1c348b85 nt!ExDeleteResourceLite+0x125
07 fffffa83`f58ca570 fffff802`1c347ee2 CLFS!CClfsLogCcb::~CClfsLogCcb+0x31
08 fffffa83`f58ca5b0 fffff802`1c36290c CLFS!CClfsLogCcb::Release+0x32
09 fffffa83`f58ca5e0 fffff802`1c3485b5 CLFS!CClfsRequest::ReadArchiveMetadata+0x164
0a fffffa83`f58ca650 fffff802`1c34785e CLFS!CClfsRequest::Dispatch+0x369
0b fffffa83`f58ca6a0 fffff802`1c3477a7 CLFS!ClfsDispatchIoRequest+0x8e
0c fffffa83`f58ca6f0 fffff802`1e6ebef5 CLFS!CClfsDriver::LogIoDispatch+0x27
0d fffffa83`f58ca720 fffff802`1eb40060 nt!IofCallDriver+0x55
0e fffffa83`f58ca760 fffff802`1eb41a90 nt!IopSynchronousServiceTail+0x1d0
0f fffffa83`f58ca810 fffff802`1eb41376 nt!IopXxxControlFile+0x700
10 fffffa83`f58caa00 fffff802`1e82bbe5 nt!NtDeviceIoControlFile+0x56
11 fffffa83`f58caa70 00007ffb`add2f454 nt!KiSystemServiceCopyEnd+0x25
12 000000e9`084ff678 00007ffb`ab34664b 0x7ffbadd2f454
13 000000e9`084ff680 00000000`00000000 0x7ffbab34664b
|
在概念验证的上下文中,Release(FsContext2)正被CClfsLogCcb::ReadArchiveMetadata()调用。然而,由于CClfsRequest::Cleanup()调用了Release(FsContext2),FsContext2此时已被释放,从而导致无效的内存访问。
变体分析
现在我们知道在Cleanup()中永久释放东西是危险的,因为其他IOCTL仍然可以被分派,我们简要探索在CClfsRequest::Cleanup()中是否有任何其他对象被释放或字段被修改。
没有其他字段或对象被释放,因为CClfsRequest::Cleanup()内部对CClfsLogCcb::Release()的唯一调用是针对FsContext2的。
在CClfsRequest::Cleanup()内部可能被修改的字段或对象如下:
- FsContext2->flag (偏移量0x1C)
- FsContext2->gap20[4] (偏移量0x20)
- FsContext2->m_pltlLifeTimeListener (偏移量0x100)
增强的检测策略
此漏洞的检测点包括:
- 创建扩展名为.blf的未知日志文件
- 监控系统事件ID 11以查找可疑活动
建议的缓解措施
- 下载并应用2025年4月8日及以后的Microsoft更新
- 您可以在此处查看PoC
参考资料