Local Privilege Escalation in Win32k.sys Through Indexed Color Palettes

December 17, 2019 | The ZDI Research Team

This is the second in our series of Top 5 interesting cases from 2019. Each of these bugs has some element that sets them apart from the more than 1,000 advisories released by the program this year. Today’s blog looks a local privilege escalation in the Windows kernel-mode driver submitted to the program by Marcin Wiązowski.


As sandboxing programs for security purposes becomes commonplace, sandbox escapes become more important. As such, we’re always looking for these types of bugs. We were especially excited when Marcin Wiązowski submitted this innovative Windows 7 local privilege escalation (LPE) bug, together with a full exploit. The bug was great, and his write-up of the vulnerability and exploit were even better. This blog goes through his analysis of the bug that would eventually become CVE-2019-1362.

This vulnerability can be exploited by an unprivileged user to execute code in the context of the kernel and gain SYSTEM privileges.

The Vulnerability

This bug resulted from a lack of validation of parameters to the win32k.sys!CreateSurfacePal kernel function. This function is involved with handling palette objects. A palette object is a kernel-mode object that serves as an array of available colors. It is used in conjunction with certain graphical output devices (displays and printers) that are configured to operate with a set of pre-defined colors.

In the original design of the Windows NT kernel, printer drivers were modules loaded into the kernel. Starting with Windows Vista, Microsoft made a major architectural change: Printer drivers would run in user mode instead of running as part of the kernel. This change was made as a fundamental security enhancement, and the security benefit it brought was quite clear: once moved into user mode, bugs in printer drivers have a much-reduced security impact.

In a sharply ironic twist, however, this architectural change turned out to have an inverse effect on the security of the remaining graphics code that stayed behind in the kernel. Refactoring printer drivers and moving them into user space created an entire new attack surface, consisting of the new interface created between kernel-mode graphics code and the driver code that had now been relocated to user space. Kernel-mode code that once was secure could now be made to fail in various ways through influence from printer driver code now running in user mode, which is far more open to tampering.

In particular, in regards to the win32k.sys!CreateSurfacePal kernel function that is the subject of this bug, the user-mode printer driver architecture made it possible for the first time for malicious code running in user mode to pass an invalid parameter. Typically, an attack would proceed by interfering with the functioning of a legitimate printer driver.

Plan of Attack

We start by hooking the user-mode printer driver.

A user-mode print driver is a DLL that is registered in the system Registry and exports some standardized functions.

For example, the default “Microsoft XPS Document Writer” driver resides in:

           C:\Windows\system32\spool\DRIVERS\W32X86\3\mxdwdrv.dll

and exports the following functions:

When the user calls the gdi32.dll!CreateDCA/W API with “Microsoft XPS Document Writer” as a parameter, the mxdwdrv.dll driver is loaded. Then a callback from kernel to user mode is made, and the mxdwdrv.dll!DrvEnableDriver function is called:

After making the call, the pded parameter returns a pointer to a DRVENABLEDATA structure:

Where c gives a number of items in the pdrvfn table, and pdrvfn table consists of DRVFN records:

The pdrvfn table is located somewhere in the memory of the mxdwdrv.dll driver, and each _DRVFN item describes one of the implemented driver’s functions – where iFunc is one of the values listed in winddi.h header file:

pfn is a pointer to the user-mode code, located somewhere in the mxdwdrv.dll driver.

By overwriting the pdrvfn table, we can redirect any function implemented by the driver to our own piece of code.

Diving Deep

Although we used “Microsoft XPS Document Writer” driver as an example, any print driver, installed in the operating system, can be used. This driver hooking algorithm is “Algorithm 1”:

