sam-b.github.io

My first Windows driver: Creating the Pink Screen Of Death

So a while ago I saw a cool blog post from Mark Russinovich - “Blue Screens” in Designer Colors with One Click where he changed the colors of the infamous BSOD, I went to look at the source of NotMyFault and soon found that everything but the code responsible for the color change was included :( @ptsecurity_uk has also looked at reversing Mark’s code in the past and published some details on their blog: Customizing Blue Screen of Death but again this didn’t include buildable and runnable code :(

So the goal for this post is too create a simple driver which supports two different IOCTL codes (explained below) which when triggered in the correct order cause Windows to crash and then show a screen which looks like the one below.

All the code for this post can be found here.

Creating a driver

To follow along with this you’ll need a reasonably new version of Visual Studio and the Driver Development Kit installed. We start off by creating a new creating a new project with type ‘Visual C++’->’Driver’->’WDF’(Windows Driver Framework)->’Kernel Mode Driver, Empty’, then pick a name and click ‘OK’. Now add a new file to the ‘Source Files’ directory called ‘Driver.c’, let’s start by including the header files we will need.

#include <ntddk.h>
#include <wdf.h>

‘ntddk.h’ is the header file for the ‘Windows Device Driver Kit’ and ‘wdf.h’ is the header file for the ‘Windows Driver Frameworks’, between them they include all the external type and function definitions we will need.

Next we declare the two functions which are required for us to have a driver which runs in kernel mode, however briefly.

NTSTATUS    DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath);

EVT_WDF_DRIVER_DEVICE_ADD KmdfPSODEvtDeviceAdd;

DriverEntry is the first function called when a driver is loaded and is responsbile for initializing the driver. At the moment all we want to do in DriverEntry is the bare minimum requried to create a driver.

NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT  DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
	NTSTATUS status;
	WDF_DRIVER_CONFIG config;

	WDF_DRIVER_CONFIG_INIT(&config, KmdfPSODEvtDeviceAdd);
	status = WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE);
    return status;
}

In DriverEntry we used the WDF_DRIVER_CONFIG_INIT function to setup the drivers config, the KmdfPSODEvtDeviceAdd function is responsible for setting this up.

NTSTATUS KmdfPSODEvtDeviceAdd(_In_ WDFDRIVER Driver, _Inout_ PWDFDEVICE_INIT DeviceInit)
{
	UNREFERENCED_PARAMETER(Driver);

	NTSTATUS status;
	WDFDEVICE hDevice;

	status = WdfDeviceCreate(&DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, &hDevice);
	return status;
}

We don’t have special requirements for our driver so we just create it as a device with no special attributes. Now we should be able to build the driver which currently does nothing. ##Using a driver In order to use our driver we need to enable test signing by running the following command in an Administrator command prompt.

bcdedit.exe -set TESTSIGNING ON 

In order to register and start the driver I used OSRLoader, by selecting the .sys file we just built, I was able to register and then start the driver which did nothing as expected.

Changing the BSOD

In order to work how Mark’s myfault.sys driver did the color change I needed to do some reversing. I started by opening it up in IDA and using my ‘totes 1337’ reversing skills went straight to the ‘strings’ subview. I immediately saw the string ‘Myfault Color Switcher’: There was only one cross reference to the string in the binary and it was exactly the function I was looking for. The basic block shows that the driver works exactly how Mark described it in his blogpost, by registering a callback function which will be executed when a Bug Check is triggered. From the blogpost we also know that the color change is achieved by directly modifying the boot video drivers settings, this means it almost definitly needs to use the out instruction. “The Myfault.sys callback executes just after the blue screen paints and changes the colors to the ones passed to it by Notmyfault by changing the default VGA palette entries used by the Boot Video driver.”

Using IDA’s search function to find occurances of the string ‘out’ we find one function which uses the instruction a bunch of times. When we go to the function in graph view we find that its taking two arguments and using them to set three different values on two different ports, here we can assume these RGB values. Now we just need to know how this actually works…

