- Notifications
You must be signed in to change notification settings - Fork17
3a1/Calypso
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A lightweight UEFI Bootkit with user-mode communication
- Written in C++ and assembly
- Compiled binary is just 7 KB in size
- Usermode communication
- KPP Safe (Kernel Patch Guard)
- EDK2 library reduced to minimal (only the necessary)
- Read/Write Kernel Memory
- Read/Write Process Memory (MMU translation)
- Kill Protected Process
- Process Privilege Escalation (LPE)
You can probably build any feature you want since you can directly call any exported function fromntoskrnl.exe
.The bootkit is designed to work onWindows 10 22H2
. However, the offsets are hardcoded, so to make it work on other versions, you’ll need to update the offsets accordingly.
InUefiMain
, the bootkit perfoms two main tasks.One is to save originalExitBootServices
address to restore it later and place anExitBootServices
hook to redirect calls toExitBootServicesWrapper
.Second is to create aSetVirtualAddressMap
event which we will explain later why it is needed.
InExitBootServicesWrapper
the goal is to retrieve the return address from theRSP
register.Once the return address is obtained, execution is passed to theExitBootServicesHook
.This is why we can't use theExitBootServices
event - in an event we cannot access the return address.
InExitBootServicesHook
the goal is to locatewinload.efi
base. SinceExitBootServices
is called fromwinload.efi
,and we have its return address, we know it points to a location somewhere insidewinload.efi
.Executable images are always loaded at the start of a memory page (0x1000), so the base address will always end with three zeros.Additionally, all executable images have a DOS header at the beggining, which starts with a specific magic value.With this knowledge, we can iterate backward through memory, page by page, reading the first bytes of each page andchecking for the DOS magic value to identify the base address.
The next step is to locate theOslArchTransferToKernel
address.Why do we needOslArchTransferToKernel
? This function is called whenwinload.efi
exits, and it passestheLoaderBlock
address. In theLoaderBlock
structure, we can find theLoadOrderListHead
list,which contains the address ofntoskrnl.exe
. To avhieve this, we use a simple pattern scan to identify theOslArchTransferToKernel
address and hook it.
Remember the event we created inUefiMain
? Now is the time for it to start.The purpose of this event is to convert address of our function hook from physical to virtual.Up to this point, the system operates only in physical memory without virtual address space.I'll provide more details in the next stage.
At this point, we have theLoaderBlock
address, and we traverse theLIST_ENTRY
structure to locate thentoskrnl.exe
base.Once we have thentoskrnl.exe
base, the next step is to decide which function in the OS kernel to hook.
I choose theNtUnloadKey
function for couple of reasons.
First, we need to keep in mind that we aim to establish communication between user-mode and the UEFI driver.For this purpose, our kernel function needs to be executed as a syscall from thentdll.dll
user-mode library.
As I mentioned, i chose this function for several reasons, with the second and primary one that it is just wrapper forCmUnloadKey
function.What does this means?As you might know, Windows has a security feature calledKernel Patch Guard
(KPP).The purpose of KPP is to scan kernel memory for changes and trigger a blue screen if any are detected.We bypass this feature by patching the OS kernel before it gets executed, so PatchGuard compares the alreadypatchedntosrknl
with the one in memory for any changes (believing that the patched one is the correct version).However, the problem that when hooking a function with a trampoline, the hooked function first jumps to the hook, performsits work, restores the modified bytes, calls the original function again and then re-applies the trampoline jump before returning.With KPP in place, we can't unhook the function because modifying the OS kernel at runtime would trigger KPP.
Here's where it gets complicated, we can't directly call the original function. Instead, we need to find a way to replacethe functionality of the original function. A function that acts as a wrapper to another function is ideal in this case.By calling this wrapper function inside our kernel hook, it will effectively replace the original function's functionality.Of course, we could just hook it without saving the original functionallity, but if function is important and called by the OS,we need to ensure it still performs as expected.
The next stage in this hook is to locate the function that callsNtUnloadKey
and its original call toCmUnloadKey
.Once we have identified this, we proceed by hookingNtUnloadKey
.
Here's the tricky part, we can't directly hook it here because we're are dealing with physical memory addresses.Our localNtUnloadKeyHook
address is physical, and when the system completes the boot process, it will translate to virtualaddresses, making that our hook won't work. This is whereSetVirtualAddressMap
event comes in. Inside it, we can translateout local function hook into a virtual memory address, ensuring that the hook will work after OS will translate into virtual memory space.
In this function we simply check if the passed parameter is our command structure, if it's not, we return toCmUnloadKey
,mimicking the behavior of the original function. If it is our command, we pass the execution to thedispatcher
.
As we remember, theNtUnloadKey
function fromntdll.dll
is a syscall to theNtUnloadKey
insidentoskrnl.exe
.So, Usermode can interact with ourNtUnloadKey
hook placed insidentoskrnl.exe
by calling theNtUnloadKey
fromntdll.dll
.Overall in usermode is nothing fancy, usermode is very simple and gets its job done.
You can check usage onYoutube.
This project is inspired by and builds upon the following repositories: