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

12. Use SetWindowLongPtrA to leak Wnd1’s spmenu kernel address and replace it with a fake spmenu object

Table of Contents: Figures

Figure 33. Line 546-549 of the PoC.
Figure 34. Line 552 of the PoC.
Figure 35. MSDN documentation for SetWindowLongPrtA’s nIndex parameter.
Figure 36. MSDN definition for the hMenu parameter.
Figure 37. Lines 355-385 of the PoC.
Figure 38. Parent tagWND structure.
Figure 39. Disassembly of xxxSetWindowData (called from SetWindowLongPtrA).
Figure 40. WinDBG output showing value of spmenu before being changed.
Figure 41. Register values just before the return from SetWindowLongPtrW.
Figure 42. Line 566 of the PoC.
Figure 43. Line 546 of the PoC.
Figure 44. WinDbg output of Wnd1’s child tagWND structure.
Figure 45. Kernel desktop heap + 0x3a850.
Figure 46. Wnd1’s parent TagWND structure + 0xa8.

12. Use SetWindowLongPtrA to leak Wnd1’s spmenu kernel address and replace it with a fake spmenu object

After the two calls to SetWindowLongW there are several calls to SetWindowLongPtrA. SetWindowLongW is an older, deprecated function that operates on 32-bit LONG integers, whereas SetWindowLongPtrA supersedes SetWindowLongW and allows for operations on both 32-bit and 64-bit LONG_PTR integers.

The first call to SetWindowLongPtrA, shown in Figure 33 (lines 546 through 549), changes the tagWND.dwExStyle of Wnd1 to 0x40000000 (WS_CHILD). This makes the window a child window. Pay close attention to the parameters and do the math. The function is given Wnd0’s handle, and we know SetWindowLongPtrA operates on the window’s pExtraBytes field, which now points to the start of Wnd0’s tagWND structure.

If we subtract Wnd0’s desktop heap base offset (0x2ad30) from Wnd1’s desktop heap base offset (0x38390) and add (0x18), we get 0xd678. If we add this to the start of Wnd0’s tagWND structure (0xffff8e820102ad30), we get 0xffff8e820102010383a8. This is offset 0x18 into Wnd1’s tagWND structure, or its tagWND.dwExStyle.

Image 33 is a screenshot of lines 546 through 549 of the POC. It is the first call to SetWindowLongPtrA.
Figure 33. Line 546-549 of the PoC.

Changing the style of Wnd1’s window to a child window is important for the next call to SetWindowLongPtrA.

The next call (shown in Figure 34) passes Wnd1’s handle directly, sets the nIndex parameter to -12, and g_pMem4 is passed as the dwNewLong parameter.

Image 34 is a screenshot of line 552 of the POC. It is the next call in the after the call shown in image 33.
Figure 34. Line 552 of the PoC.

Referring to the MSDN documentation for SetWindowLongPtrA’s nIndex parameter (shown in Figure 35) indicates that -12 refers to the GWL_ID, which is the macro for a child window. Because changing the GWL_ID cannot be done on a top-level window, the previous call to SetWindowLongPtrA was made to prepare Wnd1 for this call.

Image 35 as a screenshot of Microsoft documentation for the perimeter of SetWindowLongPrtA’s nIndex. It includes a table where the first column is value. The second column is meaning. On] nlnde* Type: int The zero-based offset to the value to be set. Valid values are in the range zero through the number of bytes of extra window memory, minus the size of an integer. To set any other value, specify one of the following values. Value GWL EXSTYLE -20 GWL HINSTANCE -6 GWL_ID -12 GWL STYLE -16 GWL USERDATA -21 GWL_WNDPROC -4 Meaning Sets a new extended window style. Sets a new application instance handle. Sets a new identifier of the child window. The window cannot be a top-level window. Sets a new window style. Sets the user data associated with the window. This data is intended for use by the application that created the window. Its value is initially zero. Sets a new address for the window procedure. You cannot change this attribute if the window does not belong to the same process as the calling thread.
Figure 35. MSDN documentation for SetWindowLongPrtA’s nIndex parameter.

