╔════════════════════════════════════════════════════════════════════════════╗ ║ ║ ║ Static Kernel Patching Redux ║ ║ ║ ║ ~bah ║ ║ ║ ╚════════════════════════════════════════════════════════════════════════════╝ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃Introduction ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ In Phrack 60-8[1], jbtzhm wrote an article discussing how to modify kernel images on disk to include a backdoor. I found this to be an interesting idea for a backdoor approach, but since then I’ve not encountered an attempt to do this for modern x86 kernel images this got me wondering on how to revive the technique. Since the original article was published we have all since moved to 64 bit systems and UEFI has became mainstream. Although the BIOS path remains in virtualised setups, it has under gone a major rework in how its implemented by the kernel and bootloaders as the years have gone by. There is limited prior work outside the Phrack article, beyond some attempts specifically supporting 32 bit ARM involving repacking the kernel and adjusting the various offsets used in decompression [2][3][4]. Also relevant was the work done in bootkitty[5], and my bootkit grabit[6] for how they implemented their hooks. There is also PciLeech[7], a DMA attack tool, that has some relevant code in how it implements its Linux UEFI support via a runtime hook. Finally, there has also been some work done on infecting LKMs[8] in Phrack 68-11, where the technique still works with modified code. In this article I will be discussing my approach to modify the kernel images on x86. In the process I will touch on both UEFI PEs, the Kernel ELFs, and relevant parts of the boot process for both UEFI and BIOS, and some linking hacks. This technique supports both UEFI and BIOS boot paths and will ultimately load a kSHELF as the final payload (described in tmp.0ut #4[9]) which can implement whatever is desired. There are some space limitations for the payload, around 128KB, but it should be sufficient for most kernel and payloads as you can otherwise stage another component. The full source can be found on GitHub at https://github.com/bahorn/skp. The core basis for this work was done in late 2024, and with a large rewrite of components done this year (2026) to fix bugs and make it more reliable. The techniques have been automated and tested on bzImages for Linux 5.15 to 7.0, with and without GRUB as the bootloader. Both the BIOS and UEFI boot paths are supported. It should mostly work, but there might be kernel configurations that break this. If you encounter one, or any weird bugs, please let me know. ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃Patching the bzImage ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Kernel images exist on x86 as a UEFI PE Executable, which also retain some structures to support BIOS bootloaders (i.e GRUB in some cases, or the direct kernel boot feature of QEMU often used in virtualisation). We will go into more details as we progress and explain the patching process. To modify the images to make the kernel load our payload, we will have to make several changes to the PE header, add a new section for our runtime components, and also modify several fields in the structures relevant to the BIOS boot path. First, we'll go over the changes that we make to the image on disk, before going over the runtime components in detail that are required to support getting a payload running in ring0 after the kernel has booted. ┌────────────────────────────────────────────────────────────────────────────┐ │On-Disk Modifications │ └────────────────────────────────────────────────────────────────────────────┘ Patching the bzImage on disk involves many steps, which is best described using the diagram set out below: ┌──────────────────────────────────┐ ┌──────────────────────────────────┐ │ │ │ │ │ Original Kernel │ │ Pre-Built Runtime │ │ │ │ │ └──────────────────┬─────────┬─────┘ └─────────┬───────────────────┬────┘ │ │ │ │ │ │ │ │ ┌────────────────────┼─────────┼──────────────────┼───────────────────┼─────┐ │On-Disk Patching │ │ │ │ │▒ │ │ │ ┌──────────▼──────────┐ │ │▒ │ │ │ │ Dummy Link │ │ │▒ │ ┌─────────────────▼─────┐ │ └─────────────────────┘ │ │▒ │ │Extract Kernel ELF with│ │ │ │ │▒ │ │ vmlinux-to-elf │ │ │ │ │▒ │ └───────────┬───────────┘ │ │ │ │▒ │ │ │ │ │ │▒ │ │ │ │ │ │▒ │ ┌───────────▼───────────┐ │ │ │ │▒ │ │Find Space and Symbols │ │ │ │ │▒ │ │ from Kernel ELF │ │ │ │ │▒ │ └─┬─────────────────────┘ │ │ │ │▒ │ │ │ │ │ │▒ │ │ ┌───────────────────────▼──────────────────▼─────────────────┐ │ │▒ │ │ │ Add Extract Section and Modify PE Header │ │ │▒ │ │ └──────────────────────────────┬─────────────────────────────┘ │ │▒ │ │ │ │ │▒ │ │ │ │ │▒ │ │ ┌──────────────────────────────▼─────────────────────────────┐ │ │▒ │ │ │ Change boot protocol values │ │ │▒ │ │ └──────────────────────────────┬─────────────────────────────┘ │ │▒ │ │ │ │ │▒ │ │ │ │ │▒ │ │ ┌──────────────────────────────▼─────────────────────────────┐ │ │▒ │ └▶│ Link Runtime with Kernel and Payload │◀┘ │▒ │ └──────────────────────────────┬─────────────────────────────┘ │▒ │ │ │▒ │ │ │▒ │ ┌──────────────────────────────▼─────────────────────────────┐ │▒ │ │ Place Runtime in New Section │ │▒ │ └──────────────────────────────┬─────────────────────────────┘ │▒ │ │ │▒ └─────────────────────────────────────┼─────────────────────────────────────┘▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ │ ┌────────────────────────────────────▼─────────────────────────────────────┐ │ │ │ Patched bzImage │ │ │ └──────────────────────────────────────────────────────────────────────────┘ -[ Extracting the Kernel and Symbolising ------------------------------------- A bzImage consist of a loader / header component and an embedded compressed ELF. The loader decompresses the ELF during boot and maps it into memory. It is not hard to unpack the kernel ELF from the bzImage, there is even a script in the kernel tree that does this, but as this ELF has no symbols it is hard to determine where everything inside the kernel is. Luckily, the majority of kernels include a structure to support /proc/kallsyms, which contains all the symbols and their offsets. Implementing code to do this is fairly complex, but there is a useful FOSS project called vmlinux-to-elf [10] that does this, which will be used. Once downloaded, it can simply be invoked via uv with:

$ uv run --tools path/to/vmlinux-to-elf source.bzImage out.elf
-[ Compiling and Linking the Runtime ----------------------------------------- The details of what the runtime does will be considered later, but one specific design decision of note is that there is just a single runtime that is built independent of the kernel as an generic object file, with each component used inside it built separately, including with different cflags for the different components (like using -msabi for the UEFI code), and packaged together with `ld -r` for use by the patcher. The runtime only becomes kernel specific during the patching process, where it is linked against the kernel using offsets extracted from the kernel ELF and other values only known when attempting to link. These values are passed in by just appending them to the linker script. This same patch time linking step is used to add in the final payload, which is a bit more kernel specific than the loading runtime. The code uses the following linker script, with each component used in the runtime arranged linearly: = linker.lds =================================================================

OUTPUT_FORMAT("binary")

# We append symbols the final output needs in patch-bzimage/badlink.py

SECTIONS
{
    .uefi_hook : {
        *(.uefi_hook)
    }

    .code32_hook : {
        *(.code32_hook)
    }

    # Stage1, which is the UEFI stage (bypassed for BIOS booting)

    .stage1.text.start : ALIGN(16) {
        *(.stage1.text.start)
    }

    # want some spacing
    .stage1.text : ALIGN(16) {
        *(.stage1.text)
        *(.stage1.rodata)
        *(.stage1.data)
        *(.stage1.bss)
        *(.stage1.data.rel.local)
    }

    # Stage 2, which is what is ran when we are in the kernel, used to load
    # the real payload.

    .stage2 : ALIGN(16) {
        *(.stage2.*)
    }

    # The loader for our paylaod
    .kshelf_loader.start : {
        *(.kshelf_loader.text.start)
    }

    .kshelf_loader.text : ALIGN(16) {
        *(.kshelf_loader.text)
        *(.kshelf_loader.rodata)
        *(.kshelf_loader.data)
        *(.kshelf_loader.bss)
        *(.kshelf_loader.data.rel.local)
    }

    # now the payload, stored in the .data section but including these for the
    # sake of it.

    .payload : ALIGN(16) {
        *(.text)
        *(.data)
        *(.bss)
    }

    # the end!

    _kshelf_loader_end = .;

    ...
}
============================================================================= A nice detail is that the payload is optional as its defined with a weak attribute in the kshelf-loader:

__attribute__((weak)) unsigned char payload[0];
__attribute__((weak, section(".data"))) unsigned int payload_len = 0;
During the patching phase the runtime is linked twice, first with dummy values to determine the final runtime size and then with the offsets of various functions and values contained in the runtime (for example, where the runtimes UEFI / BIOS entry points are relatively, etc). All the offsets can be obtained by parsing the map file the linker is capable of producing with `-Map=path/to/mapfile`. The second time is performed with the real values that have been determine by looking at the kernel (space to place the runtime in the BIOS path, the addresses to patch, symbol offsets required before kallsyms_lookup_name()) can be called, and from the patching process itself (calculated UEFI entrypoint). -[ Adjusting the PE Header and Adding the Runtime ---------------------------- There are quite a few changes that need to be made to the PE Header. First, we need to remove any signature attached to the PE (used for Secure Boot). Second, on older kernels the .reloc section needs to be removed, which is not used to boot kernels (and was removed in 6.7 [11]), to free up a space to define our own section. This section is given RWX permissions, and ensuring it has enough space for the runtime and payload (which is known from the dummy link). In addition to adding the extra section, the following changes will need to be made: * the PEs SizeOfImage and SizeofCode, will need to be corrected to account for the new section. * The previous section will need to be padded for the BIOS path in a manner similar to what was done by jbtzhm. * The NX_COMPAT flag will need to be disabled as it is not currently supported by the patching process. * The UEFI Entrypoint will need to be pointed to the newly added section. There is a bit of a dance to be done with this step and the previous linking step. The size for the runtime is determined as mentioned by using dummy values, before being linked again by passing in the original UEFI entrypoint and a few offsets for the BIOS path. To fully understand the changes made, readers are advised to read src/patch-bzimage/add_data.py in the project. That section sets out the full logic and address the quirks involved in padding properly, which are numerous and rather dull to explain. -[ Hooking the BIOS Path ----------------------------------------------------- For a BIOS bootloader to be able to boot the kernel, the x86 boot protocol is used. There are several boot protocol versions, but as the focus in this project is on only 5.15+, the only relevant boot protocol version is Protocol 2.15 [12]. The boot protocol defines a fixed header at offset 0x1F1 in the bzImage which is used as a mechanism for the kernel and bootloader to communicate between each other. There are a few relevant fields in this header: * code32_start, at offset 0x214, which is an address the kernel will jump to as it transitions to protected mode. * relocatable_kernel, at 0x234, a flag indicating if the kernel is relocatable or not. * pref_address, at 0x258, the preferred kernel loading address. All of these are modified. First code32_start is set to our hook (details discussed in the next section). This is at a fixed address (0x100_000 + its offset from the start of the kernel image), and is not the virtual address used in the PE header. The kernel is then marked as non-relocatable, and the pref_address is defined to be 0x100_000 in order to avoid any issues involving them. With those set, we can now move onto booting the kernel. ┌────────────────────────────────────────────────────────────────────────────┐ │Runtime │ └────────────────────────────────────────────────────────────────────────────┘ The runtime has multiple stages and branches depending whether it is booted via UEFI or BIOS. This is further complicated by the fact that before 6.6 a UEFI runtime service needs to be used for the UEFI path. This requires more complicated work in order to run the payload and not cause a panic(). Around the releases of 6.6/6.7 a significant amount of work was done on the x86 boot path, which is why those versions are relevant. The ultimate flow can be explained with: ┌──────────────────────────────────┐ ┌──────────────────────────────────┐ │ │ │ │ │ Boot Via BIOS │ │ Boot via UEFI │ │ │ │ │ └─────────────────┳────────────────┘ └─────────────────────────────┳────┘ ┃ ┃ ╔═══════════════════╬════════════════════════════════════════════════╬═════╗ ║Runtime ┃ ┃ ║▒ ║ ┌───────────────▼──────────────────────────────────────────────┐ ┃ ║▒ ║ │code32_start Hook │░┃ ║▒ ║ └──┬───────────────────────────────────────────────────────────┘░┃ ║▒ ║ ░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃ ║▒ ║ │ ┃ ║▒ ║ │ ┌──────────────────────────────────────────────────────────▼──┐ ║▒ ║ │ │Setup ExitBootServices() Hook │░ ║▒ ║ │ └──────────────────────────────────────────────┬──────────────┘░ ║▒ ║ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░ ║▒ ║ │ │ ║▒ ║ │ ┌─────────────────────────────────────────────┼──────────────┐ ║▒ ║ │ │ExitBootServices() Hook │ │░ ║▒ ║ │ │[Method used depends on kernel version] │ │░ ║▒ ║ │ │ │ │░ ║▒ ║ │ │ │ │░ ║▒ ║ │ │ ┌────────────────────────────┤ │░ ║▒ ║ │ │ │ │ │░ ║▒ ║ │ │ │ │ │░ ║▒ ║ │ │ ┌─────────────▼────────────┐ ┌─────────────▼────────────┐ │░ ║▒ ║ │ │ │ │ │ │ │░ ║▒ ║ │ │ │ │ │ │ │░ ║▒ ║ │ │ │ Direct Patching │ │ Hook A Runtime Service │ │░ ║▒ ║ │ │ │ (For Kernels >6.6) │ │ (Supports all Kernels) │ │░ ║▒ ║ │ │ │ │ │ │ │░ ║▒ ║ │ │ │ │ │ │ │░ ║▒ ║ │ │ └────┬─────────────────────┘ └───────────────────────┬──┘ │░ ║▒ ║ │ │ │ │ │░ ║▒ ║ │ └───────┼───────────────────────────────────────────────┼────┘░ ║▒ ║ │ ░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░ ║▒ ║ │ │ │ ║▒ ║ ┌───▼───────────▼────────────────────────────────────────────┐ │ ║▒ ║ │Decompression Hook │░ │ ║▒ ║ │ │░ │ ║▒ ║ │ ┌────────────────────────────────────────────────────────┐ │░ │ ║▒ ║ │ │ │ │░ │ ║▒ ║ │ │ Copy Payload into spare space at the end of .text │ │░ │ ║▒ ║ │ │ │ │░ │ ║▒ ║ │ └───────────────────────────┬────────────────────────────┘ │░ │ ║▒ ║ │ │ │░ │ ║▒ ║ │ ┌───────────────────────────▼────────────────────────────┐ │░ │ ║▒ ║ │ │ │ │░ │ ║▒ ║ │ │ Hook an Initcall to call the Payload │ │░ │ ║▒ ║ │ │ │ │░ │ ║▒ ║ │ └────────────────────────────────────────────────────────┘ │░ │ ║▒ ║ └──┬─────────────────────────────────────────────────────────┘░ │ ║▒ ║ ░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ ║▒ ║ │ │ ║▒ ║ │┌───────────────────────────────────────────────────────────▼────┐ ║▒ ║ ││Runtime Services Hook │░ ║▒ ║ │└───────────────────────────────────────┬────────────────────────┘░ ║▒ ║ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░ ║▒ ║ │ │ ║▒ ║ │ │ ║▒ ║ ┌───▼────────────────────────────────────────▼─────────────────────────┐ ║▒ ║ │ │ ║▒ ║ │ Run the Payload! │ ║▒ ║ │ │ ║▒ ║ └──────────────────────────────────────────────────────────────────────┘ ║▒ ╚══════════════════════════════════════════════════════════════════════════╝▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ -[ BIOS Entrypoint / code32_start hook --------------------------------------- The code32_start hook runs right as the kernel transitions to protected mode. This hook is used to make a simple patch to the boot code so that just after the kernel is decompressed, another hook that has been prepared is called. The patch is just a simple, with _offset_bios_entry being the address of our decompression hook: push _offset_bios_entry ret To make this patch, the kernel on the disk is examined to find one of two patterns, both of which jump to the decompressed kernel. One is for pre 6.6 and one for after, and using these two is enough to work reliably across all the kernel versions. For post 6.6, the pattern is

    4c 89 fe ff e0 f4 eb fd 66 0f 1f 44 00 00
And or pre 6.6 the pattern is:

    e8 XX XX XX XX 5e ff e0
These are just the jmp rax (ff e0) instructions in arch/x86/boot/compressed/head_64.S and the surrounding context. This is sufficient to be able to pattern match reliably, which is replaced with a call to the decompression hook. One slight quirk with this patch is the address that it jumps to. The kernel gets relocated before the defined hook is called, which chops off our appended code. This breaks using a relative offset, but the original mapping of the kernel remains at a low fixed address (0x100_000 + the offset to the defined code) which is why we push and ret to jump to it. Finally after setting all that up it continues booting by calling the original entrypoint (which should be 0x100_000). -[ UEFI Entrypoint ----------------------------------------------------------- As the UEFI entrypoint was adjusted to point to the our code, it is jumped to straight from the firmware. At this point we setup a hook on ExitBootServices(), which is always called during boot to leave the UEFI Boot Services context. This just requires a single pointer change in the SystemTable->BootServices structure, which the UEFI firmware kindly passes to us. This can be implemented as simply as:

EFI_EXIT_BOOT_SERVICES orig_exitbootservices = \
    (EFI_EXIT_BOOT_SERVICES) 0x41424344;
EFI_SYSTEM_TABLE *systable = (EFI_SYSTEM_TABLE *) 0x41424344;
EFI_BOOT_SERVICES *bootservices = (EFI_BOOT_SERVICES *) 0x41424344;

void main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
    bootservices = SystemTable->BootServices;
    systable = SystemTable;

    orig_exitbootservices = bootservices->ExitBootServices;
    bootservices->ExitBootServices = exit_bootservices_hook;
}
A point of note is that the code to setup the hook is implemented in C, and called by a small stub (what the UEFI entrypoint is pointing to) that saves registers and actually calls the original entrypoint. The original entrypoint is passed in via the linking step, and once it is called it the boot process continues normally. This part is one of the reasons this code in not currently suitable for use with NX_COMPAT, due to the use of globals to store references to the system table. -[ ExitBootServices() Hook --------------------------------------------------- At this point, there are two options on how to proceed for kernels more recent than 6.6, and only one for releases before that. On the more recent kernels the kernel is decompressed and in memory at this point, which means the same approach can be implemented as the BIOS path, which is discussed in more detail in the next section. In brief however, this is where the new code is copied into a part of the .text ELF section that is used nominally as padding, and an initcall hook is used to jump to it. Finding the memory region containing the kernel is implemented by checking the size of each mapping, and if one is large enough, a magic value obtained during the linking step is used to check if its really the kernel. On older kernels, or if the mapping containing the kernel can not be found, a UEFI runtime service is hooked instead to let the kernel run the payload. When booted with UEFI, the kernel will access a few UEFI runtime services. Of note is the one setting up efivarfs (normally located at /sys/firmware/efi/efivars on a running system) which occurs quite early in the boot during the initcall stage, which calls RT->GetNextVariableName(). Later on the kernel also calls RT->GetVariable() from integrity_load_keys(), which occurs after all the initcalls have finished running. In skp, the former is used as its called earlier in the boot process. In the runtime hook path, the original function after the payload is setup need to be restored and called. This is a trivial structure update. -[ Decompression Hook -------------------------------------------------------- On the BIOS path, we are continuing from the previously mentioned low address that remains mapped and contains our code. Here we copy a patch into the freshly decompressed kernel and hook an initcall. Otherwise on the UEFI path, we are still in Boot Services, but the kernel is decompressed and in a region we can access from our ExitBootServices() hook. To allow built in modules and other features to run code during boot, the kernel implements a mechanism called initcalls, which has existed since the earliest days of the kernel (which surprised me as the original article didn't use them). Initcalls are ordered in levels, stored as a list containing every initcall of that level, and then started from do_initcalls() (in /init/main.c in the kernel tree) by their level. The level lists consist of offsets from that specific location in that list to the target function being called (`&(level[initcall) + level[initcall]`). We will be hooking one to redirect execution to our payload, which we need to get into the kernel memory somehow. To do this, spare space in the .text section can be used as a cavity. This is normally filled with a large number of 0xCC bytes, so during patching time a large group of them that have enough space to contain the payload is searched for. The initcall is then modified to point to this reclaimed space, as its a known offset from the start of the kernel, which was passed in via rax register in the BIOS path, or is the start of the memory region found in the UEFI path. I chose to hook the regulator_init_complete() initcall, found by looking at the kernel ELF and searching for the symbol with a regex (`__initcall__kmod_core[_0-9a-z]*regulator_init_complete[_0-9a-z]*`). There is no specific reason to use this one beyond finding it many kernel builds. Any widely used one is viable. This decompression hook is similar to what bootkitty does, though we go further and inject the code directly into the kernel, rather than just disabling the module signature checking. -[ Running the Payload ------------------------------------------------------- Now the paths have joined together, we now need to get something useful running. For this, I used an adaption of my kSHELF loader from tmp.0ut #4[9]. When running from the initcall path, we are cleanly mapped into kernel memory. The kernel is at a known offset to where the code is being loaded, which can be used to find out the relative address of kallsyms_lookup_name() given we know it from kernel ELF at patching time. With kallsyms_lookup_name() resolved, the rest of the symbols needed to implement a basic loader to map the kSHELF into memory and make it executable can be resolved easily. Once the payload is mapped, the kernel will continue to boot after it returns and has done its setup. The UEFI runtime hook has a few pain points that need to be addressed to work reliably across kernel versions. When a UEFI runtime service is invoked, the kernel enters a temporary mm with the UEFI related memory mapped into memory, while also keeping the kernel mapped, so it is possible to call kernel functions freely (accounting for the difference in ABIs between UEFI land / Linux land) if the address of the kernel is known. To do this, read the stack to get the address of __efi_call + 40, so the following can obtain the base address of the kernel if you know the relative offset of __efi_call from the base of the kernel:

    mov rax, [rsp]
    sub rax, __efi_call + 40
With that, any symbol in the kernel can be called. However, there remains some complexity, as in some kernels you can freely call whatever you want, allocate memory, etc, but other kernels, such as 5.15, are broken. To try and fix this, preemption (stored in a percpu value) was initially disabled, but that causes issues with calls like vmalloc() in more recent kernels, and it remains difficult to transition out of this scope. To work around that, kmalloc() with the GFP_ATOMIC flag can be used freely to allocate memory for the payload. This is suitable for allocations of roughly 128KB (vmalloc() would normally be preferred but kmalloc() can achieve the same page alignment requirements since 5.4[13] for us to safely set_memory_x() the pages). The payload can then be invoked by execute_in_process_context() (part of the workqueue subsystem) so that it can be ran in a more normal environment. Calling it requires interrupts to be disabled so it doesn't try to run the provided function immediately. This can be done by calling irq_enter_rcu(), saving the eflags and then calling execute_in_process_context(), before restoring the eflags and calling irq_exit_rcu(). If you do not restore the eflags, you will have the kernel complain about a firmware bug because it expects them to be consistent (and irq enter/exit functions trash them slightly). I used the irq_{enter,exit} functions as I wanted functions that could be resolved with kallsyms_lookup_name() as it was more reliable than changing percpu values directly from this code. Essentially:

    asm ("pushf; pop %0" : "=rm" (flags) : : "memory"); // save the eflags
    irq_enter_rcu(); // this disables preemption
    execute_in_process_context(start, ew); // schedule the payload to run
    irq_exit_rcu(); // cleanup
    asm ("mov %0, %%rax; push %%rax; popf" : : "rm" (flags) : "rax" );
With that, you should be freely running the kSHELF in your patched kernel:

[  2.556448] PATCHED KERNEL
[  2.556716] payload_len: 6072
[  2.557044] Called via UEFI Runtime hook
[  2.557399] Loading payload
[  2.557903] relative relocation: 1689
[  2.558248] relative relocation: 944
[  2.558574] relative relocation: 4672
[  2.558729] relative relocation: 880
[  2.559118] relocating sym: register_ftrace_function
[  2.559772] relocating sym: commit_creds
[  2.560154] relocating sym: kallsyms_lookup_name
[  2.560599] relocating sym: prepare_creds
[  2.560777] relocating sym: ftrace_set_filter_ip
[  2.561206] relocating sym: unregister_ftrace_function
[  2.561775] relocating sym: _printk
[  2.562075] RX
[  2.562255] RW
[  2.562403] Entrypoint: ffff888101ca8370
[  2.562898] Greetings from SHELF
[  2.567905] efivars: Registered efivars operations
...
[  2.602202] rootkit: Loaded >:-)
As an alternative to using kSHELFs, it is possible to use the existing kernel infrastructure to load a normal LKM, like how kmatryoshka[14] does. ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃Conclusion ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Hopefully this was interesting! If you are interested in doing further work on this, there are a few directions you could take this: * It would be useful to support more architectures as some modern aarch64 systems also use UEFI. * Supporting UEFI secure boot setups could be done having a boot kit component to enroll a custom MOK, which can be used to boot these patched kernels. * This could be implemented by repacking the kernel, like the prior 32bit ARM work, using a "donor" kernel tree and replacing the unpacked kernel there before rebuilding. This approach probably would require more work and probably be less reliable than approaches used in this report, but is an option for other architectures beyond x86. * Finally, Unified Kernel Images (UKIs) are becoming more common, so supporting them would be interesting. Finally, I want to say my thanks to jbtzhm for their original article in Phrack, and many thanks to the FOSS maintainers who build the projects that make work like this possible: * marin-m for vmlinux-to-elf, a core part of this project which which it would not have been possible without. * WerWolv for ImHex, which provided me my initial understanding of the bzImage structure with all the handy patterns files. * hasherezade for PE-Bear, which helped me debug some some nasty bugs in my PE patching code. Clearly marking sections as not being mapped was a life saver! ~bah ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃References ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ [1] https://phrack.org/issues/60/8#article [2] https://github.com/Caesurus/CTF_Writeups/blob/main/2024-04-zImageKernelPatch/README.MD [3] https://jamchamb.net/2022/01/02/modify-vmlinuz-arm.html [4] https://stackoverflow.com/questions/76571876/how-to-repack-vmlinux-elf-back-to-bzimage-file [5] https://www.welivesecurity.com/en/eset-research/bootkitty-analyzing-first-uefi-bootkit-linux/ [6] https://blog.b.horn.uk/posts/grabit/ [7] https://github.com/ufrisk/pcileech/tree/master [8] https://phrack.org/issues/68/11.html#article [9] https://tmpout.sh/4/5.html [10] https://github.com/marin-m/vmlinux-to-elf [11] https://github.com/torvalds/linux/commit/fa5750521e0a4efbc1af05223da9c4bbd6c21c83 [12] https://www.kernel.org/doc/html/v6.1/x86/boot.html [13] https://github.com/torvalds/linux/commit/59bb47985c1db229ccff8c5deebecd54fc77d2a9 [14] https://github.com/milabs/kmatryoshka --[ PREV | HOME | NEXT ]--