1 - Call the user-mode winspool.drv!EnumPrintersA/W API with the PRINTER_ENUM_ LOCAL parameter to find any available printer. For each printer, its name is returned. In our example, it is “Microsoft XPS Document Writer”.
2 - Use the winspool.drv!OpenPrinterA/W and winspool.drv!GetPrinterDriverA/W APIs to retrieve the path to the driver DLL (i.e. path to mxdwdrv.dll).
3 - Call kernel32.dll!LoadLibraryExA/W with the LOAD_WITH_ALTERED_SEARCH_PATH flag to load the driver DLL. 4 - Call the driver’s DrvEnableDriver exported function in order to obtain the address of the pdrvfn table.
5 - Use kernel32!VirtualProtect API to make the pdrvfn table writable in memory.
6 - Modify the pdrvfn table as needed. You should save the original contents of the table to be able to call the original function(s).
7 - Call the driver’s DrvDisableDriver exported function. The print driver DLL remains loaded in memory, in its patched state.
8 - Call the gdi32.dll!CreateDCA/W API with printer name obtained in step 1. This call internally loads print driver DLL. However, it is already loaded (and patched by us), so only the DLL’s reference counter will be incremented. This way, we forced the kernel to use the in-memory patched print driver, which redirects needed driver functions to our own code.

For further exploitation, we’ll hook the print driver’s DrvEnablePDEV function. To do this, we’ll modify the _DRVFN record, having iFunc == INDEX_DrvEnablePDEV:

Our plan is to call the original DrvEnablePDEV function from our hooked code and then modify some fields returned in the pdevcaps and pdi records:

The hpalDefault field is a handle to a template palette. This palette must be created by calling a gdi32.dll!EngCreatePalette API in user mode. By default, this palette has 256 entries – where each entry is a 4-byte structure:

In the flGraphicsCaps field, we are going to set the GCAPS_PALMANAGED flag, so the palette given above will be used in the “managed” mode. This flag is not set by default.

The ulNumColors value gives a number of the reserved entries in the managed palette given above. By default, it is equal to 20, so first 10 and last 10 palette entries are reserved – they can’t be modified by the application; the remaining entries (in the middle) can be modified freely.

The ulNumPalReg value gives a number of all entries in the managed palette given above – it is 256 by default.

The Vulnerability in win32k.sys!CreateSurfacePal

A palette object is represented in kernel mode by a _PALOBJ structure, which is not publicly defined (winddi.h contains an empty declaration). However, some information can be found in a ReactOS documentation (although another structure name is used there):

There is a ppalThis pointer in the _PALOBJ structure, which by default points to the structure itself. Palette entries follow the _PALOBJ structure immediately in memory. Note the first entry still belongs to the _PALOBJ structure itself, and is declared as apalColors[1]. The pFirstColor field points to the first palette entry. By default, it just points to the apalColors[1] field.

After making a call to the gdi32.dll!CreateDCA/W API in user mode, a callback from kernel is made to call our hook , the user-mode DrvEnablePDEV function. Then the kernel performs various operations on the data returned to it by the DrvEnablePDEV call. In particular, if the flGraphicsCaps field has the GCAPS_PALMANAGED flag set, the palette returned in the hpalDefault field is used as a template. It is used to create an internal copy of it and this internal copy becomes a device palette. This is performed in a win32k.sys!CreateSurfacePal function, which uses also our ulNumColors and ulNumPalReg values. In the newly-created device palette, the palette entries (that are going to become reserved entries) are initialized by setting their peFlags values to 0x30.

The algorithm of the win32k.sys!CreateSurfacePal function (Algorithm 2) is:
1 - CreateSurfacePal accepts the following parameters:
       -- A pointer to the kernel memory of our hpalDefault palette – i.e. pointer to the palette’s _PALOBJ structure in kernel memory. Let’s name this pointer palSrc.
       -- ulNumColors (number of reserved entries)
       -- ulNumPalReg
2 - Call win32k.sys!PALMEMOBJ::bCreatePalette to create a new palette by using our palSrc palette as a template. The new palette will become a device palette; let’s name it palDst.
3 - Initialize reserved entries in palDst with 0x30 peFlags value. The pseudocode is as follows:

4 - Call win32k.sys!XEPALOBJ::vCopyEntriesFrom, to copy all the palette entries back from palDst to palSrc. On x64, this call is inlined, so only win32k.sys!memmove is called.
5 - Call win32k.sys!XEPALOBJ::ulTime, also inlined on x64, to copy the palSrc->ulTime value to:
       -- palDst->ulTime field
       -- palDst->ppalThis->ulTime field (only when palDst->ppalThis != palDst).

Under normal circumstances, CreateSurfacePal would be called with parameters as follows:
hpalDefault being a handle to a palette with 256 entries
ulNumColors (number of reserved entries) = 20
ulNumPalReg = 256

So the code, described in step 3 sets peFlags values to 0x30 in the first 10 and last 10 entries of the palDst palette:

The problem with the code described in step 3 is that the algorithm doesn’t validate the ulNumColors parameter (indicating the number of reserved entries) against the true number of palette entries, which is palSrc -> cEntries. Having hijacked a printer driver in user mode, an attacker can pass an out-of-range ulNumColors value, causing out-of-bounds memory writes.

Assuming the default palette size of 256 entries, the attacker should pass ulNumColors value of 514:

As seen above, one entry below the range and one entry above the range will be modified by setting the highest byte of the PALETTEENTRY record (which is a DWORD) to 0x30.

One entry below the range contains a palDst->ppalThis field, which allows us to alter the ppalThis pointer by setting its highest byte to 0x30.

One entry above the range – fortunately – is always unused. When allocating memory for the palette object, the following calculation is made:
       sizeof(_PALOBJ) + sizeof(PALETTEENTRY) * NumberOfEntries

However, the_PALOBJ structure contains one entry (the apalColors[1] field), so one more entry than needed is always allocated.

Exploitation on x86

On x86, palDst->ppalThis is a 4-byte value. By overwriting its highest byte with 0x30, we set it to a value having the form 0x30XXXXXX. This represents a user-mode address. The first usage of the overwritten palDst->ppalThis pointer occurs is in the CreateSurfacePal function itself. It is used to write the ulTime field. This is step 5 of Algorithm 2 described above.

If we want to just crash the operating system, it’s enough to make sure that the entire memory range from 0x30000000 to 0x30ffffff is inaccessible.

We will now consider how this vulnerability can be used to achieve escalation of privilege.

When using a printer driver without any manipulations, the order of operations is as follows:
1 - Application code calls gdi32.dll!CreateDCA/W, which finds and loads a printer driver DLL.
2 - The printer driver DLL creates a template palette by calling gdi32.dll!EngCreatePalette.
3 - The kernel makes a callback to user mode and calls the printer driver’s DrvEnablePDEV function. At this point, DrvEnablePDEV can specify a handle to the template palette to be used. The kernel will use this handle to obtain a pointer to palette’s kernel memory. In CreateSurfacePal, this will be palSrc.
4 - If the DrvEnablePDEV call returned the GCAPS_PALMANAGED flag, the kernel creates a driver palette based on the template palette. In CreateSurfacePal, this will be palDst. In the template palette palSrc, the kernel sets the hSelected field to point to the newly created device palette:

        palSrc->hSelected = palDst

5 - Now usual printing actions are performed.
6 - The user calls gdi32.dll!DeleteDC.
7 - The kernel makes a callback to user mode and calls the printer driver’s DrvDisablePDEV function. The printer driver DLL deletes the template palette by calling gdi32.dll!EngDeletePalette. Because the template palette references the device palette in its hSelected field, the device palette (which we also know as palDst) is deleted automatically.
8 - The print driver DLL is unloaded.

Let’s now focus on the “device palette is deleted automatically” part. In practice, the device palette’s palDst -> ppalThis field points to a _PALOBJ record, and it is used as follows:
      a: To access the BaseObject field. This field contains a handle to the palDst palette at the beginning of the BaseObject structure. The palette’s handle is passed to win32k.sys!HmgRemoveObject, which requires the palette’s reference counter to be 1. The reference counter is managed internally by the operating system.
      b: After the successful call to HmgRemoveObject, the ppalThis pointer is passed to a win32k.sys!FreeObject call, which in turn passes the pointer to win32k.sys!ExFreePoolWithTag to deallocate the object.