To better understand what is going on here, it is important to remember that hMenu (defined during the window creation in step 8) refers to a handle to a menu or specifies a child window identifier. The MSDN definition for the hMenu parameter is shown in Figure 36.

Type: HMENU A handle to a menu, or specifies a child-window identifier, depending on the window style. For an overlapped or pop-up window, hMenu identifies the menu to be used with the window; it can be NULL if the class menu is to be used. For a child window, hMenu specifies the child-window identifier, an integer value used by a dialog box control to notify its parent about events. The application determines the child-window identifier; it must be unique for all child windows with the same parent window.
Figure 36. MSDN definition for the hMenu parameter.

Recall from step 8 that the hMenu parameter was set to a call to CreateMenu for each window. Therefore, for each window created, a handle to a menu object (essentially a pointer to the object in the kernel) was assigned to the hMenu parameter. But because Wnd1 is now a child window, the exploit can use SetWindowLongPtrA to change the child window identifier to that of g_pMem4. Changing the hMenu handle would not have been possible if Wnd1 was still a parent window.

Lastly, g_qwExploit is set to the return value of SetWindowLongPtrA, which is equal to the value of hMenu before the call – the handle assigned during window creation. Handles are simply memory locations used by the Windows kernel to track objects. Therefore, g_qwExploit now contains the kernel address to Wnd1’s spmenu object.

It wasn’t discussed earlier, but g_pMem4 was previously defined in the exploit code. Lines 355 through 385 set up five memory allocations – g_pMem1 through g_pMem5 – shown in Figure 37.

Image 37 is a screenshot of lines 355 through 385 of the POC. These are five memory allocations.
Figure 37. Lines 355-385 of the PoC.

These allocations, along with the subsequent code, are setting up a fake menu object to replace the real menu object of Wnd1. The fake menu object is shown in Figure 58. We go into more detail on this a bit later.

Also, because SetWindowLongPtrA returns the old value of nIndex, a pointer to the hMenu object is returned. In fact, the pointer returned is the *spmenu entry within the parent tagWND, which is a kernel pointer. It resides at offset 0xa8 in the tagWND structure, shown in Figure 38 as a refresher.

Image 38 is a screenshot of the parent tagWND structure.
Figure 38. Parent tagWND structure.

The call to SetWindowLongPtrA with GWL_ID triggers an execution path that changes the *spmenu in both the parent tagWND and child/user-mode copy (offset 0x98) of the tagWND structure, shown in Figure 39, where r15 is equivalent to g_pMem4.

In Figure 39, rsi is the parent tagWND. A pointer to the child/copy tagWND is located at offset 0x28, and it is copied to the rax register. Then the spmenu entry for each tagWND structure is changed to the value in r15 (g_pMem4).

Image 39 is a screenshot of the disassembly of xxxSetWindowData.
Figure 39. Disassembly of xxxSetWindowData (called from SetWindowLongPtrA).

The value returned after this call to SetWindowLongPtrA is the value of the parent tagWND + 0xa8 (spmenu) prior to being changed. This would be rsi+0xa8 at 0xffff8eac5a15c386 in the Figure 39 above. Setting a breakpoint in WinDbg at this point, we can see the register contents and a dump of rsi+0xa8 (0xfff8e82008218c0) in Figure 40.

Image 40 is a screenshot of the output of WinDbg showing the value of spmenu before it is changed. It includes the register contents and a dump of rsi+0xa8 (0xfff8e82008218c0).
Figure 40. WinDBG output showing value of spmenu before being changed.

Just to verify that the return value is indeed what we think it should be, Figure 41 shows the registers at the end of the SetWindowLongPtrW call.

NOTE: To avoid confusion, the reason why some of the figures have SetWindowLongPtrW instead of SetWindowLongPtrA is because Windows only deals with Unicode characters directly. These are functions that end in W. To accommodate ASCII, Microsoft created wrapper functions. These functions, which end in A, simply do the conversions and then call the Unicode functions directly. Therefore, when debugging the calls to SetWindowLongPtrA, the actual heavy lifting is being done in SetWindowLongPtrW.

Image 41 is a screenshot of the register values before the return from SetWindowLongPtrW. Rax is the same value as in the previous figure. . kd> g Break instruction exception - code 8øeeeøe3 (first chance) USER 32 ! SetWindowLongPtrW+0xc4 : øe33:eøee7ff8• 664ab814 c3 ret rax=ffff8e820e8218co rdx-eeeeeeøeeeeøøeeo rip-eeee7ff8664ab814 r8-eeeeee36bd4f34e8 ril=eeøeeeøeeeeøø246 r14-eeeeeeøeeeeøøeeo iopl=e nv up rbx=eøeee15ba502839ø rsi-eeeeøøeeøeuaegø rsp=eøeeee36bd4f3528 r9-eøeeeø36bd4f363ø r12=eøeeeøeeøeeeeeeø r15-eøeeeøeeøee3839ø ei pl nz na pe nc rcx=øee07ff86535b4a4 rdi-øee07ff7ed32eeøe rbp=øeeeee36bd4f363e rlo-øeeeeeøøeeøeeøøe r13=øeee7ff866532e7e cs-ee33 ss-ee2b ds=ee2b es-øe2b fs=eø53 gs=ee2b 1 ef1=eøeeme2 USER32 ! SetWindowLongPtrW+0xc4 : ee33:eeee7ffC 664ab814 c3 ret
Figure 41. Register values just before the return from SetWindowLongPtrW.

Notice that rax is the same value as rsi+0xa8 in Figure 40. This is the kernel address of the original menu item created in step 8. This address will be used in a future step to find the kernel EPROCESS structure and copy the System token for privilege escalation. This pointer is saved in the g_qwExploit variable in the PoC, shown in Figure 34 above.

The next call (line 566) to SetWindowLongPtrA, shown in Figure 42, resets Wnd1’s tagWND.dwExStyle to the previous value. This value was saved in line 546 of the PoC and is shown in Figure 43. Therefore, Wnd1 is no longer a child window.

Image 42 as line 566 of the POC. It starts with SetWindowLongPtrA.
Figure 42. Line 566 of the PoC.
Image 42 is line 546 of the PoC. It starts with g_qwrpdesk =
Figure 43. Line 546 of the PoC.

One might ask why we would need to go through all of this work to leak the spmenu pointer when we’re able to write to kernel memory using the SetWindowLong functions. Couldn’t we just use GetWindowLong to read the kernel address of the menu object?

This could give us the menu object. However, it would read from the child tagWND structure, which does not store a kernel address for the menu object. Figure 44 shows the WinDbg output of Wnd1’s child tagWND structure. At offset 0x98 (spmenu), we can see it’s definitely not a kernel pointer. As we’ll see in a bit, this is an offset from the desktop heap to the spmenu object.

Image 44 is a screenshot of WinDbg output of Wnd1’s child tagWND structure. Highlighted within a yellow box is 0003a850 00000000.
Figure 44. WinDbg output of Wnd1’s child tagWND structure.

If you look at the kernel desktop heap base that we calculated in step 7, plus the value in the child tagWND+0x98, we find what’s shown in Figure 45.

Image 45 is a screenshot of the colonel desktop heap  + 0x3a850.
Figure 45. Kernel desktop heap + 0x3a850.

When we look at the address leaked from the parent tagWND (offset 0xa8), we see the following, shown in Figure 46.

Image 46 is a screenshot of Wnd1’s parent tagWND structure + 0xa8.
Figure 46. Wnd1’s parent TagWND structure + 0xa8.

We can see that the first value is the same. There might also be some kind of user-mode friendly spmenu object, just like there is a user-mode friendly copy of the tagWND structure.

But now that we have this kernel address, what can we do with it? We will explore this in the next step in Section 6 with step 13.

Continue Reading ➠ Section 6 –⁠ Detailed Analysis, Step 13

Back to Top

Enlarged Image