Skip to main content
Core Security Logo Core Security Logo
  • Contact Us
  • Support
  • All Fortra Products
  • FREE TRIALS
  • Contact Us
  • Support
  • All Fortra Products
  • FREE TRIALS
  • Cyber Threat

      Products

      • Core Impact Penetration testing software
      • Cobalt Strike Red team software
      • Outflank Security Tooling (OST) Evasive attack simulation
      • Event Manager Security information and event management
      • Powertech Antivirus Server-level virus protection
      • Product Bundles

      Solutions

      • Penetration Testing
      • Penetration Testing Services
      • Offensive Security
      • Threat Detection
      • Security Information and Event Management
    • Penetration Testing Services Security consulting services
  • Identity

      Products

      • Access Assurance Suite User provisioning and governance
      • Core Password & Secure Reset Self-service password management
      • Core Privileged Access Manager (BoKS) Privileged access management (PAM)

      Solutions

      • Privileged Access Management
      • Identity Governance & Administration
      • Password Management
    • See How to Simplify Access in Your Organization | Request a Demo
  • Industries
    • Healthcare
    • Financial Services
    • Federal Government
    • Retail
    • Utilities & Energy
    • Higher Education
    • Compliance
  • Resources
    • Upcoming Webinars & Events
    • Blogs
    • Case Studies
    • Videos
    • Datasheets
    • Guides
    • Ecourses
    • Compliance
    • All Resources
  • CoreLabs
    • Advisories
    • Exploits
    • Publications
    • Articles
    • Open Source Tools
  • About
    • Partners
    • Careers
    • Press Releases
    • Contact Us

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.

Image

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.

Image

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.

Image
Image

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.

Image

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.

Image

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.

Image
Image
PIS Menu
Image
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.

Image
callback return
Image
console control
Image
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.

Image
kernel callback table
Image
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.

Image
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.

Image
Image

When we encounter the kernel address leakage method, I will pause to explain the values of certain fields that are set here.

