Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungMCP Tool Poisoning: A Name Allowlist Is Not Enough(22.09.2026 um 18:56 Uhr)
Sichere ProgrammierungGiggleGigs: A Job Board That Actually Uses AI Where It Helps(22.09.2026 um 18:57 Uhr)
Sichere ProgrammierungThe Bootstrapped SaaS Flywheel: How to Hit $10k MRR Without VC Capital(22.09.2026 um 19:05 Uhr)
Sichere ProgrammierungThe container was running software nobody built(22.09.2026 um 19:08 Uhr)
Sichere ProgrammierungYour Terminal Tasks, Everywhere: Introducing Tasku Cloud(22.09.2026 um 19:08 Uhr)
Sichere ProgrammierungMCP Tool Poisoning: A Name Allowlist Is Not Enough(22.09.2026 um 18:56 Uhr)
Sichere ProgrammierungGiggleGigs: A Job Board That Actually Uses AI Where It Helps(22.09.2026 um 18:57 Uhr)
Sichere ProgrammierungThe Bootstrapped SaaS Flywheel: How to Hit $10k MRR Without VC Capital(22.09.2026 um 19:05 Uhr)
Sichere ProgrammierungThe container was running software nobody built(22.09.2026 um 19:08 Uhr)
Sichere ProgrammierungYour Terminal Tasks, Everywhere: Introducing Tasku Cloud(22.09.2026 um 19:08 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Manipulating QEMU Hardwares with Bootkit

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:…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

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.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Manipulating QEMU Hardwares with Bootkit

Thematisch verwandte Begriffe: Manipulating, QEMU, Hardwares, with · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-75517 | Novu provides an API for sending notifications through multiple channels…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick