Whilst patch diffing the November 2019 patches, VerSprite’s Research practice, VS-Labs, identified an information leak in the NtGdiEnsureDpiDepDefaultGuiFontForPlateau function of
win32kfull.sys, which was originally found by Seonung Jang (@Seonunghardt) of Stealien. This bug has been patched with CVE-2019-1436, a patch from Microsoft that fixes several functions that were found to be leaking sensitive information within Win32k.
It should be noted that the advisory for this patch suggests that the vulnerable
NtGdiEnsureDpiDepDefaultGuiFontForPlateau function exists on all Windows 10 systems. This is not the case. Only Windows 10 version 1709 to 1903, and Windows Server 2019 are affected.
In the following sections we will discuss how this bug was discovered, what its impact is, as well as how Microsoft patched the vulnerability.
The first step in performing analysis of any Windows N-Day vulnerability is to perform a patch analysis. Patch analysis can be performed using a variety of different techniques, however one of the most common and efficient methods is patch diffing.
Patch diffing is performed by taking an older version of a file that does not contain the patch in question and then comparing it against a newer version of the file that has the patch applied. Doing this across two versions of the file that were released within a relatively short time frame of one another allows researchers to quickly and accurately identify what code was altered by a specific patch, dramatically speeding up the analysis process.
A commonly used tool for patch diffing is Diaphora. Diaphora is a Python script that integrates with common disassemblers such as IDA Pro and uses a set of heuristics and algorithms to perform detailed analysis of the differences between two files, which can then be displayed in a variety of formats, including graphs.
To analyze the November 2019 patches, the VS-Labs Research Team performed patch diffing by supplying Diaphora with two files:
10.0.18362.449 (the old
win32kfull.sys file) and
10.0.18362.476 (the new
Once Diaphora had finished its analysis, Diaphora was then asked to construct a graph of the differences in the
NtGdiEnsureDpiDepDefaultGuiFontForPlateau function between the two files, which resulted in the following graph. The old version of the function is located on the left, and the new version is located on the right.
Examining this graph reveals that Microsoft patched
win32kfull.sys so that it always returns
NtGdiEnsureDpiDepDefaultGuiFontForPlateau on GitHub, one can see that this matches the function’s prototype as it is defined in ntgdi.h:
NtGdiEnsureDpiDepDefaultGuiFontForPlateau Function Prototype:
__kernel_entry W32KAPI VOID NtGdiEnsureDpiDepDefaultGuiFontForPlateau( _In_ int iDpi);
NtGdiEnsureDpiDepDefaultGuiFontForPlateau returns VOID, it makes sense that the patch would set the return value to
NtGdiEnsureDpiDepDefaultGuiFontForPlateau should not be returning any info to its caller.
Now that the reasons behind the path have been analyzed, let’s examine
GreEnsureDpiDepDefaultGuiFontForPlateau to understand what is being leaked and how this can be abused by an attacker.
GreEnsureDpiDepDefaultGuiFontForPlateau a call is first made to
win32kbase!DrvGetLogPixels to retrieve the value of
win32kbase!gdmLogPixels, which was found to be set to 60 during testing.
The next part of the code compares the value of
win32kbase!gdmLogPixels with the value of the argument the attacker passed as an argument to
If the lower 32 bits of the value passed to
NtGdiEnsureDpiDepDefaultGuiFontForPlateau matches the value of
GreEnsureDpiDepDefaultGuiFontForPlateau will exit and no leak occurs. Alternatively, if there is no match, then execution continues to the next basic block.
This block will perform a check to see whether the value of the lower 32 bits of the value the attacker supplied as an argument to
NtGdiEnsureDpiDepDefaultGuiFontForPlateau is 60.
If it is 60, then
GreEnsureDpiDepDefaultGuiFontForPlateau will exit and return execution back to
Alternatively, if it is not 60, execution will continue. The code corresponding to these two checks can be seen in the image below.
The next block takes the lower 32 bits of the argument that the attacker passed in as the argument to
NtGdiEnsureDpiDepDefaultGuiFontForPlateau, multiplies it by 0x2AAAAAAB, and saves the result in EDX:EAX.
EDX, which will contain the upper 32 bits of the multiplication result, is then divided by 4, after which the resulting number will then be checked to ensure it is not 0x80000000 or more.
If this is not the case, ECX is set to 0, otherwise if it is the case, ECX is set to -1. ECX is then added to EDX, which will contain the upper 32 bits of the multiplication result divided by 4. EDX will then be multiplied by 2, after which the value of EDX before the multiplication is then added to the result.
The resulting number is then stored back into EDX.
Finally, EDX is multiplied by 8 and the resulting number is checked to ensure it is the same as the lower 32 bits of the argument the attacker provided to
If it is then execution will continue.
The next basic block checks to see whether the value supplied by the attacker as the argument to
NtGdiEnsureDpiDepDefaultGuiFontForPlateau is less than or equal to 0x1E0.
Following this the same operation as earlier is performed again with the lower 32 bits of the argument that was passed to
NtGdiEnsureDpiDepDefaultGuiFontForPlateau being multiplied once again by 0x2AAAAAAB before then being divided by 4.
The result is then checked to ensure it is not a signed number and, if it is not, execution will continue.
Once all of this is done, the value of the
win32kbase!gahDpiDepDefaultGuiFonts pointer is saved into RAX. RAX is then dereferenced and the QWORD that it points to is saved into RCX. RDI, which will be the value of the argument the attacker passed to
NtGdiEnsureDpiDepDefaultGuiFontForPlateau multiplied by 0x2AAAAAAB and then divided by 4, will then be used as an index into the buffer RCX is pointing to.
If the QWORD at this index is a non-zero value, then execution jumps to
loc_1C028DAEC one can see that there is no adjustment to the value of RAX. This means that RAX still contains the value of the
win32kbase!gahDpiDepDefaultGuiFonts pointer, which is returned by
NtGdiEnsureDpiDepDefaultGuiFontForPlateau, which in turn leak it back to the attacker’s user mode code.
This explains why Microsoft patched
NtGdiEnsureDpiDepDefaultGuiFontForPlateau to always return 0: this prevents
NtGdiEnsureDpiDepDefaultGuiFontForPlateau from ever leaking any sensitive data such as kernel pointer addresses back to userland whilst still keeping the existing functionality of
Once an attacker has the value of the
win32kbase!gahDpiDepDefaultGuiFonts pointer, they can bypass KASLR and obtain the base address of
win32kbase.sys. To do this an attack must know where
win32kbase!gahDpiDepDefaultGuiFonts is located relative to the start of the
Once this offset has been obtained, an attack can the subtract this offset from the value of the
win32kbase!gahDpiDepDefaultGuiFonts pointer (which was leaked via the exploit) to get the base address of
win32kbase.sys in memory.
Keep in mind however that the offset between the start of
win32kbase!gahDpiDepDefaultGuiFonts can change between patch cycles so the exploit needs to be updated to target the specific version of
win32kbase.sys that exists on the target user’s system.
To obtain this information an attacker can use two approaches. The first approach is to use a disassembler such as IDA Pro or Relyze and reverse the specific version of
win32kbase.sys they wish to target to figure out the correct offset.
Alternatively, an attacker could attempt to dynamically obtain this information programmatically by reading the contents of the
win32kbase.sys file on the system and then updating the offsets within the exploit itself.
It is important to note that this bug needs to be exploited twice for maximum reliability. The reason for this lies in the check in one of the blocks within
GreEnsureDpiDepDefaultGuiFontForPlateau, which can be seen below:
In particular it is often the case that the first time the exploit is run the CMP instruction in this block will evaluate to true, which will cause the jump to be skipped as the value at [RCX+RDI*8] will be 0.
At first glance, one may think that there is no solution here. Closer examination of the following two blocks reveals a different story however. The code in the first block acquires a lock on
win32kbase!gahDpiDepDefaultGuiFonts before then checking to see if the value at the QWORD offset dictated by RDI is still 0.
If it is, then [RDX+RDI*8] will be set to
win32kbase!gahDpiDepDefaultGuiFonts. [RDX+RDI*8] will point to the same location as the [RCX+RDI*8] check shown earlier, thereby ensuring that this value no longer contains 0 so that the check can pass successfully.
All that is left is for the attacker to trigger the bug again with the same input at which point the check will observe that the value at [RCX+RDI*8] is not 0, and will take the correct path to allow the attacker to obtain the value of
win32kbase!gahDpiDepDefaultGuiFonts from user-mode code.
Maintain awareness regarding unknown threats to your products, technologies, and enterprise networks. Organizations that are willing to take the next step in proactively securing their flagship product or environment can leverage our zero-day vulnerability research offering.
This subscription-based capability provides your organization with immediate access to zero-day vulnerabilities affecting products and software. Learn More →
If you are interested in further reading of vulnerabilities in Windows software, below is a list of recent vulnerabilities and exploitation analysis:
VerSprite's Research and Development division (a.k.a VS-Labs) is comprised of individuals who are passionate about diving into the internals of various technologies. From advanced technical security training to our research for hire B.O.S.S offering, we help organizations solve their most complex technical challenges.
View our security advisories detailing vulnerabilities found in major products for MacOs, Windows, Android, and iOS.