By triggering the vulnerability, we are able to redirect the ppalThis pointer to user-mode memory. If at that address we can spoof _PALOBJ structure that is “valid enough”, we can cause this user-mode address to be passed to the ExFreePoolWithTag, which is our goal for further exploitation.

To be “valid enough”, our fake, user-mode _PALOBJ structure must be filled with zeroes, with the following exceptions:
      a: The BaseObject field must contain a valid handle to a palette that has its reference counter equal to 1.
      b: The ppalThis field must point to the beginning of our fake structure to avoid further recursion in the objection destruction algorithm.

There are two problems to overcome. The first is that the overwritten ppalThis pointer points to some 0x30XXXXXX location, but we don’t know where exactly. Fortunately, it’s enough to fill the entire memory range from 0x30000000 to 0x30ffffff memory range with zeroes in the initial preparation phase. Then, as already described in step 5 of the Algorithm 2, the CreateSurfacePal function will write a non-zero timestamp to the ulTime field by referencing the already overwritten ppalThis pointer. We can then scan the memory range for the non-zero DWORD value. This reveals the exact location of our spoofed user-mode _PALOBJ structure.

The second problem is that the BaseObject field of our fake _PALOBJ structure must contain a handle a palette having its reference counter equal to 1. Under normal circumstances, this should be just a handle to the kernel-created device palette palDst, but we don’t know the handle value of this palette. Fortunately, we can use any other palette instead. The question then becomes where to obtain a palette with reference counter equal to 1. Interestingly, such a palette can be obtained by using our hooked printer driver DLL one more time. We can make a second call to gdi32.dll!CreateDCA/W and pass a newly-created template palette to the kernel, this time without performing any additional manipulations. After returning from the CreateDCA/W call, our template palette will have its reference counter equal to 1 as long as we don’t call gdi32.dll!DeleteDC. We can now place the handle of this palette in the BaseObject field of our fake _PALOBJ structure.

We are now able to pass an address of our user-mode _PALOBJ structure to the kernel ExFreePoolWithTag call, which is definitely an unusual situation. Under normal circumstances, ExFreePoolWithTag handles only blocks of kernel memory. By surrounding our fake user-mode _PALOBJ structure by two fake pool headers, we can trick the ExFreePoolWithTag internals into writing a semi-controlled value to a fully controlled kernel memory address. This is, of course, exactly what we want to do.

The ExFreePoolWithTag function works on blocks of so-called pool memory. Describing all the details of pool memory exploitation would be far beyond the scope of this blog, and furthermore, it couldn’t be done better than it was already done in the classic work “Kernel Pool Exploitation on Windows 7” by Tarjei Mandt [PDF]. So, only the main points will be highlighted here:
      -- Each block of pool memory (with the exception of big blocks, but those are not relevant here) is preceded by a POOL_HEADER record. We need to spoof a pool header before our fake _PALOBJ structure.
      -- When freeing a block of memory, the contents of its pool header are verified with contents of the next pool header. Therefore, we need to also create an additional fake pool header above our fake _PALOBJ structure.
      -- There is a field in the pool header (PoolIndex field) that contains an index of the pool to which the block of memory belongs.
      -- Under normal circumstances, 4 pools are available in the operating system, although up to 16 pools can be theoretically allocated. Each pool is managed by using its descriptor, a POOL_DESCRIPTOR record.
      -- We are going to use a pool exploitation technique called “PoolIndex Overwrite”, although in our case, the “overwrite” part is not a trick since our fake pool headers are located in user memory, so we can write to them at will. We’ll set the PoolIndex field in our fake pool headers to 15. The kernel uses a table to convert PoolIndex values into addresses of the corresponding POOL_DESCRIPTOR records. Since only 4 pools are allocated, a PoolIndex of 15 will be translated into a null pointer.

