Posted by Mateusz Jurczyk of Google Project Zero
This is part #3 of the “One font vulnerability to rule them all” blog post series. In the previous posts, we introduced the “blend” PostScript operator vulnerability, discussed the Charstring primitives necessary to fully control the stack contents and used them to develop a reliable user-mode Adobe Reader exploit executing arbitrary C++ code embedded in the PDF file:
Today, we will learn how the ATMFD.DLL kernel attack surface can be reached from the context of the renderer process, and how the vulnerability can be then used to elevate privileges in the operating system and escape the sandbox on 32-bit builds of Windows 8.1.
Attacking the kernel
In order to reach the Charstring inteprereter found in ATMFD.DLL and trigger the vulnerability, we basically need to get the graphic subsystem to load and render a font. This is a pretty standard scenario in Windows programming, and can be easily achieved by the following series of GDI calls:
- CreateWindow - create the window to draw on.
- AddFontResource - load the font in the system.
- BeginPaint - prepare window for painting.
- CreateFont - create a logical font with specified characteristics.
- SelectObject - select the font for usage with the device context.
- TextOut - display the specified text on the window with previously defined style.
- DeleteObject - destroy the font object.
- EndPaint - mark the end of painting in the window.
Now, since the Adobe Reader sandbox is not subject to the win32k.sys lockdown, all of the above calls work just fine... except for the most important one - AddFontResource - as the kernel refuses to load any fonts from the filesystem. Loading a controlled font is absolutely essential, so this is a major hurdle on our way of a complete system compromise. When looking around for alternatives, we can spot the AddFontMemResourceEx API function, which is the equivalent of AddFontResource, but loads fonts from memory instead of disk, potentially working around the issue.
However, as noted above, the BLEND vulnerability can only be triggered using Type 1 fonts, which require two or more file resources. On the other hand, the AddFontMemResourceEx function does not provide any means of loading fonts consisting of more than one file – it will handle the TrueType and OpenType formats just fine, but it is not possible to use it for Type 1. This is confirmed both by users on the Internet who complained about it publicly on various forums, and through reverse engineering of the win32k.sys module. Specifically, the win32k!GreAddFontMemResourceEx function calls win32k!bCreateFontFileView with the last parameter set to a constant value of 1 – the argument denotes the number of file resources which should be used to load the font. The situation looks like a dead end, as there are no other official/documented functions for loading fonts that would work correctly with Type 1.
When we look at the list of cross-references of the win32k!bCreateFontFileView function, we can spot one more interesting item: “NtGdiAddRemoteFontToDC”, which calls the routine with a variable as the last argument instead a constant value! Unfortunately, there is absolutely no public information regarding the system call available, either from official sources or otherwise. Facing a general lack of clues, I reverse engineered the structure used by the service, ending up with the following C++ representation:
typedef struct tagTYPE1FONTHEADER {
ULONG IsType1Font;
ULONG NumberOfFiles;
ULONG Offsets[2];
BYTE Data[1];
} TYPE1FONTHEADER, *PTYPE1FONTHEADER;
In order to load a Type 1 font from memory, the structure should be initialized in the following way:
TYPE1FONTHEADER.IsType1Font = 1;
TYPE1FONTHEADER.NumberOfFiles = 0;
TYPE1FONTHEADER.Offsets[0] = (PfmFileSize + 3) & ~4;
TYPE1FONTHEADER.Offsets[1] = ((PfmFileSize + 3) & ~4)
+ ((PfbFileSize + 3) & ~4);
TYPE1FONTHEADER.Data = {.PFM file data aligned to 4 bytes,
.PFB file data aligned to 4 bytes}
.PFB file data aligned to 4 bytes}
After having all of the fields correctly set up and calling the win32k!NtGdiAddRemoteFontToDC system call (the necessary system call IDs for various Windows editions can be found here for 32-bit systems and here for 64-bit ones), the graphical system will gladly load the font for us from memory, making it possible to proceed with the kernel attack.
Assuming that the exploit is supposed to be fully contained within a single file, we must embed the Windows kernel x86 and x64 font exploits inside, too. This can be done in a variety of ways, for example by including them as PE resources of the 2nd stage DLL, or just appending them at the end of the file. For simplicity, I decided to go for the latter option, resulting in the following general structure of the exploit file:
Figure 1. Example file structure including an Adobe Reader exploit, a 2nd stage DLL and Windows kernel exploits for different builds.
With the kernel attack vector figured out, we can now focus on achieving arbitrary code execution in ring-0, and using the capability to escalate our privileges in the operating system. The next section covers this subject in detail.
Exploitation of Microsoft Windows 8.1 Update 1 (32-bit)
In general, elevating the privileges of a specific process is fairly easy in the Windows kernel, once we can execute arbitrary code in its context – it all boils down to traversing a linked list of processes, and replacing the security token (in the form of a single pointer) of one of them with another’s. The algorithm can be easily implemented in a short snippet of x86 assembly, so unlike in user-mode, we don’t need to load a second-stage module or perform any other complicated operations.
As a result, the ROP chain constructed using the BLEND vulnerability will be responsible for:
- Allocating writeable/executable memory and copying the EoP shellcode there,
- Jumping to the shellcode to have it do its job,
- Cleanly recovering from the payload in order to keep the operating system in a stable state.
Overall, the process of building the ROP chain is equivalent to the one we used for Adobe Reader – we can use the same techniques to freely manipulate the stack in any desired way. As the addresses of the ATMFD.DLL, win32k.sys and ntoskrnl.exe modules are all present on the stack, we can choose from a wide variety of gadgets.
One thing we have to take into account is that starting with Windows 8, most memory in kernel-mode is under protection of DEP – paged pool allocations are non-executable, and a majority of non-paged pool allocations are requested with type NonPagedPoolNx (note the “Nx” suffix). This means that our exploit will need to call ExAllocatePool(NonPagedPool) by itself, which will allocate executable, non-pageable memory that we can use to store and execute the ring-0 shellcode.
An example ROP chain used in the proof of concept exploit is shown in Figure 2. It consists of three major parts: allocating 4 kilobytes of RWE memory, copying the shellcode from the stack to the address returned by the ExAllocatePool call (which requires some register juggling), and finally jumping to the shellcode.
Figure 2. Example Windows kernel ROP chain used to achieve arbitrary code execution.
The result of using the above ROP chain with a payload consisting of four “int3” instructions is shown below – the instructions are successfully executed and caught by the attached WinDbg kernel debugger:
Figure 3. Successful arbitrary code execution in the Windows 8.1 32-bit kernel achieved with the BLEND vulnerability.
The specific algorithm used by the shellcode to elevate the privileges of all active Adobe Reader processes to “NT AUTHORITY/SYSTEM” and to loosen the sandbox process’ job restrictions is as follows:
- Find the “System” process by starting at KPCR.PcrbData.CurrentThread.ApcState.Process and traversing EPROCESS.ActiveProcessLinks.Flink, until a EPROCESS.UniqueProcessId of 4 is found.
- Save the security token from EPROCESS.Token.
- Traverse the process linked list again in search of EPROCESS.ImageFileName equal to “AcroRd32.exe”.
- Replace EPROCESS.Token with the saved, privileged security token.
- Increase EPROCESS.Job.ActiveProcessLimit to 2, in order to spawn a new calc.exe process later on.
- Jump to address 0x0.
All of the above steps are rather straight-forward and clear, except maybe the last one. If we want to recover from the corrupt execution state, why would we make it even worse by jumping to an invalid address?! Wouldn’t fixing up the stack frame or returning to a caller x frames higher in the call chain be a more intuitive solution?
The trick is highly related to a specific behavior of ATMFD.DLL, namely the aggressive exception handling employed by the module. Most, if not all code processing user data is wrapped with universal try/except blocks, which catch all non-critical exceptions (such as invalid accesses to user-mode memory) and gracefully handle them as if nothing wrong has happened. While the specific reason for implementing the safety net is unknown (we can only presume that this is to mitigate potential small out-of-bounds memory accesses or other bugs in the code from threatening system stability), its existence has some important implications. For one, it potentially makes automated vulnerability discovery much less effective, since even if a certain fuzzer triggers some kind of corruption which results in invalid user-mode memory access, the information will never actually reach the fuzzer, as it will be intercepted and buried by ATMFD. If any bug hunters were not aware of this and didn’t undertake any steps to work around it, they might have missed (and still may!) a number of potentially security relevant conditions.
On the other hand, it can work to an attacker’s advantage in situations where the exploit needs to recover from a hijacked control flow. In such case, it is sufficient to generate any exception handled by the driver, and it will take care of the rest, cleaning up and returning back to userland. In our case, jumping to address 0x0 will trigger exactly the necessary exception, returning control flow to the 2nd stage DLL. Quite elegant, isn’t it? :)
The result of running the above kernel-mode shellcode is shown in Figure 4 – both Adobe Reader processes (the broker and the renderer) are now running with the System security token, at high integrity level.
Figure 4. Adobe Reader processes running with elevated privileges as a result of successful kernel exploitation.
In order to make the proof of concept exploit truly effective and achieve the original goals, we still have to pop up calc.exe. This has already been facilitated by the kernel-mode shellcode, which increased the job’s active process limit from 1 to 2. Simply calling the CreateProcess API function still won’t work, though, as the sandboxed process has KERNELBASE!CreateProcessA hooked by the broker. Since we have all other capabilities required to start a program, we can just unhook the function by restoring the original prologue bytes with a short snippet of code shown below:
HMODULE hKernelBase = GetModuleHandleA("KERNELBASE.DLL");
FARPROC lpCreateProcessA = GetProcAddress(hKernelBase, "CreateProcessA");
// Make the kernelbase!CreateProcessA memory area temporarily writable.
DWORD flOldProtect;
VirtualProtect(lpCreateProcessA, 5, PAGE_READWRITE, &flOldProtect);
// Write the original function prologue
// (MOV EDI, EDI; MOV EBP, ESP; PUSH ESP).
// (MOV EDI, EDI; MOV EBP, ESP; PUSH ESP).
RtlCopyMemory(lpCreateProcessA, "\x8b\xff\x55\x8b\xec", 5);
// Restore the original memory access mask.
VirtualProtect(lpCreateProcessA, 5, flOldProtect, &flOldProtect);
It has also been noted that the same effect could be achieved with a single WriteProcessMemory API call, which would save us the two VirtualProtect invocations. Once this is done, the exploit is ready, taking advantage of a single vulnerability twice in latest affected Adobe Reader and Windows kernel to pop up an elevated calc.exe, bypassing all software exploit mitigations with full reliability. A demo of the exploit performance is shown in the video below:
Having successfully developed a complete remote code execution + sandbox escape exploit chain using the single “blend” vulnerability in Adobe Reader and Windows 8.1 32-bit, the only remaining part is a Windows 8.1 64-bit kernel exploit, which will take advantage of a different font security flaw to achieve its goal, due to 64-bit builds being unaffected by the issue discussed so far. The subject will be covered soon in the 4th and final blog post of the series.
No comments:
Post a Comment