Skip to Main Content

Anatomy of an exploit in Windows win32k – CVE-2022-21882

A new manipulation technique of window objects in kernel memory that leads to privilege escalation.

The Avira Threat Protection Labs is a dedicated team, with members based around the world. Its research focuses on emerging and developing cyberthreats.

Researchers from this team investigated a patched Common Vulnerability and Exposure (commonly referred to as a CVE) affecting the Microsoft Windows operating system, first published in January 2022 by Microsoft.

Throughout the paper, we are tackling subjects such as the CVE identification, its impact, and how we can analyze it. We follow-up with highly technical details about the exploitation process, samples identified in the wild and exploitation techniques. This CVE-2022-21882 has an increased risk, being a data-only attack by design of the exploit development, which bypasses important operating system protections (such as ASLR (Address Space Layout Randomization), DEP (Data Execution Prevention), CFG (Control Flow Guard)).

Why is this important? Having proof of concepts discovered in the wild means that this vulnerability has a higher risk of being exploited. We have also identified several public samples leveraging this vulnerability. This CVE could be used by cybercriminals in conjunction with other attack vectors, as exploiting it can grant the cyberattacker privileged escalation to the victim’s Windows machine. The cyberattacker needs that initial access to the affected computer before elevating the privileges. Detection of this specific CVE also directly implies that a cyberattacker already has some level of access to the victim’s local machine, potentially highlighting a compromised system. This permits us to alert the user about a potential breach and help protect our customers against malware that abuse this vulnerability.

The selected CVE-2022-21882 has a similar root cause as CVE-2021-1732. From our analysis, the same exploitation techniques are used in both cases, making a generic detection more comprehensive. Avira is providing coverage on both CVE-2022-21882 and CVE-2021-1732.

For the paper conclusions, we explore the detection that Avira engines provide, along with supplying a list of IOCs (indicators of compromise) as references for future investigations.

In January 2022 it was officially announced by Microsoft that CVE-2022-21882 allows a cyberattacker to obtain out-of-bounds write capability during a user-mode callback, which can lead to a data-only attack having obtained privilege escalation.

Usually, if a vulnerability allows for read and write privileges at arbitrary locations, in kernel memory, a cyberattacker could use this to create a new process with NT Authority privileges. Escalation of privilege is a common goal for cyberattackers because it can be further used in deploying malware, accessing confidential information and any other actions that an underprivileged user should not have. This vulnerability was patched by Windows January 2022 patch update. Although the vulnerability is recent, its flow is remarkably similar to the one used in exploits based on CVE-2021-1732.

Researchers from the Avira Threat Protection Lab, tracked the particularities of the two mentioned vulnerabilities. As a result, they recommend all Windows users keep their devices up-to-date with the latest security updates. This will reduce the risk of a possible system compromise by helping prevent malicious actors from leveraging the discovered vulnerabilities.

Win32k and user-mode callbacks

Win32k.sys is a kernel mode driver and represents an essential component for Windows architecture, being responsible for graphical device interface and window management.

Windows management is done by Windows Manager and implies keeping track of user entities such as windows, menus, cursors and being able to operate with them accordingly. Graphical Device Interface refers to graphics rendering and implements objects such as brushes, pens and so on.

Windows Manager operates its functionality through user objects which are divided into distinct types. As an example, win32k!tagWND structure represents window objects and win32k!tagMENU define menus. The concerned CVE of this study is based on window objects.

All user objects are indexed into a handle table and processes can access its information from user-mode. Windows Manager validates handles via HMValidateHandle API (Application Programming Interfaces), receiving the handle value as parameter and look for the entry which corresponds in the handle table. Cyberattackers commonly use HMValidateHandle to leak memory addresses from kernel. User objects can be stored in desktop heap, shared heap, or session pool. In general, objects associated with a desktop have their memory allocated in the desktop heap and a handle is returned. Kernel heap allocator manages objects stored in desktop heap and shared heap and is considered almost similar with user-mode heap allocator.

Win32k can perform various tasks, such as invoking application-defined hooks, event notifications and even copying data from kernel-mode to user-mode and vice versa. These actions are performed through the user-mode callback mechanism that is implemented in KeUserModeCallback and acts like a reverse system call.

NTSTATUS KeUserModeCallback (
in ULONG apiNumber,
in void* inputBuffer,
in ULONG inputLength,
out void** outputBuffer,
out ULONG* outputLength
)