The best documentation I could find on interacting with the VGA directly was on osdev.org. The port ‘0x3C8’ is used to select the color index which is being written to and port ‘0x3C9’ is used to select what color the index represents and is set by writing 3 6-bit values to it, representing red, green and blue. In the code above the user supplied values are being shifted about to set these values and the color indexes ‘4’ and ‘0xF’ are the ones being used to set the color of the background and the text.

I took the assembly and simplified it a bit - hardcoding the colors and removing some of the optimisations to make things clearer and ended up with the below as my callback function, the function is defined as described in the BugCheckDumpIoCallback documentation.

VOID BugCheckDumpIoCallback(KBUGCHECK_CALLBACK_REASON Reason, struct _KBUGCHECK_REASON_CALLBACK_RECORD *Record, PVOID ReasonSpecificData, ULONG ReasonSpecificDataLength)
{
	UNREFERENCED_PARAMETER(Reason);
	UNREFERENCED_PARAMETER(Record);
	UNREFERENCED_PARAMETER(ReasonSpecificData);
	UNREFERENCED_PARAMETER(ReasonSpecificDataLength);
	__asm {
		mov edx, 3C8h; DAC color index port
		mov al, 4; background color
		out dx, al
		mov edx, 0x3C9; DAC color component port
		mov al, 0xFF; RED
		out dx, al
		mov al, 0x69; GREEN
		out dx, al
		mov al, 0xB4; BLUE
		out dx, al
		dec edx
		mov al, 0Fh;Text color
		out dx, al
		mov edx, 0x3C9
		mov al, 0x00; RED
		out dx, al
		mov al, 0x00; GREEN
		out dx, al
		mov al, 0x00; BLUE
		out dx, al
	}
}

Now that we have our callback function we need to register it as a callback the same way Mark’s code did. We do this by initiallizing a callback record using KeInitializeCallbackRecord and then registering it using KeRegisterBugCheckReasonCallback. Supplying a KBUGCHECK_CALLBACK_REASON enum value of 1 means that the callback will always be ran when a Bug Check occurs.

KeInitializeCallbackRecord(&callbackRec);
status = KeRegisterBugCheckReasonCallback(&callbackRec, (PKBUGCHECK_REASON_CALLBACK_ROUTINE)&BugCheckDumpIoCallback, (KBUGCHECK_CALLBACK_REASON)1, (PUCHAR) "BUGCHECK");

Now we just need to add code to trigger a BugCheck - this can be done easily with the KeBugCheckEx function.

KeBugCheckEx(0x1234, 0, 1, 2, 3);

Now if we rebuild and then re-register and start the service, we see this:

Creating IOCTL handlers

Now we have the driver behaviour we want but it’s all in the ‘DriverEntry’ function, so this non-useful driver is currently rather undriver like.

In Windows the DeviceIOControl function provides a generic interface to drivers, when the function is called 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.

We start by defining our desired IOCTL codes using the CTL_CODE macro.

#define IOCTL_PSOD_CREATE_CALLBACK CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_NEITHER, FILE_READ_DATA | FILE_WRITE_DATA)
#define IOCTL_PSOD_BUG_CHECK CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_READ_DATA | FILE_WRITE_DATA)

Our IOCTLs are for an unknown device type, have function codes of 0x801 and 0x802 - drivers from vendors other than Microsoft start at 0x800, do not support any kind of IO and can be sent from a process with either read or write permissions to the device.

Next lets restart from scratch with the DriverEntry function.

NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT  DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
	NTSTATUS status;
	WDF_DRIVER_CONFIG config;
	PDEVICE_OBJECT DeviceObject = NULL;

	WDF_DRIVER_CONFIG_INIT(&config, KmdfPSODEvtDeviceAdd);
	status = WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE);
	return status;
}

Now we want our driver to actually have a device associated with it this time, so that userland programmers can acquire a handle to it, we start off by delcaring variables to store the device name. Two versions of the device name are created with different paths, some details on the reasoning for this can be found here.

