Vulnerabilities

Inside Win32k Exploitation: Analysis of CVE-2022-21882 and CVE-2021-1732

Clock Icon 36 min read
Related Products

This post is also available in: 日本語 (Japanese)

Table of Contents

Detailed analysis of CVE-2022-21882
1. Find HMValidateHandle
2. Load NtUserConsoleControl and NtCallbackReturn
3. Find KernelCallbackTable and save a pointer to the user-mode callbacks xxxClientAllocWindowClassExtraBytes and xxxClientAllocWindowClassExtraBytes
4. Define a couple of window classes
5. Groom the heap

Table of Contents: Figures

Figure 1. Call to FindHMValidateHandle function.
Figure 2. IDA disassembly of the IsMenu function.
Figure 3. Code snippet of FindHMValidateHandle.
Figure 4. Lines 285-288 in the PoC.
Figure 5. Lines 297-304 of the PoC.
Figure 6. WinDbg output of the PEB.
Figure 7. First 16 entries of the KernelCallbackTable.
Figure 8. WinDbg memory dump of KernelCallbackTable + 0x3d0 where two functions of interest are located.
Figure 9. Lines 312-325 of the PoC.
Figure 10. Return value from calling HMValidateHandle on the first window created.
Figure 11. Kernel-mode copy of the tagWND structure shared across user-mode and kernel-mode.

Detailed analysis of CVE-2022-21882

1. Find HMValidateHandle (as shown in Figure 1)
Image 1 is a screenshot of a few lines of code. It is the call to the FindHMValidateHandle function.
Figure 1. Call to FindHMValidateHandle function.

As mentioned earlier, exploit writers have historically used HmValidatehandle to leak the kernel address of objects whose handle is passed to the function. The function prototype is HMValidateHandle(HANDLE h, BYTE type), where the handle h is a handle to the object you are trying to validate, and type is a numeric constant representing the type of object.

For our purposes here, the type will be one that represents a window type 0x001. As of Windows 10 version 1803, this will return a user-mapped desktop heap pointer to the tagWND structure related to the window handle passed.

HMValidateHandle is not an exported function, so you can’t just use GetProcAddress to resolve the function. Because of this, exploit authors typically resolve the IsMenu function within User32.dll and – because the only function called by IsMenu is HMValidateHandle – the exploit code will do a search for the first E8 opcode. The E8 opcode corresponds to a CALL instruction (the only call instruction in IsMenu).

Figure 2 shows the disassembly of the IsMenu function. We can see that the E8 opcode is the first and only opcode in the call to HMValidateHandle.

Image 2 is a screenshot of the disassembly of the IsMenu function. It contains the first and only opcode in the call to HMValidateHandle.
Figure 2. IDA disassembly of the IsMenu function.

In the PoC, the author defined a function at line 58 called FindHMValidateHandle that accomplishes what is described above. Figure 3 shows a code snippet from the FindHMValidateHandle function that searches IsMenu for the E8 opcode and saves this location as a pointer to the g_pfnHMValidateHandle global variable. This will be used later to leak the user-mode mapped tagWND structure addresses.

Image 3 is a screenshot of the code snippet of the FindHMValidateHandle.
Figure 3. Code snippet of FindHMValidateHandle.
2. Load NtUserConsoleControl and NtCallbackReturn

Both NtUserConsoleControl and NtCallbackReturn functions are abused to trigger CVE-2022-21882 and CVE-2021-1732. Both of these functions are undocumented and reside in win32u.dll and ntdll.dll respectively. Lines 285 through 288 in the PoC (shown in Figure 4) resolve these functions and save pointers to them for later use.

Image 4 is a screenshot of lines 285 through 288 in the POC. These lines resolve the aforementioned functions and save pointers to them for later use.
Figure 4. Lines 285-288 in the PoC.

Descriptions of the NtUserConsoleControl and NtCallbackReturn functions are detailed in steps 7 and 9 respectively.

3. Find KernelCallbackTable and save a pointer to the user-mode callbacks xxxClientAllocWindowClassExtraBytes and xxxClientAllocWindowClassExtraBytes

The code in lines 297 through 304 (shown in Figure 5) is locating the KernelCallbackTable and saving the address pointers of the legitimate xxxClientAllocWindowClassExtraBytes and xxxClientFreeWindowClassExtraBytes functions to local variables. In order to hook xxxClientAllocWindowClassExtraBytes and xxxClientFreeWindowClassExtraBytes (explained in step 9), pointers to each function need to be found. This is because both of these functions are user-mode callbacks and not exported for use within the Windows API.

Image 5 is a screenshot of lines 297 through 304 of the POC. They locate the KernelCallbackTable and save address pointers.
Figure 5. Lines 297-304 of the PoC.

The KernelCallbackTable is located by parsing the process environment block (PEB). Offset 0x58 in the PEB contains a pointer to the KernelCallbackTable.

NOTE: The GS[0x60]register contains a pointer to the PEB on Windows x64 systems, which is why the code is referring to __readgsqword(0x60u). The KernelCallbackTable is a table that contains a pointer mapping to all of the kernel callback functions used by the Windows kernel. The KernelCallbackTable entry in the PEB is shown using WinDbg (the dt nt!_peb @$peb command was used to dump the current PEB).

