Win32k Type Confusion Vulnerability Analysis Guide
0. Preface
As we all know, win32k has contributed significantly to Windows privilege escalation vulnerabilities in recent years. I have always wanted to understand the principles of these vulnerabilities and read many related papers for this purpose. However, as a novice in kernel vulnerability research, the write-ups on win32k vulnerabilities written by experts still looked like gibberish to me.
Of course, I know what UAF (Use-After-Free) and type confusion are, and I also know that many win32k vulnerabilities are caused by callback functions. However, when all these concepts are combined together, along with technical jargon and extremely long function names, it becomes a completely different story.
Therefore, through analyzing two vulnerabilities, CVE-2021-1732 and CVE-2022-21881, I would like to review the confusion I encountered when facing such vulnerabilities and try to resolve them. This way, when encountering win32k vulnerabilities in the future, I can target the problems directly and solve them quickly.
1. Basic Knowledge
1.1 Infrastructure
Before learning about win32k, my understanding of system calls was simply that an application calls a function in a DLL to achieve some system functionality. If kernel involvement is required, the DLL will handle the kernel-related matters internally. Each DLL is just a collection of associated functionalities.
When I first tried to understand win32k, the first fact I was told was that win32k mainly handles Windows GUI-related issues. As an ordinary user who uses a GUI system daily, I had a subconscious misconception that GUI is everything. So my first reaction was: how could all functionalities be packed into a single file?
However, if you think carefully, or look at the DLL files under the C:/Windows/System32 directory, you will realize that Windows has too many features: communications, encryption, email, browsers, etc. The reason I thought GUI was everything is that many functionalities are ultimately presented to the user visually through the GUI. Therefore, it can be understood that win32k is the cornerstone of other functionalities, and other functionalities rely on win32k’s features to some degree. In fact, besides win32k, there is also csrss, and both together constitute the Win32 subsystem. For more details on this, you can refer to the Historical Background section of my previous article CSRSS Basic Knowledge.
1.2 User-Mode GUI Concepts
I recommend all friends who have doubts about win32k first read Chapter 3, “Windows and Messages”, of “Programming Windows (5th Edition)”. I read part of this book from a programming perspective during my college years, but looking back at it now, I still found many details that I had missed.
A key concept is that Windows GUI programming is actually a form of “object-oriented programming”, where windows are objects. Those who are familiar with OOP know that objects are created based on classes, and each object has its corresponding data and methods. When creating a window, you need to register a window class first and specify the window procedure.
Here is a snippet of code from the book:
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("HelloWin") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, // window class name
TEXT ("The Hello Program"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL) ; // creation parameters
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
Here, WNDCLASS is the window class, which specifies the window procedure lpfnWndProc, the class name lpszClassName, and other attributes of the window. Note the cbWndExtra field. This field tells the system to reserve some extra space internally for windows created from this window class. If this field is non-zero, you can later access the extra space using the SetWindowLong and GetWindowLong functions, which will be discussed below.
LONG SetWindowLong(
HWND hWnd,
int nIndex, // Offset in the extra space of the value to be set, valid range [0, cbWndExtra-4]
LONG dwNewLong // Value to be set
);
Then, RegisterClass is used to register the window class, and when calling CreateWindow to create the window, the class name szAppName is passed as the first parameter to instantiate a window of this class.
1.3 Kernel-Mode GUI Concepts
For this section, you can refer to the background knowledge introduction of the paper “Kernel Attacks through User-Mode Callbacks”.
Windows uses the window manager to manage the GUI. The window manager also treats user entities like windows and menus as user objects, and maintains a handle table in each user session to track these objects. When we provide a handle value of a user entity during user-mode programming, the window manager translates this handle value into the corresponding object in kernel space based on the handle table.
User objects are represented by different data structures in the kernel. The “extra space reserved internally by the system for windows created from this window class” we mentioned above is stored in the data structure win32k!tagWND. Since Microsoft has removed this structure from symbols and the structure in Win10 has changed significantly compared to Win7, we can only guess the fields through reverse engineering and dynamic debugging. Fortunately, some experts have analyzed this structure (I have modified some details based on my own debugging):
ptagWND // In the kernel, call ValidateHwnd passing the user-mode window handle to return this data pointer
0x00 hWnd
0x10 unknown
0x00 pTEB
0x220 pEPROCESS(of current process)
0x18 unknown
0x80 kernel desktop heap base // Kernel desktop heap base
0x28 ptagWNDk // We need to focus on this structure, which is defined below:
0xA8 spMenu
struct tagWNDK {
ULONG64 hWnd; //+0x00
ULONG64 OffsetToDesktopHeap; //+0x08 Offset of tagWNDK relative to the desktop heap base
ULONG64 state; //+0x10
DWORD dwExStyle; //+0x18
DWORD dwStyle; //+0x1C
BYTE gap[0x38];
RECT rect; //+0x58
BYTE gap1[0x68];
ULONG64 cbWndExtra; //+0xC8 Size of the extra space
BYTE gap2[0x18];
DWORD dwExtraFlag; //+0xE8 Determines the addressing mode of SetWindowLong
BYTE gap3[0x10]; //+0xEC
DWORD cbWndServerExtra; //+0xFC
BYTE gap5[0x28];
ULONG64 pExtraBytes; //+0x128 Mode 1: Kernel offset, Mode 2: User-mode pointer
}
The dwExtraFlag and pExtraBytes fields affect the execution flow of SetWindowLong. Since they are related to the vulnerabilities analyzed below, a detailed explanation is provided here.
First, it must be clear that when an application calls SetWindowLong to set data in the extra space, win32kfull!NtUserSetWindowLong is invoked in the kernel. This function further calls xxxSetWindowLong. The meaning of these two fields is reflected in the xxxSetWindowLong function:
__int64 __fastcall xxxSetWindowLong(struct tagWND *tagWND, int nIndex, unsigned int dwNewLong, unsigned int a4, int a5) {
...
if ( nIndex_1 + 4i64 <= cbWndServerExtra_1 )
{
v31 = *(tagWND + 0x23);
dwOldLong = *(nIndex_1 + v31);
*(nIndex_2 + v31) = dwNewLong_1;
}
else
{
v18 = nIndex_1 - cbWndServerExtra_1;
pExtraBytes = *(tagWNDK + 0x128);
if ( (*(tagWNDK + 0xE8) & 0x800) != 0 ) // When dwExtraFlag & 0x800 is non-zero
dst = (pExtraBytes + v18 + *(*(tagWND + 3) + 0x80i64));// pExtraBytes stores the offset of the extra space relative to the desktop heap base
else // When dwExtraFlag & 0x800 is zero
dst = (v18 + pExtraBytes); // pExtraBytes stores the address of the extra space
dwOldLong = *dst;
*dst = dwNewLong_1;
}
}
As we can see, the dwExtraFlag field controls the interpretation of the pExtraBytes field: in one case it stores an address offset, and in another case it stores an address. If we can mismatch these two fields through some means, it will lead to type confusion.
1.4 Transitions between Kernel-Mode and User-Mode
Sometimes win32k needs to execute some functions in user mode, such as calling hooks defined by the application, providing event notifications, or transferring data with user mode. In these cases, it transitions from kernel mode to user mode via KeUserModeCallback:
NTSTATUS KeUserModeCallback (
IN ULONG ApiNumber,
IN PVOID InputBuffer,
IN ULONG InputLength,
OUT PVOID *OutputBuffer,
IN PULONG OutputLength );
ApiNumber specifies the index of the callback function to be called in the function pointer table USER32!apfnDispatch. This function pointer table is copied into the PEB (specifically PEB.KernelCallbackTable) when the process initializes USER32.dll. We can find this table in USER32.dll:
apfnDispatch 0x00 dq offset __fnCOPYDATA ; DATA XREF: _UserClientDllInitialize+353↑o
0x01 dq offset __fnCOPYGLOBALDATA
0x02 dq offset __fnDWORD
......
0x7B dq offset __xxxClientAllocWindowClassExtraBytes
0x7C dq offset __xxxClientFreeWindowClassExtraBytes
0x7D dq offset __fnGETWINDOWDATA
0x7E dq offset __fnINOUTSTYLECHANGE
0x7F dq offset __fnHkINLPMOUSEHOOKSTRUCTEX
0x80 dq offset __xxxClientCallDefWindowProc
Therefore, if we can hook the functions in the function pointer table before the user-mode callback is triggered, the hooked function will execute our desired code when it is called.
In kernel mode, the window manager manages various resources through critical sections and global locks to prevent access conflicts or Use-After-Free (UAF) issues. However, when win32k wants to enter user mode, it must first leave the critical section. This allows user-mode code to freely modify object attributes, reallocate arrays, etc. If the kernel does not properly validate the owned resources after returning from the user-mode callback, it might use data tampered with by the user-mode callback function.
Another minor detail: because transitions between kernel mode and user mode are prone to issues, win32k follows a default naming convention where functions that may trigger such transitions are prefixed with xxx or zzz to facilitate tracking and checking.
2. CVE-2021-1732 Vulnerability Analysis
2.1 Patch Analysis
CVE-2022-21882 is a bypass of CVE-2021-1732, so I will analyze both vulnerabilities. Let’s first look at CVE-2021-1732. This vulnerability was patched in February 2021, and the flaw occurs during window creation. Let’s compare the win32kfull!xxxCreateWindowEx function between the January and February patches.
Before the patch:
...
*(*(tagWND + 5) + 0x128i64) = xxxClientAllocWindowClassExtraBytes(cbWndExtra);
if ( !*(*(tagWND + 5) + 0x128i64) )
{
v265 = 2;
goto LABEL_202;
}
if ( IsWindowBeingDestroyed(tagWND) || (*(_HMPheFromObject(v97) + 25) & 1) != 0 )
{
*(*(tagWND + 5) + 0x128i64) = 0i64; // Set pExtraBytes to zero
goto destroyWindow;
}
LABEL_212:
if ( PsGetWin32KFilterSet() != 5 || v94 )
{
v101 = v275;
}
else
...
Before the patch, the address of the space allocated by xxxClientAllocWindowClassExtraBytes was directly assigned to the pExtraBytes field. Note that the function has an xxx prefix, indicating that a transition to user mode occurs inside. Based on the analysis above, we know that pExtraBytes is associated with the dwExtraFlag field, but no check is performed on dwExtraFlag here.
After the patch:
...
v96 = xxxClientAllocWindowClassExtraBytes(v95);
v338 = v96;
if ( !v96 )
{
v261 = 2;
goto LABEL_202;
}
if ( IsWindowBeingDestroyed(tagWND_1)
|| (*(_HMPheFromObject(v97) + 25) & 1) != 0
|| (v316[0] = 0i64, tagWND::RedirectedFieldpExtraBytes::operator!=<unsigned __int64>(tagWND_1 + 0x140, v316)) )
{
// The code for destroyWindow remains unchanged here
}
tagWNDK = *(tagWND_1 + 5);
if ( (*(tagWNDK + 0xE8) & 0x800) != 0 )
{
MicrosoftTelemetryAssertTriggeredNoArgsKM();
tagWNDK = tagWND[5];
}
*(tagWNDK + 0x128) = v96;
LABEL_215:
if ( PsGetWin32KFilterSet() != 5 || v93 )
{
v102 = v270;
}
else
...
As shown, after the patch, the assignment is not done directly. Instead, an additional check is added: (v316[0] = 0i64, tagWND::RedirectedFieldpExtraBytes::operator!=<unsigned __int64>(tagWND + 0x140, v316)). If this check evaluates to true, the window destruction path is triggered directly, aborting the normal execution flow. The function is defined as:
bool __fastcall tagWND::RedirectedFieldpExtraBytes::operator!=<unsigned __int64>(__int64 a1, _QWORD *a2)
{
return *(*(a1 - 0x118) + 0x128i64) != *a2; // i.e., *(*(tagWND + 0x140 - 0x118) + 0x128) != 0
}
According to the comments, this function checks whether the value of the pExtraBytes field is non-zero. If it is non-zero, it returns true.
2.2 Limitations of the Patch
At present, there are two issues with the patch. The first one is quite obvious: the patch only compares the difference between the pExtraBytes field and 0, without considering its relationship with dwExtraFlag.
The second issue requires a bit more thought. When we mentioned above that a transition between kernel mode and user mode occurs inside xxxClientAllocWindowClassExtraBytes, it actually means that this function contains many statements, one of which is a call to KeUserModeCallback:
v2 = KeUserModeCallback(123i64, &length, 4i64, &outputbuffer, &outputlength);
The ApiNumber is 0x7B. According to the definition of USER32!apfnDispatch, this value corresponds to the function __xxxClientAllocWindowClassExtraBytes.
The actual state transition happens inside KeUserModeCallback. Therefore, a better approach would be to validate the relevant fields immediately after this function call. Otherwise, if other functions call xxxClientAllocWindowClassExtraBytes, the same issue will reoccur.
The reason it was patched in this specific way is related to the public exploit code.
3. Vulnerability Exploitation
Note: Since this is the first time analyzing a win32k exploit, which involves many concepts unrelated to the specific vulnerability itself, a separate section is dedicated to introducing them.
The exploit code analyzed in this section is obtained from here.
3.1 How to Trigger Type Confusion
According to the analysis above, if we want to exploit the relationship between pExtraBytes and dwExtraFlag, we need to find a way to modify these two fields when KeUserModeCallback calls the 0x7B user-mode callback, making their meanings mismatch.
The function that achieves this goal is NtUserConsoleControl, which calls xxxConsoleControl(int num, struct _CONSOLE_PROCESS_INFO *proInfo, int size). When the num parameter is 6, the function executes the following code:
ptagWND = ValidateHwnd(*proInfo); // proInfo stores the window handle
pptagWNDK = ptagWND + 5;
if ( (*(*pptagWNDK + 0xE8i64) & 0x800) != 0 )// dwExtraFlag & 0x800 != 0
{
heapMem = (*(*(ptagWND_1 + 0x18) + 0x80i64) + *(ptagWNDK_1 + 0x128));// Kernel desktop heap base + pExtraBytes
}
else // dwExtraFlag & 0x800 == 0
{
heapMem = DesktopAlloc(*(ptagWND_1 + 0x18), *(ptagWNDK_1 + 0xC8), 0i64);
Object[3] = heapMem;
if...
if ( *(*pptagWNDK + 0x128i64) ) // If pExtraBytes was previously non-zero, it is treated as the address of the extra space
{
curProc = PsGetCurrentProcess(v24);
cbWndExtra = *(*pptagWNDK + 0xC8i64);
pExtraBytes = *(*pptagWNDK + 0x128i64);
memmove(heapMem, pExtraBytes, cbWndExtra);// Move the data in the extra space to the newly allocated space
if ( (*(curProc + 0x464) & 0x40000008) == 0 )
xxxClientFreeWindowClassExtraBytes(ptagWND_1, *(*(ptagWND_1 + 40) + 296i64));
}
*(*pptagWNDK + 0x128i64) = heapMem - *(*(ptagWND_1 + 0x18) + 0x80i64);// Re-assign pExtraBytes to the offset of the newly allocated space relative to the kernel desktop heap base
}
if...
*(*pptagWNDK + 0xE8i64) |= 0x800u; // Set the 0x800 flag for dwExtraFlag
goto rtn;
Note that this function actually sets the 0x800 flag for dwExtraFlag and, if necessary, updates pExtraBytes to the corresponding offset value.
When the 0x7B user callback occurs, the function actually called is user32!_xxxClientAllocWindowClassExtraBytes:
NTSTATUS __fastcall _xxxClientAllocWindowClassExtraBytes(unsigned int *a1)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v3 = 0;
v4 = 0i64;
Result = RtlAllocateHeap(pUserHeap, 8u, *a1);
return NtCallbackReturn(&Result, 0x18u, 0);
}
As we can see, it passes the address of the newly allocated space back to the kernel as the first parameter via NtCallbackReturn.
Thus, when we hook __xxxClientAllocWindowClassExtraBytes, we can call NtUserConsoleControl to modify the dwExtraFlag field and set its 0x800 flag, and then call NtCallbackReturn with a custom pExtraBytes value as the first parameter. Afterwards, the system will treat the pExtraBytes we set as the offset of the extra space relative to the desktop heap base.
In other words, by triggering type confusion, we can read/write data at desktop heap base + a limited offset.
3.2 From Type Confusion to Local Privilege Escalation
When learning HEVD previously, I ignored this part of the content, which left me lacking clues when studying the exploitation of this vulnerability. Kernel vulnerabilities generally achieve arbitrary read/write capabilities, but how can such capability be leveraged to achieve privilege escalation? For the vulnerability analyzed this time, we can only read/write desktop heap base + a limited offset. How can we escalate privileges under such constraints?
Reference [7] provides a great answer to this question. Escalating from a kernel vulnerability to local privilege escalation requires three steps:
- Expand the kernel vulnerability into arbitrary read/write primitives;
- Find a kernel address leak method to obtain the address of any kernel object, and thus find the System process via the
EPROCESSlist; - Copy the System process token to the exploit process, thereby achieving privilege escalation.
3.2.1 Arbitrary Read/Write Primitives
First, a key detail needs to be clarified. From learning HEVD and the IDA code analysis above, we know that we can call the HMValidateHandle function, passing a window handle as an argument, to obtain the address of the tagWNDK structure of that window. Since HMValidateHandle is not an exported function, we need to locate its address by scanning the memory using the method described in learning HEVD.
Next, we proceed to obtain arbitrary read/write primitives. Acquiring the primitives mainly involves 3 windows: Window 0, Window 1, and Window 2.
- Create multiple windows (e.g., 50) through window spraying, and destroy the subsequent windows, keeping only the first two as Window 0 and Window 1. Meanwhile, call
NtUserConsoleControlto switch Window 0 to the offset mode. Note that we are not exploiting the vulnerability here; it is a normal function call and does not cause type confusion. At this point, Window 0’spExtraBytesfield stores the offset of Window 0’s extra space; - Retrieve the offset addresses of the
tagWNDKstructures for Window 0 and Window 1 by callingHMValidateHandle; - Create Window 2, and switch Window 2 to offset mode via hooking, while modifying its
pExtraBytesfield to the offset of Window 0’stagWNDKstructure. At this point, we can modify thetagWNDKstructure of Window 0 using Window 2’sSetWindowLongW; - “Arbitrary” write primitive: Modify the
cbWndExtrafield of Window 0 to the maximum possible value. Now we can perform a very large range of Out-Of-Bounds (OOB) writes via Window 0’sSetWindowLongW/SetWindowLongPtrW. Although this is not strictly an “arbitrary” write, it is sufficient for exploitation;
The arbitrary read primitive is more complex, and its key lies in the function GetMenuBarInfo:
BOOL GetMenuBarInfo(
[in] HWND hwnd,
[in] LONG idObject, // When the value is OBJID_MENU(-3), it represents retrieving the menu bar of the specified window
[in] LONG idItem, // When the value is 1, it represents retrieving the information of the first item on the menu, and so on
[in, out] PMENUBARINFO pmbi // Buffer to receive the information
);
This function eventually calls win32kfull!xxxGetMenuBarInfo. When the value of idObject is -3, it executes the following code block:
if ( (*(ptagWNDK + 0x1F) & 0x40) != 0 ) // 0x1C offset is dwStyle. The value of WS_CHILD is 0x40000000L. Here it checks if WS_CHILD is set, and if so, it aborts
goto toEnd;
spMenu = *(ptagWND + 0xA8); // Continues execution only if idObject == -3
SmartObjStackRefBase<tagMENU>::operator=(&spMenu_1, spMenu);
if ( !SmartObjStackRef<tagMENU>::operator bool(&spMenu_1)
|| idItem_1 < 0
|| idItem_1 > *(*(*spMenu_1 + 0x28i64) + 0x2Ci64) )// Continues execution only if [+28h]+2ch >= idItem
{
goto toEnd;
}
...
if ( *(*spMenu_1 + 0x40i64) && *(*spMenu_1 + 0x44i64) )// Offsets +40h and +44h are non-zero
{
if ( idItem_1 ) // idItem is non-zero
{
ptagWNDK_2 = *(ptagWND + 0x28);
x60 = 0x60 * idItem_1; // idItem is set to 1 for ease of calculation
pdesired = *(*spMenu_1 + 0x58i64); // +58h stores the target address to read minus 0x40
desired = *(0x60 * idItem_1 + pdesired - 0x60);
if ( (*(ptagWNDK_2 + 0x1A) & 0x40) != 0 ) // Checks if dwExStyle is WS_EX_LAYOUTRTL. Since it's not set, it enters the else block
{
// pass
}
else // Enters here
{
v64 = *(desired + 0x40) + *(ptagWNDK_2 + 0x58);// desired + 0x40h, so the input is target - 0x40h
pmbi->rcBar.left = v64; // In the output mbi, rcBar.left stores target(0~3) + Rect.left
pmbi->rcBar.right = v64 + *(*(x60 + pdesired - 0x60) + 0x48i64);// In the output mbi, rcBar.right stores rcBar.left + target(8~11)
}
v65 = *(*(ptagWND + 0x28) + 0x5Ci64) + *(*(x60 + pdesired - 0x60) + 0x44i64);
pmbi->rcBar.top = v65; // In the output mbi, rcBar.top stores Rect.top + target(4~7)
v40 = v65 + *(*(x60 + pdesired - 0x60) + 0x4Ci64);
}
else
{
// pass
}
pmbi->rcBar.bottom = v40; // In the output mbi, rcBar.bottom stores rcBar.top + target(12~15)
}
spMenu is located at offset 0xA8 of tagWND. If we can modify this field to point to a fake spMenu structure constructed by ourselves, we can achieve arbitrary reads. The fakeSpMenu must satisfy the following conditions:
- The data at offsets 0x40 and 0x44 must be non-zero;
- The pointer at offset 0x28 points to a buffer where the data at offset 0x2C is non-zero;
- The pointer at offset 0x58 points to a buffer which stores the value of the target address we want to read minus 0x40;
Setting fakeSpMenu cannot be done using the arbitrary write primitive because, up to this point, we have only obtained the offset of tagWNDK and cannot determine the exact location of tagWND. Fortunately, there is a more convenient method to set spMenu — SetWindowLongPtr:
LONG_PTR SetWindowLongPtrW(
[in] HWND hWnd,
[in] int nIndex,
[in] LONG_PTR dwNewLong
);
This function is similar to SetWindowLong but operates on 8-byte data. It further calls win32kfull!xxxSetWindowLongPtr. When nIndex is GWLP_ID(-12), it calls win32kfull!xxxSetWindowData and executes the following code:
switch ( nIndex )
{
case -12:
ptagWNDK = *(ptagWND + 40);
if ( (*(ptagWNDK + 0x1F) & 0xC0) == 0x40 )// If WS_CHILD is set
{
v18 = *(ptagWND + 0xA8); // +A8h spmenu
*(ptagWNDK + 0x98) = dwNewLong;
*(ptagWND + 0xA8) = dwNewLong;
}
else
{
// pass
}
// pass
}
// pass
return v18;
That is to say, if the nIndex parameter is -12 and the window represented by hWnd has the WS_CHILD style, SetWindowLongPtr will set the spMenu of the window and return the old spMenu value as the return value.
Summary of the arbitrary read primitive:
- Set the
WS_CHILDstyle for Window 1 using the arbitrary write primitive. Note:dwStyleis at offset 0x1C oftagWNDK. As long as Window 1’stagWNDKis located after Window 0’stagWNDK, we can modify Window 1’sdwStylevia Window 0’sSetWindowLongW/SetWindowLongPtrW; - Construct a
fakeSpMenustructure that satisfies the conditions listed above; - Set Window 1’s
spMenutofakeSpMenuviaSetWindowLongPtr; - Remove the
WS_CHILDstyle of Window 1 using the arbitrary write primitive; - Call
GetMenuBarInfo(hWND1, -3, 1, &mbi), and calculate the value at the target address according to the code comments above.
3.2.2 Obtaining the EPROCESS Address
The kernel address leak method has actually been mentioned above: when calling SetWindowLongPtr to set Window 1’s spMenu field, it returns the old spMenu value, which is a kernel address. However, the problem is how to obtain the EPROCESS address from it.
The author of reference [8] mentioned that spMenu stores the EPROCESS of the current process, but did not mention how this information was discovered. If we search purely for the EPROCESS address, we might luckily find its location:
// First, determine during debugging that the old spMenu address is fffff0ce808211e0
3: kd> !process 0 0 exp.exe
PROCESS ffff9d07b784c340
SessionId: 1 Cid: 0498 Peb: a67dffa000 ParentCid: 1048
DirBase: 153bd000 ObjectTable: ffffdf09dfef9140 HandleCount: 131.
Image: exp.exe
3: kd> s fffff0ce808211e0 l10000000 40 c3 84 b7 07 9d ff ff
fffff0ce`85229010 40 c3 84 b7 07 9d ff ff-01 00 00 00 30 c0 0c 14 @...........0...
3: kd> dqs fffff0ce808211e0
fffff0ce`808211e0 00000000`000301a7
fffff0ce`808211e8 00000000`00000001
fffff0ce`808211f0 00000000`00000000
fffff0ce`808211f8 ffff9d07`b61ffde0
fffff0ce`80821200 fffff0ce`808211e0
fffff0ce`80821208 fffff0ce`81221890
fffff0ce`80821210 00000000`00021890
fffff0ce`80821218 00000008`00000000
fffff0ce`80821220 00000013`00000078
...
// Observing the data in spMenu, the first thing to check is ffff9d07`b61ffde0 at offset 0x18
3: kd> s ffff9d07`b61ffde0 l10000000 10 90 22 85 ce f0 ff ff
ffff9d07`b61ffee0 10 90 22 85 ce f0 ff ff-00 00 00 00 00 00 00 00 ..".............
ffff9d07`b784c848 10 90 22 85 ce f0 ff ff-b0 44 94 b8 07 9d ff ff .."......D......
The above steps confirm that PEPROCESS is stored at [spMenu + 0x18] + 0x100. However, the above derivation was done because I already knew the answer. If all information were unknown, it might be difficult to reach the same conclusion. Perhaps there are more resources online, or the author went through more debugging and analysis.
3.3 Section Summary
The final process of replacing the token of the current process is a fairly common technique and is not difficult, so it won’t be repeated here. At this point, we can leverage the CVE-2021-1732 vulnerability to achieve local privilege escalation.
As mentioned in Section 2.2, the reason Microsoft patched the vulnerability by checking if the pExtraBytes field is 0 is that the leaked exploit only set the 0x800 flag of dwExtraFlag and modified pExtraBytes to the desired offset by hooking and calling NtUserConsoleControl. Under normal circumstances, if __xxxClientAllocWindowClassExtraBytes is not hooked, the pExtraBytes field would not be modified and would remain 0. Therefore, Microsoft patched the vulnerability solely targeting this exploitation method.
However, as it turned out, there was more than one way to exploit this vulnerability, which brings us to CVE-2022-21882, which we will introduce next.
4. CVE-2022-21882 Vulnerability Analysis
4.1 Vulnerability Mechanism
As mentioned in Section 2.2 regarding the limitations of the CVE-2021-1732 patch, Microsoft fixed the issue in the win32kfull!xxxCreateWindowEx function by checking the pExtraBytes field after the call to __xxxClientAllocWindowClassExtraBytes had finished. However, the root cause was in the __xxxClientAllocWindowClassExtraBytes function itself. When CVE-2021-1732 was discovered, only xxxCreateWindowEx called __xxxClientAllocWindowClassExtraBytes. However, with system updates and upgrades, new code was added to win32k, and other functions started calling __xxxClientAllocWindowClassExtraBytes. Checking the win32kfull.sys file from December 2021 reveals that functions such as xxxDesktopWndProcWorker, xxxMenuWindowProc, xxxSBWndProc, xxxSwitchWndProc, and xxxTooltipWndProc all call __xxxClientAllocWindowClassExtraBytes. These functions did not implement the same checks as xxxCreateWindowEx, leading to the recurrence of the same vulnerability.
4.2 Patch Analysis
In the updated win32kfull, Microsoft first refactored similar code by extracting the common logic from the aforementioned functions that call __xxxClientAllocWindowClassExtraBytes into a single function named xxxValidateClassAndSize, which in turn calls __xxxClientAllocWindowClassExtraBytes. This refactoring is unrelated to the vulnerability itself.
The patch then added the following code snippet at the end of the xxxClientAllocWindowClassExtraBytes function:
v4 = KeUserModeCallback(123i64, &Length_1, 4i64, &OutputBuffer, &OutputBufferLength);
// pass some unchanged checks
if ( (*(*(tagWND + 0x28) + 0xE8i64) & 0x800) != 0 )// Check dwExtraFlag
result = 0i64;
else
result = Mem_1;
return result;
This time, dwExtraFlag is checked directly after the callback function finishes execution, preventing the callback function from switching the window to the offset mode.
4.3 Exploitation
Since the vulnerability mechanisms of CVE-2022-21882 and CVE-2021-1732 are completely identical, their exploitation methods are also the same, so they will not be repeated here.
5. Conclusion
This article mainly focuses on the prerequisites/basic knowledge and vulnerability exploitation, which were the parts I could never understand when I first started learning about win32k vulnerabilities. Through studying these two similar vulnerabilities, I have gained a preliminary understanding of win32k vulnerabilities, learned to distinguish “basic knowledge” from “vulnerability concepts”, and clarified the starting points for analyzing and finding such vulnerabilities.
However, I have only studied this category of vulnerabilities so far, and win32k contains other types of flaws. Thus, this article alone cannot address all aspects. Nonetheless, I hope that reading this article can guide you, as a fellow win32k vulnerability beginner, to facilitate your future analyses of other vulnerabilities.
6. References
- Classic Kernel Vulnerability Debugging Notes
- Deep Analysis of Win32k Kernel Privilege Escalation Vulnerability CVE-2022-21882
- Programming Windows 5th
- Kernel Attacks through User-Mode Callbacks, by Tarjei Mandt
- Deep Analysis of Win32k Kernel Privilege Escalation Vulnerability CVE-2022-21882
- 0DAY Attack! Bitter APT Group First Spotted Using Windows Kernel Privilege Escalation 0DAY (CVE-2021-1732) in Targeted Attacks Against Domestic Targets
- In-Depth Analysis of CVE-2021-1732 Vulnerability
- CVE-2021-1732 Study and Exploit Development
- CVE-2022-21882: Win32k Window Object Type Confusion