parsing EFI Configuration table and RSDP
Continuing with the topic from the previous post (DMAReaper attack), the next step after finding the EFI System Table is to parse the EFI Configuration Table and search for the Root System Description Pointer (RSDP).
These intermediate steps are crucial to successfully execute the DMAReaper attack. First, you must parse the EFI Configuration Table to search for the ACPI Table GUID. Second, you must parse the struct that resides in the vendor table pointer of the ACPI Table GUID.
introduction
Before we get to the fun part, we need to understand what the configuration table and RSDP are for :)
EFI Configuration Table
If we parse the fields in the EFI System Table, there is one named ConfigurationTable that is "a pointer to the system configuration tables". Also, there is another field related to the Configuration Table that specifies the number of entries in the table (NumberOfTableEntries).
The EFI Configuration Table is a set of GUID/Pointer pairs (EFI_CONFIGURATION_TABLE struct) and links the UEFI firmware to system description and configuration data.
typedef struct{
EFI_GUID VendorGuid;
VOID *VendorTable;
} EFI_CONFIGURATION_TABLE;
The VendorGuid is a 128-bit value that identifies the vendor table. In the UEFI specification you can see some defined GUIDs (the important one for our task is EFI_ACPI_20_TABLE_GUID):
#define 2 \
{0x8868e871,0xe4f1,0x11d3,\
{0xbc,0x22,0x00,0x80,0xc7,0x3c,0x88,0x81}}
#define ACPI_TABLE_GUID \
{0xeb9d2d30,0x2d88,0x11d3,\
{0x9a,0x16,0x00,0x90,0x27,0x3f,0xc1,0x4d}}
#define SAL_SYSTEM_TABLE_GUID \
{0xeb9d2d32,0x2d88,0x11d3,\
{0x9a,0x16,0x00,0x90,0x27,0x3f,0xc1,0x4d}}
#define SMBIOS_TABLE_GUID \
{0xeb9d2d31,0x2d88,0x11d3,\
{0x9a,0x16,0x00,0x90,0x27,0x3f,0xc1,0x4d}}
#define SMBIOS3_TABLE_GUID \
{0xf2fd1544, 0x9794, 0x4a2c,\
{0x99,0x2e,0xe5,0xbb,0xcf,0x20,0xe3,0x94})
#define MPS_TABLE_GUID \
{0xeb9d2d2f,0x2d88,0x11d3,\
{0x9a,0x16,0x00,0x90,0x27,0x3f,0xc1,0x4d}}
//
// ACPI 2.0 or newer tables should use EFI_ACPI_TABLE_GUID
//
#define EFI_ACPI_TABLE_GUID \
{0x8868e871,0xe4f1,0x11d3,\
{0xbc,0x22,0x00,0x80,0xc7,0x3c,0x88,0x81}}
#define EFI_ACPI_20_TABLE_GUID EFI_ACPI_TABLE_GUID
#define ACPI_TABLE_GUID \
{0xeb9d2d30,0x2d88,0x11d3,\
{0x9a,0x16,0x00,0x90,0x27,0x3f,0xc1,0x4d}}
#define ACPI_10_TABLE_GUID ACPI_TABLE_GUID*
Don't let yourself be intimidated (like me the first time xD). The format is separated into 4 fields. The first three fields are numeric and are stored in little-endian. The fourth field is an array of 8 bytes (UINT8[8]), so it is stored sequentially in memory exactly as defined, without endianness transformations. So, for example, the EFI_ACPI_20_TABLE_GUID:
{
0x8868e871,
0xe4f1,
0x11d3,
{0xbc,0x22,0x00,0x80,0xc7,0x3c,0x88,0x81}
}
This is equal to 71 e8 68 88 f1 e4 d3 11 bc 22 00 80 c7 3c 88 81 (to compare it more easily).
More information about the EFI Configuration Table can be found at uefi.org.
Root System Description Pointer
The root system description pointer (RSDP/XSDP) is a data structure in the ACPI specification and is a starting point for discovering all other ACPI hardware and tables.
// structure for revision 2 (version 2.0+)
struct XSDP_t {
char Signature[8];
uint8_t Checksum;
char OEMID[6];
uint8_t Revision;
uint32_t RsdtAddress; // deprecated since version 2.0
uint32_t Length;
uint64_t XsdtAddress;
uint8_t ExtendedChecksum;
uint8_t reserved[3];
} __attribute__ ((packed));
Struct from: wiki.osdev.org/RSDP
The most useful attribute in this struct is the XSDT pointer (XSDTAddress). This is the next step in the DMAReaper attack (it will be parsed in another post ;) ).
To locate it, on UEFI, the RSDP is located in the EFI Configuration Table with the previously specified GUIDs.
parsing
The same lab as the previous post is used (screamer PCIe squirrel, leechcorepyc and uefishell). The full code used is on github.com/12nio and the non-crucial parts or those that are explained in another post will be omitted.
With the EFI Configuration Table address extracted from the EFI_CONFIGURATION_TABLE attribute in the EFI System Table, you can parse the table as GUID (16 bytes) and Vendor Table (8 bytes) and make a constructor from raw bytes.
class EfiConfigurationTable(BaseModel):
guid: bytes = Field(min_length=16, max_length=16)
vendor_table: bytes = Field(min_length=8, max_length=8)
@classmethod
def from_raw_bytes(cls, raw: bytes) -> Self:
if len(raw) < 16:
raise ValueError(f"Raw bytes length invalid.")
guid, vendor_table = struct.unpack("<16s8s", raw)
return cls(guid=guid, vendor_table=vendor_table)
To match GUIDs you can define a dictionary with bytes as keys and names as values (the keys could be extended to more GUIDs):
UEFI_GUID_MAP = {
b'\x71\xe8\x68\x88\xf1\xe4\xd3\x11\xbc\x22\x00\x80\xc7\x3c\x88\x81': "EFI_ACPI_20_TABLE_GUID",
b'\x30\x2d\x9d\xeb\x88\x2d\xd3\x11\x9a\x16\x00\x90\x27\x3f\xc1\x4d': "ACPI_10_TABLE_GUID",
b'\x31\x2d\x9d\xeb\x88\x2d\xd3\x11\x9a\x16\x00\x90\x27\x3f\xc1\x4d': "SMBIOS_TABLE_GUID",
b'\x44\x15\xfd\xf2\x94\x97\x2c\x4a\x99\x2e\xe5\xbb\xcf\x20\xe3\x94': "SMBIOS3_TABLE_GUID",
b'\x1d\x91\xfa\xdc\xeb\x26\x9f\x46\xa2\x20\x38\xb7\xdc\x46\x12\x20': "EFI_MEMORY_ATTRIBUTES_TABLE_GUID"
}
Now, you can make a function to read a page (4096 bytes) with the leechcorepyc read function and for each 24 bytes try to create an EfiConfigurationTable. All the parsed entries will be saved in a dictionary to return it.
The size argument is the number of entries extracted from the NumberOfTableEntries field in the EFI System Table.
def parse_efi_configuration_table(lc:leechcorepyc.LeechCore, addr: int, size: int) -> Dict[str, bytes]:
raw = lc.read(addr, 4096, True)
efi_configuration_table_dict = {}
for entry in range(size):
try:
efi_configuration_table = EfiConfigurationTable.from_raw_bytes(raw=raw[entry*24:entry*24+24])
vendor_table_name = UEFI_GUID_MAP.get(efi_configuration_table.guid, "UNKNOWN_GUID")
if vendor_table_name != "UNKNOWN_GUID":
efi_configuration_table_dict[vendor_table_name] = efi_configuration_table.vendor_table
except Exception as e:
print(f"** Error parsing EFI Configuration Table at 0x{hex(addr+(entry*24))}. {e}")
return efi_configuration_table_dict
After the configuration table has been parsed, the RSDP address is in the EFI_ACPI_20_TABLE_GUID or ACPI_TABLE_GUID. The RSDP struct can be defined using the UEFI specification as:
class RSDP(BaseModel):
signature: Literal[b'RSD PTR ']
checksum: bytes = Field(min_length=1, max_length=1)
oemid: bytes = Field(min_length=6, max_length=6)
revision: bytes = Field(min_length=1, max_length=1)
rsdtaddress: bytes = Field(min_length=4, max_length=4)
length: bytes = Field(min_length=4, max_length=4)
xsdtaddress: bytes = Field(min_length=8, max_length=8)
extended_checksum: bytes = Field(min_length=1, max_length=1)
reserved: bytes = Field(min_length=3, max_length=3)
Also, the constructor method from raw bytes is defined to make it easier:
@classmethod
def from_raw_bytes(cls, raw: bytes) -> Self:
if len(raw) < 36:
raise ValueError("Raw bytes length invalid.")
fields = struct.unpack("<8s1s6s1s4s4s8s1s3s", raw)
return cls(
signature = fields[0],
checksum = fields[1],
oemid = fields[2],
revision = fields[3],
rsdtaddress = fields[4],
length = fields[5],
xsdtaddress = fields[6],
extended_checksum = fields[7],
reserved = fields[8]
)
With the RSDP address returned by the parse_efi_configuration_table function, you can make another function to read a memory page and try to create an RSDP object from raw bytes.
def parse_rsdp(lc:leechcorepyc.LeechCore, addr:int) -> RSDP:
raw = lc.read(addr, 4096, True)
try:
rsdp = RSDP.from_raw_bytes(raw[:36])
return rsdp
except Exception as e:
print(f"** Error parsing RSDP at 0x{hex(addr)}. {e}")
Now, you can make a main function that orchestrates the initialization and other functions:
def main() -> int:
print("> Initializing LeechCore via FPGA")
MAX_RETRIES = 5
attempt = 0
lc = None
while attempt < MAX_RETRIES and not lc:
try:
lc = leechcorepyc.LeechCore(device='fpga')
print(">> LeechCore Object initialized")
except Exception as e:
print(f">> Error initializing LeechCore Object ({attempt+1}/{MAX_RETRIES}): {e}")
lc = None
attempt += 1
if attempt == MAX_RETRIES and not lc:
return -1
efi_configuration_table_addr = 0x000000008F61DC98
efi_configuration_table_size = 15
print(f"> Searching EFI Configuration table in {hex(efi_configuration_table_addr)} ({efi_configuration_table_size} entries)")
try:
efi_configuration_table = parse_efi_configuration_table(lc=lc, addr=efi_configuration_table_addr, size=efi_configuration_table_size)
for key in efi_configuration_table.keys():
print(f"{key} : {hex(int.from_bytes(efi_configuration_table[key], 'little'))}")
rsdp_addr = int.from_bytes(efi_configuration_table["EFI_ACPI_20_TABLE_GUID"], 'little')
rsdp = parse_rsdp(lc=lc, addr=rsdp_addr)
print(rsdp)
finally:
lc.close()
return 0
The result will look like this:
❯ python efi_configuration_table.py
> Initializing LeechCore via FPGA
[+] using FTDI device: 0403:601f (bus 4, device 2)
[+] FTDIFTDI SuperSpeed-FIFO Bridge000000000001
>> LeechCore Object initialized
> Searching EFI Configuration table in 0x8f61dc98 (15 entries)
EFI_ACPI_20_TABLE_GUID : 0x8ea40000
ACPI_10_TABLE_GUID : 0x8ea40000
SMBIOS_TABLE_GUID : 0x8f439000
EFI_MEMORY_ATTRIBUTES_TABLE_GUID : 0x89025018
signature=b'RSD PTR ' checksum=b'\xd8' oemid=b'ALASKA' revision=b'\x02' rsdtaddress=b'(\x00\xa4\x8e' length=b'$\x00\x00\x00' xsdtaddress=b'\xb0\x00\xa4\x8e\x00\x00\x00\x00' extended_checksum=b'\xfa' reserved=b'\x00\x00\x00'final thoughts
Before wrapping up, a quick heads-up on the lc.read(addr, 4096, True) call. We are reading a full 4KB block from an unaligned address. Why doesn't the target crash? Because the PCILeech FPGA is doing the dirty work for us, silently splitting the malformed request into two valid packets under the hood.
If the adjacent physical page is protected or is unmapped MMIO space, the memory controller will return an Unsupported Request, causing an unrecoverable system hang.