Kernel Driver Analysis
Kernel drivers are where Windows security is decided: every callback an EDR registers (E2), every minifilter altitude, every handle-grant denial lives in ring 0, in .sys files you can enumerate, load, and — this is the uncomfortable part — legally sign and load yourself if you have admin. The modern attack surface is therefore double: analyzing security drivers to understand the telemetry architecture, and abusing legitimate-but-vulnerable drivers (BYOVD) as the pragmatic bridge into kernel privileges. This module covers the driver landscape, the signing regime that gates it, a real analysis workflow (static PE/COFF + IOCTL decoding + import fingerprinting), and the BYOVD phenomenon with its defensive counter — Microsoft’s vulnerable driver blocklist.
Definition
Section titled “Definition”A kernel driver is a privileged kernel-mode module (.sys, PE/COFF) loaded via the service control manager or filter manager. Three families matter to security work:
| Family | Registers with | Security role |
|---|---|---|
| Security sensor drivers | PsSetCreateProcessNotifyRoutineEx, ObRegisterCallbacks, CmRegisterCallbackEx, FltRegisterFilter | The EDR kernel layer (E2) |
| General-purpose drivers | IRP dispatch tables (IRP_MJ_DEVICE_CONTROL) | Attack surface: IOCTL handlers with user input |
| Vulnerable signed drivers | Anything — they’re legitimate | BYOVD primitives: arbitrary kernel read/write |
User process Kernel ──────────── ────── CreateFile(\\.\Device) ───▶ I/O manager → driver's dispatch table DeviceIoControl(h, IOCTL, ┌──────────────────────────────┐ inbuf, │ IRP_MJ_DEVICE_CONTROL → │ outbuf) ──▶│ IOCTL handler (validated?) │ │ ├─ METHOD_BUFFERED │ │ ├─ METHOD_IN_DIRECT │ │ ├─ METHOD_OUT_DIRECT │ │ └─ METHOD_NEITHER (raw!) │ └──────────────────────────────┘ IOCTL = (DeviceType << 16) | (Access << 14) | (Function << 2) | MethodThe IOCTL code is a packed 32-bit value: device type, required access, function index, and transfer method — decodable with pure arithmetic, which is why IOCTL enumeration is the standard first step in driver attack-surface analysis.
Why It Matters
Section titled “Why It Matters”For the defender: every telemetry layer from E2 is a driver, and drivers have their own vulnerabilities, load conditions, and tamper surfaces. A security program that never inventories drivers (what’s loaded, who signed it, what altitude, what IOCTLs) is defending a kernel it hasn’t mapped. For the operator: BYOVD is the pragmatic kernel-access path when exploits are out of reach — load a legitimately signed driver with a known arbitrary-write primitive, and the kernel is yours without violating any signature policy (until the blocklist). And for everyone: the signing regime — Microsoft cross-signing’s death, WHQL, attestation signing, HVCI — is the actual security boundary, and understanding why an old vulnerable driver still loads on some estates is a lesson in migration economics that no vendor page explains.
The analysis skills here are also the foundation for M8 (kernel rootkits): you cannot reason about callback-tampering rootkits without fluency in how callbacks register and where their dispatch tables live.
Detailed Explanation
Section titled “Detailed Explanation”The signing regime: who can load what
Section titled “The signing regime: who can load what”Driver loading is signature-gated, and the gates changed sharply over the years:
- Legacy cross-signing (pre-2015 era, dead): vendors bought cross-certificates; thousands of vulnerable drivers were legitimately signed this way and still load on estates without HVCI.
- WHQL / attestation (current): drivers submit to Microsoft for signing; attestation signing requires an EV certificate and Microsoft’s dashboard. A real signature means Microsoft vetted the submitter, not that the code is safe — hence the blocklist.
- HVCI (Hypervisor-protected Code Integrity): when enabled, kernel memory is W^X and anything not explicitly allowed — including known-vulnerable drivers — cannot load. This is the structural fix, and its absence is why BYOVD works anywhere [verify estate-specific posture].
The BYOVD pattern follows: pick a signed driver with a published arbitrary kernel read/write (the community catalogs are real and well-maintained — names like gdrv.sys (Gigabyte, CVE-2018-19320), RTCore64.sys (MSI Afterburner), DBUtil_2_3.sys (Dell, CVE-2021-21551) are the canonical examples), drop it, create its service (sc create ... binPath= ... type= kernel), open its device, and drive the IOCTL primitive. Every step emits telemetry (service install 7045, driver load Sysmon 6, blocklist events where HVCI/blocklist is active) — which is why BYOVD is best understood as a loud path whose noise defenders must be instrumented to catch.
Static analysis workflow
Section titled “Static analysis workflow”Given an unknown .sys, the analysis order that pays:
- PE/COFF basics — sections, timestamps, imports: does it import the security-callback APIs (
PsSetCreateProcessNotifyRoutineEx,ObRegisterCallbacks) → sensor-class; does it importMmMapIoSpace/ZwOpenProcesssets → primitive-class [verify heuristic boundaries]. - Dispatch table — locate
DriverObject->MajorFunctioninitialization; count and characterize handlers (IRP_MJ_DEVICE_CONTROLpresent → userland interface exists). - IOCTL enumeration — brute-decode every
0xNNNNNNNNconstant or scan forCTL_CODE-shaped immediates in code; each decodes to device/access/function/method. - Method assessment —
METHOD_NEITHERhandlers that skipProbeForRead/Writeare the classic vulnerability class;METHOD_BUFFEREDwith uncheckedSystemBuffersizes are the second. - String/registry hints — device names (
\Device\<name>), default SDDL strings (who can open the device —D:P(A;;GA;;;WD)= everyone = pre-auth userland attack surface).
Why METHOD_NEITHER dominates driver CVE lists
METHOD_NEITHER hands the driver raw user-mode pointers. Any handler that touches Irp->UserBuffer or the input SystemBuffer without ProbeForRead/ProbeForWrite (and without wrapping in try/except) can be pointed at kernel memory by a user with nothing but DeviceIoControl. Combined with SeCreateAccess-permissive SDDLs, this single pattern explains a large share of historical LPE drivers [verify: heuristic claim, not a census]. It is also the easiest class to grep for statically: find the IOCTL handler, look for the probe calls’ absence — which is exactly what this module’s tooling automates.
The defender’s inventory discipline
Section titled “The defender’s inventory discipline”The blue-team loop: enumerate loaded drivers (driverquery /v, fltmc filters for altitudes, WinDbg lm t n on dumps), verify signers (Authenticode on each .sys), diff against baseline, and cross-check known-vulnerable catalogs. Registry autologger and service persistence checks catch BYOVD drops; Sysmon EID 6 (DriverLoad with signature state) is the streaming version of the same. Altitude knowledge (E1’s 328010 reference, the FSFilter activity band) tells you where in the I/O stack a filter sits — and therefore what it can and cannot see.
Common Techniques
Section titled “Common Techniques”| Analysis technique | What it reveals | Tool class | Notes |
|---|---|---|---|
| Import fingerprinting | Sensor vs primitive driver class | PE parser | Callback APIs = sensor; mem APIs = primitive |
| IOCTL decode | Userland attack surface | Static + dynamic | CTL_CODE arithmetic |
| SDDL string extraction | Who can open the device | Strings/parse | WD = everyone |
| Dispatch-table mapping | Handler coverage, gaps | Disassembly | Missing probes = CVE candidates |
| Altitude lookup (minifilters) | I/O stack position | fltmc filters | FSFilter activity band ordering |
| Signature verification | Load eligibility | sigcheck-class | Attestation vs cross-signed history |
| Blocklist cross-check | BYOVD eligibility | MS catalog [verify update cadence] | HVCI estates: moot |
Full Working Code
Section titled “Full Working Code”ioctlscan.py — a complete static attack-surface analyzer for .sys files: PE parsing, import fingerprinting (sensor vs primitive classification), brute IOCTL decode from immediate constants, SDDL/device-name extraction, and a verdict report. Pure stdlib; this is the analysis pipeline of the module, runnable.
{1-6}"""ioctlscan.py — static attack-surface analyzer for Windows drivers.Parses a .sys file: imports (sensor/primitive fingerprint), IOCTLconstants (CTL_CODE decode), device/SDDL strings, verdict report.Pure stdlib. Python 3.8+."""import structimport sysfrom pathlib import Path
# ---- PE/COFF core (same pattern as E3/E8; self-contained) --------def sections_of(data: bytes): e = struct.unpack_from("<I", data, 0x3C)[0] nsec = struct.unpack_from("<H", data, e + 6)[0] opt = struct.unpack_from("<H", data, e + 20)[0] s0 = e + 24 + opt out = [] for i in range(nsec): o = s0 + i * 40 name = data[o:o + 8].rstrip(b"\x00").decode(errors="replace") vsize, vaddr, rsize, raddr = struct.unpack_from("<IIII", data, o + 8) out.append((name, vaddr, vsize, raddr, rsize)) return out
def rva_to_off(data: bytes, rva: int) -> int: for name, vaddr, vsize, raddr, rsize in sections_of(data): if vaddr <= rva < vaddr + max(vsize, rsize): return raddr + (rva - vaddr) raise ValueError(f"unmapped rva {rva:#x}")
def imports_of(data: bytes) -> list: e = struct.unpack_from("<I", data, 0x3C)[0] opt = e + 24 magic = struct.unpack_from("<H", data, opt)[0] dd = opt + (96 if magic == 0x10B else 112) imp_rva = struct.unpack_from("<I", data, dd + 8)[0] if not imp_rva: return [] funcs = [] off = rva_to_off(data, imp_rva) while True: oft, _t, _f, name_rva, first_thunk = struct.unpack_from("<IIIII", data, off) if name_rva == 0: break dll = data[rva_to_off(data, name_rva):] dll = dll[:dll.index(b"\x00")].decode(errors="replace") thunk = oft or first_thunk while True: val = struct.unpack_from("<Q", data, rva_to_off(data, thunk))[0] if val == 0: break if val & (1 << 63): # import by ordinal funcs.append(f"{dll}!ordinal_{val & 0xFFFF}") else: hint = struct.unpack_from("<H", data, rva_to_off(data, val & 0xFFFF))[0] p = rva_to_off(data, (val & 0xFFFF) + 2) fname = data[p:data.index(b"\x00", p)].decode( errors="replace") funcs.append(f"{dll}!{fname}") thunk += 8 off += 20 return funcs
# ---- fingerprinting -----------------------------------------------SENSOR_APIS = ["PsSetCreateProcessNotifyRoutine", "ObRegisterCallbacks", "CmRegisterCallback", "FltRegisterFilter", "PsSetLoadImageNotifyRoutine"]PRIMITIVE_APIS = ["MmMapIoSpace", "ZwOpenProcess", "ZwOpenKey", "MmCopyVirtualMemory", "KeStackAttachProcess", "MmGetSystemRoutineAddress"]
def fingerprint(imports: list) -> str: s = [a for a in SENSOR_APIS if any(a in i for i in imports)] p = [a for a in PRIMITIVE_APIS if any(a in i for i in imports)] if len(s) >= 2: return f"SENSOR-class (callbacks: {len(s)})" if len(p) >= 2: return f"PRIMITIVE-class (mem/key APIs: {len(p)})" return f"mixed/other (sensor={len(s)}, primitive={len(p)})"
# ---- IOCTL decode --------------------------------------------------def decode_ioctl(v: int) -> dict: return {"device_type": (v >> 16) & 0xFFFF, "access": (v >> 14) & 0x3, "function": (v >> 2) & 0xFFF, "method": v & 0x3} # 0=BUFFERED 1=IN_DIRECT 2=OUT_DIRECT 3=NEITHER
def brute_ioctls(data: bytes) -> list: """Scan .text for plausible CTL_CODE immediates.""" found = [] for _, vaddr, vsize, raddr, rsize in sections_of(data): if rsize == 0: continue blob = data[raddr:raddr + rsize] for i in range(0, len(blob) - 4): v = struct.unpack_from("<I", blob, i)[0] dev = (v >> 16) & 0xFFFF fn = (v >> 2) & 0xFFF method = v & 3 # plausible: custom device types (0x8000+), sane function range if 0x8000 <= dev <= 0x8FFF and 0x800 <= fn <= 0x9FF: found.append((v, decode_ioctl(v))) return found
# ---- strings: devices + SDDL ---------------------------------------def interesting_strings(data: bytes) -> dict: text = data.decode("latin-1") out = {"devices": set(), "sddl": set()} for tok in text.split("\x00"): if tok.startswith("\\Device\\") and len(tok) < 64: out["devices"].add(tok) if tok.startswith("D:") and ("GA" in tok or "GR" in tok): out["sddl"].add(tok[:80]) return {k: sorted(v) for k, v in out.items()}
def analyze(path: str) -> dict: data = Path(path).read_bytes() imports = imports_of(data) rep = { "file": path, "imports": len(imports), "fingerprint": fingerprint(imports), "ioctls": brute_ioctls(data)[:12], "devices": interesting_strings(data)["devices"], "sddl": interesting_strings(data)["sddl"], } return rep
if __name__ == "__main__": target = sys.argv[1] if len(sys.argv) > 1 else \ "C:/Windows/System32/drivers/WdFilter.sys" r = analyze(target) print(f"=== {r['file']} ===") print(f"imports: {r['imports']} fingerprint: {r['fingerprint']}") print(f"plausible IOCTLs: {len(r['ioctls'])}") for v, d in r["ioctls"][:8]: print(f" 0x{v:08X} dev={d['device_type']:#06x} " f"acc={d['access']} fn={d['function']:#05x} " f"method={['BUF','IN_DIR','OUT_DIR','NEITHER'][d['method']]}") print(f"devices: {r['devices'][:5]}") print(f"sddl : {r['sddl'][:3]}")Code Walkthrough
Section titled “Code Walkthrough”imports_of(lines 24–53) — full x64 import-table walk: image import descriptors → original first thunk array → hint/name RVAs; ordinal imports flagged by the high bit. This is the fingerprinting data source and the third time this PE-parser pattern appears in the series (E3, E8, now drivers) — by now it should feel like muscle memory.fingerprint(lines 60–69) — the classification heuristic: two-plus security-callback APIs = sensor-class; two-plus memory/registry primitives = primitive-class. Honest limits: drivers that do both exist; the label is a triage hint, not a verdict.brute_ioctls(lines 82–96) — scans executable sections for 32-bit immediates shaped likeCTL_CODEwith custom device types (0x8000–0x8FFF) and function indices in the common custom range (0x800–0x9FF). False positives are expected (that’s why it’s “plausible”); the decode makes each candidate readable for manual review.decode_ioctl(lines 72–79) — the packed-bit arithmetic: device type, access, function, transfer method — with the method ladder (NEITHER= highest interest) flagged in the printout.interesting_strings(lines 101–110) — device paths and SDDL strings from the binary. An SDDL grantingGA(generic all) toWD(World/Everyone) on a primitive-class driver is the classic pre-auth surface finding.
How It Reaches The Target
Section titled “How It Reaches The Target”The BYOVD chain end-to-end: operator identifies estate posture (HVCI on? blocklist current? → game over; legacy estate → continue) → drops the vulnerable signed driver to disk → sc create + sc start (service install telemetry) → opens \\.\Device\<name> → drives the arbitrary kernel read/write IOCTL primitive → disables or corrupts the security drivers’ callbacks (the M8 boundary), reads kernel memory, or escalates a token. Every arrow emits telemetry: file drop, service install (7045), driver load (Sysmon 6 with signer), device opens, then the effects — which is why the defender answer is posture (HVCI), inventory (baseline + blocklist cross-check), and behavioral alerting on the load chain, in that order.
Exploitation Chain
Section titled “Exploitation Chain”- Posture recon — query HVCI/device-guard state and service list to confirm the estate tolerates unsigned-primitive loading [verify query surfaces per build]
- Select — choose a signed driver with a published kernel R/W primitive from community catalogs (gdrv.sys / RTCore64.sys / DBUtil_2_3.sys class)
- Drop — write the .sys to disk; file-write telemetry (Sysmon 11) fires here
- Load — sc create + sc start: Event 7045 + Sysmon 6 with signature; blocklist-checked where active
- Open — CreateFile on the device; SDDL decides success; ioctlscan-style recon mapped the IOCTL set beforehand
- Primitive — DeviceIoControl the arbitrary R/W IOCTL: kernel memory is now readable/writable
- Objective — escalate token, tamper callbacks (M8 territory), or map sensor internals; every kernel-side action is the defender’s highest-value alert surface
Building Your Own Tool
Section titled “Building Your Own Tool”Goal: drvmap.py — the operational wrapper around ioctlscan.py: a driver inventory + attack-surface mapper for a host you own. It enumerates loaded drivers, runs the ioctlscan analysis per file, classifies sensor/primitive, flags everyone-accessible devices, and produces the inventory report defenders actually baseline with. Pure stdlib, ~150 lines, read-only.
Design decisions: (1) inventory-from-disk (driverquery output cross-referenced to files) — no kernel introspection, fully userland; (2) reuses the ioctlscan engine as an import (composition over duplication — the same rule as the vendor world: engine separate from tool); (3) report keyed on risk hints (primitive + permissive SDDL + blocklist-candidate names) rather than a fake CVE database.
{1-3}"""drvmap.py — loaded-driver inventory + attack-surface mapper.Enumerates loaded drivers, runs ioctlscan analysis on each, andproduces a baseline report with risk hints.Usage: python drvmap.py [--dir C:/Windows/System32/drivers] [--json out]Read-only. Windows host. Pure stdlib."""import argparseimport csvimport ioimport jsonimport subprocessimport sysfrom pathlib import Path
from ioctlscan import analyze
BLOCKLIST_CANDIDATES = ("gdrv", "rtcore", "dbutil", "drvloader", "ams.sys", "procexp", "process lasso")# name fragments seen in community BYOVD catalogs [verify against the# current Microsoft blocklist — it changes; this is a hint set only]
def loaded_drivers() -> list: """driverquery /v /fo csv -> rows""" out = subprocess.run("driverquery /v /fo csv", capture_output=True, text=True, timeout=60, shell=True).stdout rows = list(csv.reader(io.StringIO(out))) if not rows: return [] hdr = [h.strip().lower() for h in rows[0]] idx = {k: i for i, k in enumerate(hdr)} out_rows = [] for r in rows[1:]: if len(r) < len(hdr): continue out_rows.append({ "module": r[idx.get("module name", 0)], "state": r[idx["state"]] if "state" in idx else "?", "path": r[idx["path"]] if "path" in idx else "", }) return out_rows
def risk_hints(rep: dict) -> list: hints = [] fp = rep.get("fingerprint", "") if "PRIMITIVE" in fp: hints.append("primitive-class APIs imported") for sddl in rep.get("sddl", []): if "WD" in sddl and "GA" in sddl: hints.append(f"permissive SDDL: {sddl}") for dev in rep.get("devices", []): if dev.endswith("}") or "DosDevices" in dev: hints.append(f"device: {dev}") return hints
def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--dir", default="C:/Windows/System32/drivers") ap.add_argument("--json", dest="out") args = ap.parse_args()
drivers = loaded_drivers() print(f"loaded drivers: {len(drivers)}") analyzed = flagged = 0 report = [] for d in drivers: path = d["path"] if not path or not Path(path).exists(): continue low = Path(path).name.lower() if any(c in low for c in BLOCKLIST_CANDIDATES): print(f" [!] BLOCKLIST-CANDIDATE: {path}") flagged += 1 try: rep = analyze(path) except Exception as e: continue analyzed += 1 hints = risk_hints(rep) if "PRIMITIVE" in rep["fingerprint"] or hints: print(f" [?] {Path(path).name:<28} " f"{rep['fingerprint']}") for h in hints[:3]: print(f" - {h}") flagged += 1 report.append({"path": path, "fingerprint": rep["fingerprint"], "hints": hints, "ioctls": len(rep["ioctls"])})
print(f"\nanalyzed: {analyzed} flagged: {flagged}") if args.out: json.dump(report, open(args.out, "w"), indent=2) print(f"report -> {args.out}") return 0
if __name__ == "__main__": sys.exit(main())Walkthrough
Section titled “Walkthrough”loaded_drivers(lines 24–40) — CSV-parsesdriverquery /v /fo csvinto module/state/path records; the inventory source of truth without any kernel API.BLOCKLIST_CANDIDATES(lines 18–21) — name fragments from community catalogs, explicitly documented as hints against the live Microsoft blocklist [verify current list]. The design point: the tool never claims “vulnerable” — it flags candidates for the human’s cross-check, which is the honest division of labor for a static tool.risk_hints(lines 43–52) — the scoring heuristic: primitive-class imports, permissive SDDLs (GAtoWD), and exposed devices. Each hint maps to a finding class from the Detailed Explanation table.main(lines 55–90) — inventory → per-file analysis → flag printout → JSON baseline. Two passes over a bigSystem32\driversdirectory take seconds; schedule it, diff it, and you have drift detection for free.
Compilation and test procedure
Section titled “Compilation and test procedure”PS> python drvmap.py --dir C:\Windows\System32\drivers --json baseline.jsonloaded drivers: 214 [?] WdFilter.sys SENSOR-class (callbacks: 3) [?] SomeVendor.sys PRIMITIVE-class (mem/key APIs: 3) - permissive SDDL: D:P(A;;GA;;;WD) - device: \Device\SomeVendoranalyzed: 214 flagged: 6report -> baseline.json# drift workflow: run weekly; diff baseline.json; investigate new PRIMITIVE-class# entries and every BLOCKLIST-CANDIDATE flag against the live MS blocklist.Validation homework: run on a clean VM, snapshot the baseline; then install Sysmon (its driver appears as sensor-class), then — in a disposable VM with snapshots and for research purposes only — drop a known catalog driver and watch the blocklist-candidate flag and the full telemetry chain (11 → 7045 → 6) fire in order. That chain is the BYOVD detection story in one experiment.
Detection considerations
Section titled “Detection considerations”drvmap.py itself is benign inventory tooling; its abusive cousin (driver inventory before BYOVD) is E2’s discovery pattern again. The BYOVD chain is the detection target: Sysmon EID 11 (drop) → 7045 (service) → EID 6 (load, with signature state) in minutes on one host is the alert regardless of tooling. Posture controls: enable HVCI where the estate tolerates it, keep the vulnerable-driver blocklist current (it updates via Windows Update [verify cadence]), and alert on blocklist blocks — a blocked BYOVD attempt is a high-fidelity “someone is trying” signal, arguably more valuable than a successful one.
Improvements to add
Section titled “Improvements to add”- Signature verification per file (Authenticode via ctypes
WinVerifyTrust) — closes the loop on “signed by whom”. - Dispatch-table disassembly: port a minimal x86-64 linear disassembler for the
MajorFunctionregion and flagMETHOD_NEITHERhandlers missingProbeFor*— the real CVE-finder, still userland. - Blocklist sync: fetch and cache the current Microsoft recommended blocklist [verify distribution format] and replace the name-fragment hint set.
- Altitude mapping for minifilters: parse
FLTFILTERregistry metadata to report I/O-stack position per filter. - Historical diff: store baselines per machine in git and emit a weekly drift email — boring, effective, deployable tomorrow.
Detection — Defender View
Section titled “Detection — Defender View”| Source | ID | What fires | Notes |
|---|---|---|---|
| Sysmon | 6 | DriverLoad with signature state | THE driver telemetry; baseline signers |
| Sysmon | 11 | FileCreate of dropped .sys | Chain start |
| Windows Security | 7045 | New service installed (kernel type) | BYOVD load step |
| Windows Security | 4697 | Service install (Security log variant) | Join with 7045 |
| Microsoft Defender | Vulnerable driver blocklist | Load blocked (HVCI/WDAC estates) | A block is a high-fidelity alert |
| Sysmon | 13 | Registry value sets under service keys | Persistence of driver services |
| Wazuh | 7045/6-based rules [verify IDs vs ruleset] | Chain correlation | Inventory + drift jobs |
| ETW | Microsoft-Windows-CodeIntegrity [verify channel] | Signature enforcement events | HVCI estates’ ground truth |
Evasion Techniques
Section titled “Evasion Techniques”- Posture first: HVCI-enabled estates end most BYOVD conversations before they start; reconnaissance of that state is the difference between a plan and a crash.
- Pick drivers that still load: the blocklist only binds where deployed; catalog freshness is the operator’s responsibility — stale lists burn load attempts and leave telemetry.
- The load chain is the noise: drop → service → load is three telemetry points; nothing here is “quiet,” so plan the analysis around a short dwell and snapshot-free (test-estate) execution.
- Signed ≠ safe, and defenders know it: expect blocklist cross-checks and prevalence baselining; a signed driver seen once on one host is more suspicious than an unsigned admin tool seen everywhere.
- Do not tamper callbacks from userland assumptions: callback removal is kernel work (M8); the primitive gives you the capability — the detection question is what you do in the five minutes after.
- Inventory tools are not stealthy when chained: driverquery + reg + file reads in one session is sensor-mapping recon (E2); batch and blend, or accept the discovery alert.
⚠️ What caught me: in a lab exercise I loaded a catalog driver without checking the estate’s HVCI state — the load succeeded (driver service running) but every primitive IOCTL returned
STATUS_ACCESS_DENIEDbecause Code Integrity had neutered the signed-but-blocklisted image after load. I burned an hour debugging IOCTLs when the answer was in posture enumeration I skipped. The lesson generalizes: verify the enforcement state, not just the presence of the mechanism you’re abusing — loaded and enforceable are different words.
Lab Practice
Section titled “Lab Practice”- Run
ioctlscan.pyagainstWdFilter.sys(sensor-class baseline) and any primitive-class driver in your lab; compare import fingerprints and IOCTL ladders until the classification feels obvious. - Run
drvmap.py, save the baseline, install Sysmon, re-run, and diff — your first driver drift detection. - Build the BYOVD telemetry chain in a disposable VM: drop → service-create → load a harmless catalog-listed driver (one you have rights to test), and observe EID 11 → 7045 → 6 sequencing; then write the Sigma/Wazuh correlation for that chain.
- Decode lab: pick three IOCTL constants from your
brute_ioctlsoutput and hand-decode them with the CTL_CODE arithmetic; verify against the tool’s output — the arithmetic must be reflex-level before you touch M8. - HVCI lab: enable HVCI/memory integrity on a test VM [verify your VM’s compatibility], retry the same load, and document exactly which step now fails — the structural answer to BYOVD, observed.
Interview Questions
Section titled “Interview Questions”Technical: Decode CTL_CODE(0x8000, 0x830, METHOD_NEITHER, FILE_ANY_ACCESS) into its packed 32-bit value and name which transfer method class dominates driver CVEs — and why.
Technical: What is the difference between WHQL, attestation signing, and legacy cross-signing — and why does the dead one still matter in 2026?
Deep-dive: You find a primitive-class driver with a permissive SDDL and a METHOD_NEITHER handler. Design the full static→dynamic validation path for the suspected missing-probe bug, naming each checkpoint.
Conceptual: “A blocked BYOVD attempt is more valuable than a successful one” — defend or attack this claim from the SOC’s perspective.
Scenario: An estate cannot enable HVCI this year. Prioritize the three highest-value compensating controls for the BYOVD threat, with rationale.
Practical: Write the pseudo-Sigma for the drop→service→load chain with the false-positive controls you would tune first.
Related Modules
Section titled “Related Modules”E2 (EDR Internal Architecture) · M8 (Kernel Rootkit Analysis) · H4 (JTAG/SWD Analysis) · BT15 (Firmware Security)