返回 TI 主页

Background

On October 10, 2018, Kaspersky disclosed a Win32k Elevation of Privilege Exploit (CVE-2018-8453) captured in August.

This vulnerability was used as 0day in attacks targeting the Middle East to escalate privileges on the compromised Windows systems. It is related to window management and graphic device interfaces (win32kfull.sys) and could be used to elevate user privileges to system permissions. It can also be used to bypass sandbox protection such as PDF, Office and IE which makes the exploit extremely valuable.

QiAnXin Threat Intelligence Center performed deep analysis of this vulnerability and came up with PoC exploit that could work on part of the affected Windows systems (Both x86 and x64 version of Windows10).

Analysis Environment

The work was performed on Windows 10 x64 Version 1709 with patches before fixing CVE-2018-8453:

Root Cause

This vulnerability is caused by a fault in the win32kfull!NtUserSetWindowFNID function which fails to check whether the window object has been released while setting the FNID. This causes a new FNID to be set for a window that has already been released (FNID_FREED: 0x8000). By exploiting this defect, we can control the fnDWORD callback called in xxxFreeWindow when the window object get destroyed to cause UAF of pSBTrack in win32kfull!xxxSBTrackInit.

About FNID:By checking the leaked source code of WIN2000 and related documentations in ReactOs, we figure out that FNID is used to record what the window looks like, such as a button or an edit box. It can also be used to record the state of the window, for example, FNID_FREED(0x8000) means the window has been released.

POC – How to Trigger the Vulnerability

The vulnerability could get triggered by following steps:

Step 1: We need to hook two callbacks in the KernelCallbackTable first.

Step 2: Create the main window and the ScrollBar.

Step3: Send a WM_LBUTTONDOWN message to the scroll bar to trigger the call to the xxxSBTraackInit function.

Hint: When you perform a left click on a scroll bar, it will trigger the call to win32kfull!xxxSBTrackInit function. After that, function xxxSBTrackLoop will be called to capture mouse events in a loop, until the left mouse button is released or some other messages are received.

Step4: Call DestoryWindow(g_hMAINWND) in callback function fnDWORD_hook when it get executed by xxxSBTrackLoop.

This will result in calling win32kfull!xxxFreeWindow function. Because cbWndExtra is not 0 while registering the main window, this makes win32kfull!xxxFreeWindow to call xxxClientFreeWindowClassExtraBytes function in order to release the extra data which belongs to the main window.

Function in the above picture would execute KernelCallbackTable[126] callback which result in the calling of our second hook.

Step5: After entering our second hook function (fnClientFreeWindowClassExtraBytesCallBack_hook), we must manually call NtUserSetWindowFNID(g_hMAINWND,spec_fnid) to set the FNID of the main window (a value from 0x2A1 to 0x2AA, here we set spec_find to 0x2A2). Meanwhile create a new scroll bar (g_hSBWNDNew) and call SetCapture(g_hSBWNDNew) to set g_hSBWNDNew as the window to capture mouse events in the current thread.

Step6: Since the main window is destroyed, xxxSBTrackLoop will return and continue to execute HMAssignmentUnLock(&pSBTrack->spwndNotify) to perform related dereference that makes the main window get released completely. This will cause xxxFreeWindow to be called again:

From the above picture, we know that once xxxFreeWindow is called, the window's FNID will be marked with 0x8000.

Since the FNID of the main window was set to 0x2A2 in step 5, LOWORD(FNID) would be 0x82A2 (DestoryWindow function that get executed in step 4 called xxxFreeWindow to mark the main window with 0x8000). So SfnDWORD will be executed and then get into our hook through callback fnDWORD.

When get into fnDWORD_hook function again, it is our last chance to come back to R3. At this time, if SendMessage(g_hSBWNDNew, WM_CANCLEMODE) is called, xxxEndScroll (see win2k code as shown below) will be executed to release pSBTrack.