When a user-mode callback is performed, the provided ApiNumber in KeUserModeCallback refers to an index from a function pointer table (USER32!apfnDispatch). This can be accessed via Process Environment Block (PEB.KernelCallbackTable) pointing to the user32!apfnDispatch symbol after initialization by user32!UserClientDllInitialize (USER32.dll). In user-mode it will be present a call on ntdll!KiUserCallbackDispatcher function, used in the callback process. Cyberattackers could use the PEB.KernelCallbackTable to hijack a callback by hooking it and altering data before the return. Once a user-mode callback is complete, it calls NtCallbackReturn to copy the result of the callback to the original kernel stack.

Vulnerability context

CVE-2022-21882 and CVE-2021-1732 are based on the user-mode callback user32!_xxxClientAllocWindowClassExtraBytes that is triggered by win32kfull!xxxClientAllocWindowClassExtraBytes to allocate space for extra bytes for a window object.

The tagWND structure is not formally documented, but the significance of certain offsets has been found over the years.

Figure 1. Certain offsets for tagWND structure
Figure 1. Certain offsets for tagWND structure

In CVE-2021-1732, the callback was made while creating a malicious window object that has extra data.

When CreateWindowEx is called to create a window, the tagWND.cbWndExtra field (tagWND+0xC8) is checked to verify whether the window object has extra data. When the value is not empty, win32kfull!xxxClientAllocWindowClassExtraBytes is triggered, to call the user mode function user32!_xxxClientAllocWindowClassExtraBytes and allocate space in the user-mode desktop heap.

The allocated memory address is in tagWND.pExtraBytes field (tagWND+0x128). Exploitation consists in hooking the user32!_xxxClientAllocWindowClassExtraBytes function in user mode and modifying the properties of the window object extra data in the hook function manually. Once user32!_xxxClientAllocWindowClassExtraBytes is hooked, the NtUserConsoleControl function is called to convert the window object to a console window. NtUserConsoleControl will call xxxConsoleControl – that will allocate memory in the kernel desktop heap, will convert the pointer located in tagWND.pExtraBytes field to an offset value that points to the kernel allocated memory and modify the tagWND.dwExtraFlag to 0x800 as shown in Figure 2.

Figure 2. win32kfull!xxxConsoleControl
Figure 2. win32kfull!xxxConsoleControl

Manually calling the NtCallbackReturn function will return the controllable offset value of the pExtraBytes field to the kernel before the callback returns. This will create window type confusion that leads to an out of bound write capability during the callback.

The patch for CVE-2021-1732 updated the win32kfull!xxxCreateWindowEx and solved the problem that arises on CreateWindowEx call.

In CVE-2022-21882, the user32!_xxxClientAllocWindowClassExtraBytes callback is obtained using any relevant GUI API that makes the kernel call one of the following: xxxMenuWindowProc, xxxSBWndProc, xxxSwitchWndProc, xxxTooltipWndProc – instead of triggering the callback with xxxCreateWindowEx. Following the previously mentioned steps triggers the same window type confusion.

In the POC (Proof of Concept), the NtUserMessageCall function is used to obtain the USER32!_xxxClientAllocClassExtraBytes callback.

Windows January 2022 patch update resolved the CVE-2022-21882 by adding a check before the xxxClientAllocWindowClassExtraBytes method ends. It will be checked if the ConsoleWindow flag (0x800) is set on tagWND.dwExtraFlag field (tagWND+0xE8) and in an affirmative case, the xxxClientAllocWindowClassExtraBytes will return false.

Steps to trigger vulnerability and achieve escalation of privileges

In late January the first public version of a proof-of-concept was released. In this section are present the steps triggering that vulnerability.

This in-depth analysis follows the proof of concept and includes the steps that lead to privileged escalation.

1. Obtain a pointer to the HMValidateHandle (not exported function of user32.dll) that will be used to leak memory addresses from the kernel. Example (this source code was also used in the CVE-2021-1732 and CVE-2022-21882 proof of concept)

2. Call CreateWindowEx API two times to obtain two windows objects that have extra data and use HMValidateHandle obtained previously. HMValidateHandle will leak the memory addresses of the created window objects in user mode mapping. Those addresses represent the mapping in user mode of the kernel object tagWND and name them entryDesktopAdr. At entryDesktopAdr+0x08, which is tagWND+0x08, we find an offset stored in the desktop heap (having the same offset for user and kernel desktop heap) as shown before in the ‘Vulnerability context’ section.

