Analysis of CVE-2022-21882 "Win32k Window Object Type Confusion Exploit"
I wanted to write this blog to show the analysis I did in the context of developing the Core Impact exploit “Win32k Window Object Type Confusion” that abuses the CVE-2022-21882 vulnerability.
It’s based on the existing Proof of Concept (POC), which is both interesting and quite complex.
It may be difficult to understand everything that is happening by just reading the blogpost. I encourage readers to treat this as an interactive guide and to use a debugger to check what they are reading.
The Vulnerability
To exploit the "type confusion" vulnerability, the attacker first intercepts the KernelCallbacktable located in the user space and replaces the entry corresponding to user32!xxxClientAllocWindowClassExtraBytes with its own malicious version.
In the POC, it is able to write to and change the permissions of the KernelCallbackTable and then it replaces the address of the original xxxClientAllocWindowClassExtraBytes with its own version.
The original function user32!xxxClientAllocWindowClassExtraBytes assigns the size provided in the user space and returns the address to this assignment.
Instead of doing this, the malicious version of xxxClientAllocWindowClassExtraBytes changes the window style to console mode using the NtUserConsoleControl function and replaces the return value with an offset, which is not checked when returning to the kernel, causing type confusion.
The Patch
The patch was performed on win32kfull!xxxClientAllocWindowClassExtraBytes. When the program returns to the kernel from the malicious xxxClientAllocWindowClassExtraBytes, it checks whether flag tagWND+0xE8 (ExStyle2) has been set to 0x800 to indicate the change to a console window style and does not use the provided offset return value.
The Diff
The vulnerable version does not check the windows style and continues using the malicious offset returned, producing a “type confusion.”
The patched version checks the style and does not use the malicious return offset value.
The Proof of Concept
The author of the public POC has done an excellent job. However, while the POC is very reliable, it can also be a bit challenging to fully understand since many structures are not documented.
In this next section, I’ll walk through how the POC works and will also add in the directions and values of my own attempt. This will provide some clarification, making it easier to understand the relationship between complex structures that point to one another.
This document was written testing the exploit on Windows 10 21h1 build 19043.1110. The exploit works as this build is on the latest versions of Windows 10 prior to the January 2022 patch.
The steps that the POC performs to achieve the elevation of privilege are as follows:
1. Get HMValidateHandle Address.
Initially, because the user32.dll module does not export HMValidateHandle, it obtains the address of the exported function IsMenu and uses its address as a starting point to find the byte 0xe8 of the call to HMValidateHandle and calculate its address.
g_pfnHmValidateHandle= 00007FFD9B26EE40 //HmValidateHandle
2. Get Some Useful Exported Functions Addresses.
Next, it retrieves the exported functions addresses of NtUserConsoleControl and NtCallbackReturn, which will be used later on.
g_pfnNtUserConsoleControl = 00007FFD9A252A70 //NtUserConsoleControl g_pfnNtCallbackReturn=00007FFD9C64CEB0 // NtCallbackReturn
3. Get KernelCallbackTable Address.
It then gets the PEB address of gs:0x60 (TEB offset 0x60) and retrieves the KernelCallbackTable address in the offset 0x58 of the PEB structure.
KernelCallbackTable = 00007FFD9B2F1070 //offset 0x58 en PEB
// user32.dll!apfnDispatch typedef struct _KERNELCALLBACKTABLE_T { ULONG_PTR __fnCOPYDATA; ULONG_PTR __fnCOPYGLOBALDATA; ULONG_PTR __fnDWORD; ULONG_PTR __fnNCDESTROY; ULONG_PTR __fnDWORDOPTINLPMSG; ULONG_PTR __fnINOUTDRAG; ULONG_PTR __fnGETTEXTLENGTHS; ULONG_PTR __fnINCNTOUTSTRING; ULONG_PTR __fnPOUTLPINT; ULONG_PTR __fnINLPCOMPAREITEMSTRUCT; ULONG_PTR __fnINLPCREATESTRUCT; ULONG_PTR __fnINLPDELETEITEMSTRUCT; ULONG_PTR __fnINLPDRAWITEMSTRUCT; ULONG_PTR __fnPOPTINLPUINT; ULONG_PTR __fnPOPTINLPUINT2; ULONG_PTR …………… ………… } KERNELCALLBACKTABLE;
4. Read and Store the Old Addresses of the Function xxxClientAllocWindowClassExtraBytes to be Replaced.
The existing address of xxxClientAllocWindowClassExtraBytes is read and stored at the offset 0x3d8 of KernelCallbackTable.
g_oldxxxClientAllocWindowClassExtraBytes = 00007FFD9B287830 //offset 0x3d8 in kernelcallbacktable
It simply replaces the function xxxClientAllocWindowClassExtraBytes with the malicious version. It will not replace xxxClientFreeWindowClassExtraBytes since it will not use it.
5. Allocate Five Chunks to Use Later in Leaking Addresses.
These five chunks are all different sizes. Later, we’ll see how they help with the leak of kernel addresses.
When we encounter the kernel address leakage method, I will pause to explain the values of certain fields that are set here.
Allocated chunk addresses:
g_pMem1 000001DF649FFFD0 size 0x200 g_pMem2 000001DF649FF650 size 0x30 g_pMem3 000001DF649FF6B0 size 4 g_pMem4 000001DF649FF6F0 size 0xa0 g_pMem5 000001DF649FF7C0 size 8
6. Create Windows.
The POC then creates ten windows in a loop using CreateWindowEx. However, it only uses the first two windows created so that two windows are created in close memory proximity.
Inside the function win32kfull!xxxCreateWindowEx, there are three interesting points to place a breakpoint for additional analysis.
It is necessary to specify the Eprocess value of the POC at each breakpoint (/p EPROCESS), to prevent other processes from stopping when passing through these directions.
EPROCESS of my POC = ffffd10f`48518080
-
ba e1 /p ffffd10f`48518080 win32kfull!xxxCreateWindowEx+0x8e0
-
ba e1 /p ffffd10f`48518080 win32kfull!xxxCreateWindowEx+0x972
-
ba e1 /p ffffd10f`48518080 win32kfull!xxxCreateWindowEx+0x1409
The program will then stop at the first breakpoint, as seen in the image.
After the WINDOWS 7 release, the way the tagWND structure is allocated was changed.
Now, an undocumented structure is created first, which I named tagWND_BASE.
Looking inside HMAllocObject, we can see where the isolated allocation of the tagWND_BASE object is made. The object tag_WND with the Uswd label (USERTAG_WINDOW) is stored in the offset 0x28 of tagWND_BASE.
The address of each tag WND_BASE in each cycle can be copied. Note that only the first two need to be copied—the others will not be used.
First_tagWND_BASE= fffffda540834bd0 Second_tagWND_BASE=fffffda542e35150
Next, the program will stop at the second breakpoint.
We have the tagWND address in each cycle in the offset 0x28 of tagWND_BASE. The first two tagWND addresses will also need to be copied.
First_tagWND= fffffda5`4102b7e0 Second_tagWND= fffffda5`41020610
We can verify that in addresses of each tagWND can be found in the offset 0x28 of each tagWND_BASE.
dps fffffda540834bd0 + 0x28 fffffda5`40834bf8 fffffda5`4102b7e0 dps fffffda542e35150 + 0x28 fffffda5`42e35178 fffffda5`41020610
Next, the program will stop at the third breakpoint.
Inside xxxCreateWindowEx, the POC will call win32kfull!xxxClientAllocWindowClassExtraBytes and will call the original function user32!xxxClientAllocWindowClassExtraBytes using the address in KernelCallbacktable that is not yet changed.
It will allocate the size of cWndExtra we have passed (cbWndExtra= 0x20 (32d)) in the user space.
The address of the newly assigned chunk is then returned to the kernel.
We can then copy the address of every assignment that the program makes in each cycle.
After that, this address will be stored in the offset 0x128 of tagWND (ExtraBytes).
It is also worth noting that this offset is stored in the field 0x8 of each tagWND.
I named this undocumented field offset_from_base.
We can also find where offset_from_base is calculated.
To calculate the offset, it subtracts the address of tagWND against a value that is the same in all cycles.
Breakpoint 0 hit
win32kbase!HMAllocObject+0x3dc:
fffffdd9`16853d5c 492b8c2480000000 sub rcx,qword ptr [r12+80h]
kd> r rcx
rcx=fffffda541028dd0
kd> dps r12+80 L1
ffffd10f`45dcce60 fffffda5`41000000 (basis)
This address seems to be the basis where all the tagWNDs are located.
Later, this will help us find the distance between the first two tagWND in kernel, even if we don’t have the kernel addresses.
7. Call HMValidateHandle.
In each cycle of the POC, it also calls the user32.dll! HMValidateHandle, whose address has been previously calculated.
The program also makes a copy of tagWND in the user space using the same structure but not copying kernel pointers. This address is returned by HMValidateHandle.
First_userTAGWND=000001DF64F7B7E0 Second_userTAGWND=000001DF64F70610
All the userTAGWNDs are stored in an array named arrEntryDesktop.
The POC then starts to evaluate the addresses of the first two tagWNDs to see which one has the lowest address in the memory of both and proceeds to sort them out.
First_tagWND= fffffda5`4102b7e0 Second_tagWND= fffffda5`41020610
From here on out, all the variables of each WND in the POC will have the Min and Max added to the end of their names to determine their position in memory.
I will do the same and call WND0 to the WND with the lowest address and WND1 to the one with the highest address. However, in my case they were created in reverse order: WND1 and then WND0.
WND0 Second tagWND created = fffffda5`41020610
WND1 First tagWND created = fffffda5`4102b7e0
This table contains all the important values uncovered so far.
We can check that kerneltagWND – offset_from_base is equal to the same base in either of the two WNDs.
kerneltagWND0 – offset_from_base WND0= fffffda541020610- 20610= fffffda541000000 kerneltagWND1 – offset_from_base WND1= fffffda54102b7e0-2b7e0= fffffda541000000
For this reason, with those offsets, we can calculate the distance between both WNDs. The formula is as follows:
Distance between WND0 and WND1= 0x2b7e0 – 0x20610 = 0xb1d0
We can also check that each KERNEL TAGWND_BASE in its offset field 0x28 has the pointer to KERNEL TAGWND.
WND0
dps fffffda542e35150 + 0x28 fffffda5`42e35178 fffffda5`41020610
WND1
dps fffffda540834bd0 + 0x28 fffffda5`40834bf8 fffffda5`4102b7e0
8. Obtain the Offsets.
The user copy of tagWND does not have kernel pointers but it does have the same offset_from_base values as the kernel version. They are in offset 8.
WND0
dps 1df64f70610 + 8 L1 000001df`64f70618 00000000`00020610 dps fffffda5`41020610 + 8 L1 fffffda5`41020618 00000000`00020610
WND1
dps fffffda5`4102b7e0 + 8 L1 fffffda5`4102b7e8 00000000`0002b7e0 dps 1df64f7b7e0 + 8 l1 000001df`64f7b7e8 00000000`0002b7e0
As we have the addresses of tagWND in the user space, we can obtain the offset_from_base of both WNDs easily.
The POC continues adding Min and Max to the variable name based on the address value.
The minimum offset_from_base is stored in kernel_desktop_heap_base_offset_Min (WND0).
The maximum offset_from_base is stored in kernel_desktop_heap_base_offset_Max (WND1).
9. Destroy Windows That Will Not be Used.
As noted above, the POC only uses the first two windows and destroys the remaining ones.
10.Call NtUserConsoleControl.
As mentioned earlier, the offset 0x128 of tagWND in kernel has stored the allocation address that was made in the user space for the ExtraBytes.
WND0
dps fffffda5`41020610 + 128 L1
fffffda5`41020738 000001df`64a09550
dps 1df64f70610 + 128 L1
000001df`64f70738 000001df`64a09550
WND1
dps fffffda5`4102b7e0 + 128 L1
fffffda5`4102b908 000001df`64a00200
dps 1df64f7b7e0 + 128 l1
000001df`64f7b908 000001df`64a00200
tagWndMin_offset_0x128 = 000001df`64a09550 tagWndMax_offset_0x128 = 000001df`64a00200
These are the values of the WND0 and WND1 ExtraBytes fields before NtUserConsoleControl is called.
The call to NtUserConsoleControl will change the WND0 type to console and changes the type of data saved in ExtraBytes from being a pointer to an offset.
Let’s put a hardware breakpoint on write on the ExtraBytes field.
It will stop here, where it will subtract the kernel pointer at r15=fffffda541028c50, against the base fffffda5'41000000. It will then save the result in the ExtraBytes field 0x128.
fffffda541028c50- fffffda541000000= 00028c50
Additionally, it saves the same offset value in 0x128 of the tagWND in user mode.
Returning from NtUserConsoleControl, it reads the offset value in field 0x128 ExtraBytes from WND0, and stores in a variable tagWndMin_offset_0x128.
Since the POC didn't transform it into a console, it reads the bottom of the WND1 pointer and saves it to tagWndMax_offset_0x128.
We can also see that the field 0xe8 (ExStyle2) of WND0 changes its value, adding 0x800 of the console mode to the original value. Meanwhile, WND1 remains unchanged.
11. Create the Magic Window.
WND0 and WND1 have the ClassName "normal," with the field cbWndExtra=32.
Then, one more window is created with the ClassName "magictype," with a random number in the cbWndExtra field. Let’s call it WND Malicious.
After that, it replaces the original xxxClientAllocWindowClassExtraBytes with the malicious version in the KernelCallbacktable.
When the undocumented NtUserMessageCall function is called, the callback to the malicious function newxxxClientAllocWindowClassExtraBytes is triggered.
This function changes WND MALICIOUS to a console style and returns the offset_from_base value of WND0.
When we reach the kernel version of xxxClientAllocWindowClassExtraBytes, the offset_from_base of WND0 is returned, instead of returning the pointer to the allocation in the user space.
12. Use SetWindowLong to Write.
Now that the WND_MALICIOUS has the offset to WND0 in the ExtraBytes field, every time we call SetWindowLong using the handle of WND_MALICIOUS, it will use the offset of WND0 as the base to write to.
After that, it will write in the offset 0x128 of WND0, the offset of WND0.
Image
It adds 0x10 to the destination to compensate because the function subtracts 0x10 from it before writing in it.
It subtracts 0x138 – 0x10.
It tests the field 0xe8 (ExStyle2) with 0x800.
It gets the base of the offsets, fffffda541000000, and it adds 0x128 and the offset of WND0 to it.
It stores the offset that the POC had in offset 0x128 and then replaces it with the new value that is in r15.
The value of ExtraBytes of WND0 is replaced with the offset to WND0.
Using the same method, it replaces the value in the 0xc8 (cbWndExtra) field with the value 0xFFFFFFF.
WNDO
The tagWND 0xc8 and 0x128 of WND0 are then changed.
13. Leak the Kernel Address.
Now that the wnd0.cbwndextra field has been changed to a very large value (0xFFFFFFF), each time SetWindowLongPtr is called to wnd0, it will write to the adjacent wnd1 in kernel memory.
Using the WND0 handle, it will write to the offset 0x18 of WND1 as it adds the value of 0x18 to the difference between the two offset_from_base WNDs.
It will temporarily alter the value of g_qwrpdesk located in offset 0x18 in order to change to a child window style.
g_qwrpdesk ^ 0x4000000000000000
tagWND1=fffffda54102b7e0
It will then replace the old style value.
Replacing the spmenu (TagWND offset 0x98) with the address of a false spmenu g_pMem4 allocated at the beginning will leak a kernel pointer.
This will also smash the offset 0xA8 of tagWND_BASE with gpmem4 address.
Though r15 has the gpmem4 address and smashes the old value, the old value is returned and will be the leaked kernel pointer.
The following shows the leaked kernel pointer:
According to the SetWindowLongPtrA documentation, the return value will give us the original value in the overwritten offset. That is, the spmenu data structure pointer, which is a kernel memory address. Therefore, we have now leaked a pointer to a spmenu data structure (tagMENU type) in kernel memory and replaced the pointer in WND1.spmenu with a fake spmenu data structure.
14. Create Kernel Arbitrary Read.
After that, it restores the style to the original value.
Then it uses the GetMenuBarInfo function with the MenuBarInfo structure to create a read primitive.
This is the most important part of the read primitive.
It now reads g_pmem3 and the five chunks created at the beginning have pointers to the next chunk to link to each other in the correct offsets. Additionally, there is no access error.
Next, it reads g_pmem1.
Then, it reads g_pmem5.
Next, the pointer to the pTemp table is read.
While in the first attempt only the pmbi.rcBar.left was read, this is a necessary in order to build the read primitive.
The value of pmbi.rcBar.left is returned.
Using pmbi.rcBar.left, a second call is made, subtracting the destination to read with it. The result is stored in the contents of ref_g_pMem5.
It reads the destination value and adds the pmbi.rcBar.left= 0x40 previously read.
The lower part of the address read is stored in tagMENUBARINFO.rcBar.left.
The upper part is stored in tagMENUBARINFO.rcBar.top.
This value is returned, and the read address is reconstructed.
This read value was the content of the destination address we wanted to read using the myRead64 read primitive.
We will then check if the value obtained is the one that we wanted to read.
g_qwExpLoit dq 0FFFFFDA540820780h
It works!
We have a read primitive for any address.
Chaining several reads from the leaked pointer from kernel leaks the EPROCESS value.
Then it browses through all the EPROCESS using the ActiveProcessLinks field and compares the PIDs against my process and the system’s.
It loops until it gets the Token address of my process and reads the value of the System Token using the read primitive and the EPROCESS.
15. Execute Kernel arbitrary write.
First, using SetWindowLongPtrA with the same techniques explained above replaces the direction of WND1. ExtraBytes with my process token address.
It reads the field 0x128. This is not an offset since WND1 is not of the console type.
The r15 value is my token address.
Finally, it writes the system token value in my process token address.
My token will be replaced with the Token system.
PROCESS ffffd10f4505c080
SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 001aa000 ObjectTable: ffffe688bce9ae00 HandleCount: 1885.
Image: System
This is the system Token value in which the last byte is rounded.
16. Become SYSTEM.
With what the POC did, my process was elevated to SYSTEM.
17. Restore Smashed Values.
Finally, it restores all the smashed values.
This concludes my deep dive into the Win32k Window Object Type Confusion Exploit.
To close, I would like to reiterate my amazement at the great work of the author the POC. It performs very well and its exploitation is incredibly clean and complex. It really is a great job—congratulations to the author, KaLendsi.
Explore Other Core Impact Exploits
Core Impact provides up-to-date exploits they need in one place in a robust library designed to enable pen testers to safely and efficiently conduct successful penetration tests.