On a default install of Windows 7 on x86, it’s possible to allocate a null page (memory starting at address 0) by calling the ntdll.dll!NtAllocateVirtualMemory native API. (As an aside, note that null page allocation may be enabled or disabled by using the EnableLowVaAccess of registry key HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory.) This allows us to allocate a fake POOL_DESCRIPTOR structure there. For our purposes, it’s enough to initialize the PendingFrees, PendingFreeDepth, and ListHeads fields of this structure as described in the aforementioned work “Kernel Pool Exploitation on Windows 7”. This will lead to the ability to overwrite kernel memory of our choice with a semi-controlled value.

Depending on the amount of physically installed RAM memory, the operating system either returns blocks of memory being freed to the pool immediately, or in larger bursts, known as “delayed frees”. Depending on that variable, by using our crafted POOL_DESCRIPTOR structure, we can either overwrite kernel memory of our choice with an address of the memory block being freed (a DWORD of the form 0x30XXXXXX, in our case), or with an address of some user-mode memory block that was allocated when preparing our crafted POOL_DESCRIPTOR structure (which would be some value from range roughly from 0x00010000 to 0x7fff0000). Our last non-obvious task is to find a good target in kernel for overwriting with some value that is mostly unknown but guaranteed to be at least 0x10000.

With such constraints, a good choice is a palette object. Palette entries can be written to by using the gdi32.dll!SetPaletteEntries API. In its kernel implementation, this API enforces a limit of 0xffff on the index requested to write to. This is because larger palettes cannot be created in user mode. We can create some palette (let’s name it PaletteLO), having, for example, 256 entries. Calling SetPaletteEntries on PaletteLO will succeed only if the requested palette entry to write will be in the range from 0 to 255. We can then use the exploitation method described above to overwrite the cEntries field in the PaletteLO’s _PALOBJ kernel structure with an unknown value that is at least 0x10000. Once we have done this, SetPaletteEntries will work on PaletteLO within the maximum possible range from 0 to 0xffff range. That will allow us to overwrite memory locations of our choice within the 0xffff limit, located above our PaletteLO, and we will be able to fully control the contents via color parameters we pass when calling SetPaletteEntries.

For the next step of the attack, we will want to create some another palette, located a bit above PaletteLO. Let’s name it PaletteHI. We will then call SetPaletteEntries on PaletteLO to set the pFirstColor field in the PaletteHI’s _PALOBJ kernel structure with a pointer of our choosing. Since pFirstColor indicates the base address of a palette’s color array, subsequent calls to SetPaletteEntries on PaletteHI will overwrite memory pointed to by the pFirstColor value with fully controlled contents. At this point, it’s really game over. We have gained the ability to overwrite any chosen memory location with any chosen contents by calling SetPaletteEntries on PaletteHI.

Our palettes before manipulations look like:

After overwriting the cEntries field in PalleteLO:

After overwriting pFirstColor field in PaletteHI:

To find the kernel addresses of our palettes, we can use a global kernel GDI table that holds information about each GDI object and is mapped in read-only mode into the user address space of each running process. The table contains 65536 entries. The entry structure is not publicly documented, but it’s commonly known as GDICELL:

The meaning of the GDICELL fields are as follows:
      -- KernelAddress: pointer to the kernel memory allocated for the object
      -- ObjectOwnerPid: PID of the object’s owning process
      -- Upper: upper 16 bits of the object’s 32-bit handle value
      -- ObjectType: an enumeration representing the object type
      -- Flags: apparently a field containing some flags
      -- UserAddress: pointer to user memory optionally allocated for the object

To find the kernel address of a palette, it’s enough to use 16 lowest bits of palette’s handle value as an index to the GDI table and then read the GDICELL.KernelAddress field.

To create the PaletteLO and PaletteHI palettes that are close enough in kernel memory, it’s enough to create palette objects in a loop until the location of any two created palettes meets our requirements. When addressing an entry within the 0xffff limit, PaletteLO must be able to overwrite the pFirstColor pointer in the PaletteHI’s kernel memory. Such pairs of palettes can be created easily in practice.

