This post is also available in: 日本語 (Japanese)
Table of Contents
14. Create a new process with System privileges and restore changes to modified structures
Analysis of the read64 Function
Conclusion
Table of Contents: Figures
Figure 56. Lines 206-266 of the PoC.
Figure 57. Lines 355-385 of the PoC.
Figure 58. Memory layout of the fake spmenu object.
Figure 59. Function prototype for GetMenuBarInfo.
Figure 60. PMENUBARINFO structure.
Figure 61. Memory dump of ref_g_pMem5 after initialization.
Figure 62. MSDN documentation for the RECT (rcbar) structure.
Figure 63. Memory dump of pmbi after first call to GetMenuBarItem.
Figure 64. Line 249 of the PoC.
Figure 65. Line 261 of the PoC.
Figure 66. Memory dump of pmbi after second call to GetMenuBarItem
Figure 67. Memory dump showing address pointer Wnd1’s parent TagWND structure.
14. Create a new process with System privileges and restore changes to modified structures
The remaining lines of the PoC (640 through 726) are simply creating a new process that inherits the current process’ security token, which is now System. Then the previously modified structures are being reset to their original state to prevent a system crash in the event Windows accesses any of these structures in the future.
Analysis of the read64 Function
During the discussion of the read64 function, we’ll be referring to the code in Figure 56. The code in this figure was cleaned up by eliminating white space.
Before we analyze the read64 function, let’s start by discussing the spmenu object. This object is important to discuss because this is where GetMenuBarInfo is pulling the information that is returned inside the pmbi structure. It’s important to know the general layout of the spmenu object.
Unfortunately, Microsoft does not provide debugging symbols for any of the relevant structures here, so we’ll have to rely on the PoC and some reverse engineering to identify important structure offsets and pointer relationships. If we look at the PoC code shown in Figure 57, where the fake spmenu was created (lines 355 through 385), we can begin to guess some important offsets and structure sizes of an spmenu object.
Figure 58 shows a diagram of the fake spmenu layout based on the PoC code shown above.
Also recall, in step 12 above, the legitimate spmenu for Wnd1 was replaced by g_pMem4. As we can see in Figure 58, this contains a pointer to the larger fake spmenu that was set up in the code snippet in Figure 57 above.
At a high level, the read64 function takes a kernel address and returns the pointer located at that address. We need this functionality because, even though we can calculate the address of spmenu + 0x50 (0xfff8e82008218c0 + 0x50 = 0xfff8e8200821910) based on the leaked spmenu address, that address isn’t what we need. It’s the pointer located at that address that is required.
But since the leaked spmenu address is a kernel address, we cannot directly read the pointers within the spmenu structure from user-mode. The read64 function was designed to provide this capability.
Within the read64 function there are two calls to GetMenuBarInfo, an initialization flag (g_bIsInit), a menubar information structure (pmbi), and an allocated memory chunk that is 0x200 bytes in size. We’ll discuss each of these next.
The GetMenuBarInfo function allows for the retrieval of a specified window’s (hwnd) menu bar information via the PMENUBARINFO (pmbi) structure. The function prototype for the GetMenuBarInfo function is shown in Figure 59.
The GetMenuBarInfo function takes four parameters. The first parameter (hwnd) is a handle to the window that owns the menu bar for which the information is being queried. The second parameter (idObject) is the menu object being queried. This can be one of three values: popup menu, menu bar or system menu. However, the PoC only uses -3 (0xFFFFFFFD), which corresponds to a menu bar.
The third parameter (idItem) indicates the item to retrieve information for. If this parameter is 0, the function retrieves information about the menu itself. If this parameter is 1, the function retrieves information about the first item on the menu, and so on.
All calls within the PoC use the value 1, therefore referring to the first item on the menu. The final parameter (pmbi) is the structure that stores the returned information. The structure is of the type PMENUBARINFO and is shown in Figure 60.
Looking back at the read64 function, we can see that a PMENUBARINFO structure named pmbi is allocated. Then the g_bIsInit flag is checked. The g_bIsInit flag is set after the first call to GetMenuBarInfo.
Looking at the PoC (lines 246 through 252), we see that the first call to GetMenuBarInfo uses the value of pmbi.rcbar.left after the initial call to set up the global variable g_pmbi_rcBar_left (line 249). Once this is done, the g_bIsInit flag is set to true (line 252).
The PoC also allocates and creates an array that is 0x200 bytes in size. The array is then initialized in a for loop in a way that indexes each DWORD (32 bits). A partial dump of this memory is shown in Figure 61.
Finally, the global variable ref_g_pMem5 is set to point to this allocated memory. Recall that ref_g_pMem5 is part of the fake spmenu that the PoC code replaced the real spmenu of Wnd1 with (shown in Figure 58).
We know that the GetMenuBarInfo function uses pmbi to store the requested menu bar information. Looking at the read64 function, the only pmbi members that are referenced are pmbi.rcbar.left (lines 249 and 265) and pmbi.rcbar.top (line 265). Therefore, to understand what is going on here, we need to identify how GetMenuBarInfo calculates these values. After reverse engineering the GetMenuBarInfo function, we determined the pmbi.rcbar values are calculated in the following way:
- pmbi.rcbar.left = pmbi[0x4] = ref_g_pmem5[0x40] + tagWND[0x58]
- pmbi.rcbar.top = pmbi[0x8] = ref_g_pmem5[0x44] + tagWND[0x5c]
- pmbi.rcbar.right = pmbi[0xc] = ref_g_pmem5[0x40] + g_pmem5[0x48]
- pmbi.rcbar.bottom = pmbi[0x10] = ref_g_pmem5[0x4c] + pmbi[0x8]
We know which rcbar variable corresponds to which offsets in pmbi from Microsoft’s MSDN documentation, shown in Figure 62.
Therefore, based on the analysis above, after the first call to GetMenuItem, pmbi.rcbar is:
- pmbi.rcbar.left = 0x40 + 0x00 = 0x40
- pmbi.rcbar.right = 0x44 + 0x00 = 0x44
- pmbi.rcbar.top = 0x40 + 0x48 = 0x88
- pmbi.rcbar.bottom = 0x4c + 0x44 = 0x90
Figure 63 shows a memory dump of pmbi after the first call GetMenuBarItem, verifying our calculations above are correct.
Based on the above variables we know that at line 249 in the PoC, shown in Figure 64, g_pmbi_rcBar_left is set equal to pmbi.rcbar.left or 0x40.
Line 261 of the PoC, shown in Figure 65, is subtracting the value of g_pmbi_rcBar_left from qwDestAddr. We know that qwDestAddr is the input parameter to read64, or the address we are trying to dereference. Therefore, the PoC is subtracting 0x40 from this address and assigning it to ref_g_pMem5.
At first this may seem confusing, but based on how GetMenuBarInfo calculates pmbi, we know that the first two values of pmbi are taken from ref_g_pMem5[0x40] and ref_g_pMem5[0x44]. Therefore, if we want to dereference qwDestAddr by calling GetMenuBarInfo by using pmbi.rcbar.left and pmbi.rcbar.top, we need to account for the fact that GetMenuBarInfo references ref_g_pMem5[0x40] and ref_g_pMem5[0x44] to calculate these values. This is what line 261 of the PoC is doing.
NOTE: We need to use both pmbi.rcbar.left and pmbi.rcbar.top because each value is only 32 bits and we need to store a 64-bit value.
Now that ref_g_pMem5 points to qwDestAddr - 0x40, the next call to GetMenuBarInfo will result in the following values:
- ref_g_pMem5 = qwDestAddr - 0x40
- pmbi.rcbar.left = qwDestAddr (low order bits)
- pmbi.rcbar.right = qwDestAddr (high order bits)
Figure 66 shows a memory dump of pmbi after the second call to GetMenuBarItem. We can see that pmbi[0x4] does indeed contain the lower order bits of qwDestAddr and pmbi[0x8] contains the high order bits of qwDestAddr.
We already know that spmenu + 0x50 is supposed to point to the parent tagWND structure, and we can see from Figure 67, 0xffff8e82008427e0 is in fact the parent tagWND structure of Wnd1.
We can now see that any call to read64 will return a dereferenced address located at the input parameter qwDestAddr, successfully providing an arbitrary read primitive.
Conclusion
This concludes the two part series on Win32k. In part one, we covered how to use the Win32 API to create GUI objects such as windows and menus. We then covered the user-mode and kernel-mode data structures that are used to manage these objects and how they have changed over the years to help optimize and secure the transition between user-mode and kernel-mode.
In part two we analyzed a recent vulnerability (CVE-2022-21882) and how this vulnerability can be exploited to elevate privileges. We discussed the inner workings of a public PoC to demonstrate what is required today to evade the protections Microsoft has worked so hard to implement over the last 20 years.
We showed how CVE-2022-21882 was similar to CVE-2021-1732 and why the patch for CVE-2021-1732 wasn’t sufficient to prevent CVE-2022-21882. Finally, we discussed how the exploit used the GetMenuBarItem function (in conjunction with a fake menu structure) to provide the arbitrary read primitive required to locate and copy the System token for privilege escalation.