CVE-2021-27077: Selecting Bitmaps into Mismatched Device Contexts

July 28, 2021 | Guest Blogger

In March 2021, Microsoft released a patch to correct a vulnerability in the Windows GDI subsystem. The bug could allow an attacker to execute code with escalated privileges. This vulnerability was reported to the ZDI program by security researcher Marcin Wiązowski. The patch for CVE-2021-27077 addresses several of his bug submissions. He has graciously provided this detailed write-up of the vulnerabilities and an analysis of the patch from Microsoft.


To handle devices on which drawing can be performed (pixel-based devices), Windows provides so-called device contexts (DCs). A device context object encapsulates a device object where a device object represents either a screen or a printer. Device contexts are an abstraction layer between user-mode code and low-level device drivers. They also act as containers, referencing various graphic objects used during drawing operations, such as pens, brushes, bitmaps, palettes, regions, and paths. To establish a connection between a device context and some other graphic object, the user-mode code calls SelectObject. For example:

In this example, we create a screen-related device context and three other GDI objects: a region, a font, and a bitmap. By selecting them into the device context, we cause them to be used during the ExtTextOut call below. This call will draw text on our TestBitmap by using our TestFont, and the drawing area will be clipped by our TestRegion.

For us, the important facts about device contexts are:

1 - They can be either screen-related or printer-related.
2 - To reference the screen or printer, each device context internally keeps a handle, referred to as hdev. Internally, a device context object resides in kernel memory, and its hdev field is a pointer to another object residing in kernel memory, this one being a device object.

Bitmaps selected into device contexts

Bitmaps are represented in kernel memory as surface objects. The partial structure definition shown here includes some fields documented by Microsoft, as well as some undocumented fields that have been reconstructed:

As we can see, bitmaps can contain a hdev value. This value is initially set to NULL, but selecting the bitmap into a device context copies the hdev value from the device context to the bitmap’s hdev field.

Now let’s do an experiment:

In this example:

1) We create one printer-related device context (DCPrn) and one screen-related device context (DCScr). For the printer DC, we used the printer named “Microsoft XPS Document Writer” which is available by default, but any installed printer could be used instead. If necessary, the EnumPrinters API could be used to get the names of all installed printers.
2) We create a bitmap (Bitmap). Note that it’s a monochrome (1 bit deep) bitmap, so it’s compatible with any device context. This includes 1-bit or 8-bit printer-related DCs and 32-bit screen-related DCs.
3) We select the bitmap into the printer-related DC. This sets the bitmap’s hdev field so that it points to the printer device object in kernel memory. The variable PrevBitmap is set to the default bitmap that was created together with the printer DC.
4) We deselect our bitmap (by selecting PrevBitmap instead), so we’ll be able to select Bitmap into some other device context again. Bitmaps are specific in that sense. They can be selected into only one device context at a time, so deselecting is required.
5) Finally, we select Bitmap into a screen-related DC. This updates bitmap’s hdev field so that it no longer points to the printer device object, but rather to the screen device object.

This code isn’t malicious, but we’ll make it malicious in the next step. But first, let’s look at some changes that were introduced in Windows 2000.

User-Mode Printer Drivers (UMPD)

Historically, both screens and printers used to be handled by kernel-mode drivers. While there are only a small number of vendors that manufacture graphic cards and write corresponding driver code, the world of printers is quite a different story. Undoubtedly there were plenty of printer manufacturers who introduced numerous security risks and instabilities in their drivers. To make things safer and more stable, Windows 2000 introduced an architecture that allowed printer drivers to work in user mode instead. Both kernel-mode and user-mode printer drivers coexisted until Windows Vista. Since then, Windows has supported only user-mode drivers for printing.

Consequently, only some stubs remained in kernel code for printer handling. These stubs mainly just make callbacks to user mode. In user mode, these calls are first passed to some internal code in user32.dll, then to some more code in gdi32.dll, then to yet more internal code in gdi32full.dll, which finally passes them to user-mode printer drivers for handling:

To provide user-mode drivers with the needed functionalities, some kernel-mode APIs now also have their user-mode API counterparts. One such API is EngAssociateSurface. Display drivers use calls to the kernel-mode function win32kbase.sys!EngAssociateSurface, whereas printer drivers use the user-mode function gdi32.dll!EngAssociateSurface:

For our purposes, we can consider the user-mode EngAssociateSurface function to be an extended version of the SelectObject API. When passing a bitmap as the first parameter, we can set not only the bitmap’s hdev field but also its dhpdev and flags fields. Here are a few things to consider:

1) The hdev parameter will be a valid value obtained from an existing printer-related device context. User-mode code can obtain this value since it’s forwarded by the kernel to the user-mode printer driver. This will be explained below in greater detail. After validation, the passed hdev parameter will be copied to the bitmap’s hdev field.
2) The flHooks parameter may contain any combination of HOOK_XXX flags, documented here. These flags will be set in the bitmap’s flags field.
3) The dhpdev value is not passed as a parameter. However, the kernel maintains its internal list of hdev-dhpdev pairs, so setting the bitmap’s hdev field will also set its dhpdev field.

During device initialization, the device driver can pass a dhpdev value of the driver’s choice to the operating system, thus creating a hdev-dhpdev pair. In practice, the dhpdev value points to a block of driver-allocated memory, which will be either kernel-mode memory for display drivers or user-mode memory for printer drivers. This observation is very important for us.

Whenever executing a GDI graphics primitive (for example, ExtTextOut, which renders text to a DC), there are two code paths that could be chosen. One code path implements the primitive via a call to a corresponding driver function (supposing one exists) that knows how to perform that graphics primitive natively. The other code path uses a generic implementation of the primitive as supplied by win32k. The bitmap’s flags field, containing a combination of values from the HOOK_XXX enumeration, tells the operating system which GDI operations should be handled by the win32k subsystem and which should be directed to a corresponding device driver function. When so requested, the driver is identified by the device context’s hdev field.

Since we used ExtTextOut in our example above, let’s consider the HOOK_TEXTOUT flag. The ExtTextOut GDI function takes a device context as a parameter and passes it to the kernel-mode implementation of GDI. The kernel obtains the bitmap that is selected into this device context and checks if the bitmap’s flags field has the HOOK_TEXTOUT bit set. If this bit is not set, the kernel will pass our request to the generic win32kfull.sys!EngTextOut function to render the text. If HOOK_TEXTOUT is set, though, the kernel will forward our request to the device driver as specified by the bitmap’s hdev field. In this case, execution will be passed in kernel mode to one of three possible places (a few more are used in some specific cases, but they are not interesting for us):

Cdd.dll!DrvTextOut, which is a part of a top-level display driver, used for screen devices when only one monitor is active.
win32kfull.sys!MulTextOut, which is a part of a top-level display driver, used for multi-monitor configurations. This driver is embedded directly in the win32k subsystem.
win32kfull.sys!UMPDDrvTextOut, which is one of the printer stubs in the kernel. It uses a callback to pass the request through to user mode. This allows the request to be handled by the appropriate user-mode printer driver.

We are now ready to make our piece of code malicious. To achieve this, we’ll replace one of the SelectObject calls with a call to EngAssociateSurface call, passing HOOK_TEXTOUT as the flags parameter:

Other modifications are as follows:

1) We no longer need to call CreateCompatibleDC when creating DCPrn. This is because we no longer call SelectObject on DCPrn but rather EngAssociateSurface on DCPrn’s hdev value.
2) Because of internal EngAssociateSurface requirements, we can’t use the CreateBitmap call anymore. We must instead call CreateCompatibleBitmap on DCPrn.
3) We no longer need to deselect our bitmap from DCPrn. This is because there is no SelectObject call on DCPrn but rather EngAssociateSurface on DCPrn’s hdev value.
4) Note that we explicitly call a Unicode (wide) version of the ExtTextOut API. We also provide an ETO_IGNORELANGUAGE flag to this call, along with a very long string. This way, our ExtTextOut request will be passed directly to the kernel mode without any further user-mode handling.

Let’s look at the comments in the code snippet above. The EngAssociateSurface call sets the bitmap’s hdev field so it points to a printer device in kernel mode. As explained above, this will also affect the bitmap’s dhpdev field. As we remember, this value, in practice, points to a block of driver-allocated memory. Since printer drivers are handled in user mode, this will be some block of user-mode memory. Finally, the HOOK_TEXTOUT bit is set in the bitmap’s flags field.

The SelectObject call is then made, which ties the bitmap to DCScr and modifies the bitmap’s hdev field so it no longer points to the printer device but rather to a screen device. Other bitmap fields are not modified. In particular, SelectObject will not alter the dhpdev field.

Finally, we have the ExtTextOutW call on the screen-related DCScr. When handling this call, the kernel will see the HOOK_TEXTOUT flag set in the bitmap’s flags field, so drawing will not be handled by the win32k subsystem. Instead, our request will be passed to the device driver specified by the DCScr’s hdev value, which in our case is the display driver. This way, thanks to our manipulations, the display driver will get the bitmap containing a pointer to some user-mode memory in the bitmap’s dhpdev field while it expects to see a pointer to its own, kernel-mode memory there. We definitely found a security problem.

By preparing a block of user-mode memory appropriately, we can trick the display driver into overwriting kernel memory of our choice. This means privilege escalation is possible. But first, we must perform some interactions with a user-mode printer driver to help us fulfull the following four requirements:

Requirement 1: We must obtain the printer’s hdev value so we will be able to make the EngAssociateSurface call. This is a kernel-mode pointer.
Requirement 2: We must obtain the printer’s dhpdev pointer (note that this is a user-mode pointer), so we can prepare the memory that the display driver will access there. Even better, we can also modify this pointer at will, as explained below.
Requirement 3: Calling CreateCompatibleBitmap on DCPrn must return a bitmap that can be then selected into DCScr, so it must be either a 1-bit or a 32-bit bitmap. For this reason, we must force the user-mode printer driver to create either a 1-bit or a 32-bit device context, which will become our DCPrn.
Requirement 4: We must force the user-mode printer driver to report that it has a native text rendering function. Otherwise, calling EngAssociateSurface with HOOK_TEXTOUT will fail.

Hooking the UMPD implementation

Printer drivers are user-mode DLLs registered in the Registry. Printer drivers may implement functions that are defined in the winddi.h header file and are identified by integer constants:

To implement INDEX_DrvEnablePDEV functionality, device drivers provide a function with the following definition:

As we can see, this function gets the hdev value as a parameter and returns the dhpdev value. This means that we need to install some wrapper around this function in the printer driver to be able to obtain and modify these values.

To reach our goal, we could hook the user-mode printer driver itself, as described in detail here. However, this is quite a complex approach. Instead, we’ll try another solution that is both simpler and more universal. Instead of placing hooks in the driver itself, we will hook the user-mode code that is executed above the driver:

By injecting our own layer between user32.dll!ClientPrinterThunk and gdi32.dll!GdiPrinterThunk, we can obtain a central point at which all data passed to or returned from printer drivers can be read and modified. This can be achieved easily, since gdi32.dll!GdiPrinterThunk is a public export from the gdi32.dll module. Because user32.dll is a PE image mapped into memory, it’s enough to enumerate its import table and replace all pointers to gdi32.dll!GdiPrinterThunk with pointers to our own code. This gives us the ability to modify the incoming data, call the original gdi32.dll!GdiPrinterThunk function, and modify the returned data as needed.

The gdi32.dll!GdiPrinterThunk function, although exported by gdi32.dll, is not publicly defined. As of today, it has four parameters and can be reconstructed as:

In this call, the input buffer contains the identifier of the driver function to be called (i.e., one of the INDEX_XXX values) along with data needed for the call. The output buffer receives the results returned by the printer driver. The needed parts of the input and output buffer can be reconstructed as:

For our exploit, we need custom handling of the following INDEX_XXX requests in our GdiPrinterThunk wrapper:

1) INDEX_DrvEnablePDEV
2) INDEX_UMPDDrvDriverFn (undocumented, called from win32kfull.sys!UMPDDrvDriverFn)
3) INDEX_DrvDisablePDEV

Aside from the INDEX_XXX values defined in winddi.h, GdiPrinterThunk also receives a few undocumented commands. These have INDEX_XXX values greater than the highest documented value. Undocumented commands are never forwarded down to the lowest level (the user-mode printer driver DLL). They are designed for internal gdi32full.dll use only. The first command passed to the GdiPrinterThunk call is always a particular undocumented command, which, after increasing by 3, gives us the needed INDEX_UMPDDrvDriverFn value.

After using CreateDC to create a printer-related device context, we’ll catch many requests in our installed GdiPrinterThunk wrapper. Most of them should be just passed directly to the original gdi32.dll!GdiPrinterThunk implementation, with the three exceptions as mentioned above:

• When handling INDEX_DrvEnablePDEV, we can find the hdev value needed for the EngAssociateSurface call in the input buffer, which satisfies Requirement #1. Then, after calling the original gdi32.dll!GdiPrinterThunk function, we can find the dhpdev value in the output buffer, which points to some block of user-mode memory. Disassembling gdi32full.dll shows that this block of memory contains only two handles/pointers. At this moment, we can allocate some large (64 kB) block of user-mode memory, copy these two handles/pointers to our block, and overwrite the original dhpdev value with a pointer to our own block. By disclosing dhpdev as well as overwriting it with a new value to our liking, we have satisfied Requirement #2. Additionally, the output buffer contains a pointer to a pdi record. We should set pdi->iDitherFormat to BMF_1BPP and also ensure that pdi->flGraphicsCaps has the GCAPS_PALMANAGED flag cleared. This way, Requirement #3 will be met as well.

• When handling INDEX_UMPDDrvDriverFn, we get the DriverFn array in the output buffer. After calling the original gdi32.dll!GdiPrinterThunk function, we should set DriverFn[INDEX_DrvTextOut] to TRUE so the kernel will think that text drawing functionality is handled natively by the printer driver. Thanks to this, passing HOOK_TEXTOUT flag to our EngAssociateSurface call will succeed. This will satisfy Requirement #4.

• When handling INDEX_DrvDisablePDEV, we’ll find our own dhpdev value in the input buffer. It’s good to replace it with the original value before calling the original gdi32.dll!GdiPrinterThunk function to avoid problems with heap memory. gdi32full.dll treats this value as a pointer to its own block of heap memory and tries to release it.

Kernel Exploitation with a Fake dhpdev Block

We are now ready to pass our block of user-mode memory to the display driver. Our choice will be the multi-monitor display driver. More precisely, we will target its win32kfull.sys!MulTextOut function. This will limit our exploitation to multi-monitor machines only, but exploitation will be a bit tricky. The multi-monitor driver expects to see its own data structures in the dhpdev block, so we must craft this block of memory properly to obtain a controllable memory write. A detailed description of the needed memory contents would be quite boring and not really cutting-edge, so let’s immediately focus on the tricky part. There are some flags stored in the dhpdev block where we can set the RC_PALETTE bit. This informs the multi-monitor driver that our displays need a palette for drawing. A palette is used for dealing with 8-bit color modes. Enabling such modes for displays has not been possible since Windows 8. However, the code for handling them is still present. This allows us to use the RC_PALETTE flag and thus force the driver to create a palette object while, by preparing contents of the fake dhpdev block carefully, we can control the memory address where the palette object will be stored. In other words, we can overwrite kernel memory of our choice with a palette handle value.

Our target for overwriting will be our process token’s privileges – see here for details on how to obtain the address of our privileges in kernel memory. In short, we can treat privileges as two 64-bit bitfields located in kernel memory. One of these bitfields tells which privileges are present and another one tells which are enabled. For our purposes, it’s enough to gain the following four privileges:

1) SeCreateTokenPrivilege
2) SeSecurityPrivilege
3) SeAssignPrimaryTokenPrivilege
4) SeTcbPrivilege

To obtain these privileges, we need to set their corresponding bits in the present bitfield. Generally speaking, once a privilege is present in the token, it can be enabled by calling the AdjustTokenPrivileges API. However, not all privileges can be enabled so easily. Each process token contains not only its privileges but also a so-called integrity level. Normal processes don’t have an integrity level high enough to enable the most critical privileges just by making an API call. For this reason, we must perform our exploitation twice: once to overwrite the bitfield indicating which privileges are present and a second time to overwrite the bitfield indicating which privileges are enabled. This means we must:

1) Prepare our fake dhpdev block in such a way that the handle to the palette object will be stored in the present bitfield.
2) Call ExtTextOutW.
3) Prepare our fake dhpdev block once again so the handle to the palette object will be stored in the enabled bitfield.
4) Call ExtTextOutW again.

