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: win32kfull.sys version 10.0.18362.449 (the old win32kfull.sys file) and win32kfull.sys version 10.0.18362.476 (the new win32kfull.sys file).
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.
Visualizing the Patch with Diaphora
Examining this graph reveals that Microsoft patched NtGdiEnsureDpiDepDefaultGuiFontForPlateau in win32kfull.sys so that it always returns 0x0.
Looking up 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
_In_ int iDpi);
Since NtGdiEnsureDpiDepDefaultGuiFontForPlateau returns VOID, it makes sense that the patch would set the return value to 0x0, as 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.
Within 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 NtGdiEnsureDpiDepDefaultGuiFontForPlateau.
If the lower 32 bits of the value passed to NtGdiEnsureDpiDepDefaultGuiFontForPlateau matches the value of win32kbase!gdmLogPixels, then 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 NtGdiEnsureDpiDepDefaultGuiFontForPlateau.
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 NtGdiEnsureDpiDepDefaultGuiFontForPlateau.
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.
Looking at 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 GreEnsureDpiDepDefaultGuiFontForPlateau to 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 GreEnsureDpiDepDefaultGuiFontForPlateau intact.
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 win32kbase.sys.
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.sys and 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.
Final Exploitation Notes
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 →
Vulnerabilities in Windows Software
If you are interested in further reading of vulnerabilities in Windows software, below is a list of recent vulnerabilities and exploitation analysis: