Boot a fresh QEMU instance and run dmidecode -t 1 inside it:
System Information
Manufacturer: QEMU
Product Name: Standard PC (Q35 + ICH9, 2009)
Version: pc-q35-8.2
Serial Number: Not Specified
UUID: ...
Family: Not Specified
That output is not just cosmetic. Every piece of software that has a reason to know whether it is running inside a virtual machine reads exactly this table — EDRs, anti-cheat engines, sandbox detectors, hypervisor fingerprinting routines. The string "QEMU" in the Manufacturer field is, for most of them, a hard stop. Analysis terminates, behavior changes, the sample goes quiet.
The question is where this data comes from and whether it can be changed before any of those tools get a chance to read it. The answer to the second part is yes — and the mechanism is a DXE driver that runs inside the firmware, before the OS loader is even invoked. This is what makes it a bootkit in the strict sense: code that executes at the firmware level and shapes what the operating system inherits.
What SMBIOS Actually Is
SMBIOS — System Management BIOS — is a specification maintained by DMTF. It defines a standard format for firmware to expose hardware identity information to the operating system. The spec has nothing to do with BIOS in the sense of boot firmware. It is purely a data contract: a layout agreement that says "here is where we put the machine's identity, and here is the structure you use to read it."
At some point during POST, the firmware constructs a contiguous block of records in physical memory and registers its location in one of two places depending on the SMBIOS version. For versions below 3.0, a 32-bit entry point structure sits somewhere between 0xF0000 and 0xFFFFF** carrying the signature SM. For 3.0 and above, a 64-bit entry point with SM3 can sit anywhere in the low 4GB. The OS finds this entry point, pulls the table address out of it, and from that point on it has everything it needs.
The table itself is a flat list. No index, no hash map, no random access mechanism. Records sit in memory back to back, and the only way to find a specific one is to walk from the beginning.
Each record is called a structure, and each structure has a type number. Type 0 is BIOS information. Type 1 is system information — manufacturer, product name, serial number, UUID. Type 2 is baseboard. Type 4 is processor. The list goes on to Type 127, which is the End-of-Table marker. When you hit 127, you stop walking.
A Single Record's Memory Layout
Each structure has two distinct sections sitting contiguously in memory.
The first section is the formatted region: a fixed-length header followed by typed fields. The header is always four bytes — one byte for the type, one for the length of the formatted region, two for a handle that uniquely identifies this record. After the header come the actual fields, and their sizes and meanings are defined by the spec per type. For Type 1, the formatted region is 27 bytes total.
The second section is the string pool. Immediately after the last byte of the formatted region, the firmware writes a sequence of null-terminated ASCII strings back to back. The entire pool is terminated by a double null — an empty string that signals the end.
Here is the part that trips people up: the fields in the formatted region do not hold strings. They hold one-byte indices. The Manufacturer field in Type 1 is not "QEMU" — it is 0x00, meaning "first string in the pool." ProductName is 0x02. SerialNumber is 0x03. An index of zero means the field is not populated.
So in memory, QEMU's Type 1 record looks roughly like this:
[ Type=1 | Len=27 | Handle=0x0001 | Manufacturer=1 | ProductName=2 | Version=3 | ... ]
[ "QEMU\0" ][ "Standard PC (Q35 + ICH9, 2009)\0" ][ "pc-q35-8.2\0" ][ \0 ]
To read Manufacturer, you skip Len bytes from the start of the record to land in the string pool, then walk forward past (index - 1) null terminators to reach the target string. This is not an abstraction — this is literally what every tool that reads SMBIOS does at the bottom of its call stack.
Why This Structure Exists
The split between fixed fields and a string pool is not accidental. The fixed region has a known, spec-defined size per type, which means a parser can jump over any record it does not understand without reading its contents — it just takes Hdr.Length bytes, skips to the string pool terminator, and moves on. This forward-compatibility property is intentional. A parser written against SMBIOS 2.4 can safely traverse a 3.1 structure it has never seen before.
The string pool being variable-length and trailing means the spec does not have to reserve fixed-width character buffers inside the struct for every string field. Serial numbers vary. Product names vary. Packing them into the formatted region would either waste space with padding or impose arbitrary length limits. The trailing pool sidesteps both problems.
The Code
Now that the memory layout is clear, the driver makes considerably more sense.
#include <Uefi.h>
#include <IndustryStandard/SmBios.h>
#include <Library/BaseMemoryLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiLib.h>
#include <Protocol/Smbios.h>
#include "CloakProfile.h"
typedef struct {
EFI_SMBIOS_HANDLE Handle;
UINT8 StrNo;
CONST CHAR8 *Val;
} CLOAK_STR;
#define CLOAK_MAX_STR 64
STATIC CLOAK_STR mStr[CLOAK_MAX_STR];
STATIC UINTN mStrCount = 0;
#define CLOAK_PUSH(H, IDX, VAL) \
do { \
if ((IDX) != 0 && mStrCount < CLOAK_MAX_STR) { \
mStr[mStrCount].Handle = (H); \
mStr[mStrCount].StrNo = (UINT8)(IDX); \
mStr[mStrCount].Val = (VAL); \
mStrCount++; \
} \
} while (0)
EFI_STATUS
EFIAPI
VmCloakDxeEntryPoint (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_SMBIOS_PROTOCOL *SmBios;
EFI_STATUS Status;
EFI_SMBIOS_HANDLE Handle;
EFI_SMBIOS_TABLE_HEADER *Rec;
SMBIOS_TABLE_TYPE1 *Type = NULL;
Status = gBS->LocateProtocol (&gEfiSmbiosProtocolGuid, NULL, (VOID **)&SmBios);
if (EFI_ERROR (Status)) {
Print (L"Failed to locate SMBIOS protocol: %r\n", Status);
return Status;
}
mStrCount = 0;
Handle = SMBIOS_HANDLE_PI_RESERVED;
while (SmBios->GetNext (SmBios, &Handle, NULL, &Rec, NULL) == EFI_SUCCESS) {
switch (Rec->Type) {
case EFI_SMBIOS_TYPE_SYSTEM_INFORMATION:
Type = (SMBIOS_TABLE_TYPE1 *)Rec;
UINT8 Uuid[16] = CLOAK_SYS_UUID;
CopyMem (&Type->Uuid, Uuid, sizeof (Uuid));
CLOAK_PUSH (Handle, Type->Manufacturer, CLOAK_SYS_MANUFACTURER);
CLOAK_PUSH (Handle, Type->ProductName, CLOAK_SYS_PRODUCT);
CLOAK_PUSH (Handle, Type->Version, CLOAK_SYS_VERSION);
CLOAK_PUSH (Handle, Type->SerialNumber, CLOAK_SYS_SERIAL);
CLOAK_PUSH (Handle, Type->SKUNumber, CLOAK_SYS_SKU);
CLOAK_PUSH (Handle, Type->Family, CLOAK_SYS_FAMILY);
break;
}
}
for (UINTN i = 0; i < mStrCount; i++) {
EFI_SMBIOS_HANDLE h = mStr[i].Handle;
UINTN sn = mStr[i].StrNo;
SmBios->UpdateString (SmBios, &h, &sn, (CHAR8 *)mStr[i].Val);
}
// UpdateString may have reallocated — Type pointer is now stale. Re-fetch.
Handle = SMBIOS_HANDLE_PI_RESERVED;
while (SmBios->GetNext (SmBios, &Handle, NULL, &Rec, NULL) == EFI_SUCCESS) {
if (Rec->Type == EFI_SMBIOS_TYPE_SYSTEM_INFORMATION) {
Type = (SMBIOS_TABLE_TYPE1 *)Rec;
break;
}
}
if (Type != NULL) {
CHAR8 *StrPtr = (CHAR8 *)Type + Type->Hdr.Length;
UINT8 Idx = Type->Manufacturer;
for (UINT8 i = 1; i < Idx; i++) {
while (*StrPtr) StrPtr++;
StrPtr++;
}
Print (L"Manufacturer after update: %a\n", StrPtr);
}
return EFI_SUCCESS;
}
Locating the Protocol
Status = gBS->LocateProtocol (&gEfiSmbiosProtocolGuid, NULL, (VOID **)&SmBios);
The driver's first move is finding EFI_SMBIOS_PROTOCOL. This is the firmware's own interface for reading and modifying SMBIOS records at runtime — before the OS is handed control. LocateProtocol walks the installed protocol list and hands back a pointer to the implementation. If this call fails, there is nothing to work with and the driver bails immediately.
Walking the Table
Handle = SMBIOS_HANDLE_PI_RESERVED;
while (SmBios->GetNext (SmBios, &Handle, NULL, &Rec, NULL) == EFI_SUCCESS) {
switch (Rec->Type) {
case EFI_SMBIOS_TYPE_SYSTEM_INFORMATION:
SMBIOS_HANDLE_PI_RESERVED is the sentinel value that tells GetNext to start from the beginning of the table. Each call advances Handle to the next record and writes the record pointer into Rec. This is the same traversal described in the layout section — no shortcut exists, so the driver walks every record until it finds what it needs. The switch on Rec->Type means everything except Type 1 passes through untouched.
UUID vs. Strings — Two Different Problems
Inside the Type 1 handler, the driver deals with two categories of fields.
UUID is a binary field — 16 raw bytes embedded directly in the formatted region. It has no pool index, it does not live in the string section. The write is direct:
UINT8 Uuid[16] = CLOAK_SYS_UUID;
CopyMem (&Type->Uuid, Uuid, sizeof (Uuid));
CopyMem overwrites the bytes in place. No pool traversal, no protocol call needed.
String fields are a different problem. Manufacturer, ProductName, SerialNumber and the rest are one-byte indices into the string pool. To change what they resolve to, you cannot touch the index byte — it already points to the right slot. You have to rewrite the actual string sitting at that slot in the pool, which may be a different length than what was there before. That is UpdateString's job, and it is the reason the queue exists.
The Queue — Why Not Call UpdateString Immediately
When UpdateString replaces a string with one of a different length, it has to resize the record in memory. The firmware may reallocate the entire block. If it does, Type — the pointer the driver is currently holding — becomes stale. It points to an address the firmware has already moved on from.
The driver avoids this by never calling UpdateString during the traversal loop. Every pending update is queued into mStr instead:
#define CLOAK_PUSH(H, IDX, VAL) \
do { \
if ((IDX) != 0 && mStrCount < CLOAK_MAX_STR) { \
mStr[mStrCount].Handle = (H); \
mStr[mStrCount].StrNo = (UINT8)(IDX); \
mStr[mStrCount].Val = (VAL); \
mStrCount++; \
} \
} while (0)
IDX != 0 guards against unpopulated fields — index zero means "not present" in the SMBIOS spec, and passing it to UpdateString is undefined territory. Everything else is pushed onto the queue with its handle, its string slot number, and the replacement value.
Once the traversal loop exits and Type is no longer being touched, the driver drains the queue:
for (UINTN i = 0; i < mStrCount; i++) {
EFI_SMBIOS_HANDLE h = mStr[i].Handle;
UINTN sn = mStr[i].StrNo;
SmBios->UpdateString (SmBios, &h, &sn, (CHAR8 *)mStr[i].Val);
}
Reallocation can now happen freely. Nothing holds a live pointer into the record at this point.
Refreshing the Pointer and Verifying
After UpdateString runs, the old Type pointer cannot be trusted regardless of whether reallocation actually occurred — the driver has no way to determine that. So it runs GetNext again from scratch to obtain a fresh pointer to the Type 1 record, then manually traverses the string pool to confirm the write landed:
CHAR8 *StrPtr = (CHAR8 *)Type + Type->Hdr.Length;
UINT8 Idx = Type->Manufacturer;
for (UINT8 i = 1; i < Idx; i++) {
while (*StrPtr) StrPtr++;
StrPtr++;
}
Print (L"Manufacturer after update: %a\n", StrPtr);
Type + Hdr.Length lands exactly at the start of the string pool — the formatted region ends there and strings begin immediately after. The loop skips forward (Manufacturer - 1) null terminators to reach the correct slot. The inner while (*StrPtr) StrPtr++ walks past the current string's characters; the StrPtr++ after it steps over the null terminator. What remains at StrPtr is the target string, read back directly from the record the firmware now holds.
Top comments (0)