UNICODE_STRING DeviceName, Win32Device;

Next we initialize the variables with our chosen device name.

RtlInitUnicodeString(&DeviceName, L"\\Device\\PSOD");
RtlInitUnicodeString(&Win32Device, L"\\DosDevices\\PSOD");

Next we use the IoCreateDevice function to create a device object for our driver.

status = IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &DeviceObject);

Now we’re going to setup the DriverObjects main function table, I’ll explain the functions we put in the table later on when they are defined. We start by setting ‘IrpNotImplementedHandler’ as the entry for every function in the MajorFunction table which is used to store references to all the IRP handler functions our driver supports. Having a not implemented handler for any functions we don’t support is a good practise and avoids causing confusion when we return error codes to requests which should have been passed to another driver.

for (int i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) {
	DriverObject->MajorFunction[i] = IrpNotImplementedHandler;
}

Next we create entries for the IRP handlers which we need. IRP_MJ_CREATE is used when someone requests a handle to our device, for example by using CreateFile and IRP_MJ_CLOSE when they close the handle. Finally IRP_MJ_DEVICE_CONTROL is called when an IOCTL is sent to the driver.

DriverObject->MajorFunction[IRP_MJ_CREATE] = IrpCreateHandler;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = IrpCloseHandler;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = PSOD_IoControl;

Following this we install an Unload Handler, the purpose of this is pretty obvious as it will be called when the driver is unloaded.

DriverObject->DriverUnload = IrpUnloadHandler;

Next we modify the flags on our device object so that it is no longer marked as initializing and can start recieving IO requests. More details on this are available here.

DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;

Finally we add the following just before we return from DriverEntry to actually create the symbolic link used to access the device.

status = IoCreateSymbolicLink(&Win32Device, &DeviceName);

So now that DriverEntry is sorted we need to create the functions we just placed in the devices MajorFunctions table. We start by defining the ‘PSOD_IoControl’ function which is called everytime the driver is sent an IOCTL. The first thing the function does is call IoGetCurrentIrpStackLocation to find the location of the callers I/O stack location which contains any paramters passed with the Irp. If the stack has been correctly retrieved it then enters a switch which calls the relevant handler functions based off the IoControlCode which was sent. Once the correct handler has returned the status is set and so is the number of bytes written to the return buffer if one was provided.

NTSTATUS PSOD_IoControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
	UNREFERENCED_PARAMETER(DeviceObject);
	
	NTSTATUS NtStatus = STATUS_NOT_SUPPORTED;
	unsigned int dwDataWritten = 0;

	PIO_STACK_LOCATION pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);

	if (pIoStackIrp)
	{
		switch (pIoStackIrp->Parameters.DeviceIoControl.IoControlCode){
			case IOCTL_PSOD_CREATE_CALLBACK:
				NtStatus = PSOD_HandleIOCTL_CREATE_CALLBACK(Irp, pIoStackIrp, &dwDataWritten);
				break;

			case IOCTL_PSOD_BUG_CHECK:
				NtStatus = PSOD_HandleIOCTL_BUG_CHECK(Irp, pIoStackIrp, &dwDataWritten);
				break;
			default:
				NtStatus = STATUS_INVALID_DEVICE_REQUEST;
				break;
		}
	}

	Irp->IoStatus.Status = NtStatus;
	Irp->IoStatus.Information = dwDataWritten;

	IoCompleteRequest(Irp, IO_NO_INCREMENT);

	return NtStatus;
}

The create callback handler registers our bug check callback function and returns success if it was registered and returns ‘STATUS_UNSUCCESSFUL’ if it fails.

