Spoofing Secure Boot on Windows

Tricking windows into thinking I have secure boot on.

Background

Windows works out Secure Boot status by reading EFI variables during boot. winload.efi reads the SecureBoot and SetupMode variables, and if SecureBoot == 1 and SetupMode == 0, it sets the kernel global nt!SeSecureBoot accordingly. Everything downstream reads from this, NtQuerySystemInformation(SystemSecureBootInformation), ExGetFirmwareEnvironmentVariable, usermode GetFirmwareEnvironmentVariable, all of it.

So the question was, can you intercept that read before Windows even sees it?

The Hook

UEFI exposes a RuntimeServices table, a set of function pointers that persist from firmware all the way into the OS. One of those is GetVariable, which is how any UEFI aware code reads EFI variables. Because it is just a pointer in a table, you can swap it from an EFI driver before ExitBootServices, and it stays swapped.

So that is exactly what I did. Hook RuntimeServices->GetVariable, intercept calls for SecureBoot and SetupMode, and return what Windows expects to see on a real Secure Boot system.

unsafe extern "efiapi" fn hooked_get_variable(
    variable_name: *const u16,
    vendor_guid: *const Guid,
    ...,
) -> Status {
    if cmp_name(variable_name, SECURE_BOOT_NAME) && *vendor_guid == EFI_GLOBAL_VARIABLE {
        *data = 1;
        return Status::SUCCESS;
    }
    if cmp_name(variable_name, SETUP_MODE_NAME) && *vendor_guid == EFI_GLOBAL_VARIABLE {
        *data = 0;
        return Status::SUCCESS;
    }
    ORIG_GET_VARIABLE(variable_name, vendor_guid, ...)
}

Anything that is not one of those two variables falls through to the real firmware function, so nothing else breaks.

Finding the Call Chain in Ghidra

To work out exactly what winload reads and when, I loaded winload.efi into Ghidra and traced down to SbIsEnabled2, which is the function that decides whether Secure Boot is considered active. Here is the actual decompiled output:

undefined8 SbIsEnabled2(undefined8 param_1, undefined1 *param_2)
{
    int iVar1;
    char local_res10 [8];  // SecureBoot value
    char local_res18 [8];  // SetupMode value
    undefined8 local_res20;

    if (DAT_1803115ff == '\0') {  // only runs once, result is cached
        local_res10[0] = '\0';
        local_res18[0] = '\x01';
        local_res20 = 1;

        // read SetupMode first, bail out entirely if it fails
        iVar1 = FUN_1800460fc(L"SetupMode", &DAT_1802bf4c8, 0, &local_res20, local_res18);
        if (-1 < iVar1) {
            local_res20 = 1;

            // read SecureBoot only if SetupMode succeeded
            iVar1 = FUN_1800460fc(L"SecureBoot", &DAT_1802bf4c8, 0, &local_res20, local_res10);

            // SecureBoot must be 1 AND SetupMode must be 0
            if (((-1 < iVar1) && (DAT_1803388d0 = DAT_1803388d0 | 8, local_res10[0] == '\x01')) &&
               (local_res18[0] == '\0')) {
                DAT_180311644 = 1;  // cached secure boot state, everything else reads this
            }
        }
        DAT_1803115ff = '\x01';  // mark as initialised
    }
    *param_2 = DAT_180311644;
    return 0;
}

And FUN_1800460fc is the actual GetVariable wrapper it calls:

void FUN_1800460fc(undefined8 param_1, undefined8 param_2, undefined4 *param_3,
                   undefined8 param_4, undefined8 param_5)
{
    int iVar1;
    undefined8 uVar2;
    undefined4 local_res18 [4];
    undefined8 local_48;
    undefined8 local_40;
    undefined8 local_38 [2];

    local_res18[0] = 0;
    local_48 = 0;
    local_40 = 0;
    local_38[0] = 0;

    iVar1 = FUN_180043518();  // check current TPL (task priority level)
    if ((iVar1 != 1) && (DAT_180351b90 != 0)) {
        // virtual address fixups for runtime mode
        FUN_1801ce4a4(param_5, &local_48, 0);
        param_5 = local_48;
        FUN_1801ce4a4(param_1, &local_40, 0);
        param_1 = local_40;
        FUN_1801ce4a4(param_2, local_38, 0);
        param_2 = local_38[0];
        FUN_180043458(1);
    }

    // actual GetVariable call into the runtime services table
    // this is where our hook intercepts
    uVar2 = thunk_FUN_1802a9020(param_1, param_2, local_res18, param_4, param_5);

    if (iVar1 != 1) {
        FUN_180043458(iVar1);  // restore TPL
    }
    if (param_3 != (undefined4 *)0x0) {
        *param_3 = local_res18[0];  // write attributes back to caller if requested
    }
    FUN_180046c54(uVar2);  // status check
    return;
}

thunk_FUN_1802a9020 is where it actually calls into the runtime services table, so that is where the hook sits and intercepts before the real firmware ever sees the request.

Some thigns that stand out from SbIsEnabled2 are, It reads SetupMode first and only bothers reading SecureBoot if that succeeded. Then it requires SecureBoot == 1 AND SetupMode == 0 before setting the cached state. So you have to spoof both variables, not just SecureBoot. My first attempt only spoofed SecureBoot and used the wrong GUID on top of that, so nothing worked and I had no idea why :')

Pretty neat for what is essentially just a pointer swap :3