Unlike executables running above OS, UEFI modules doesn't have import functions or systemcalls which is usually the important factor in reverse engineering. Therefore, things to focus on when reversing UEFI modules and executables above OS are quite different.
In this post, I'll explain what factors am I focusing on when reversing UEFI modules.
Abstract
The important factors, which I think should be focused on are below.
- UEFI Services
- UEFI Protocols
- Register and those IO types info of devices
UEFI Services and Protocols are the most valuable information (just like Windows API and system call when reversing above OS executables) and this can be auto-analyzed by plugins of decompilers such as Ghidra and IDA. Therefore, I'll mostly focus around device-specific register here.
Environment Setup
UEFI Services and Protocols can be auto-analyzed by below plugins. I'll be using Ghidra+efiseek in this post.
- efiseek (for Ghidra)
- efiXplorer (for IDA Pro)
UEFI Services
While Protocols are mostly for accessing devices' functionality, UEFI Services are for offering more general functionality. Therefore, if you want to reverse malware that infect boot chains or UEFI applications (not DXE drivers or the modules before DXE), UEFI Services are the most important factor to focus on.
Entrypoint of UEFI modules
Most UEFI modules derives EFI_SYSTEM_TABLE as a second parameter of entrypoint.
EFI_STATUS EFIAPI UefiMain(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
UEFI Services can be accessed from this EFI_SYSTEM_TABLE.
- SystemTable->BootServices
- SystemTable->RuntimeServices
In most cases, these BootServices and RuntimeServices are stored in a global variable like below. (When developing UEFI modules in EDK2, people mostly include <Library/UefiBootServicesTableLib.h>
or <Library/UefiRuntimeServicesTableLib.h>
and these offeres gBS
and gRT
)
You also can see HOBs and DXE Service Tables are stored in global variables DAT_8000f120
and DAT_8000f128
. These can be seen in DXE driver's entrypoint. Main function is the function that has the above feature (refer ghidra comment).
Analyzing UEFI Services
For this, just google the UEFI Service you want to analyze and rename arguments. In this post, I'll just explain below.
- Memory Allocation (AllocatePool, AllocatePages)
- Protocol (LocateProtocol, InstallMultipleProtocolInterfaces)
- Events (CreateEvent, CreateEventEx, RegisterProtocolNotify)
I'll explain Protocol in the Protocol section so I'm going to explain the other two.
Memory Allocation
This is like UEFI version of heap allocation.
In UEFI, memory is seperated into several regions and these can be referenced by gBS->GetMemoryMap
. You can specify where in this region to allocate by EFI_MEMORY_TYPE argument of below function.
- gBS->AllocatePool (for small 8B aligned buffer)
- gBS->AllocatePages (for large 4KB aligned buffer)
Events
UEFI Events are created by these 2 BootServices.
- gBS->CreateEvent
- gBS->CreateEventEx (Ex can specify event groups named by GUID)
For both functions, it will execute the 3rd argument (EFI_EVENT_NOTIFY) function when the events specified by last
argument (EFI_EVENT) is signaled.
There are actually 2 types of event EVT_NOTIFY_SIGNAL(0x200)
and EVT_NOTIFY_WAIT(0x100)
which can be specified in the 1st argument. The way of signaling the event differs among this argument as follows.
- EVT_NOTIFY_SIGNAL(0x200): gBS->SignalEvent
- EVT_NOTIFY_WAIT(0x100): gBS->CheckEvent, gBS->WaitForEvent
But mostly in DXE drivers, you'll see gBS->RegisterProtocolNotify
used as a set with gBS->CreateEvent
.
gBS->RegisterProtocolNotify is a function that signals 2nd argument (EFI_EVENT) when protocol specified with the 1st argument (EFI_GUID) is installed.
More detailed information about UEFI Events can be found here.
UEFI Protocols
If you want to analyze some device specific functions or DXE driver, the most important factor is UEFI Protocol.
Analyzing Protocol's function usage
If you want to see the use of Protocol's function, you can search by the GUID of protocol, and use xref to find gBS->LocateProtocol
.
Then the handle of that protocol is stored in the last argument of LocateProtocol, so you can see if any function call is made like handleOfProtocol[offset](args...)
.
Analyzing Protocol's functions
If you want to analyze specific function of a protocol, you first have to look for the installation of that protocol. This can be done by searching binary with protocol's GUID and refer to that xref to find the gBS->InstallProtocolInterface or gBS->InstallMultipleProtocolInterfaces.
In most cases, you can find a list of protocols that the DXE driver produces by looking for InstallMultipleProtocolInterfaces
called mostly in the driver's entrypoint.
For example, if we have below lines, it means,
- DAT_80000518: function list of EFI_TREE_PROTOCOL
- DAT_80000550: function list of Protocol that has GUID in DAT_800003f0
- DAT_80000560: function list of Protocol that has GUID in DAT_80000400
- DAT_80000590: function list of EFI_TCG_PRIVATE_INTERFACE
(Actually, efiseek is a bit old. It's not TreeProtocol which is for TPM1.2 but it's Tcg2Protocol for TPM2.0)
So, if we want to analyze this SubmitCommand
of Tcg2Protocol,
you have to look at below's address 0x800034CC
.
(My Ghidra's ImageBase is 0x80000000
)
Then, if you jump to that address, there will be the content of SubmitCommand as below.
Register and those IO types info of devices
When you want to see the actual interaction with the device, you first need to know what kind of register does that device has, and how can we access to that register (the IO type).
As an example, I will use TPM and analyze the above SubmitCommand function (Tcg2Protocol's function).
Knowing Device's registers
You have to refer to that device's specification for this (though, some devices specify the way to IO in their spec). For TPM, the list of registers are defined in here (PTP spec).
(There are FIFO and CRB columns but you can ignore CRB)
Each register's explanaitions are written in the following page of the link above. There are tons of registers so you can refer to this when you actually see the access to that register.
Knowing Device's IO types
For this, you actually have to see the platform (SoC) specification not the device's specification. Because the "connection" between device and the cpu is defined in SoC like below.
(This is from "Intel® Pentium® and Celeron® Processor N- and J- Series Datasheet")
In this SoC specification, the IO of every device is listed like below.
We can see here that, TPM's IO type is MMIO (Memory Mapped IO). MMIO is an IO that maps device's register to the specific memory range, so that you can read/write register values by read/write to that memory address.
We also can see from the specification that the TPM is mapped to the memory address 0xFED40000
(We have 2 TPM in the spec but ignore the 0xFED41000
one).
Reversing SubmitCommand
Now we know that the TPM's register is mapped to 0xFED40000
and we know where to look at for each register's definition. Then you can reverse the function below.
We see the exact same address above. Let's see inside FUN_8000d1bc
to see how this address specified in the first argument is used.
We can see a lots of *TpmMmioBase & 0x20
here. *TpmMmioBase
means it's referring the first content of the mapped register so let's look at the PTP spec to find out this register.
It's TPM_ACCESS_0
so *TpmMmioBase & 0x20
is equivalent to TpmMmioBase->TPM_ACCESS_0 & 0x20
. 0x20 = 0b00100000
so it's checking the 6th bit of TPM_ACCESS_0 register is set or not.
Therefore, let's look at the 6th bit of TPM_ACCESS_0 register in the same spec.
Now We can see that it's checking if this activeLocality
is set or not.
You can create struct refering to the spec to make decompile more readable. As an example, it will get clean like this.
Before
After
(By the way, EFI_STATUS values can be found referring UefiBaseType.h and Base.h of EDK2.)
Top comments (0)