At this step, we consider wndMin the window object that has the smallest kernel desktop heap offset- between the two window objects that were created and wndMax.

3. Call NtUserConsoleControl with wndMin as second parameter, to convert it to a console window. As shown before in Figure 2, this will convert wndMin.pExtraBytes from a user desktop heap pointer to an offset in the kernel desktop heap after reallocation. This step will allow an out of bound write from wndMin to wndMax later.

Figure 3. Call stack for NtUserConsoleControl API call in user land
Figure 3. Call stack for NtUserConsoleControl API call in user land

In Figure 3. we observe that USER32!_xxxClientFreeWindowClassExtraBytes is called during NtUserConsoleControl.

4. Call CreateWindowEx API again to create a new window object (wndMagic) with extra data. The cbWndExtra field must be different than 0 and it does not matter if the value is constant or random.

When this window object is created, win32kfull!xxxClientAllocWindowClassExtraBytes will be called to allocate memory and the user32!_xxxClientAllocWindowClassExtraBytes callback will be triggered. In CVE-2021-1732 this callback was hooked before the CreateWindowEx call, to obtain the object type confusion. Now, in CVE-2022-21882, the user32!_xxxClientAllocWindowClassExtraBytes is allowed to complete its regular behavior due to the patch from 2021 that added value checks on pExtraBytes field in CreateWindowEx.

Figure 4. Call stack for CreateWindowEx API call in user land
Figure 4. Call stack for CreateWindowEx API call in user land

5. Obtain function user32!_xxxClientAllocWindowClassExtraBytes and user32!_xxxClientFreeWindowClassExtraBytes using the KernelCallbackTable.

Figure 5. Obtaining USER32!_xxxClientAllocWindowClassExtraBytes and USER32!_xxxClientFreeWindowClassExtraBytes offsets from KernelCallbackTable
Figure 5. Obtaining USER32!_xxxClientAllocWindowClassExtraBytes and USER32!_xxxClientFreeWindowClassExtraBytes offsets from KernelCallbackTable

Figure 5. Obtaining USER32!_xxxClientAllocWindowClassExtraBytes and USER32!_xxxClientFreeWindowClassExtraBytes offsets from KernelCallbackTable

To obtain the offsets of the functions, the KernelCallbackTable address is subtracted from the function address and the offset 0x3D8 is obtained for USER32!_xxxClientAllocWindowClassExtraBytes, respectively 0x3E0 for USER32!_xxxClientFreeWindowClassExtraBytes as shown in Figure 5.

Figure 6. Code snippet – Accessing the KernelCallbackTable functions
Figure 6. Code snippet – Accessing the KernelCallbackTable functions

In Figure 6. is illustrated how USER32!_xxxClientAllocWindowClassExtraBytes and USER32!_xxxClientFreeWindowClassExtraBytes are obtained in the POC.

6. Hook the previously obtained functions using VirtualProtect

Figure 7. Code snippet – Hooking USER32!_xxxClientAllocWindowClassExtraBytes and USER32!_xxxClientFreeWindowClassExtraBytes
Figure 7. Code snippet – Hooking USER32!_xxxClientAllocWindowClassExtraBytes and USER32!_xxxClientFreeWindowClassExtraBytes

The hooking process will allow the cyberattacker to call a custom function that replaces user32!_xxxClientAllocWindowClassExtraBytes when the KeUserModeCallback is performed in win32kfull!_xxxClientAllocWindowClassExtraBytes.

Figure 8. win32kfull!xxxClientAllocWindowClassExtraBytes - callback on user32!_xxxClientAllocWindowClassExtraBytes
Figure 8. win32kfull!xxxClientAllocWindowClassExtraBytes – callback on user32!_xxxClientAllocWindowClassExtraBytes

Figure 8 illustrates how the callback is performed in win32kfull!xxxClientAllocWindowClassExtraBytes, by calling KeUserModeCallback with 0x7B as ApiNumber value.

Being on a 64-bit system, the pointers size is 8 bytes, therefore, 0x7B * 8 bytes = 0x3D8 offset in KernelCallbackTable, which points to user32!_xxxClientAllocWindowClassExtraBytes.

The implementation of the custom functions is illustrated in Figure 7. In the custom function that replaces user32!_xxxClientAllocWindowClassExtraBytes, NtUserConsoleControl is called to convert the last created window object (wndMagic) to a console window and NtCallbackReturn is used to assign the kernel desktop heap base offset value to wndMagic.pExtraBytes field and return it to the kernel before the callback returns.

