Booty Windows 7 Bootkit View on GitHub
A project working around boot-time kernel mode security that I did in 2011.
With the new Windows NT6 series, Microsoft have removed the traditional NTLDR method of booting the system. It’s replacement, bootmgr and winload, aim to simplify boot management, minimise code duplication and provide decent support for up and coming UEFI systems running on Intel and AMD machines.
With this, however, comes an increased number of attack vectors. This report will look at these, and discuss the implementation details on an attack which circumvents the boot process and attempts to provide a method to steal user data without alerting the user of its existence.
The standard boot procedure is shown in below. Follows is a brief overview of the boot process for Windows NT6.
bootmgr will be executed by the MBR or VBR (volume boot record). It then verfies the checksum of
bootmgr.exe, decompresses it, loads it into memory, and finally jumps to protected mode and begins to execute it.
On Windows 7, bootmgr is comprised of 3 concatenated exectuables:
- a 16-bit stub which does intialisation
- uncompressed PE executable
- a 64-bit (or 32-bit, depending on the platform) PE executable that contains the real ‘bootmgr.exe’ application. This handles selecting the chosen boot device, and any boot options.
Note: Vista, too, has a bootmgr application, however its 16-bit stub does not include decompression as the native bootmgr.exe appended to the end itself is not compressed. Although it should be possible to use Vista’s 16-bit stub with uncompressed native code following it on Windows 7, this was not tested.
On an UEFI system, bootmgr itself is an EFI boot application. It does not have a stub that does initialisation; instead, it is already executing in protected mode and begins to run as normal. In either case, the same
winload.exe is executed whether the system is booted from BIOS or from UEFI. This, of course, has the implication that any modifications upon
winload.exe would be pre-boot environment agnostic.
It may be useful to think of
bootmgr as a similar application as GRUB - it enables you to select boot targets and runs them.
bootmgr.exe is loaded into memory, it verifies itself and key Windows system files using certificates built into itself.
Once the user selects a valid boot option (or one is automatically selected),
bootmgr.exe loads the boot entry’s loader program and passes any boot flags. By default, this is
%windir%\system32\winload.exe. Winload loads the registry hive, any boot drivers, and loads
NTOSKRNL.exe and its dependencies into memory, sets up paging and starts executing the kernel. Drivers must be signed against one of the 5 built-in root CAs.
The following issues stood out:
winload.exeassumes that the bootmgr.exe that called it was valid and unmodified. (this however may be intentional; to enable another bootloader - such as GRUB - to boot Windows)
bootmgr.exeassumes that it is not modified
- if a certificate is compromised, it cannot be revoked (without an update to the binary being pushed out, which may have its own implications).
We have chosen to take advantage of the first two issues.
We proceed to explain how we have been able to circumvent the bootloader, and in doing so, are able to deploy a kernel-based keylogger and intercept network traffic before reaching other kernel-based filters (ie Winpcap and firewalls) as well as userland applications.
This following information applies to PC/AT BIOS systems, however to the best of the authors’ knowledge, this should also apply to UEFI systems.
We are targeting a Windows 7 x64 system for this project, though these ideas can be carried over to different architectures and different versions of Windows (provided they’re in the NT6 series).
winload.exe cannot be modified, as they are owned by the TrustedInstaller group. However, since it assumed we have gained admin privilleges to perform this attack, it is possible just to change ownership of the file to the Administrators group, and then grant ourselves write permissions.
This can be easily done programatically by calling
advapi32.dll. Using the
cacls utilities included with Windows can be used to do the job from a batch script if required.
We first need to disable any verification done on bootmgr and winload. This was surprisingly trivial.
Other papers discussing Windows Vista bootloader security12 (only found after this project had already half-started, unfortunately) have attempted to circumvent the checks by monkey-patching (in the same vein as poo-flinging, not so much the regular definition of monkey patch) throughout, by changing several functions, which if their definitions change or more/less code is added, means that they require modification and different offset targets.
I, however, opted to modify the single
BlImgQueryCodeIntegrityBootOptions function. This function, given the current bootmgr application context and two pointers, asks the BCD if certain debugging options are set, and returns their values. One or both are checked to see particular checks should be performed (such as code signing). The modified function always returns that these flags are set, and so allows booting the system with an unsigned
bootmgr.exe (see below).
The reason this works is that if the first of these flags is set when code integrity is initialised,
bootmgr sets an internal state so that any calls to
ImgpValidateImageHash and similar calls (
ResInitializeMuiResource - checking if the MUI is valid, and so on) will always return true.
To create the modified bootmgr executable, I utilised a tool called
bmzip3. This decompresses and extracts the PE+ (MZ)
bootmgr.exe from the bootmgr file, instead of writing one ourselves. I then recompressed
bootmgr.exe, and appended the 16-bit initialisation code to complete the binary.
The easiest way would merely to have turned these options on in the BCD. However, these can easily be read out by a system tool to determine whether or not the system is in a non-standard state. Also, this would only have disabled file verification for bootmgr, not for the drivers. Previously in Windows Vista,
DISABLE_INTEGRITY_CHECKS could be passed as a boot option string to the loader application (
winload), but in Windows 7 this has been disabled.
By this stage, it is now possible to modify winload.exe without any repercussions. To test this hypothesis, I changed a string inside
winload, updated the file checksum and booted to see the on-screen text had changed in response to our string change.
I then tried to integrate the our payload into the bootkit, and it was decided that modifying
winload would be the best option. This however, turned out to take a few tries - although each stage provided valuable experience and information, so it wasn’t completely a waste to have prior attempts.
Initally, a code cave was implemented. This was supposed to load the driver image into memory and in doing so load the driver. This was done as follows:
- Create an additional segment at the end of the file (we made this 0x1000 bytes).
- Update the PE header and image size.
- After the driver linked list had been enumerated and each driver called with
LoadImageEx, add code to jump out to our new segment (codecave).
- In the code cave, load our driver image into memory (also via
LoadImageEx), and return to the original program flow.
Steps 3 and 4 did cause some issues. For starters, the codecave was in a different segment, so one cannot just use a
JMP instruction. There were two options here: either use a
LJMP (long jump) to jump across the segment, or load the address pointer into a register and jump to that address.
The second option was taken, merely because it was easier to code. This had its own issues though. Initially, running the code in virtual machine used for testing would cause the CPU to be reset. Because it was difficult to debug the issue, boot mode debugging was enabled in the BCD and Windbg attached to the virtual machine over a virtual COM port.
Windbg isn’t the most easy to use debugger. It has a poorly designed user interface and a high learning curve. However, after spending some TLC with it, the problem was discovered. The issue was that code execution would jump to an incorrect place in memory. However, what was confusing is that disassembling with IDA Pro showed that the modified executable loads in the correct addresses to the code cave and in theory, should be jumping correctly.
It turns out that
winload.exe is loaded into memory, but not at the expected virtual address.
bootmgr relocates the
winload.exe image after it loads it by running
LdrRelocateImageWithBias. Additionally, bootmgr, after loading
winload, does not appear to do rebasing on the address we load into the register to jump to. As such, we need to do it manually. winload.exe always appears to loaded 0x116000 bytes before its preferred base address and so we subtract this value from our pointers. Suddenly the code jumps to the correct place!
Unfortunately, this did not cause the kernel module to be loaded. In hindsight, this made sense.
LoadImageEx merely loaded the driver into memory. The kernel does not know about the existence of the driver - it is not running until the I/O manager in the Windows Kernel initalises it (Phase 1 of the NT boot process).
After spending some time researching how the kernel, boot loader and service control manager all interact with each other, and further analysis of
winload, an easier way to load the driver at boot time became apparent.
After loading the base system modules,
HAL.DLL, the boot debugger(s), and then resolving their imports,
winload iterates over the keys located in
HKEY_LOCAL_MACHINE\CurrentControlSet\Control\Services and finds those with the
Order DWORD set to 0 (those with order 1 or higher are loaded by
NTOSKRNL). These are then sorted by the
GroupOrderList registry keys, and then, as mentioned previously, iterated over and
LoadImageEx called on each driver and its dependencies.
Simply by adding a registry key with
Order set to 0 will cause our driver to load at boot time. In a stealth situation, this registry key could be created before the list iterated and then deleted in the kernel after the driver had been loaded (see Detection).
Of course, being unsigned, means by default the driver would not load, unless the specified option was selected by hitting F8 on boot and choosing Load unsigned drivers. To work around this, a similar patch was developed for
bootmgr. This tells bootmgr to always ignore any signing and verification checks that would normally occur. The patch is listed verbaitum below:
; intended to replace BlImgQueryCodeIntegrity ; offset 0x30230 mov [rsp+0x10], rbx push rdi sub rsp, 0x20 ; setup stack mov r9b, 1 mov [rsp+0x30], r9b ; argument 1 mov [rsp+0x38], r9b ; argument 2 mov rbx, 0 ; success? add rsp, 0x20 pop rdi retn
Finally, the driver loads! Note that disabling PatchGuard is not required.
While the NDIS code works, we cannot perform keylogging, as the keyboard class driver and HID drivers are initialised later on in the boot process. This means we need to increase the Order setting for our rootkit. Since increasing the boot order means we load the driver from the Service Control Manager, we must patch that also to ignore unsigned drivers.
If we were running 32-bit Windows 7, this would not be a problem, as there is no driver signing enforcement.
Windows 8, being a continuation of the NT6 series, continues to use to winload and either bootmgr.exe or its UEFI equivilent.
Unfortunately, we were unable to get
bmzip to decompress bootmgr. A cursory look shows that one of the three executables have been removed, leaving only the 16-bit initialisation code and the architecture dependent PE binary. Otherwise, the organisation of the file looks exactly the same; the main executable is compressed, and it’s imagined it would function largely in the same way, apart from small additions of new code.
More importantly, however, is the addition of a ‘chain of trust’ into the boot process. Note that this applies only to UEFI machines. The Windows 8 boot loader will be signed, and the UEFI chipset will verify that the bootloader is signed with a key that matches the built in keys. OEMs (those conforming to the Windows 8 logo program, anyway) are additionally required that this signing be shipped enabled, although it’s likely support for disabling this will exist from firmware.
How self-signing (for example, Linux would require this) and key management would work has not been disclosed, but will certainly prove interesting.
More than likely this will mean that bootkits will need to target yet another layer to get a foothold of the system, although their complexity may mean that this can all be done inside the operating system if an exploit is found in calling out to the boot firwmare, or alternatively if a certificate is compromised, this would make bootkits much easier to distribute, as keys cannot be revoked.
Other operating systems¶
Mac OS X¶
Mac OS X has supported UEFI since 2005, with the introduction of Intel CPUs at the core of their system designs. The first few iterations used the Tiano Core implementation (Intel’s reference implementation).
However, Apple decided to stop developing on this platform and use their own implementation. It is up to the reader to decide whether or not this was because of ideas about the unsightly nature of the Tiano codebase, or merely because Apple’s boot team were suffereing from Not Invented Here syndrome.
In any case, the current implementation of the platform is very nonstandard. By default, the UEFI boot application is loaded off the filesystem from
/System/Library/CoreServices/boot.efi, unless a different path is specified in the NVRAM, the user holds one of the boot keys (ie holding Option to select a different boot device), or an attached bootable device is detected (such as USB key).
This file can easily be modified on the filesystem, and the firmware will happily boot this file4. Additionally, unlike Windows NT6, no verification or signing is performed on the bootloader by any of Apple’s personal computer offerings, and the file is also written entirely in native code.
Creating a bootkit would therefore be trivially easy.
Since the source code to the bootloader is available, malicious code can be added to the GRUB source code, recompiled and then installed on the target system. Optionally, the kernel image can be replaced.
Once again, creating a bootkit would be trivially easy.
A special checksum algorithm is used on later Apple TV models to make sure the bootloader is ‘signed’ by Apple. Since it is only a checksum algorithm, the code could easily be reference engineered or even bruteforced to produce a potentially valid bootloader. It is also worth noting that iPhones do not use this bootloader. ↩