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

By

Category: Vulnerability

Tags: , , ,

A pictorial representation of vulnerabilities like CVE-2021-1732 and CVE-2022-21882

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

Table of Contents

6. Calculate offsets between windows
7. Call NtUserConsoleControl on lowest window address
8. Create a third (magic) window
9. Hook xxxClientAllocWindowClassExtraBytes with a malicious version that calls NtUserConsoleControl and NtCallbackReturn, then returns to the real xxxClientAllocWindowClassExtraBytes

Table of Contents: Figures

Figure 12. Lines 472-499 of the PoC.
Figure 13. WinDBG dump of the HMValidateHandle return value (rax) for Wnd1.
Figure 14. WinDBG dump of the HMValidateHandle return value (rax) for Wnd0.
Figure 15. Line 501 of the PoC.
Figure 16. Wnd0’s pExtraBytes value before and after the call to NtUserConsoleControl.
Figure 17. Creation of WndMagic.
Figure 18. Memory layout just after creation of WndMagic.
Figure 19. Lines 522-530 of the PoC.
Figure 20. KernelCallbackTable before pointer overwrite.
Figure 21. KernelCallbackTable after pointer overwrite.
Figure 22. Lines 170-190 of the PoC.
Figure 23. Line 496 of the PoC.
Figure 24. Lines 151-164 of the PoC.

6. Calculate offsets between windows

Lines 472 through 499 (shown in Figure 12) simply determine which of the first two windows created have the lowest offset to the kernel desktop heap base. They then assign the tagWND pointers and tagWND.OffsetToDesktopHeap to variables tagged with _min or _max based on the window order in memory.

Image 12 is a screenshot of lines 472 through 499 of the POC. It shows the pointers and how they are assigned to variables.
Figure 12. Lines 472-499 of the PoC.

In this case, the first window created was not actually the lowest in memory, therefore it’ll be referred to as Wnd1. The second window, which is lower in memory, will be referred to as Wnd0 from now on, to reflect their locations in memory.

Based on what’s shown in Figure 13, you now know that the user-mode desktop heap must be at 0x15ba5028390 - 0x38390 or 0x15ba4ff0000.

Image 13 is a screenshot of the WinDbg dump of HMValidateHandle return value for WND1.
Figure 13. WinDBG dump of the HMValidateHandle return value (rax) for Wnd1.

If we take the tagWND.OffsetToDesktopHeap for Wnd1 and add it to the desktop heap address calculated above, you get 0x15ba4ff0000 + 0x2ad30 or 0x15ba501ad30. This is exactly what HMValidateHandle returned for the tagWND structure for Wnd0, as shown in Figure 14.

Image 14 is a screenshot of the WinDbg dump of HMValidateHandle return value for WND0.
Figure 14. WinDBG dump of the HMValidateHandle return value (rax) for Wnd0.

This part of the PoC is simply doing this math and assigning the tagWND pointers and offsets to the desktop heap to the following tracking variables:

  • kernel_desktop_heap_base_offset1
  • kernel_desktop_heap_base_offset2
  • kernel_desktop_heap_base_offset_Min
  • tagWndMin_offset_0x128
  • tagWndMin_offset_0x128
  • kernel_desktop_heap_base_offset_Min
  • hWndMin
  • hWndMax
7. Call NtUserConsoleControl on lowest window address

Next, the author calls NtUserConsoleControl (line 501 of the PoC, shown in Figure 15) on hWndMin (Wnd0).

Image 15 is line 501 of the POC. It is g_pfnNtUserConsoleControl(6, &hWndMin, 0x10);
Figure 15. Line 501 of the PoC.

This converts Wnd0 into a console window and, as a result, changes the tagWND.pExtraBytes from a user-mode address pointer to an offset. Windows will then treat this offset as an offset to the kernel-mode desktop heap base. It does this because NtUserConsoleControl adds 0x800 to the tagWND.dwExtraFlag value, which tells the window manager this window is a console window and it should treat the pExtraBytes field as an offset from the kernel desktop heap base address.

Wnd0’s tagWND structure before and after the call to NtUserConsoleControl is shown in Figure 16.

Image 16 is a screenshot of Wnd0’s pExtraBytes value before and after the call to NtUserConsoleControl. Two areas of the code are highlighted by yellow rectangles.
Figure 16. Wnd0’s pExtraBytes value before and after the call to NtUserConsoleControl.

The tagWND.pExtraBytes has changed from the user-mode virtual address 0x15ba4b74370 to the kernel-mode desktop heap offset 0x2ae80. The tagWND.dwExtraFlag has changed from 0x100100018 to 0x100100818, which is 0x100100018 + 0x800.