For successful exploitation, we need to hold only the four privileges listed above. All other privileges may be either set or cleared. Having these four privileges is enough to make a successful call to an undocumented NtCreateToken native API. This allows us to create a token with all privileges present and enabled and with the highest possible integrity level. Passing such a token to a CreateProcessAsUser call allows us to create a maximally powerful user-mode process.

Our last task is to force the kernel to create palettes with the proper handle values so that writing so those values to the privileges bitfields will set the required bits.

Controlling GDI handle values

The lowest 16 bits of each GDI handle act as an index to a so-called global GDI table. For example, a handle value may be:

0x6c0859b2

where:
1) 0x59b2 is an index to a slot in the global GDI table.
2) 0x08 is an object type. 0x08 stands for a palette object.
3) 0x6c is a counter that is incremented each time a slot is assigned for a new object.

Slots in the global GDI table are used in a LIFO (Last In, First Out) manner. We can experiment and create 3 GDI objects: Object1, Object2, and Object3. Obtained handle values may be, for example:

0x74xx25c8
0x21xx1287
0x15xx4f34

Now we can delete these objects in reverse order (Object3, Object2, and Object1) and recreate them in the original order again. The handle values will most probably still reference the same slots, and their values will now be:

0x75xx25c8
0x22xx1287
0x16xx4f34

To achieve our goal, we should:
1) Create many GDI objects of any type. Since there is a per-process limit of 10,000 GDI handles, we can create 9,500 objects. Assuming a linear distribution, 1/16 of the obtained handle values (nearly 600 of them) will have the needed bit combination.
2) Delete the objects that don’t have the needed bit combination in their lowest 16 bits.
3) Delete the objects that do have the needed bit combination in their lowest 16 bits.

The slots of the most recently deleted objects will be first to be reused again when the multi-monitor driver allocates its palette objects during exploitation. Since the most recently deleted objects have the required bit combination in their handle values, the kernel-generated palette objects will also have this same bit combination.

Due to multitasking, other processes may interfere with our manipulations and release additional slots or reuse our just-released slots before they are used by the multi-monitor driver when creating its internal palette objects. This is not a big problem. We can call PrivilegeCheck to see if our process token’s privileges have been overwritten as expected. In case of failure, we can just attempt our exploitation once again.

As a result of our exploitation:
1) The needed privileges in our process token will always be set.
2) There are some other privileges that will always be in a predictable state (set or cleared) due to their being overwritten by the 0x08 part of the handle value.
3) The remaining privileges will be overwritten randomly.

The Patch

When the vulnerability was found, it was first thought that the problem was that the multi-monitor driver doesn’t validate that the dhpdev pointer in the received bitmaps falls within the kernel-mode memory range. We should note that some other HOOK_XXX flags could also be used for exploitation, for example, HOOK_LINETO along with a LineTo call. Therefore, the validation would need to be implemented in multiple places in the multi-monitor driver.

However, under the hood, things are probably more complicated than they appear, so another solution has been implemented. Now, when a bitmap’s dhpdev field is non-NULL (which would be a consequence of making an EngAssociateSurface call), and the bitmap is also printer-compatible (one of the bitmap’s undocumented flags indicates this), selecting the bitmap into a screen-related device context is no longer possible. A newly-added win32kbase.sys!bIsSurfaceAllowedInDC function checks these conditions and causes the SelectObject call to fail. The introduced change shouldn’t cause any problems in real-life scenarios, since the algorithm used for the exploitation is highly artificial. However, this change still breaks compatibility to some extent. For this reason, the patch can be disabled by adding an undocumented registry key from an elevated command prompt and rebooting:

reg.exe add HKEY_LOCAL_MACHINE\System\CurrentControlSet\Policies /v {b7ca08da-d52e-4acb-9866-7dad281e4489} /t REG_DWORD /d 1

Another newly-added function named win32kbase.sys!UMPDAllowPrinterSurfaceInDisplayDC is now called during system initialization. It reads the registry key (if it exists) and stores the result in a global variable called win32kbase.sys!gAllowPrinterSurfaceInDisplayDC. This variable is then read by the aforementioned win32kbase.sys!bIsSurfaceAllowedInDC function, which is internally called during each SelectObject call.


Thanks again to Marcin for providing this thorough write-up. He has contributed many bugs to the ZDI program over the last couple of years, and we certainly hope to see more submissions from him in the future. Until then, follow the team for the latest in exploit techniques and security patches.