Video Game Hacking Part 2 — Kernel Driver Anti-Cheat Bypass

Curiosity project: This was the first project I decided to do when I made the choice to dive into how video game hacking works. After doing some research, I came to the conclusion that I first need to beat a user mode anti-cheat as they are on the weaker side of anti-cheat software which is what I needed to start learning. I found out the easiest way to do this is to simply read and write memory at the kernal level where a user mode anti cheat does not have any privilages and can not access.
Windows Driver Dev Kernel Bypass Reverse Engineering Anti-Cheat Detections

My first step was to build a tiny driver that can read and write memory while communicating with my user mode application that will render the wall hack. Then I watched it read/write game memory without tripping the user-mode hooks. The fun part (maybe not) is seeing where it breaks, then making it stable, such as all the BSOD crashes when creating the driver, to making sure the user mode app renders correctly.

Screenshot of the wall hack overlay driven by the kernel driver reads
read/write via kernal driver MmCopyVirtualMemory → user-mode client renders the overlay. *This picture is not mine but represents what my overlay code does*

What I built (learning notes)

Driver code

#include <ntifs.h>

extern "C" {
    NTKERNELAPI NTSTATUS IoCreateDriver(PUNICODE_STRING DriverName,
        PDRIVER_INITIALIZE InitializationFunction);

    NTKERNELAPI NTSTATUS MmCopyVirtualMemory(PEPROCESS SourceProcess, PVOID SourceAddress,
        PEPROCESS TargetProcess, PVOID TargetAddress,
        SIZE_T BufferSize, KPROCESSOR_MODE PreviousMode,
        PSIZE_T ReturnSize);
}

void debug_print(PCSTR text) {
#ifndef DEBUG
    UNREFERENCED_PARAMETER(text);
#endif // DEBUG

    KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, text));
}

namespace driver {
    namespace codes {
        constexpr ULONG attach =
            CTL_CODE(FILE_DEVICE_UNKNOWN, 0x696, METHOD_BUFFERED, FILE_SPECIAL_ACCESS);

        constexpr ULONG read =
            CTL_CODE(FILE_DEVICE_UNKNOWN, 0x697, METHOD_BUFFERED, FILE_SPECIAL_ACCESS);

        constexpr ULONG write =
            CTL_CODE(FILE_DEVICE_UNKNOWN, 0x698, METHOD_BUFFERED, FILE_SPECIAL_ACCESS);

    }

    struct Request {
        HANDLE process_id;

        PVOID target;
        PVOID buffer;

        SIZE_T size;
        SIZE_T return_size;
    };

    NTSTATUS create(PDEVICE_OBJECT device_object, PIRP irp) {
        UNREFERENCED_PARAMETER(device_object);

        IoCompleteRequest(irp, IO_NO_INCREMENT);

        return irp->IoStatus.Status;
    }

    NTSTATUS close(PDEVICE_OBJECT device_object, PIRP irp) {
        UNREFERENCED_PARAMETER(device_object);

        IoCompleteRequest(irp, IO_NO_INCREMENT);

        return irp->IoStatus.Status;
    }


    
    NTSTATUS device_control(PDEVICE_OBJECT device_object, PIRP irp) {
        UNREFERENCED_PARAMETER(device_object);

        debug_print("[+] Device control called.\\n");

        NTSTATUS status = STATUS_UNSUCCESSFUL;

        // determine which code was passed through.
        PIO_STACK_LOCATION stack_irp = IoGetCurrentIrpStackLocation(irp);

        // Access the request object sent from user mode.
        auto request = reinterpret_cast<Request*>(irp->AssociatedIrp.SystemBuffer);

        if (stack_irp == nullptr || request == nullptr) {
            IoCompleteRequest(irp, IO_NO_INCREMENT);
            return status;
        }

        //target process we want to accees
        static PEPROCESS target_process = nullptr;


        const ULONG control_code = stack_irp->Parameters.DeviceIoControl.IoControlCode;

        switch (control_code) {
        case codes::attach:
            status = PsLookupProcessByProcessId(request->process_id, &target_process);
            break;
        case codes::read:
            if (target_process != nullptr)
                status = MmCopyVirtualMemory(target_process, request->target,
                    PsGetCurrentProcess(), request->buffer,
                    request->size, KernelMode, &request->return_size);
            
            break;

        case codes::write:
            if (target_process != nullptr) 
                status = MmCopyVirtualMemory(PsGetCurrentProcess(), request->buffer,
                    target_process, request->target,
                    request->size, KernelMode, &request->return_size);
            
            break;

        default:
            break;
        }

        irp->IoStatus.Status = status;
        irp->IoStatus.Information = sizeof(Request);


        IoCompleteRequest(irp, IO_NO_INCREMENT);

        return status;
    }




} // namespace driver


NTSTATUS driver_main(PDRIVER_OBJECT driver_object, PUNICODE_STRING registry_path) {
    UNREFERENCED_PARAMETER(registry_path);

    UNICODE_STRING device_name = {};
    RtlInitUnicodeString(&device_name, L"\\Device\\marlDriver");

    // Create driver device obj.
    PDEVICE_OBJECT device_object = nullptr;
    NTSTATUS status = IoCreateDevice(driver_object, 0, &device_name, FILE_DEVICE_UNKNOWN,
        FILE_DEVICE_SECURE_OPEN, FALSE, &device_object);

    if (status != STATUS_SUCCESS) {
        debug_print("[-] Failed to create driver device.\\n");
        return status;
    }

    debug_print("Driver creation success\\n");

    UNICODE_STRING symbolic_link = {};
    RtlInitUnicodeString(&symbolic_link, L"\\DosDevices\\marlDriver");

    status = IoCreateSymbolicLink(&symbolic_link, &device_name);
    if (status != STATUS_SUCCESS) {
        debug_print("[-] Failed to establish symbolic link.\\n");
        return status;
    }

    debug_print("Driver symbolic link created\\n");

    SetFlag(device_object->Flags, DO_BUFFERED_IO);

    // Set the driver handlers to our functions with our logic.
    driver_object->MajorFunction[IRP_MJ_CREATE] = driver::create;
    driver_object->MajorFunction[IRP_MJ_CLOSE] = driver::close;
    driver_object->MajorFunction[IRP_MJ_DEVICE_CONTROL] = driver::device_control;

    //initialized our device.
    ClearFlag(device_object->Flags, DO_DEVICE_INITIALIZING);

    debug_print("[+] Driver initialized successfully.\\n");


    return status;
}




NTSTATUS DriverEntry() {
    debug_print("Driver entry debug print\\n");


    UNICODE_STRING driver_name = {};
    RtlInitUnicodeString(&driver_name, L"\\Driver\\marlDriver");



    return IoCreateDriver(&driver_name, &driver_main);
}