我的CVE-2025-29824"盲测"之旅 | STAR Labs
首页 » 博客
我的CVE-2025-29824"盲测"之旅
2025年7月16日 · 10分钟阅读 · Ong How Chong
目录
- TL;DR
- 漏洞技术深度分析
- 摘要
- CVSSv3.1评分系统
- 微软如何修复该漏洞
- 补丁分析 - 前后对比
- CClfsRequest::Cleanup()实现
- CClfsLogCcb::Cleanup()变更
- CClfsRequest::Close()变更
- Cleanup与Close对比
- FsContext2结构深度解析
- 如何触发漏洞
- 触发漏洞
- 寻找正确函数
- 发现的易受攻击函数
- 攻击向量
- 分步利用过程
- 概念验证
- 崩溃上下文
- 变体分析
- 增强检测策略
- 建议缓解措施
- 参考文献
2025年4月,微软修补了一个已成为复杂勒索软件攻击链关键组件的漏洞。CVE-2025-29824是Windows通用日志文件系统(CLFS)驱动程序中的一个释放后使用(use-after-free)漏洞,并非攻击者的初始入口点。相反,威胁行为者首先入侵Cisco ASA防火墙,然后使用此Windows内核漏洞作为关键权限提升步骤,将有限的网络访问转变为完全的系统控制。这种多阶段方法代表了现代勒索软件操作的演变:复杂威胁行为者将网络基础设施漏洞与Windows内核漏洞串联起来,造成毁灭性影响。
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;
}
|
Cleanup与Close对比
补丁修复漏洞的原因是将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
|
__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解引用
...
}
|
1
2
3
4
5
6
7
8
9
10
|
__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解引用
...
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
__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解引用,导致释放