It’s time to draft the full exploitation algorithm:
1 - Make sure that the entire memory range from 0x30000000 to 0x30ffffff is writable and filled with zeroes.
2 - Create a pair of palettes that are close enough: PaletteLO and PaletteHI.
3 - Allocate a null page and prepare a fake POOL_DESCRIPTOR there. Its contents should lead to overwriting the cEntries field in PaletteLO’s _PALOBJ kernel structure. The value written will be unknown but guaranteed to be at least 0x10000.
4 - Find and hook any printer driver DLL using Algorithm 1 above, but without step 8. Hook the driver’s DrvEnablePDEV function.
5 - Calling gdi32.dll!EngCreatePalette to create a template palette having 256 entries. This will be called PaletteSRC.
6 - Instruct the hooked DrvEnablePDEV function to return PaletteSRC, along with ulNumColors parameter equal to 514 and GCAPS_PALMANAGED flag set in the flGraphicsCaps field.
7 - Call gdi32.dll!CreateDCA/W to create an internal device palette with its ppalThis field pointing to some 0x30XXXXXX address. This device palette will be called PaletteDEVICE. The reference counter of the PaletteSRC will become 1.
8 - Scan the memory range from 0x30000000 to 0x30ffffff memory range and find the fake, user-mode _PALOBJ structure there.
9 - Call gdi32.dll!DeleteDC. The reference counter of PaletteSRC will become 0. This allow us to delete it later in step 15.
10 - Call gdi32.dll!EngCreatePalette to create another template palette. This will be called PaletteHELPER.
11 - Instruct the hooked DrvEnablePDEV function to return PaletteHELPER. This time, along with an in-range ulNumColors value, the GCAPS_PALMANAGED flag should be set in the flGraphicsCaps field.
12 - Call gdi32.dll!CreateDCA/W. The reference counter of the PaletteHELPER will become 1, which is exactly what we need.
13 - In our fake, user-mode _PALOBJ structure, set the handle value contained in the BaseObject field to PaletteHELPER. Also set the ppalThis field to point to the beginning of the user-mode _PALOBJ structure.
14 - Prepare a fake pool header before the user-mode _PALOBJ structure, and also another one above it.
15 - Call gdi32.dll!EngDeletePalette on PaletteSRC. Since the hSelected field in the PaletteSRC’s _PALOBJ kernel structure references PaletteDEVICE, PaletteDEVICE will be also deleted. Since PaletteDEVICE’s ppalThis pointer points to the fake _PALOBJ structure in user memory, this user address will be passed to the win32k.sysExFreePoolWithTag. And since we created fake pool headers before and after the fake _PALOBJ, our fake null-page pool descriptor will be used by the ExFreePoolWithTag. This way, the cEntries field in PaletteLO’s _PALOBJ kernel structure will be overwritten with a value that is at least 0x10000.
16 - Call gdi32.dll! SetPaletteEntries on PaletteLO to overwrite the pFirstColor pointer in PaletteHI’s _PALOBJ kernel structure with any chosen address.
17 - Call gdi32.dll! SetPaletteEntries on PaletteHI to overwrite any chosen address with any chosen value.

We now have a write-what-where primitive. Choosing a final target for overwriting will be described below.

Choosing the Memory Location to Overwrite

A process access token is a great choice for an overwrite.

An access token is a kernel object that contains the security context of a process or thread. This includes the identity of the user account that started the process or thread and privileges of that user. Currently, 34 possible privileges are defined:

These privileges may be present or absent. They may also be enabled or disabled. A privilege that is present in the token may get disabled and then reenabled again. It may be also removed, so it becomes absent and cannot be made present anymore so that a process cannot elevate its privileges. Making privileges present is possible only during creation of the token, and creating a token is available only for highly privileged system processes that hold SeCreateTokenPrivilege and SeSecurityPrivilege. If a process is privileged to create a token, it can create a token with any privileges that it wants. Such privileges can be used later to launch a process as any user.

To exploit the vulnerability, a kernel address of a token structure must be obtained. To achieve this, a handle to the token must be obtained first by calling advapi32.dll!OpenProcessToken. Then the internal API ntdll.dll!NtQuerySystemInformation can be used to obtain the kernel address of the token structure. The simplified token’s structure is:

