Local Privilege Escalation in Win32k.sys Through Indexed Color Palettes
December 17, 2019 | The ZDI Research TeamThis 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.