Based on the WinDbg output shown in Figure 6, you can see that the PEB+0x58 contains a pointer to the KernelCallbackTable address.

Image 6 is a screenshot of the WinDbg output. It contains a pointer to KernelCallbackTable.
Figure 6. WinDbg output of the PEB.

The first few entries of the kernel callback are shown in Figure 7.

Image 7 is a screenshot of the first 16 entries of the KernelCallbackTable.
Figure 7. First 16 entries of the KernelCallbackTable.

The PoC is saving two pointers (offsets 0x3d8 and 0x3e0) into the KernelCallbackTable, to g_oldxxxClientAllocWindowClassExtraBytes and g_oldxxxClientFreeWindowClassExtraBytes respectively.

The KernelCallbackTable+0x3d8 contains a pointer to xxxClientAllocWindowClassExtraBytes and the next entry (0x3e0) contains a pointer to xxxClientFreeWindowClassExtraBytes. This is shown in Figure 8.

Image 8 is a screenshot of the memory dump by WinDbg of KernelCallbackTable + 0x3d0.
Figure 8. WinDbg memory dump of KernelCallbackTable + 0x3d0 where two functions of interest are located.
4. Define a couple of window classes

The code in Figure 9 (lines 312 through 325) should look familiar. As covered in part one of this blog series, it is defining two window classes and registering one of the two (wndClass). One is given the class name normalClass, while the other one is given the class name of magicClass.

It also appears that the magic window class is given a random cbWndExtra value. This will be used later to differentiate between the two window classes when calling the hooked functions, and it will be analyzed in more detail later.

Image 9 a screenshot of lines 312 through 325 of the POC. Highlighted in yellow are two instances of g_nRandom.
Figure 9. Lines 312-325 of the PoC.
5. Groom the heap

Lines 413 through 467 in the PoC define a do/while loop that creates 10 windows of the normalClass class type. Each of the windows is given the window name somewnd.

The exploit author creates 10 windows (0 through 9) and then, subsequently, deletes windows 2 through 9. This is likely an attempt to groom the heap to ensure that the magic window, created later, will be allocated just after the two remaining windows in this portion of the PoC. However, as we’ll see later in the case of this execution example, the magic window is allocated in between the first two.

During the creation of each window, the author is storing the handle to each window in an array called arrhwndNoraml[]. Next, a pointer to each window’s tagWND structure is stored in another array called arrEntryDesktop[]. As described earlier, this is done by calling HMValidateHandle, which you now know returns a pointer to the window’s user-mode copy of each tagWND structure.

Figure 10 shows the return value (rax) after the first call to HMValidateHandle (i.e., after creating the first window).

Image 10 is a screenshot of the return value from calling HMValidateHandle on the first window created. The return value is rax.
Figure 10. Return value from calling HMValidateHandle on the first window created.

Some labels have been added to the tagWND structure that will be important during the analysis of the PoC. Take note that the tagWND.cbWNDExtra value is 32 (0x20), which is exactly what was declared during the normalClass registration in Figure 9 above.

Also, take note of the tagWND.dwExtraFlags. This value is what will change during the call to NtUserConsoleControl, and will indicate that the value in the tagWND.pExtraBytes field is an offset into the kernel rather than a user-mode address. However, you can clearly see it is a user-mode address (0x0000015ba4b73fb0) immediately after the window is created.

The kernel-mode desktop tagWND structure for the same window is shown in Figure 11. It was found by statically analyzing the CreateWindowEXW function to find where the memory was allocated and breaking on this point during execution.

You can see the tagWND structure in the kernel is in fact the same as the one returned in user-mode after calling HMValidateHandle. As mentioned before, the user-mode desktop heap is simply a copy of the kernel-mode desktop heap, which is what is actually used by Win32k to manage windows.

As we’ll see later, there is actually a parent tagWND structure located in the kernel. The parent structure is where all pertinent kernel addresses are stored, and Microsoft has ensured any user-mode access to window structures is done through the user-mode safe tagWND structures. Because Microsoft has gone through great lengths to obscure kernel pointer leaks from user-mode applications, only the user-mode desktop heap addresses are accessible from user-mode, and a method will need to be used to bypass this restriction a little later.

Image 11 is a screenshot of the kernel mode copy of the tagWND structure that is shared across user mode and kernel mode.
Figure 11. Kernel-mode copy of the tagWND structure shared across user-mode and kernel-mode.

It’s important to note that tagWND.OffsetToDesktopHeap becomes more clear once you see the actual addresses where the tagWND structures reside. Above, the tagWND.OffsetToDesktopHeap value is 0x38390, while the kernel tagWND address is 0xffff8e8201038390. If you subtract the tagWND.OffsetToDesktopHeap value from the tagWND address you are able to determine the address of the kernel desktop heap, 0xffff8e8201000000. The same goes for the user-mode tagWND structures as well.

Later on, once an arbitrary write primitive has been obtained, these offsets will be used to aid in navigating the kernel memory space. We are now moving into Section 3, where we will continue with steps 6-9.

Continue Reading ➠ Section 3 – Detailed Analysis, Steps 6-9

Back to Top

Enlarged Image