The Privileges_Present field is a 64-bit bitfield that determines present and absent privileges. Currently, only privileges from 2 to 35 are defined, so only these bits are in normal use.

The Privileges_Enabled field is a 64-bit bitfield that determines which present privileges are enabled and which are disabled.

To elevate privileges, we’ll set the pFirstColor field of PaletteHI to point to the TOKEN.Privileges_Present field. Then we’ll call gdi32.dll!SetPaletteEntries on PaletteHI to write to four consecutive palette entries with indices from 0 to 3. The new contents of these palette entries should contain all bits set to 1. This will make all privileges present and enabled. Setting undefined privileges doesn’t cause any problems, although they can be removed by using advapi32.dll!AdjustTokenPrivileges.

Now we have all possible privileges, but still some things we cannot do, as we are still a standard user. The next step is to make use of the SeCreateTokenPrivilege and SeSecurityPrivilege privileges we now have. Using these privileges, we can create any token we want using the undocumented API ntdll.dll!NtCreateToken. In fact, we can create a token representing the most powerful SYSTEM user with all privileges present – a more powerful token than usual SYSTEM tokens. The operating system itself uses SYSTEM tokens, that have some privileges absent. Then, having the most powerful token and also the SeAssignPrimaryTokenPrivilege privilege, we can use advapi32.dll!CreateProcessAsUserW to create the most powerful process that can exist.

Since we have also a SeTcbPrivilege, we can change the SessionId field of the newly-created token. It is set to 0 by default, so a process created by using this token works like system services and cannot interact with a desktop. After setting the token’s SessionId to the SessionId of some logged-in user, the newly created process can draw windows on the user’s desktop. This is nice for demonstration purposes, although it is not required to seize the operating system.

64-bit Exploitation

On 64-bit systems, the palDst -> ppalThis pointer is an 8-byte value. By overwriting its highest byte with 0x30, we set it to a value of the form 0x30XXXXXX’XXXXXXXX value. Current hardware implementations of the 64-bit architecture support only 48 bits for addressing, and the most significant 16 bits of the virtual address (bits 48 through 63) must be copies of bit 47. As a consequence, the highest 16 bits must be either all 0s or all 1s. By setting highest byte to 0x30, we break this rule, which causes an exception and crash. This means that on 64-bit systems the vulnerability can be exploited only for DoS.

Potentially, it might be possible to exploit this vulnerability for elevation of privileges on Itanium machines with Windows Server installations.

Changes in Windows 8 and Above

According to disassembled win32k.sys (Windows 8 and 8.1) or win32kbase.sys (Windows 10), the vulnerability in CreateSurfacePal function doesn’t exist. The value passed from user mode is verified properly.

It’s also worth noting that since Windows 10 Version 1607 (Redstone 1 “Anniversary Update”), the GDICELL. KernelAddress field no longer holds a direct pointer to kernel memory. You can read more on this in “A Tale Of Bitmaps: Leaking GDI Objects Post Windows 10 Anniversary Edition”.

The Patch

Microsoft addressed this vulnerability in October and assigned this bug CVE-2019-1362. The accompanying bulletin simply states the problem was fixed by “correcting how the Windows kernel-mode driver handles objects in memory.” In all likelihood, the patch backports the behavior from Windows 8 since that version of the OS is not affected.

Conclusion

Thanks again to Marcin Wiązowski for providing such a thorough and well-documented analysis of this LPE. It’s easy to see why this bug (and its analysis) stood out from others when looking back at 2019.

These types of bugs are often combined with other vulnerabilities to forge a complete exploit chain. A similar LPE was recently seen in the wild alongside a use-after-free in Chrome targeting Korean news sites. Although the LPE itself isn’t thought to have critical severity, the pairing with other remote code execution bugs makes for an effective attack.

Stay tuned for the next Top 5 bug blog, which will be released tomorrow. Until then, follow the team for the latest in exploit techniques and security patches.