Because the POC program is single threaded, all windows created by the thread point to the same thread information structure. Even if the Scrollbar window that SBTrack belongs to has been released, as long as the new window is created by the same thread, pSBTrack still points to the same one. The condition qp->spwndCapture==pwnd will be satisfied since we are sending the WM_CANCLEMODE message to the newly created scroll bar g_hSBWNDNew, and we have previously called SetCaputure(g_hSBWNDNew) to set the current thread to capture the mouse events in g_hSBTWNDNew window. Finally, UserFreePool(pSBTrack) gets executed to release pSBTrack which makes pSBTrack get released before executing HMAssignmentUnLock(&pSBTrack->spwndSB) and results in Use After Free for pSBTrack.

Exploit on Windows 10 x64

Since we can make the pSBTrack in win32kfull!xxxSBTrackInit get released early to make a Use After Free by hooking callbacks in KernelCallbackTable, pool fengshui technology can be used to occupy pSBTrack that has been released early in order to achieve arbitrary memory value deduction in a loop. It can be used with desktop heap memory [2] leak and GDI Palette Abuse technology to achieve arbitrary memory read/write, and finally to achieve privilege escalation!

Implementation of Arbitrary Memory Value Deduction

From the above analysis, we know that the memory pointed by pSBTrack has been released after calling HMAssignmentUnlock(&pSBTrack->spwndSBNotify).

Continue to the next HMAssignmentUnlock(&pSBTrack->spwndSB), then take a look at the disassembly code of HMAssignmentUnlock and you will find a very interesting place:

Execution of lock xadd dword ptr [rdx+8],eax will perform minus one operation to the DWORD pointed by rdx+8. After debugging the code, we figure out that pSBTrack->spwndSB is assigned to* rdx*!

So, if we can control the value of pSBTrack->spwndSB, then we can perform minus one operation on any memory DWORD. pSBTrack is released after we call SendMessage(g_SBWNDNew, WM_CANCELMODE). So if we can allocate an object (such as Bitmap) with the same size as SBTrack immediately and could control the data of the object, there is a great probability that the pool get freed will be reassigned to the object.

Test Results:

Similarly, continue to call HMAssignmentUnlock (&pSBTrack->spwndSBTrack), there will be another arbitrary memory value minus one operation, while the memory is pointed by pSBTrack->spwndSBTrack+8.

So we can reduce the arbitrary memory value by one or two through controlling the data in the Bitmap that get sprayed into the space previously used by pSBTrack. Minus one operation only requires either pSBTrack->spwndSB or pSBTrack->spwndSBTrack to be 0, and the other one to be address - sizeof(PVOID).

As long as we repeatedly trigger this process, we can reduce the memory value by one or two for many times in order to change the value to a specified number.

result = target - repeat_count

result = target - repeat_count * 2

Obviously we have to know the original value first in order to make it reduced to the value we want. Therefore, there are some limitations when compared with setting the value directly.

Hint: If we need to change 0x02000000 to 0x00000000, do we need to repeat the minus two operation for 0x01000000 times? The answer is no. Because we are able to deduct arbitrary memory DWORD value by one or two, the memory address could be adjusted to turn "0x02" into a low Byte in the DWORD. Then it becomes to change 0x00000002 to 0x00000000, here just need one loop and no need to worry about the loop count limitations.

Use the GDI Palette to Achieve Arbitrary R/W

Below is the documented PALETTE data structure:

typedef struct _PALETTE64
{
    BASEOBJECT64      BaseObject;    
    ...
    ULONG64       pRGBXlate;    
    PALETTEENTRY    *pFirstColor; 
    struct _PALETTE *ppalThis;     
    PALETTEENTRY    apalColors[3]; 
}

Member apalColors is an array. Each member in the array is 4 bytes in size and the content can be specified by user. pFirstColor, similar to the pvScan0 pointer in the Bitmap, is pointed to the array and could be used to construct the R/W primitive. The following relationship is satisfied and by using this we can know the initial value of the memory pointed by pFirstColor:

Address of PALETTEENTRY = Address of pFirstColor + sizeof(PVOID)*2

Similar to manipulating data in the Pixel area by Bitmap through GetBitmapBits and SetBitmapBits, PALETTE will use GetPaletteEntries and SetPaletteEntries to manipulate the data pointed by the pFirstColor.

