From 41853e5b107ea8cb887497dc6e83e5a164b52c71 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Fri, 18 Sep 2026 02:12:09 -0400 Subject: [PATCH] fix: analyze standalone UEFI variable stores that have no FV header The boot pass only ran the UEFI Secure Boot analyzer when the input carried a firmware-volume header signature ("_FVH"). A UEFI variable store extracted on its own -- the usual chipsec/UEFITool artifact -- has no FV wrapper, so it was silently skipped and reported nothing, even though it contains the PK/KEK/db/dbx variables. A full BIOS image (which does carry "_FVH") was unaffected. Trigger the analyzer on a standalone store too: an EDK2 authenticated/variable store begins with its store GUID, and an AMI store begins with an "NVAR" entry, so route the input to the analyzer when either sits at offset 0, in addition to the existing "_FVH" path. The analyzer already self-filters (it emits nothing unless it recovers a real variable), so the looser trigger adds no false positives. Add a synthetic regression test: a raw AMI NVAR store with no FV wrapper (and a leading defaults-container entry, as real BIOSes have) must still recover the Platform Key / Secure Boot posture and enumerate dbx. --- src/boot.cpp | 11 ++++++++++- tests/test_boot.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/boot.cpp b/src/boot.cpp index a447022..ace46a0 100644 --- a/src/boot.cpp +++ b/src/boot.cpp @@ -775,7 +775,16 @@ std::vector scan_boot(const std::string& path, std::span { static const uint8_t kFvh[4] = {'_', 'F', 'V', 'H'}; std::span head = data.subspan(0, std::min(data.size(), 64u << 20)); - if (find_bytes(head, kFvh, 4, 0) != std::string::npos) { + // A full BIOS image carries the FV header signature "_FVH" somewhere. A + // variable store extracted on its own (the usual chipsec/UEFITool artifact) + // has no FV wrapper: an EDK2 authenticated/variable store begins with its + // store GUID, an AMI store begins with an "NVAR" entry. Trigger on either + // so an extracted store is analyzed too; analyze_uefi self-filters (emits + // nothing without a real recovered variable), so the loose trigger is safe. + bool is_varstore = guid_at(data, 0, kAuthVarStore) || guid_at(data, 0, kVarStore) || + (data.size() >= 4 && data[0] == 'N' && data[1] == 'V' && + data[2] == 'A' && data[3] == 'R'); + if (is_varstore || find_bytes(head, kFvh, 4, 0) != std::string::npos) { analyze_uefi(data, out); if (!out.empty()) return out; } diff --git a/tests/test_boot.py b/tests/test_boot.py index d4a9bd6..4a21f55 100644 --- a/tests/test_boot.py +++ b/tests/test_boot.py @@ -457,6 +457,24 @@ def check(cond, msg): check("uefi-platform-key" in types(ph) and "uefi-secureboot-on" in types(ph), "nvar: FV at a nonzero offset (full-flash dump) still analyzed") + # A variable store extracted on its own (a chipsec/UEFITool artifact) has no FV + # wrapper, so no "_FVH" signature. It must still be analyzed -- the raw AMI store + # begins with an "NVAR" entry. A large defaults-container entry ("StdDefaults") + # sits up front, as on a real BIOS; the top-level PK/dbx after it are still + # recovered. Regression: without the store-start trigger this was silently + # skipped, so an extracted NVAR store reported nothing. + raw = b"".join(_nvar_entry(n, d) for (n, d) in + [("StdDefaults", b"\x00" * 200), ("PK", b"PKCERT" * 40), + ("KEK", b"KEK" * 30), ("db", b"DB" * 300), ("dbx", sha256_siglist(5)), + ("SecureBoot", b"\x01")]) + check(raw[:4] == b"NVAR", "nvar: standalone store begins with an NVAR entry (no FV)") + rh = run("varstore.bin", raw) + check("uefi-platform-key" in types(rh) and "uefi-secureboot-on" in types(rh), + "nvar: standalone extracted store (no FV wrapper) is analyzed") + rdbx = [x for x in rh if x["type"] == "uefi-dbx"] + check(bool(rdbx) and "5 revocation" in rdbx[0]["label"], + "nvar: standalone store dbx enumerated past a leading container entry") + # PKFAIL through the NVAR store: PK is the AMI "DO NOT TRUST" test key. nvpk = nvar_store([("PK", efi_sig_list(ami_cert)), ("SecureBoot", b"\x01")]) check("uefi-test-platform-key" in types(run("bios.bin", nvpk)),