NTSTATUS PSOD_HandleIOCTL_CREATE_CALLBACK(PIRP Irp, PIO_STACK_LOCATION pIoStackIrp, unsigned int *pdwDataWritten)
{
	UNREFERENCED_PARAMETER(Irp);
	UNREFERENCED_PARAMETER(pIoStackIrp);
	UNREFERENCED_PARAMETER(pdwDataWritten);

	NTSTATUS status = STATUS_UNSUCCESSFUL;
	KeInitializeCallbackRecord(&callbackRec);
	status = KeRegisterBugCheckReasonCallback(&callbackRec, (PKBUGCHECK_REASON_CALLBACK_ROUTINE)&BugCheckDumpIoCallback, (KBUGCHECK_CALLBACK_REASON)1, (PUCHAR) "BUGCHECK");
	Irp->IoStatus.Information = 0;
	if (status == STATUS_SUCCESS){
		Irp->IoStatus.Status = STATUS_SUCCESS;
	}
	else {
		Irp->IoStatus.Status = STATUS_UNSUCCESSFUL;
	}
	return status;
}

The bug check callback does exactly what we were doing in DriverEntry before and just calls ‘KeBugCheckEx’ to trigger a bug check.

NTSTATUS PSOD_HandleIOCTL_BUG_CHECK(PIRP Irp, PIO_STACK_LOCATION pIoStackIrp, unsigned int *pdwDataWritten)
{
	UNREFERENCED_PARAMETER(Irp);
	UNREFERENCED_PARAMETER(pIoStackIrp);
	UNREFERENCED_PARAMETER(pdwDataWritten);
	KeBugCheckEx(0x1234, 0, 1, 2, 3);
}

The ‘IrpCreateHandler’ function just always returns ‘STATUS_SUCCESS’, we aren’t keeping track of open handles or restricting them in anyway so there’s nothing we need to do here.

NTSTATUS IrpCreateHandler(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
	UNREFERENCED_PARAMETER(DeviceObject);
	
	Irp->IoStatus.Information = 0;
	Irp->IoStatus.Status = STATUS_SUCCESS;

	IoCompleteRequest(Irp, IO_NO_INCREMENT);

	return STATUS_SUCCESS;
}

The ‘IrpCloseHandler’ function just return ‘STATUS_SUCCESS’, we aren’t keeping track of open handles or anything, so we don’t need to do anything.

NTSTATUS IrpCloseHandler(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
	UNREFERENCED_PARAMETER(DeviceObject);

	Irp->IoStatus.Information = 0;
	Irp->IoStatus.Status = STATUS_SUCCESS;

	IoCompleteRequest(Irp, IO_NO_INCREMENT);

	return STATUS_SUCCESS;
}

The ‘IrpUnloadHandler’ function deletes the symbolic link created for the driver and then deletes the device object associated with it.

VOID IrpUnloadHandler(IN PDRIVER_OBJECT DriverObject) {
	UNICODE_STRING DosDeviceName = { 0 };
	RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\PSOD");
	IoDeleteSymbolicLink(&DosDeviceName);
	IoDeleteDevice(DriverObject->DeviceObject);
}

Finally the ‘IrpNotImplementedHandler’ just returns STATUS_NOT_SUPPORTED.

NTSTATUS IrpNotImplementedHandler(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
	UNREFERENCED_PARAMETER(DeviceObject);
	
	Irp->IoStatus.Information = 0;
	Irp->IoStatus.Status = STATUS_NOT_SUPPORTED;

	IoCompleteRequest(Irp, IO_NO_INCREMENT);

	return STATUS_NOT_SUPPORTED;
}

With this done it should be possible to rebuild the driver, register it as a service and start it. Nothing should happen until we start sending it IOCTLs. The full code for the driver can be found here.

Sending IOCTLs

In order to send IOCTLs to our driver once it has started we need two functions, one to get a handle to the driver and another to send the IOCTL. I used the following functions which were taken from this gist and which provide simple python function wrappers around some ctypes magic used to directly call the needed Windows API functions.

import ctypes
import ctypes.wintypes as wintypes
from ctypes import windll

# open_divice and send_ioctl taken from https://gist.github.com/santa4nt/11068180
LPDWORD = ctypes.POINTER(wintypes.DWORD)
LPOVERLAPPED = wintypes.LPVOID
LPSECURITY_ATTRIBUTES = wintypes.LPVOID

GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000

OPEN_EXISTING = 3

FILE_ATTRIBUTE_NORMAL = 0x00000080

INVALID_HANDLE_VALUE = -1

NULL = 0

def open_device(device_path, access, mode, creation, flags):
	"""See: CreateFile function
	http://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx
	"""
	CreateFile_Fn = windll.kernel32.CreateFileW
	CreateFile_Fn.argtypes = [
	wintypes.LPWSTR,                    # _In_          LPCTSTR lpFileName
	wintypes.DWORD,                     # _In_          DWORD dwDesiredAccess
	wintypes.DWORD,                     # _In_          DWORD dwShareMode
	LPSECURITY_ATTRIBUTES,              # _In_opt_      LPSECURITY_ATTRIBUTES lpSecurityAttributes
	wintypes.DWORD,                     # _In_          DWORD dwCreationDisposition
	wintypes.DWORD,                     # _In_          DWORD dwFlagsAndAttributes
	wintypes.HANDLE]                    # _In_opt_      HANDLE hTemplateFile
	CreateFile_Fn.restype = wintypes.HANDLE

	return wintypes.HANDLE(CreateFile_Fn(device_path,
	access,
	mode,
	NULL,
	creation,
	flags,
	NULL))

def send_ioctl(devhandle, ioctl, inbuf, inbufsiz, outbuf, outbufsiz,):
	"""See: DeviceIoControl function
	http://msdn.microsoft.com/en-us/library/aa363216(v=vs.85).aspx
	"""
	DeviceIoControl_Fn = windll.kernel32.DeviceIoControl
	DeviceIoControl_Fn.argtypes = [
	wintypes.HANDLE,                    # _In_          HANDLE hDevice
	wintypes.DWORD,                     # _In_          DWORD dwIoControlCode
	wintypes.LPVOID,                    # _In_opt_      LPVOID lpInBuffer
	wintypes.DWORD,                     # _In_          DWORD nInBufferSize
	wintypes.LPVOID,                    # _Out_opt_     LPVOID lpOutBuffer
	wintypes.DWORD,                     # _In_          DWORD nOutBufferSize
	LPDWORD,                            # _Out_opt_     LPDWORD lpBytesReturned
	LPOVERLAPPED]                       # _Inout_opt_   LPOVERLAPPED lpOverlapped
	DeviceIoControl_Fn.restype = wintypes.BOOL

	# allocate a DWORD, and take its reference
	dwBytesReturned = wintypes.DWORD(0)
	lpBytesReturned = ctypes.byref(dwBytesReturned)

	status = DeviceIoControl_Fn(devhandle,
	ioctl,
	inbuf,
	inbufsiz,
	outbuf,
	outbufsiz,
	lpBytesReturned,
	None)

	return status, dwBytesReturned

Next we need to get the IOCTL codes actual values so we can send them, these were calculated using the translate.py script I have on Github which maps the constant names to thier values and then uses them to calculate the IOCTL code.

if __name__ == "__main__":
	#define IOCTL_PSOD_CREATE_CALLBACK CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_NEITHER, FILE_READ_DATA | FILE_WRITE_DATA)
	create_callback_ioctl = 0x22e007
	#define IOCTL_PSOD_BUG_CHECK CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_READ_DATA | FILE_WRITE_DATA)
	bug_check_ioctl = 0x22e00b

Sending the IOCTLs is easy enough - first we get a handle to the device using standard permissions and the path we chose earlier, then we send the create_callback_ioctl followed by the bug_check_ioctl. We aren’t transferring any data so the all the input and output buffers can just be None.

	device_handle = open_device("\\\\.\\PSOD", GENERIC_READ | GENERIC_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL)
	send_ioctl(device_handle, create_callback_ioctl, None, 0, None, 0)
	send_ioctl(device_handle, bug_check_ioctl, None, 0, None, 0)

When we run this from the command line, we see the pink and black screen of death appear which means…