Image

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
 
  1. ba e1 /p ffffd10f`48518080 win32kfull!xxxCreateWindowEx+0x8e0
  2. ba e1 /p ffffd10f`48518080 win32kfull!xxxCreateWindowEx+0x972
  3. ba e1 /p ffffd10f`48518080 win32kfull!xxxCreateWindowEx+0x1409

The program will then stop at the first breakpoint, as seen in the image.

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.

Image

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.

Image
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.

Image

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.

Image

The address of the newly assigned chunk is then returned to the kernel.

Image

We can then copy the address of every assignment that the program makes in each cycle.

Image

After that, this address will be stored in the offset 0x128 of tagWND (ExtraBytes).

Image

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.

Image

We can also find where offset_from_base is calculated.

Image

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.

Image

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
Image

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
Image
min and max

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.

Image

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.

Image

The POC continues adding Min and Max to the variable name based on the address value.

Image

The minimum offset_from_base is stored in kernel_desktop_heap_base_offset_Min (WND0).

Image

The maximum offset_from_base is stored in kernel_desktop_heap_base_offset_Max (WND1).

Image

 

9. Destroy Windows That Will Not be Used.

As noted above, the POC only uses the first two windows and destroys the remaining ones.

Image

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.

Image

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.

Image

 

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.

Image
Image

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
Image
extrabytes

Additionally, it saves the same offset value in 0x128 of the tagWND in user mode.

Image
same offset value

  Returning from NtUserConsoleControl, it reads the offset value in field 0x128 ExtraBytes from WND0, and stores in a variable tagWndMin_offset_0x128.

Image

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.

Image
Image
RAX

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.

Image

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.

Image

After that, it replaces the original xxxClientAllocWindowClassExtraBytes with the malicious version in the KernelCallbacktable.

Image

When the undocumented NtUserMessageCall function is called, the callback to the malicious function newxxxClientAllocWindowClassExtraBytes is triggered.

Image

This function changes WND MALICIOUS to a console style and returns the offset_from_base value of WND0.

Image
offset from base

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.

Image
Image
magic usage

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.

Image

After that, it will write in the offset 0x128 of WND0, the offset of WND0.

Image
Set Window LonfW
Image

It adds 0x10 to the destination to compensate because the function subtracts 0x10 from it before writing in it.

Image

It subtracts 0x138 – 0x10.

It tests the field 0xe8 (ExStyle2) with 0x800.

Image

It gets the base of the offsets, fffffda541000000, and it adds 0x128 and the offset of WND0 to it.

Image

It stores the offset that the POC had in offset 0x128 and then replaces it with the new value that is in r15.

Image

The value of ExtraBytes of WND0 is replaced with the offset to WND0.

Image

Using the same method, it replaces the value in the 0xc8 (cbWndExtra) field with the value 0xFFFFFFF.

Image

 WNDO

The tagWND 0xc8 and 0x128 of WND0 are then changed.

Image

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.

Image
write to adjacent

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
Image

tagWND1=fffffda54102b7e0

Image

It will then replace the old style value.

Image

Replacing the spmenu (TagWND offset 0x98) with the address of a false spmenu g_pMem4 allocated at the beginning will leak a kernel pointer.

Image
spmenu

This will also smash the offset 0xA8 of tagWND_BASE with gpmem4 address.

Image

Though r15 has the gpmem4 address and smashes the old value, the old value is returned and will be the leaked kernel pointer.

Image
Image

The following shows the leaked kernel pointer:

Image

 

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.

Image

Then it uses the GetMenuBarInfo function with the MenuBarInfo structure to create a read primitive.

Image
Image

This is the most important part of the read primitive.

Image

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.

Image

 Next, it reads g_pmem1.

Image

Then, it reads g_pmem5.

Image

 Next, the pointer to the pTemp table is read.

 
Image

While in the first attempt only the pmbi.rcBar.left was read, this is a necessary in order to build the read primitive.

Image
rcBar left
Image

The value of pmbi.rcBar.left is returned.

Image

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.

Image

It reads the destination value and adds the pmbi.rcBar.left= 0x40 previously read.

Image

The lower part of the address read is stored in tagMENUBARINFO.rcBar.left.

Image
rdi tag

The upper part is stored in tagMENUBARINFO.rcBar.top.

Image
Image

This value is returned, and the read address is reconstructed.

Image

This read value was the content of the destination address we wanted to read using the myRead64 read primitive.

Image

We will then check if the value obtained is the one that we wanted to read.

g_qwExpLoit dq 0FFFFFDA540820780h
Image
value obtained

It works!

We have a read primitive for any address.

Image

Chaining several reads from the leaked pointer from kernel leaks the EPROCESS value.

Image
Image

Then it browses through all the EPROCESS using the ActiveProcessLinks field and compares the PIDs against my process and the system’s.

Image

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.

Image
set window
Image
Image
Image
Image
rbx

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.

Image

Finally, it writes the system token value in my process token address.

Image
process token address
Image

My token will be replaced with the Token system.

Image
token

 

PROCESS ffffd10f4505c080
    SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 001aa000  ObjectTable: ffffe688bce9ae00  HandleCount: 1885.
    Image: System
Image
Image

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.

Image

 

17. Restore Smashed Values.

Finally, it restores all the smashed values.

Image
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.

Ricardo Narvaja
Meet the Author

Ricardo Narvaja

Cybersecurity Specialist Developer
View Profile
Related Content
Article
How to Deal with Microsoft Monthly Updates to Reverse Engineer Binary Patches
Article
Reversing and Exploiting Free Tools Series
Article
Analysis of CVE-2021-26897 DNS Server RCE

Explore Other Core Impact Exploits

CTA Text

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.

BROWSE EXPLOITS
  • Email Core Security Email Us
  • Twitter Find us on Twitter
  • LinkedIn Find us on LinkedIn
  • Facebook Find us on Facebook

Products

  • Access Assurance Suite
  • Core Impact
  • Cobalt Strike
  • Event Manager
  • Browse All Products

Solutions

  • Identity Governance

  • PAM
  • IGA
  • IAM
  • Password Management
  • Vulnerability Management
  • Compliance
  • Cyber Threat

  • Penetration Testing
  • Red Team
  • Phishing
  • Threat Detection
  • SIEM

Resources

  • Upcoming Webinars & Events
  • Corelabs Research
  • Blog
  • Training

About

  • Our Company
  • Partners
  • Careers
  • Accessibility

Support

Privacy Policy

Contact

Impressum

Copyright © Fortra, LLC and its group of companies. All trademarks and registered trademarks are the property of their respective owners.