Now that the tagWND.dwExtraFlag indicates a kernel offset to the kernel-mode desktop heap, the offset in tagWND.pExtraBytes will now point at the kernel desktop heap plus the offset, or 0xffff8e8201000000 + 0x2ae80 = 0xffff8e820102ae80. This address is meaningless at the moment, but its use will be explained in more detail in step 9.

8. Create a third (magic) window

A third window is created next. This window belongs to the magicClass class that was registered in step 4 above. It is also given the name somewnd, just like the first two windows, Wnd0 and Wnd1. This window will be referred to as WndMagic from now on.

The call to CreateWindowExW is shown in Figure 17.

Image of 17 is a screenshot of the call to CreateWindowExW. This is in the magicClass.
Figure 17. Creation of WndMagic.

After creating the WndMagic window, the memory layout for all of the windows in our specific example is shown in the diagram in Figure 18. Notice that Wnd0’s tagWND.dwExtraFlag (0x100100818 versus 0x10010018) indicates that the tagWND.pExtraBytes (0x2ae80) is now an offset into the kernel desktop heap.

In the diagram, you can see that Wnd0’s kernel tagWND structure and the user-mode copy both indicate the same offset within the kernel desktop heap, while the other two tagWND structures pExtraBytes point to memory in user land.

Image 18 is a diagram of the memory layout just after the creation of WNDMagic. On the left-hand side is the user land and its layout, and on the right-hand side is the kernel land.
Figure 18. Memory layout just after creation of WndMagic.

NOTE: You can see that the order in which windows are created doesn’t imply order in memory. Wnd0 in this case was the second window created, and it lies at the lowest address, while WndMagic is the second lowest despite being created last. It would be interesting to see if increasing the number of initial windows created in step 5 would make the window memory layout more predictable and alleviate the math required in step 6 to determine the memory order of each window.

9. Hook xxxClientAllocWindowClassExtraBytes with a malicious version that calls NtUserConsoleControl and NtCallbackReturn, then returns to the real xxxClientAllocWindowClassExtraBytes

After the creation of WndMagic, the code shown in Figure 19 is executed (lines 522 through 530 in the PoC).

Image 19 is a screenshot of lines 522 through 530 of the POC. It is what is executed after the creation of WndMagic.
Figure 19. Lines 522-530 of the PoC.

First, the memory protections for the KernelCallbackTable entries for xxxClientAllocWindowClassExtraBytes and xxxClientFreeWindowClassExtraBytes are being changed from PAGE_READONLY (0x2) to PAGE_EXECUTE_READWRITE (0x40) through a call to VirtualProtect. Next you can see the kernel callback table pointer entries for xxxClientAllocWindowClassExtraBytes and xxxClientFreeWindowClassExtraBytes are being overwritten with pointers to the attacker defined functions g_newxxxClientAllocWindowClassExtraBytes and g_newxxxClientFreeWindowClassExtraBytes respectively.

Figure 20 shows the KernelCallbackTable before the function pointers have been overwritten.

Image 20 is a screenshot of the KernelCallbackTable before the function pointers are overwritten.
Figure 20. KernelCallbackTable before pointer overwrite.

Figure 21 shows the KernelCallbackTable after hooking both functions.

Image 21 is the KernelCallbackTable after both functions are hooked and the pointers are overwritten.
Figure 21. KernelCallbackTable after pointer overwrite.

Now that the legitimate functions have been successfully hooked with the malicious functions g_newxxxClientAllocWindowClassExtraBytes and g_newxxxClientFreeWindowClassExtraBytes, anytime the legitimate functions are called, execution will be directed to the g_new functions.

To understand why someone would want to hook these functions, it’s helpful to understand what the legitimate functions do. The real xxxClientAllocWindowClassExtraBytes function takes the tagWND.cbWndExtra value as a parameter, and then it allocates that number of bytes to the desktop heap. The pointer to the allocation is then returned, via a call to NtCallbackReturn (this will be important later), and is stored in the tagWND.pExtraBytes field of the windows structure.

If you can hook xxxClientAllocWindowClassExtraBytes, you know – at the very least – you can control the pointer address that is written to the tagWND.pExtraBytes field of the referenced window to one of your choosing. However, this address pointer would be a user-mode pointer, which doesn’t really do much good if the objective is to gain code execution in the kernel to escalate privileges by stealing the System token. Therefore, something else will be needed to enable access to kernel memory, and this will be discussed shortly.

Figure 22 shows the malicious g_newxxxClientAllocWindowClassExtraBytes function definition.