The user32!_xxxClientFreeWindowClassExtraBytes is also called during NtUserConsoleControl and its custom function is rewritten to do nothing and not interfere in the process.

7. Call NtUserMessageCall (or any other API call that will make the kernel call on xxxMenuWindowProc, xxxSBWndProc, xxxSwitchWndProc, xxxTooltipWndProc) on wndMagic leads to the desired callback and uses the custom function instead.

Figure 9. NtUserMessageCall subroutine call
Figure 9. NtUserMessageCall subroutine call

In Figure 9 is illustrated the call on NtUserMessageCall and the associated assembly syscall.

Figure 10. Call stack simplified on NtUserMessageCall subroutine call with user32!_xxxClientAllocWindowClassExtraBytes hooked in user land
Figure 10. Call stack simplified on NtUserMessageCall subroutine call with user32!_xxxClientAllocWindowClassExtraBytes hooked in user land

In Figure 10  the stack call can be observed when user32!_xxxClientAllocWindowClassExtraBytes and user32!_xxxClientFreeWindowClassExtraBytes are hooked and the call on NtUserMessageCall is performed.

The vulnerability is set and will allow a cyberattacker to write to wndMin any time when SetWindowLongW is called on wndMagic. This is possible due to the NtCallbackReturn performed in custom xxxClientAllocWindowClassExtraBytes that set the pExtraBytes of wndMagic to the kernel desktop base heap offset of wndMin.

8. Call SetWindowLongW with wndMagic as its first parameter, 0xC8 as second parameter and a large value as third parameter (e.g., 0xFFFFFFF). This will write the large value to the wndMin.cbWndExtra (0xC8). This large value will allow to write in kernel memory after its tagWND structure to fields of the wndMax.

At this moment, the wndMin.pExtraBytes field have the offset to itself. When SetWindowLongPtrA will be called on wndMin will write to an offset in kernel desktop heap that is relative to wndMin.

9. Call SetWindowLongPtrA(wndMin, 0x18 offset (dwStyle) + difference between the kernel desktop heap base offset of wndMax and wndMin, (entryDesktopAdr of wndMax + 0x18) ^ 0x40). Due to the calculated distance at the second parameter, this will change the style (dwStyle field) of the wndMax to WFCHILD (0x40). This change will make the wndMax object a child window and it will be used for kernel address leaking. Being a child window will allow to change its identifier using SetWindowLongPtrA.

10. Call SetWindowLongPtrA on wndMax with second parameter –12 (GWLP_ID) to allow the spMenu field of wndMax to be overwritten with a fake spMenu data structure as shown in Figure 11. This fake spMenu data structure is allocated manually by the cyberattacker with VirtualAlloc or LocalAlloc, any time before the SetWindowLongPtrA call from this step. The modifications are possible as the window style was previously changed to child.

Figure 11. win32kfull!xxxSetWindowData - setting a new identifier of the child window
Figure 11. win32kfull!xxxSetWindowData – setting a new identifier of the child window

The return value of SetWindowLongPtrA provides the initial value at the spMenu data structure pointer that is a kernel memory address.

At this point a leaked pointer from kernel memory is obtained and the wndMax.spMenu pointer is overwritten with a fake spMenu data structure from user desktop heap.

11. Call SetWindowLongPtrA as in step 9 to restore the wndMax style to a top-level window.

12. Obtain read primitive using the fake spMenu, the previously leaked pointer GetMenuBarInfo API on wndMax as first parameter and with -3 (OBJID_MENU) as a second parameter.

13. Using the read primitive leak, the memory addresses are as follows:

  • first read: the original spMenu pointer + 0x50 to obtain tagWND
  • second read: result of first read + 0x10 to obtain THREADINFO
  • third read: result of second read + 0x00 to obtain pTEB
  • fourth read: result of third read + 0x220 to obtain pEPROCESS

    Figure 12. Using read primitive to obtain pEPROCESS
    Figure 12. Using read primitive to obtain pEPROCESS

In Figure 12 it is illustrated how the pointer to EPROCESS is obtained, using the read primitive on tagWND offsets.

14. Iterates through the process list to find process that have the PID (process identifier) = 4 (SYSTEM process PID = 4).

This is done using the previously obtained pEPROCESS address as the starting point.

The EPROCESS.ActiveProcessLinks field points to the ActiveProcessLinks field of the next EPROCESS node. Therefore, the calculus in the picture below will make the iteration possible.

Figure 13. Code snippet – Obtain SYSTEM token
Figure 13. Code snippet – Obtain SYSTEM token

15. At this step, it is important to remember that wndMax is still a normal window unlike wndMin and wndMagic that were converted to console window. This allows the cyberattacker to obtain the kernel write primitive because will write to any address present in wndMax.pExtraBytes.

Call SetWindowLongPtrA(wndMin, 0x128 offset (pExtraBytes) + difference between the kernel desktop heap base offset of wndMax and wndMin, tokenAddressOfCurrentProcess). This will write to wndMax.pExtraBytes the token address of the current process. At this point, a call of SetWindowLongPtrA on wndMax with 0 as second parameter will write to the address placed in wndMax.pExtraBytes that will modify the token of the current process as shown in Figure 14.

16. Call SetWindowLongPtrA(wndMax, 0, systemToken) to write the system token to the address in wndMax.pExtraBytes (that was set to be the token address of the current process). This will allow escalation of privilege.

Figure 14. Code snippet – Writing token and breakpoint address
Figure 14. Code snippet – Writing token and breakpoint address

In Figure 14 the code snippet is illustrated that modifies the token of the current process – with SYSTEM token and the breakpoint address after the exchange is done.

Figure 15. Kernel debugging – Token exchange process
Figure 15. Kernel debugging – Token exchange process

In Figure 15 illustrates how the token exchange process succeeded, and the current process runs as NT AUTHORITY\SYSTEM.

17. New processes can be created having the system token and the escalation of privilege is achieved.

18. Restore the modified window objects to prevent a blue screen of death due to kernel mode heap corruption.

Patch analysis

In the Windows January 2022 patch update, the tagWND.dwExtraFlag (tagWND in kernel layer + 0xE8 offset) will be compared with the ConsoleWindow flag (0x800). If the flag is set before the KeUserModeCallback then it will trigger Windows telemetry.

The allocation will succeed if the flag is not set after the callback as shown in Figure 16.

Figure 16. Patch analysis on win32k!xxxClientAllocWindowClassExtraBytes
Figure 16. Patch analysis on win32k!xxxClientAllocWindowClassExtraBytes

Different techniques and conclusions

In our investigation we found different techniques used in samples related to CVE-2021-1732.
Usually, malware developers obfuscate their code and pack their samples to prevent detections and avoid being caught by Yara rules used for hunting.

We found different string manipulations of key words used in these CVEs such as NtUserConsoleControl and NtCallbackReturn. Also, we observed diverse approaches regarding the user32!_xxxClientAllocWindowClassExtraBytes function as KernelCallbackTable + 0x7B * 8, or as KernelCallbackTable[0x7B].

The exploit is mainly targeting x64-bit systems where pointer size is 8 bytes therefore the calculus will locate the mentioned function via KernelCallbackTable. Multiple packed samples we investigated turned out to be exploits based on proof of concepts of CVE-2021-1732 after memory dump analysis.

We consider the moment when the vulnerability is ready to be exploited is when step 7 from the previous section is complete. From that moment, different techniques can be used to obtain a token with high privileges and create an EOP process.

CVE-2022-21882 follows the same exploit flow as CVE-2021-1732, both being based on exploitation of user32!_xxxClientAllocWindowClassExtraBytes callback. The way in which the two CVEs can be differentiated is based on the call stack. When CreateWindowEx will call user32!_xxxClientAllocClassExtraBytes that was replaced with a custom function which calls NtUserConsoleControl and NtCallbackReturn then the exploit is based on CVE-2021-1732.

In other cases, the custom function is called by any other function except CreateWindowEx is CVE-2022-21882.

Having a similar flow implies that different techniques used in exploits based on CVE-2021-1732 could be also used in CVE-2022-21882.

Avira helps protect against malicious executables that exploit this vulnerability, through dynamic and static detection techniques, preventing payload execution. This research highlights the need to keep your devices up to date with the latest security updates!
Vulnerabilities and exploits are a continuous threat. At Avira’s Threat Protection Lab, we are constantly monitoring exploitation activities and analyzing the latest vulnerabilities to help provide advanced protection and detection capabilities for our customers.

References:
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-21882
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-1732
https://github.com/KaLendsi/CVE-2022-21882
https://github.com/sam-b/windows_kernel_address_leaks/blob/master/HMValidateHandle/HMValidateHandle/HMValidateHandle.cpp
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlongptra
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmenubarinfo
https://keramas.github.io/2020/06/21/Windows-10-2004-EPROCESS-Structure.html