Future, and It Doesn't Boot
The bugs I reported in the GRUB2 bootloader are finally public, my patches landed and sbat.csv should be updated soon. This means my oath of silence is finally over and I can talk about my research, where I found:
- A ton of Use-After-Frees involving filesystems, devices and modules in general.
- Some heap and stack buffer overflows.
- Various stack clashes (where you collide the stack with the heap).
- A few boring non-exploitable things (OOB reads, uninitialized variables, null derefs).
These bugs were found by a mixture of fuzzing, manual auditing and variant analysis. Did have a few of the bugs dupe with other researchers. I’m sorry if I killed your bugs, please keep me off your list.
I have exploited the majority of the bugs I found to get arbitrary code execution1, with one I developed into a real secure boot bypass. So if you are a distro maintainer, merge the patches and update your SBAT generation. If you are a user, install the updated packages when they become available.
Disto maintainers, please DO NOT CHERRY-PICK COMMITS. There are potentially exploitable issues that do not have CVEs. Just merge everything, there are only like 5 commits of the 75 that aren’t direct security fixes.
It is a bit too soon to go into precise exploitation details for the bugs2, maybe I’ll share more later on. I do however want to demonstrate Grabit, my proof-of-concept UEFI Linux bootkit, as I was watching everyone talk about bootkitty last November and was laughing because I had a way better private thing that I wrote a few weeks prior.
Only going to be discussing techniques with brief snippets of code, as I will be keeping my repo private because I don’t want stuff I wrote to end up in a Mandiant report (the real reason is that my code is trash). Fairly similar to what I did in skp3, specifically the UEFI runtime hook and the kSHELF loader4.
If you have never looked into developing bootkits, its worth noting the hard part is surviving all the transitions in the boot process. So if you have code exec early on, the challenge is how do you persist throughout the boot process without having to do really invasive and unreliable patching of the next stages code? Nobody wants to be forced to pattern match on code generated by a compiler, especially if you want it to work generically across kernel versions.
In Grabit I managed to get away with no hard coded kernel offsets
(with the exception of +40 to find the address of __efi_call
from the stack)
, with only TPM PCRs 8 + 9 changing.
PCR 10 also changes, but that is because linux changes it EVERY time you boot as
its based on the order modules get loaded in…
If you care, the bootkit and payload (written in C) along with the exploit (a python script that generates GRUB configs) are ~1500 lines or so together. So all achievable to implement by yourself. I’m just a 20-something who had a bit too much free time and I managed it.
Background
GRUB is the main bootloader used by most
linux distros nowadays (with the only real alternatives being systemd-boot,
unified kernel images, and Das U-Boot in certain contexts).
In the secure boot case, GRUB is loaded by a binary called
shim which is signed by Microsoft.
shim is meant to be a small loader that only exists to verify the second stage
bootloader is correctly signed and passes the SBAT checks (a mechanism to avoid
wasting dbx space).
It embeds its own keys for your distro, and lets you have machine owner keys
(MOK) if you want to run custom kernels / modules / bootloaders / etc.
To enrol your own MOK you do get prompted by MokManager, as you submit requests
to it via EFI variables
which only get moved into the real variables after user interaction to confirm.
The core management variables shim uses are only accessible before a call to
ExitBootServices()
so you can’t tamper with them from the operating system
after boot.
A good5 secure boot bypass is one that avoids all the interaction required for MOK key enrolment, can be triggered with just root on the operating system you boot normally, fast, reliable, and not need physical access. Exploiting GRUB can offer all of them, which makes it an interesting target.
A secondary concern is Measured Boot, where during the boot components are fed to the TPM to keep track of whats been loaded. This is a deterministic process, so if you run the same software you should get the same measurements. These measurments are split into different registers in the TPM, called PCRs. This is important as there are disk encryption setups that will automatically decrypt the disk using keys stored in the TPM, if the PCRs are storing the right value. How useful GRUB exploits are in breaking these setups depends on what PCRs are used, though its mixed as the best advice out there does suggest using the PCRs 8 and 9 which measure the GRUB config and files read by GRUB but its common to not do it as well. You can do a GRUB exploit without changing these PCRs if you manage to one-shot exploit a filesystem bug, but that will be an impressive feat to pull off. Endless glory awaits the hacker who reliably manages it.
GRUB has had a few security issues over the years. You might remember the classic “press backspace 28 times” bug to bypass the optional password feature GRUB has, though this isn’t relevant to us in our pursuit of breaking secboot. More relevant is BootHole, found by Eclypsium in 2020, and there has been a variety of CVEs that reported over the last few years (2020, 2021, 2022, 2023 + Maxim Suhanov’s blog post on his NTFS bugs, and this years). Various downstream patches from the various distros have also had issues. Surprisingly, there has been been only one public exploit that I’ve seen, which was for CVE-2020-14372. Beyond that, the most I’m aware of is Damn Vulnerable UEFI which has a challenge to crash GRUB with BootHole, but nothing on exploitation.
This is fairly surprising as mitigation wise, it’s the 90s. There isn’t much that is relevant for stopping exploitation:
- DEP/NX is not relevant right now for most firmware. GRUB allocates its heap
with
EFI_LOADER_CODE
(seeadd_memory_regions()
ingrub-core/kern/efi/mm.c
), which is RWX by default on pretty much all UEFI firmware6. It seems booting linux with UEFI NX still needs work (see Gerd Hoffmann’s 2023 blog post on booting linux with UEFI NX). - ASLR doesn’t exist, but if you are writing exploits you don’t want to depend on fixed addresses anyway7.
- Stack Canaries. Code exists for it (see
grub-core/kern/efi/init.c
), not really enabled in signed builds AFAIK. - Stack Clashing. Guard pages are dependent on the UEFI firmware (See this). Default OVMF builds are exploitable, Project Mu should not be.
- GRUB has its own heap allocator (seperate from the default UEFI one) which has inline heap metadata with no hardening. At the start of every allocation is the size, a pointer to the next free block and a 4 byte magic value to indicate if the allocation is free or not. These magic values are not ascii hardened, though they have values larger than 0x7f. So tricks like overwriting the size then freeing, hijacking the freelist, etc work.
I should note that UEFI secure boot in general is not really regarded as that strong, as its dependent on all the signed binaries out being safe and not leaving any bad ones unrevoked. This has been a major issue in practice. As I was writing this ESET dropped CVE-2024-7344 which was a signed binary implementing its own PE loader with no checks, which is a pretty good example of what is out there, and Binarly noticed the revocation lists used by the UEFI forum are missing revocations listed by Microsoft. Bill Demirkapi’s talk from Offensive Con 2024 (which also briefly covers CVE-2023-40547, which he found in shim) gives some good background on this. I’d also checkout the 38c3 talk “Windows BitLocker: Screwed without a Screwdriver” by th0mas on exploiting bitpixie as that bug was still exploitable even after being patched, as it was still possible to chainload vulnerable versions.
For UEFI bootkits, there is a pretty rich history, though all beyond bootkitty target windows. If you care you should probably read “Rootkits and Bootkits” by Alex Matrosov, Eugene Rodionov, and Sergey Bratus.
My Approach
The video is demonstrates my bootkit on Ubuntu 24.04 running in KVM with OVMF
set up and enrolled in secboot, which should have been pretty close to
up-to-date at the time I recorded it
(Note the date shown in my tmux session is November 11th 2024, latest GRUB
release for noble was 2.12-1ubuntu7 at the time).
First half of the video is showing the system before the bootkit is installed,
showing the system is clean, and the second half shows the bootkit running.
I purposely made it obvious that is running, with a read
command in the GRUB
config to slow the boot down.
The TL;DR:
- Exploit a bug in GRUB that is reachable from the config file, to disable the verifiers framework.
- The bootkit is implemented as a GRUB module, which you can insmod after you disable the verifiers.
- Hook the GRUB linux command to figure out which kernel you loaded, so you can read System.map off disk to avoid hard coding offsets.
- Hook
ExitBootServices()
to setup a Runtime services hook, which gets called by linux during boot. - All to load a boring ftrace rootkit (non-LKM!) that just hooks
sys_kill()
to let you privesc.
As always, there are probably better tricks but this works and sucks a bit less than what I was seeing publicly. I assume people have private things even better than this (plz share).
GRUB Config Exploits
GRUB has a scripting language (I’ve written a verilog simulator that uses it lol), which lets you access a variety of commands that you’d expect a bootloader to support. Its a bit limited, lacking things like mathematical operations (If you need them, I just constructed boolean adders and stuff, but never actually needed those in my exploit pocs). It does however let you define functions, variables, loops, etc.
As an example:
function blah {
set a=b
}
echo Change the value of a
set a=a
if [ a = a ] ; then
blah
fi
echo Refer to a variable, stored by name in a
set ${a}=b
while [ ${a} = b ] ; do
set ${a}=c
done
set ${a}=${a}${a}
unset a
And if you want to loop 2^N times and not construct an adder you can do things like:
function a_0 {
echo a
}
function a_1 {
a_0
a_0
}
...
function a_N {
a_N-1
a_N-1
}
This scripting language is how you define how the system boots, and on the most common secure boot setups you can still make GRUB parse an untrusted config (If you make your own signed GRUB build you can enforce PGP sigs however). So if you want to exploit GRUB, a bug in one of the commands is an ideal way to go.
The easiest way of checking what commands your specific build has is via the
lsmod
and help
commands.
You can also look at the build script used by the distro (i.e
Ubuntu,
Fedora),
just be aware that these have dependencies that you’ll only notice from lsmod
.
Finally, using a tool like binwalk
also works as you can dump the raw ELFs for the built-in modules.
One thing to note that when secure boot is active, GRUB has something called
lockdown mode enabled.
This restricts you from using doing things that could be used to break secboot
(writing memory, changing acpi settings, loading GRUB modules, etc).
Its implemented by looking at the files you load and from where, a wrapper
around how commands get registered (grub_register_command_lockdown()
) to block
the insecure ones, and some checks with grub_is_lockdown()
around the code
base.
So you need to be more creative in your bugs.
The bug I used to get code exec for my bootkit poc was CVE-2025-0622
(which I call freehandle8), which I targeted against a signed build of GRUB
provided by Ubuntu.
This bug was a simple Use-After-Free involving a function from a module being
called after it had already been unloaded, due to a hook being left in place
when it shouldn’t have been.
I assume the exploitation strat is fairly obvious, given that DEP isn’t a thing.
Unload the target module, heap spray your code, trigger the bug and
w00t w00t w3 g0t r00t b00ts3rv1ces.
I won’t be going into more details than that for now.
I wrote my exploits as Python scripts that just output GRUB configurations alongside any intermediate artifacts they needed. Far easier to work in Python over GRUBs scripting language as you can wrap things like your primitives / heap manipulation / etc into classes that can be called as needed throughout the exploit.
To trigger my exploit, I just wrote custom.cfg
to the directory GRUB loads its
configs from, as Ubuntu’s default configuration /boot/grub/grub.cfg
will
automatically source this file in:
### BEGIN /etc/grub.d/41_custom ###
if [ -f ${config_directory}/custom.cfg ]; then
source ${config_directory}/custom.cfg
elif [ -z "${config_directory}" -a -f $prefix/custom.cfg ]; then
source $prefix/custom.cfg
fi
### END /etc/grub.d/41_custom ###
Disabling Verifiers
Once you get code exec with an exploit you need to think about what you do to further tamper with the boot process. I focused on GRUBs verifiers subsystem, which will block you loading your own GRUB modules (and unsigned kernels, etc) in lockdown mode (which secboot enables). Each verifier implemented is stored in a linked list (so you have the normal lockdown verifiers, pgp if you load your own key in, the secboot one that checks kernels are signed, etc), and GRUB goes though the verifiers to see which ones apply to the file you are opening.
The implementation in grub-core/kern/verifiers.c
is as follows:
struct grub_file_verifier *grub_file_verifiers;
...
static grub_file_t
grub_verifiers_open (grub_file_t io, enum grub_file_type type)
{
...
FOR_LIST_ELEMENTS(ver, grub_file_verifiers)
{
enum grub_verify_flags flags = 0;
err = ver->init (io, type, &context, &flags);
if (err)
goto fail_noclose;
if (flags & GRUB_VERIFY_FLAGS_DEFER_AUTH)
{
defer = 1;
continue;
}
if (!(flags & GRUB_VERIFY_FLAGS_SKIP_VERIFICATION))
break;
}
if (!ver)
{
if (defer)
{
grub_error (GRUB_ERR_ACCESS_DENIED,
N_("verification requested but nobody cares: %s"), io->name);
goto fail_noclose;
}
/* No verifiers wanted to verify. Just return underlying file. */
return io;
}
...
}
To disable all the verifiers you just need to do a one word patch to make
grub_file_verifiers
NULL so that for loop never asks for a single thing to be
verified, letting it return early.
I implemented this in some assembly to dynamically calculate the offset to it
based on a register.
After the patch that I restored corrupted values to allow GRUB to continue on as
before after ret
‘ing.
For example, the following was what I used, alongside how I got the values from my target build:
# r10 contains a pointer to grub_real_dprintf, so we are using the offset from
# there.
GRUB_REAL_DPRINTF = ...
# binwalk the image, finding the pgp module. look at the offset it uses:
# 000000001abc 003400000001 R_X86_64_64 0000000000000000 grub_file_verifiers + 0
# If you then find where the pgp module is loaded (from RIP, making it page
# aligned etc), you can use the 0x1abc (or whatever) offset to find where it
# was patched in if you inspect with gdb.
GRUB_FILE_VERIFIERS = ...
FV_OFFSET = GRUB_FILE_VERIFIERS - GRUB_REAL_DPRINTF
# obtain this one from observing R14 when we crash with an invalid instruction
R14 = ...
R14_OFFSET = R14 - GRUB_REAL_DPRINTF
# Clean resumption after disabling verifiers.
DISABLE_VERIFIERS_SHELLCODE = f"""
mov rax, r10
// maybe adjust to a sub, etc.
add rax, {hex(FV_OFFSET)}
movq [rax], 0
add rsp, 48
// issue here is we need to preserve r14, but the encoder trashes it.
mov r14, r10
add r14, {hex(R14_OFFSET)}
mov rax, 0
ret
"""
For the place I jumped to this from I saw r10 contained a pointer to
grub_real_dprintf()
, so I used that to determine the address of
grub_file_verifiers
.
r14 was trashed by the alphanumeric shellcode encoder I used9, so I just had to
restore that.
If you completely trash things to the point process continuation isn’t viable,
you can call grub_dl_load()
from your shellcode and not bother returning.
But you’ll have to rework how you continue booting.
GRUB Modules
GRUB modules are just nostdlib DT_REL
ELFs, which GRUB will link for you.
You do not really have to worry about GRUBs version beyond making sure the
symbols you want are around.
I compiled against master and they loaded fine on downstream builds.
They are loaded by the code in grub-core/kern/dl.c
, without any interaction
with the firmwares code signing infrastructure.
The code to load modules is always included in GRUB builds as the built in
modules also get loaded this way.
To write one, I just git clone
’d GRUBs repo, built it normally and stole the
CFLAGS everything else was being built with and changed a few paths to point to
this directory.
You could also just modify the example one in the repo if you don’t wanna go
that far and build it directly within the repo.
To build, I just used a variant of the following Makefile:
SRC=./src/main.c
GRUBPATH=../grub-upstream/
TARGETPATH=../testvm/artifacts/hda/
CFLAGS=-I $(GRUBPATH) -I $(GRUBPATH)/include -DGRUB_FILE=__FILE__ -DGRUB_UTIL \
-nostdlib -DGRUB_TARGET_SIZEOF_VOID_P=8
GRUBCFLAGS = -std=gnu99 -fno-common -Os -m64 -Wall -W -Wshadow -Wpointer-arith \
-Wundef -Wchar-subscripts -Wcomment -Wdeprecated-declarations \
-Wdisabled-optimization -Wdiv-by-zero -Wfloat-equal -Wformat-extra-args \
-Wformat-security -Wformat-y2k -Wimplicit -Wimplicit-function-declaration \
-Wimplicit-int -Wmain -Wmissing-braces -Wmissing-format-attribute \
-Wmultichar -Wparentheses -Wreturn-type -Wsequence-point -Wshadow \
-Wsign-compare -Wswitch -Wtrigraphs -Wunknown-pragmas -Wunused \
-Wunused-function -Wunused-label -Wunused-parameter -Wunused-value \
-Wunused-variable -Wwrite-strings -Wnested-externs -Wstrict-prototypes -g \
-Wredundant-decls -Wextra -Wattributes -Wendif-labels -Winit-self \
-Wint-to-pointer-cast -Winvalid-pch -Wmissing-field-initializers -Wnonnull \
-Woverflow -Wvla -Wpointer-to-int-cast -Wstrict-aliasing -Wvariadic-macros \
-Wvolatile-register-var -Wpointer-sign -Wmissing-include-dirs -Wformat=2 \
-freg-struct-return -mno-mmx -mno-sse -mno-sse2 -mno-sse3 -mno-3dnow \
-Wa,-mx86-used-note=no -msoft-float -fno-omit-frame-pointer \
-fno-dwarf2-cfi-asm -mno-stack-arg-probe -fno-asynchronous-unwind-tables \
-fno-unwind-tables -fno-ident -mcmodel=large -mno-red-zone -fno-PIE \
-fno-pie -fno-stack-protector -Wtrampolines -Werror
TARGET_LDFLAGS= -m64 -Wl,-melf_x86_64 -no-pie -Wl,--build-id=none
build:
$(CC) -c $(GRUBCFLAGS) $(CFLAGS) -o pwn.mod $(SRC) $(TARGET_LDFLAGS)
install: build
cp pwn.mod $(TARGETPATH)
Which should be enough to get the following to build, and be able to use all of GRUBs headers.
#include <grub/types.h>
#include <grub/misc.h>
#include <grub/mm.h>
#include <grub/err.h>
#include <grub/dl.h>
#include <grub/extcmd.h>
#include <grub/i18n.h>
char modname[] __attribute__((section(".modname"))) = "pwn";
char moddeps[] __attribute__((section(".moddeps"))) = "\0";
GRUB_MOD_LICENSE("GPLv3+");
GRUB_MOD_INIT(pwn)
{
grub_printf("h4ck th4 pl4n3t\n");
for (int i = 0; i < 8; i++) {
grub_printf("%u\n", i);
}
while (true) {}
}
GRUB_MOD_FINI(pwn)
{
}
After building that, you can do something like the following to load the module if there is no verifiers blocking you:
insmod (hd0,gpt2)/pwn.mod
Which is enough of a template to progress on to the next steps.
Hooking the linux
command
With the ability to load a custom module, we want to see how we’d boot linux with GRUB and see how we can change that up. Which you can do with something like:
linux (device)/path/to/kernel console=ttyS0,9600 root=/dev/sdb
boot
Where the linux command will read and load the kernels bzImage into memory, with the boot command transferring control over to it.
GRUB stores its commands in a linked list, which I just iterated though and strcmp’d until I found the linux command:
grub_printf("[!] Hooking `linux` command\n");
grub_command_t q;
for (q = grub_command_list; q; q = q->next) {
if (grub_strcmp(q->name, "linux") != 0) {
continue;
}
grub_cmd_linux_orig = q->func;
q->func = grub_cmd_linux_hook;
grub_printf("[!] found command, hooked.\n");
break;
}
With my basic hook just being:
static grub_err_t
grub_cmd_linux_hook (grub_command_t cmd, int argc, char *argv[])
{
if (argc > 0)
setup_kernel(argv[0]);
return grub_cmd_linux_orig(cmd, argc, argv);
}
I hooked this so that I could see which kernel was being booted by reading the
first argument, which includes its version in the file name on Ubuntu and Fedora,
which I used to determine the path to System.map as System.map-KVERSION
exists
alongside it in /boot
.
I then just read System.map using grub_open()
and grub_read()
to extract a
few symbols (_text
, __efi_call
, kallsyms_lookup_name
and the offset for
the preempt count10) so I could calculate offsets for the runtime hook.
System.map is filled with lines like:
ffffffff81000000 T _text
...
ffffffff810b74d0 T __efi_call
...
ffffffff811a1f40 T kallsyms_lookup_name
So easy to write a function to pull those out and parse the hex address which will let you calculate offsets.
Hooking GetVariable()
Hooking runtime services is an obvious trick, though I get the vibe from looking at the Windows ones found in the wild that its fell out favour as an approach. But it works well, and we can remove the hook after our code has been triggered.
The other approach is modifying the kernel post decompression, which is viable
for post 6.6 kernels with just an ExitBootServices()
hook (invoked by linux
early on in the boot) but is painful in many ways.
If you want to introduce a new module, you have to either:
- find space in the kernel to place your module (when I implemented this technique in skp I used a cavity between
.rodata
and.text
), then get code execution somehow (hooking initcalls normally). - patch the kernel to disable security features, which makes your bootkit potentially depend on more things on disk.
So for my case, I just decided it was easier to just hook the runtime service
GetVariable()
as its called while linux boots but after enough stuff has been
setup that it is useful to run there.
Worth noting that Linux will handle all the translations from physical to
virtual memory so you don’t need to worry about adjusting your addresses.
To setup the hook, in my modules main()
, I allocated some memory with the
RuntimeServicesCode
attribute to store our runtime hook so we can persist after
we leave UEFI land.
I then just memcpy()’d the hook there.
Next, I did implement an ExitBootServices()
hook as I found it more reliable
to setup the runtime hook from there.
GetVariable()
can be called by many other things which causes problems.
Setting this up is as simple as11:
grub_printf("[!] Installing exitbootservices hook\n");
exit_boot_services_orig =
grub_efi_system_table->boot_services->exit_boot_services;
grub_efi_system_table->boot_services->exit_boot_services =
exit_boot_services_hook;
The final setup in ExitBootServices()
is filling in a few values so the
runtime hook can determine two offsets it needs to pass to my main loader
written in C:
- The kernel base address, found by looking at the stack for where we would
return from after the hook, which is the address of
__efi_call+40
and subtracting its offset in the kernel. (+40 is fine going back to 5.6, I guess I lied about no hardcoded offsets?) - The offset to
kallsyms_lookup_name
from_text
so we can lookup symbols freely.
This loader is the same kSHELF loader I used in @bahorn/skp/src/runtime
which derives from an updated / fixed version of my
klude project 12.
This let me load a LKM-less backdoor in the system, which in this case is
slightly modified version of the sample sys_kill()
rootkit by
xcellerator13.
After the loader returns, my code just removes the GetVariable()
hook so it
doesn’t accidentally get invoked again and call the the original function.
Further Adaptions
I have not implement it but you can bypass the need to exploit GRUB on subsequent boots (much faster boots and persistent but with different PCR values) if you enrol a hash or a key into the MOK list after you get code execution. This then gives you a few options:
- Boot a patched version of GRUB.
- Use skp and boot a patched kernel, that you signed, from the legitimate GRUB.
- Use a small shim binary that sets
MokSBState
to 1, then chainload the legitimate GRUB. This makes GRUB think secure boot is not active, so lockdown mode never gets enabled. Remember to restore this from the module as its a NV variable.
I lean towards the second/third options being the best choices, but as always you will need to tamper with the operating systems upgrade process to avoid having your changes wiped out. Not having to worry about shim or GRUB upgrades is pretty nice, though you will need to clean up the exploit next boot.
Conclusion
Bootkits have always been mysterious to me, so hopefully my approach demystified them a bit and helped you learn something. I had a lot of fun working on this.
If you are a less technical reader, I’d advise not panicking over anything I mentioned. You still need root or physical access to the box to perform these attacks. Reliable exploits are also painful to make, tiny variations in allocations and memory layout kill what looks to be good pocs. The bug I exploited is actually fairly unique in how readily it could be exploited, and hopefully the bug class is now dead. Just be careful what PCRs you bind your LUKS keys to.
All this descends from seeing Rairii talk about his Baton Drop exploit14 on mastodon a while back and wondering how I could make a Linux bootkit. This stuck in my mind until the end of March 2024 when I needed a new project to work on after I got back from a month backpacking around the Philippines15. A few days after I got back I ended up finding the blog posts by Daniel Axtens (Part 1, Part 2) on fuzzing GRUB which scored me a few easy wins16. The easy wins let me get to know GRUBs internals pretty well and find my initial variants17. I took a break for a bit before eventually finding the bug I used to implement the bootkit in late October though manual auditing, writing the bootkit a few days after I reported the bug.
Was pleasant to work with the GRUB maintainers, so don’t hold off on reporting any issues you find. Fairly easy to understand C code base, with only some of the legacy filesystem code being hard to understand18.
Please let me know if I made any technical errors, I tried my best with a lot of double checking / proof reading, but some things in the UEFI landscape are easy to misunderstand (like the state of mitigations) so some may have slipped though.
Anyway, I’m logging off to go touch grass. I’ll be in Brum for Fusion next week before heading off to backpack around asia again for a few months. If for some reason you want to hire me, I’m looking for full time positions in the netherlands/remote (b AT horn DOT uk).
-
Mainly on dev builds, but there is no issues porting my PoCs to signed builds. This includes the function recursion stack clash which was a wild one to exploit… Had a really nice approach for determining the call depth needed to get a controlled write over the targeted object, with an constructed info leak. Sadly was a very slow exploit, but it was a thing of beauty. ↩︎
-
You are an adult, I’m sure you can figure out how to do it. ↩︎
-
Amusingly, skp was completely unrelated to my GRUB work. I started it in Spring 2023 (see my mastodon posts from then) and was trying to do it for a zine article that I ultimately ran out of time to finish it back then. ↩︎
-
What I’m calling a kSHELF loader is an adaption of SHELFs from tmp.0ut 1-10, which are ELFs that are meant to be much easier to load due to having a single RWX section with no dynamic linking, to the kernel. kSHELF is a bit of a misnomer, as they aren’t static as we are directly linking against the kernel but the name as stuck for me now as my linker script came from me adapting the original SHELF one. Also I have more than one
PT_LOAD
to remove the need for RWX as the kernel complains about that now, so even further from the idea at this point. So sorry for another bad name in the field of CS. ↩︎ -
I’m using the hacker definition of a “good bug”. ↩︎
-
Ok, this is where things get a bit confusing. Firmware based off Project Mu, a sort of fork/distro of EDK2, do have the Enhanced Memory Protections. I think this is mainly Surface Devices? Maybe others but I couldn’t find anything on how widely its deployed. If
NX_COMPAT
is set in the PE (which as of recent commits GRUB does, just not in the build I exploited though), these protections makeEFI_LOADER_CODE
andEFI_LOADER_DATA
both just RW initially, requiring the use of the memory attribute protocol to make them executable. GRUB by default allocates heap regions withEFI_LOADER_CODE
on the assumption that its RWX, which makes me wonder ifNX_COMPAT
should be set. But… I have booted linux with GRUB on aarch64 with strictnx ovmf build usingEFI_LOADER_DATA
for the heap so its probably fine for that, just xnu/bsd/whatever probably won’t work? The linux loader code does useBS->LoadImage()
. strictnx OVMF builds just changePcdDxeNxMemoryProtectionPolicy
to make regions allocated withEFI_LOADER_DATA
RW,EFI_LOADER_CODE
remains RWX. EDK2/OVMF did finally merge the memory attribute protocol into x86 builds. ↩︎ -
Worth noting while ASLR is not technically a thing, you really really want to be independent from any specific address (outside heap sprayed ones, its the 90s baby!). Changes in how much memory, firmware, etc will result in things being moved around. This has burned me by my pocs breaking because I introduced a new file to the emulated fat fs qemu has. ↩︎
-
This is a joke about free handling venomous snakes, as a guy got bit by an inland taipan he was free handling (VERY VERY STUPID) a month prior. Tbh, I’m thinking now PrettyGoodPwnage would have been a better name, if a little cringe, but for some reason it didn’t occur to me at the time. I can’t find any other exploits called that which is surprising, been tons of bugs in pgp implementations over the years. ↩︎
-
The only forbidden bytes for my preferred heap spraying strat are 0x00 and 0x0a, but I kinda just ended up using the encoder because I only learned the way of using an expanded character set after I wrote this exploit. The only real disadvantage to alphanumeric encoders is that you have to make sure the start address of your payload is in a register, so no nopsledding. Writing a GetPC routine with only alphanumeric x86-64 is AFAIK not possible, but with the extended character set it is possible so you can nopsled to sweet victory. ↩︎
-
So there is a long story on why you need to read the preempt count, which I described in issue #1 for skp. ↩︎
-
You also don’t directly need to hook
ExitBootServices()
as there is an events framework you can use instead. See Alex Ionescu’s talk at Offensive Con 2018. ↩︎ -
I submitted the updated version to a zine, so waiting to see if that gets accepted. ↩︎
-
If you want to detect this payload, see MatheuZ’s blog post. ↩︎
-
GRUB actually had CVE-2020-27779 which was essentially the same bug as Baton Drop. ↩︎
-
Travel Advice! If you go, do try to plan it and not yolo it. You need to catch internal flights to get around the country, though they are fairly cheap. Siquijor was my favourite island. There is a sick underground river, go see that. Palawan was fine, worth seeing and very accessible (the boat tours from El Nido and Coron were affordable and worthwhile!) but just more expensive and touristy. Diving is top notch, I learnt while I was there. Also apparently I’m in a tiktok from a travel blogger giving basically the same advice. I didn’t get the guys name so I haven’t found it though, so if you do let me know! ↩︎
-
In Bill Demirkapi’s talk “Booting with Caution: Dissecting Secure Boot’s Third-Party Attack Surface” at Offensive Con 2024, he said: “Fuzz GRUB2. Guaranteed low hanging fruit.”. This was very funny to me as I just spent the prior month fuzzing and finding the very low hanging fruit he suggested existed :) ↩︎
-
My filesystem UAFs come from a fuzzed HFS+ bug which crashed with a null deref, which showed a pattern and logic issue that existed across a bunch of filesystems. How AFL++ found it was also really interesting, as I was fuzzing commands at the time. It created a polygot HFS+ / GRUB config that sourced itself, triggering the bug. A variant of the HFS+ bug was actually found a year or so before by Lidong Chen (Commit 61b13c187c9f2ef4dc2f1b450ff5de4008f28a50) but the impact wasn’t know at the time. Whether a null deref crashes or not in UEFI land is dependent on the firmware, so even accidentally triggering one doesn’t render a bug unexploitable on everything. Its actually a really handy thing to be able to do with one structure I liked overwriting, though that primitive just needed an address which pointed to null bytes. ↩︎
-
I HATE NTFS. I literally read a book on filesystems to try and give myself a better chance of understanding it. ↩︎