So we can construct two Palettes, named as hManager and hWorker respectively:

If we can get the value of hManager's pFirstColor and hWorker's pFirstColor, then we can use the above arbitrary memory value deduction approach to reduce the hManager->pFirstColor value to the same as hWorker's pFirstColor. After that we can use hManager to call SetPaletteEntries to control hWorker->pFirstColor, then use hWorker to call SetPaletteEntries and GetPaletteEntries to achieve arbitrary memory read/write.

Fortunately, we can use the following techniques to stabilize the value of hManager's pFirstColor and hWorker's pFirstColor, and make hManager's pFirstColor value not quite larger than hWorker's pFirstColor value.

Use the Desktop Heap to Leak GDI Palette Address

Since the name of window menu could be quite long, lpszMenuName and Palette are in the same memory pool, and we can get the kernel address of lpszMenuName through the tagWND pointer returned by HmValidateHandle, we can use the desktop heap[2] to help us predict the kernel address of the pFirstColor pointer. With proper construction, the accuracy rate could reach to 100%.

First we need to repeatedly create and delete a window object to allocate and release a pit. When the address becomes unchanged, it means the next time you construct a Palette object with a size equal to lpszMenuName, the Palette object will be allocated at the address of the lpszMenuName that has just been released:

Then we can get the kernel address of pFirstColor by using its offset inside _PALETTE64:

hManager->pFirstColor can be changed to hWorker's pFirstColor value by using the above arbitrary deduction operation in order to achieve arbitrary memory read/write.

Privilege Escalation by Arbitrary Memory R/W

Since arbitrary memory read/write is available at this moment, we could enumerate EPROCESS chain to get the token value of the system process as well as the token address of the current process. Then we could perform privilege escalation by copying the token value from the system process to the current one.

How to get the EPROCESS of the system at the user level? You can get it by looking up PsInitialSystemProcess[3] in ntoskrnl.exe:

Code to get _EPROCES of the current process:

Use arbitrary memory read/write to copy Token:

Exploit Process in Summary

QiAnXin Threat Intelligence Center summarized the entire process as follows:

  • Get the pFirstColor value of hManager and hWorker by using desktop heap leak technology

  • Triggering the vulnerability multiple times to change the value of hManager->pFirstColor to the value of pFirstColor in hWorker

  • Perform privilege escalation by arbitrary memory read/write

  • Using arbitrary memory read/write to spoof the operating system not to clean up the Bitmap object. Without this step, the system will release the Bitmap object when the program gets closed. It will cause a Double Free and result in Blue Screen.

Screenshot:

Patch Analysis

By using Bindiff, we find that IsWindowBeingDestroyed is called to check if the window has been released before setting a new FNID in the patched version of win32kfull!NtUserSetWIndowFNID. It will return directly if the window object has been released, and will not allow setting a new FNID value. So when we call DestoryWindow, we will fail to call NtSetUserWindowFNID to set FNID. The vulnerability gets fixed since this approach prevents us from releasing pSBTrack in advance.

Conclusion

After investigations, we come up with PoC exploit on Windows 10 pro v1709 x86/x64 and perform privilege escalation successfully when the system is not patched. For other Windows versions, only need to change offsets of corresponding data structures, such as the offset of Token inside _EPROCESS.

References

[1].https://securelist.com/cve-2018-8453-used-in-targeted-attacks/88151/

[2].https://blogs.msdn.microsoft.com/ntdebugging/2007/01/04/desktop-heap-overview/

[3].https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/mm64bitphysicaladdress

[4].https://mp.weixin.qq.com/s/ogKCo-Jp8vc7otXyu6fTig

[5].https://www.anquanke.com/post/id/168572#h2-1

[6].https://www.anquanke.com/post/id/168441#h2-0

[7].ed2k://|file|cn_windows_10_multi-edition_vl_version_1709_updated_sept_2017_x64_dvd_100090774.iso|4630972416|8867C5E54405FF9452225B66EFEE690A|/

CVE-2018-8453 0DAY VULNERABILITY