Image 22 is a screenshot of lines 170 through 190 of the POC. Included is the malicious g_newxxxClientAllocWindowClassExtraBytes function definition.
Figure 22. Lines 170-190 of the PoC.

The primary purpose of the g_newxxxClientAllocWindowClassExtraBytes function is to call NtUserConsoleControl to change the currently referenced window handle to that of a console window. As discussed in step 7, console windows do not manage the extra bytes field within the user-mode copy of the desktop heap, but are instead managed directly within the kernel desktop heap. Because of this, the tagWND.pExtraBytes field of a console window is treated as an offset into the kernel desktop heap versus a user-mode desktop heap pointer.

Because NtUserConsoleControl is not normally within the call stack of the legitimate xxxClientAllocWindowClassExtraBytes function, Windows does not expect, nor programmatically account for, the changes that NtUserConsoleControl makes to the tagWND.dwExtraFlag (addition of 0x800) and tagWND.pExtraBytes fields. This results in a type confusion bug (CVE-2022-21882) that leads to an unexpected kernel memory access.

Because standard GUI windows are initialized with a user-mode desktop heap pointer in the pExtraBytes field and the window manager is now treating this value as an offset into the kernel desktop heap, the value of this field must be changed. The new value should reflect a useful offset value from the kernel desktop heap base as opposed to the much larger pointer value that currently exists.

When the real xxxClientAllocWindowClassExtraBytes function is done allocating the requested memory, it passes a pointer to the allocated memory to the NtCallbackReturn function to return execution to the kernel. This ultimately results in the pointer to the pExtraBytes memory allocation being stored in the tagWND.pExtraBytes field.

Just prior to g_newxxxClientAllocWindowClassExtraBytes calling NtCallbackReturn, qwRet is set to the value of kernel_desktop_heap_base_offset_Min which, if you remember from earlier (step 6, Figure 12), is Wnd0’s tagWND.OffsetToDesktopHeap value. Figure 23 shows the code in the PoC that assigns kernel_desktop_heap_base_offset_Min to Wnd0’s tagWND.OffsetToDesktopHeap.

Image 23 is line 496 of the POC starting with kernel_desktop_heap_base_offset_Min.
Figure 23. Line 496 of the PoC.

To recap, the call to NtUserConsoleControl results in the pExtraBytes field being interpreted as an offset into the kernel. The call NtCallbackReturn overwrites the window’s pExtraBytes field with an offset to Wnd0’s tagWND.OffsetToDesktopHeap. Therefore, it is now possible to change a window’s pExtraBytes field to point to the kernel address of Wnd0’s tagWND structure. This is assuming we can find a function that calls the user-mode callback xxxClientAllocWindowClassExtraBytes, which will be discussed in more detail in the next step.

Figure 24 shows the code (lines 151 through 164) for the second hooked function, g_newxxxClientFreeWindowClassExtraBytes.

Image 24 is a screenshot of lines 151 through 164 of the POC. Highlighted in brown is g_nRandom.
Figure 24. Lines 151-164 of the PoC.

The reason this function is hooked is because calls to NtUserConsoleControl subsequently make a call to xxxClientFreeWindowClassExtraBytes. However, after going through all of the trouble to set the tagWND.dwExtraFlag and tagWND.pExtraBytes to values that will facilitate the exploit by hooking xxxClientAllocWindowClassExtraBytes, it would be counterproductive to have these values freed by a call to xxxClientFreeWindowClassExtraBytes.

To avoid this, the real function has been hooked and the malicious function compares the random value g_nRandom to the parameter passed to g_newxxxClientAllocWindowClassExtraBytes, which is the tagWND.cbExtraBytes field. WndMagic’s tagWND.cbExtraBytes was set to this value earlier in the PoC (see step 4), therefore the comparison is using this value to ensure the real xxxClientFreeWindowClassExtraBytes is not called during any references to the WndMagic window.

If the value matches, then g_newxxxClientFreeWindowClassExtraBytes simply returns 1. If the value does not match, execution is redirected to g_oldxxxClientAllocWindowClassExtraBytes, or the real xxxClientAllocWindowClassExtraBytes function.

This is likely an error on the part of the author since it would make more sense to redirect control back to the intended function (xxxClientFreeWindowClassExtraBytes) through a call to g_oldxxxClientFreeWindowClassExtraBytes. This call was defined as a pointer to the real xxxClientFreeWindowClassExtraBytes in line 304 of the PoC.

This error does not affect the success of the PoC exploit because the hooked functions are never called with other windows as inputs. Now we move on to steps 10-11.

Continue Reading ➠ Section 4 – Detailed Analysis, Steps 10-11

Back to Top