Skip to content

Latest commit

 

History

History
933 lines (819 loc) · 57.7 KB

File metadata and controls

933 lines (819 loc) · 57.7 KB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

2.5.1 - 2026-07-08

Added

  • HTML report: client-side sortable columns. Clicking any column header sorts by it (click again to reverse); Severity and Address sort numerically via a per-row data-sort key (severity rank; numeric EA, -1 for address-less rows), the other columns as text. Dependency-free vanilla JS, so it works from a local file with no network.
  • INFO-severity findings now render light blue (#e6f0ff), completing the report severity scale red/orange/yellow/green/blue in both the HTML report (.s0) and the in-IDA ResultsChooser (_SEVERITY_COLORS).

Changed

  • Default finding order (reporting.default_sort_key, now shared by the HTML report and the ResultsChooser window): severity descending, then category priority so confirmed callchain findings -- provable user-input -> dangerous-sink paths -- lead their severity tier, ahead of ioctl/heuristic/opcode/etc., then address as a stable tie-break. Previously ordering was severity-only.

2.5.0 - 2026-07-03

Detection-accuracy pass driven by an evaluation against eight known-vulnerable drivers (BS_RVSIO64, LgCoreTemp, MODAPI, RTCore64, SSPORT, amp, dbutil_2_3, zam64), with every false positive and false negative confirmed in the binary. Dispatch discovery now covers non-WDM drivers, IOCTL provenance and severity are attributed per-IOCTL, and several false-positive classes are removed. The severity changes intentionally alter findings, so the HEVD/WinRing0x64 goldens were re-baselined and new goldens added.

Added

  • wdm.find_majorfunction_dispatchers(): binary-wide scan for stores into MajorFunction[IRP_MJ_DEVICE_CONTROL] (+0E0h) / [..._INTERNAL_...] (+0E8h), now the primary DDC source. It works for every driver type and finds the handler even when the MajorFunction assignment lives in a helper rather than DriverEntry, so a minifilter/WDF driver that also exposes a legacy control device is fully analysed (zam64: 0 -> 31 IOCTLs recovered, with call-chain and heuristics unblocked). locate_ddc() and the CFG-guess find_dispatch_function() become fallbacks used only when the store scan finds nothing.
  • wdm.references_iocontrolcode(): corroboration predicate - true when a function loads the IRP's IO_STACK_LOCATION (+0B8h) and reads the control code (+18h). Robust to both raw offsets and struct-annotated operands (IoControlCode / CurrentStackLocation).
  • signatures.OUTBOUND_IOCTL_BUILDERS + ioctl_decoder._precedes_outbound_builder(): find_ioctls() now excludes IOCTL codes the driver sends downstream (IoBuildDeviceIoControlRequest, ZwDeviceIoControlFile, FltDeviceIoControlFile, ...) so outbound codes are not reported as the driver's own dispatch surface (amp: 5 outbound disk-IOCTL false positives removed).
  • Per-case severity attribution: _collect_hexrays_consts() records each switch case's handler EA and its address span(s) - finding.data['case_range'], a list of [lo,hi) spans merged across goto-shared case labels so a code that reaches its work through a shared handler (e.g. WinRing0's 0x9C4060CC/D0 port I/O) is still attributed. scoring.score() uses these to attribute danger to the specific IOCTL.
  • Driver-level heuristic finding "Privileged primitive present; IOCTL linkage unconfirmed": emitted when a HIGH/CRITICAL inline primitive (wrmsr/rdmsr/port I/O) exists but no IOCTL scored HIGH+, so an arbitrary-MSR/physmem driver whose dispatcher-to-primitive path was not established (e.g. BS_RVSIO64) is not left looking benign.
  • New golden regressions in tests/drivers/: zam64 (minifilter + control device), amp (outbound-IOCTL filter), MODAPI (monolithic multi-case dispatcher) - covering the failure modes the existing clean single-dispatcher WDM goldens (beep/HEVD/ALSysIO64/WinRing0x64) did not. Seven new pure-Python regression checks in tests/test_dbr.py (79 total).

Changed

  • utils.get_driver_id(): DDC discovery now runs for every driver type, not only WDM. Previously it was gated inside the WDM fall-through branch, so a Mini-Filter / WDF / Stream / AVStream / PortCls driver returned with ctx.ddc_addresses empty and the entire IOCTL / call-chain / heuristic pipeline was skipped on its dispatch surface (zam64's 31 IOCTLs were all missed). Detecting a framework type now adds its label without replacing the legacy dispatch analysis.
  • ioctl_decoder.scan_dispatchers(): the low-precision immediate-operand scan (last resort) now runs only on a function that references_iocontrolcode() confirms actually reads the IoControlCode. This stops a CFG-misidentified library/CRT helper from leaking its internal constants as IOCTLs - RTCore64's SepSddlGetAclForString had emitted 0xCCCCCCCD (an unsigned divide-by-5 reciprocal magic), a 0x6C416553 pool tag, and NTSTATUS codes as IOCTLs (RTCore64: 22 with 4 false positives -> 18 clean).
  • scoring.score(): severity is attributed per-IOCTL in three tiers (data['sink_attribution']): handler (precise, the IOCTL's resolved handler), case-inline (sinks/opcodes found inside the IOCTL's own switch-case body), and dispatcher-wide (imprecise fallback). Only precise/in-case evidence can force METHOD_NEITHER to CRITICAL; a dispatcher-wide bump is capped at HIGH. This removes the blanket-CRITICAL inflation where every IOCTL in a monolithic dispatcher inherited the union of all sinks (MODAPI 16 CRITICAL -> 8 CRITICAL / 5 HIGH / 2 MEDIUM / 2 LOW / 1 INFO; the genuinely dangerous handlers stay CRITICAL).
  • heuristics.check_user_copy_validation(): scoped to handler-reachable functions (handler_eas) instead of the whole binary, so internal / logging / statically-linked-library copies unrelated to the dispatch surface are no longer flagged - the dominant "Unvalidated copy" false-positive class (amp 15 -> 0, RTCore64 3 -> 0, all now scoped to the attack surface).
  • ioctl_decoder._NTSTATUS_FALLBACK: expanded to cover the common 0xC00000xx error range (incl. STATUS_INSUFFICIENT_RESOURCES 0xC000009A / STATUS_INVALID_ACL 0xC0000077) as cheap insurance for IDBs with no NTSTATUS enum loaded. The filter never blanket-rejects high-bit values, since real vendor IOCTLs use device types 0x8000+ (0x8000xxxx, 0x9C40xxxx).
  • wdm.find_dispatch_function(): extended excluded_functions with CRT/security thunks (__GSHandlerCheck, __GSHandlerCheckCommon) and now skips FUNC_LIB candidates, so a library helper is less likely to be mis-selected as a dispatcher (dbutil_2_3 previously enqueued __GSHandlerCheckCommon).

Fixed

  • tests/run_cross_version.ps1: passed -S<script> <resultpath> - a -S value containing a space, which PowerShell's Start-Process mangles - so every cell reported no_result (the exact bug run_golden.ps1 was written to avoid). It now passes no -S argument and globs the <idb>.smoke.json that ida_smoke.py derives from the IDB. Cross-version smoke passes on IDA 7.6 SP1 and 8.4; IDA Free 9.3 cannot be driven headlessly (-S batch scripting is disabled in the Free edition).

2.4.0 - 2026-07-03

The 2026-06/07 two-part code review (findings B1-B19, N20-N29), the new pure-Python register-tracking helpers, and a settings-dialog robustness fix. Each review-derived Fixed bullet below is tagged with its finding id.

Added

  • DriverBuddyReloaded/registers.py: pure-Python x86/x64 register helpers (alias groups, memory-operand base extraction) with no IDA dependency, unit-tested offline and shared by the register-tracking heuristics.

Fixed

  • settings_ui.show_settings(): the pre-analysis settings dialog never appeared on IDA installs whose bundled PyQt5 was compiled for a different Python than the one idapyswitch selected. On the test IDA 7.6 SP1 (bundled PyQt5 built for python38.dll, interpreter switched to Python 3.10) from PyQt5 import QtCore raised ImportError: DLL load failed while importing sip; the old broad except Exception -> return True swallowed it and auto-analysis ran with no dialog. The PyQt5 dialog is now the primary path (unchanged on IDA 8.4, whose bundled PyQt5 matches its Python 3.10), with a new ida_kernwin.Form fallback (_show_settings_kernwin) that opens automatically when PyQt5 cannot be imported -- covering the 7.6 ABI mismatch and IDA 9.x (which ships PySide6, not PyQt5). The fallback exposes the same feature flags and tuning constants and enforces the same rules via config.Feature.validate(); its form string is generated from the shared _FEATURE_GROUPS/_TUNING tables and was verified to compile headlessly in IDA 7.6.
  • (B1, B2, B3, B4, B7, B16) heuristics.check_use_after_free(): rewritten on top of registers.py. Previously it (B1) matched the free via raw idc.print_operand, so an imported call cs:__imp_ExFreePoolWithTag was never recognised and the check never started; (B2) put mov in its write-set, so the canonical mov rax, [rcx] dereference-after-free was treated as a kill and never flagged; (B3) killed freed state with op0.startswith(reg), so mov ecx, edx did not clear a freed rcx (x64 zero-extension) and produced false positives; (B4) matched the freed register anywhere in the operand text, firing on index-only uses like [rdx+rcx*8] and on cmp rcx, 0. It now seeds the CFG walk at the true entry block (B7), frees via _callee_name (import-aware), flags a use only on a base dereference of the freed register, kills alias-aware, and clears tracking across intervening (caller-saved-clobbering) calls. Sixteen new regression checks (register helpers + end-to-end synthetic instruction streams). NOTE: this intentionally changes use-after-free findings (removing prior false positives); the golden regression must be re-run in IDA and re-baselined.
  • (N26) ida_compat.import_std_type() (IDA < 9 path): the failure check tid in (None, BADADDR, -1) treated ordinal 0 as a valid type. Type ordinals are 1-based, so a non-positive result now also counts as "not found" (the None test short-circuits before the numeric comparison). Latent robustness fix.
  • (B19) settings_ui._on_ok() / config.Feature.validate(): the settings dialog reimplemented the feature-flag coherence rules, so a new constraint added to Feature.validate() would not be enforced by the UI. The rules now live once in Feature._coherence_errors(); validate() accepts an optional proposed {flag: bool} mapping and the dialog calls it, so UI and startup validation can no longer drift. Three new regression checks.
  • (B18) dump_pool_tags.py: the pool-tag scanners compared the operand type against the bare literal 5 instead of the named idc.o_imm, an opaque magic number inconsistent with the rest of the codebase. Now use idc.o_imm.
  • (B5, B13) ioctl_decoder.find_ioctls(): the fuzzy IoControlCode fallback scanner called idc.op_dec() on every match to coerce the operand to decimal before reading it back as text. op_dec is a persistent IDB write -- it silently rewrote the operand display radix at every match site, including ones that were not IOCTLs. Now the immediate is read directly with idc.get_operand_value() (no IDB mutation, no format-dependent text round-trip), and the second (source) operand is tried before the first, matching where the code actually sits in the observed compare/move patterns. Two new regression checks.
  • (N22, N23) heuristics.check_double_fetch(): (N22) the memory-load regex \[(\w+)(?:\+(\w+))?\] matched only [reg]/[reg+disp] and silently skipped SIB/indexed loads, so a double-fetch through [rcx+rdx*4] was missed even though the operand-type filter already admitted them; the regex now captures the base plus the full bracket remainder. (N23) only the first two reads of a (reg, offset) were compared, so a probe between reads 1 and 2 masked a genuine race between reads 2 and 3; now every adjacent read pair is checked and the first unprotected, CFG-reachable pair is reported. Four new regex regression checks.
  • (B9) scoring._opcode_reach_sev() and heuristics._user_pointer_tainted(): these handler-subtree reachability queries used the default CALLCHAIN_MAX_DEPTH, which is independent of the HANDLER_SEED_DEPTH at which handler bodies are discovered. Both are user-tunable, so lowering CALLCHAIN_MAX_DEPTH below HANDLER_SEED_DEPTH could make attribution/taint shallower than discovery. They now reach max(CALLCHAIN_MAX_DEPTH, HANDLER_SEED_DEPTH) -- identical to today at the shipped defaults (6 >= 4), but robust to tuning.
  • (N28) callchain.py: investigated the apparent < vs <= depth-bound mismatch between trace() and transitive_callees() and confirmed they reach the same hop count (the <= loop spends its first iteration seeding the start set). Added a comment so the intentionally-different bounds are not "aligned" into an off-by-one later. No behavioural change.
  • (B8) ioctl_decoder._collect_switch_cases(): scanned only the primary chunk (start_ea..end_ea via next_head), so a jump-table switch located in a secondary/tail function chunk (SEH funclet, __guard_* thunk) was skipped and its IOCTLs lost. Now iterates every instruction head across all chunks via idautils.FuncItems.
  • (N29) analysis.run_analysis(): utils.get_driver_id() was the only pipeline step not wrapped in _stage(), so an exception in its fragile WDF/DDC code aborted the entire run. _stage() now returns the wrapped call's value (a no-op for the void stages), and get_driver_id runs through it, falling back to an "unknown" driver type so IOCTL and heuristic analysis still proceed.
  • (N21) wdf.populate_wdf(): the result of idc.get_first_dref_to(idx - 2) was used directly in addr + ptr_size + ... reads and then passed to ida_bytes.del_items() / apply_struct_ptr() with no BADADDR check. When the library string had no data reference the code read a garbage VA and could apply the WDFFUNCTIONS type at a wrong-but-valid address. Now bails cleanly when the reference or the derived WdfFunctions pointer is invalid, and guards the K/U prefix read against the segment boundary.
  • (N24, N25) wdm.define_ddc() (cosmetic struct-member labelling): the IO_STACK_LOCATION.OutputBufferLength test io_stack_reg + "+8" in disasm also matched +80h/+88h (substring), so a store to offset 0x80/0x88 was mislabelled as the +8 field; now anchored on the closing bracket (+8]). The IRP / IO_STACK_LOCATION canary registers are now guarded by explicit *_resolved flags so the placeholder sentinel strings can never match real disassembly. Both are labelling-only fixes; IOCTL recovery is unaffected.
  • (N20) wdm.locate_ddc(): matched the dispatch-slot offset with _DDC_OFFSET in idc.print_operand(i, 0)[4:], hard-coding a 4-character [reg prefix. For a 2-character base register the slice ate the leading + ("[r8+0E0h]"[4:] == "0E0h]"), so MajorFunction[IRP_MJ_DEVICE_CONTROL] stores through r8/r9 were not recognised and the DDC (and the internal-DC variant) went undetected. Replaced with a width-independent _operand_targets_offset() helper (the offset tag already includes the trailing ]). Four new regression checks.
  • (N27) device_name_finder.extract_unicode_strings(): decoded matches with the BOM-/native-endian-dependent "utf-16" codec while every other decode site uses explicit "utf-16-le". The regex matches <ascii><00> pairs (LE), so the result was correct on the little-endian hosts IDA runs on but latently non-portable and inconsistent. Now decodes "utf-16-le" explicitly. One new regression check.
  • (B12) utils.is_driver(): returned the first entry-point-named function found in segment/address order, so a driver carrying both a GsDriverEntry stub and a DriverEntry (or DriverEntry_0) resolved nondeterministically depending on PE layout. Now collects all matches and returns by a fixed preference (GsDriverEntry > DriverEntry > DriverEntry_0); GsDriverEntry is the true /GS entry point and is unwrapped downstream by check_for_fake_driver_entry. Two new regression checks.
  • (B10) DriverBuddyReloaded.py (IOCTL row-delete / "Invalid IOCTL"): removing an IOCTL only called idc.del_extra_cmt(ea, E_PREV + 0), which clears just the first anterior line. make_comment can append several anterior lines (E_PREV, E_PREV+1, ...), so the rest leaked and stayed in the decompiler view. New ida_compat.del_anterior_cmts(ea) deletes every anterior line (highest index first); both delete paths now use it.
  • (B11) DriverBuddyReloaded.make_comment(): the duplicate-suppression check (string not in current_comment) compared the whole IOCTL #define, whose macro name is derived from the input filename. Re-decoding the same IOCTL after the input was renamed produced a different macro name, defeated the check, and appended a second (near-identical) #define, so comments grew on every re-run. Dedup now keys on the driver-name-independent CTL_CODE(...) tail; a non-IOCTL comment string is unaffected.
  • (B14) utils._build_sddl_map(): decoded every candidate string as UTF-16LE regardless of the recorded string type, so an ASCII SDDL was mangled to garbage and dropped (and get_strlit_contents transcodes wide strings to UTF-8, which also mis-decodes when read back as UTF-16). Replaced with _decode_sddl_at(), which reads raw bytes and tries wide-then-narrow, cutting at the first NUL (same approach as the symlink-path decoder). Two new regression checks.
  • (B17) signatures.py: filled gaps in the function/instruction sets. VALIDATION_FUNCS gains the RtlUIntAdd/Sub/Mult safe-arithmetic family. FREE_POOL_FUNCS gains IoFreeMdl (its freed pointer is the first argument, which the UAF register model tracks); lookaside frees and MmFreePagesFromMdl are deliberately excluded and documented, since their freed pointer is not the first argument / the MDL stays valid. PRIV_INSN_SEVERITY gains the descriptor/task-register stores sidt/sgdt/sldt/str (KASLR-leak primitives); rdmsr/wrmsr/rdpmc are intentionally kept out (already scanned whole-binary via OPCODES, so listing them here would double-report). Five new regression checks in tests/test_dbr.py.

2.3.0 - 2026-06-29

Added

  • DriverBuddyReloaded/signatures.py: single source of truth for all function-name sets, opcode lists, and severity maps previously scattered between config.py and vulnerable_functions_lists/. Every heuristic, callchain, scoring, and utility module now imports from signatures by name; config.py holds only genuine configuration concerns (feature flags, tuning constants, severity model, IOCTL risk weights, output paths).
  • DriverBuddyReloaded/settings_ui.py: PyQt5 scan-settings dialog (uses the Qt bundled with IDA 7.6+, no extra dependency). Opens automatically before every auto-analysis run (Ctrl+Alt+A) so per-run flags and tuning constants can be reviewed and adjusted without editing config.py. Feature flags are presented as checkboxes grouped by function (IOCTL, Deep Analysis, Audit & Discovery, Annotation, Output) rather than a flat two-column grid; each checkbox and tuning spinbox shows a tooltip on hover explaining what the option does. Incoherent combinations (e.g. CALLCHAIN without IOCTL_SCAN) are rejected with an inline warning that keeps the dialog open. A "Reset to Defaults" button restores the values shipped in config.py (captured at import time, before any runtime mutations). Changes are session-scoped -- config.py on disk is never touched.
  • DriverBuddyReloaded/custom.py promoted to package root (was vulnerable_functions_lists/custom.py); the now-empty directory is deleted.
  • signatures.DEVICE_CREATE_UNSECURED_FUNCS and signatures.SYMLINK_CREATE_FUNCS: the API name sets consumed by the ACL audit and the symbolic-link finder, now wired into those checks instead of each consumer hard-coding a single API. The ACL audit (utils.find_device_create_calls) additionally flags WdfDeviceCreate (KMDF device whose DACL is set out-of-band via WdfDeviceInitAssignSDDLString / INF). The symbolic-link finder (device_name_finder.find_symbolic_links) additionally covers IoCreateUnprotectedSymbolicLink (rated LOW: the link object has a NULL DACL, so any user can delete and redirect it) and WdfDeviceCreateSymbolicLink. Both checks now resolve names via ctx.functions_map (a superset of ctx.imports_map) so WDF functions resolved as named subs are covered alongside the ntoskrnl imports.

Changed

  • callchain.transitive_callees(): max_depth parameter now defaults to None and the real default (config.CALLCHAIN_MAX_DEPTH) is read at call time. The previous max_depth=config.CALLCHAIN_MAX_DEPTH was evaluated at import time, so runtime changes from the settings UI were silently ignored by all callers that omitted the argument (scoring.py, the TOCTOU taint pass).
  • config.py section order rationalised: feature flags -- analysis tuning constants -- severity model -- IOCTL risk weights -- output paths. All function-name sets removed (now in signatures.py).
  • Ctrl+Alt+F reassigned from the removed "Decode ALL IOCTLs in Function" to "Show Findings" (previously Ctrl+Alt+W).
  • UiAction.register_action() / unregister_action(): menu-path calls are now skipped when menu_path is empty, fixing a silent False return that affected all hotkey-only actions.

Fixed

  • heuristics.run() / analysis.py: the TOCTOU / double-fetch and Use-after-free settings-UI checkboxes had no effect unless Heuristics was also enabled, because both checks lived inside heuristics.run() which only ran under Feature.HEURISTICS. The structural checks are now gated as a group on Feature.HEURISTICS, while double-fetch and use-after-free are gated independently on Feature.TOCTOU_CHECK / Feature.UAF_DETECT; the heuristics stage is entered whenever any of the three is enabled, so each checkbox takes effect on its own.
  • settings_ui.show_settings(): now fails open. If the Qt dialog cannot be constructed or shown it logs a warning and returns True so analysis proceeds with the current config, instead of conflating the failure with a user cancel and silently aborting the run.

Removed

  • vulnerable_functions_lists/ directory (c.py, winapi.py, opcode.py, __init__.py): content consolidated into signatures.py; custom.py promoted to package root.
  • "Decode ALL IOCTLs in Function": find_all_ioctls(), track_ioctls(), decode_all_ioctls(), DecodeAllHandler, the Ctrl+Alt+F UiAction, and the right-click popup entry. Auto-analysis covers the same ground with higher precision (decompiler ctree
    • switch-table recovery + NTSTATUS/sentinel filtering) and without the false-positive noise of a raw immediate scan.

2.2.0 - 2026-06-29

Added

  • Golden-output regression for the four reference drivers (beep, HEVD, ALSysIO64, WinRing0x64). The current pipeline output is captured as tests/drivers/<driver>.golden.json and is the authoritative FP/FN baseline: tests/run_golden.ps1 runs the full analysis on a pristine copy of each .i64 and fails on any added finding (false positive), missing finding (false negative) or severity change. tests/ida_smoke.py now derives its result path from the IDB and auto-discovers an adjacent <idb>.golden.json, so the runner needs no -S arguments (which PowerShell's Start-Process mangles when they contain a space).

Changed

  • Heuristic tuning constants consolidated into config.py: COPY_VALIDATION_LOOKBACK / COPY_VALIDATION_LOOKAHEAD (was heuristics._VALID_LOOKBACK/_VALID_LOOKAHEAD), UAF_GLOBAL_BACKWALK (was a literal 16), and SYMLINK_DECODE_LOOKBACK (was device_name_finder._SYMLINK_LOOKBACK). Values unchanged; behaviour-preserving.

Removed

  • The DeviceIoControl PoC harness generator (poc.py, ioctl_pocs.c, Feature.POC_HARNESS). All IOCTL data is already in findings.json / report.html and the IOCTL window; the C skeleton added little value.
  • The per-decode IOCTLs.txt file written by the interactive "Decode IOCTL(s)" actions. The same rows are printed to the Output window, shown in the severity-coloured IOCTL window, and recorded in findings.json / report.html. (pooltags.txt -- the WinDbg-format pool-tag dump -- and autoanalysis.txt -- the run diagnostic log -- are retained.)
  • Stale precedent artifacts under tests/drivers/ superseded by the committed goldens: the 2026-06-24 *-findings.json and *-autoanalysis.txt for beep / WinRing0x64, and the manual IOCTL reference lists (ALSysIO64_IOCTLs.txt, HVED_IOCTLs.txt) -- the decoded IOCTLs are now embedded in each golden.

2.1.0 - 2026-06-29

Changed

  • Per-IOCTL sink attribution (ioctl_decoder + callchain + scoring). Previously every IOCTL in a monolithic dispatcher inherited the union of all sinks reachable from the dispatcher, so e.g. all 28 HEVD IOCTLs showed memmove and all 17 ALSysIO64 IOCTLs showed MmMapIoSpace, memmove (including a benign constant-write IOCTL marked CRITICAL). Now:
    • the decompiler collector resolves the per-case handler function (first in-binary call in the switch case body, skipping logging imports) and stores data['handler_ea']/handler_name;
    • callchain additionally seeds the tracer from each handler, so it reports per-handler paths;
    • scoring attributes sinks to the IOCTL's own handler when known (falling back to the dispatcher tagged sink_attribution: dispatcher-wide), and bumps an IOCTL whose handler transitively reaches a privileged inline primitive (wrmsr/rdmsr, port I/O, mov cr*) -- which are opcode findings, not callable sinks -- so MSR/port IOCTLs are not lost to LOW. Result on the corpus (IOCTL recovery unchanged at 28/17/18/2): HEVD 11 CRITICAL (the handlers that actually reach memmove) + 17 HIGH (raw METHOD_NEITHER, no sink) instead of a flat 28 CRITICAL; ALSysIO64 6 CRITICAL / 4 HIGH / 7 LOW (the constant-write IOCTL is now LOW, the MSR-write CRITICAL); WinRing0x64 16 CRITICAL / 1 HIGH / 1 MEDIUM.
  • heuristics.check_privilege_gate(): now a path-level analysis instead of single-function. A privileged primitive (MmMapIoSpace, __writemsr-class APIs, Zw* memory/section/process ops) is usually reached through a wrapper, so the old per-function check on the dispatcher never saw it. For each dispatcher subtree the check now collects the transitively-reachable functions, skips the whole subtree if a privilege gate (SeAccessCheck / SeSinglePrivilegeCheck / token query / ...) appears anywhere on it -- so a gate in the dispatcher correctly protects a deeper sink, no false positive -- and otherwise flags every reachable sensitive-op call site once. Verified: ALSysIO64 reports 3 and WinRing0x64 reports 1 ungated MmMapIoSpace (none before); beep/HEVD unaffected.
  • heuristics: the deep checks (double-fetch, pool-alloc-trust, privilege-gate, IRQL, MDL, alloca) now run on the dispatcher and the functions it transitively calls, not just the dispatcher itself. handler_seed_eas() only ever returned the dispatch routine, so for drivers that route each IOCTL to its own handler (e.g. HEVD's Trigger* / *IoctlHandler functions) these checks scanned only the dispatcher prologue and emitted nothing. heuristics.run() now expands the seed set via callchain.transitive_callees(..., config.HANDLER_SEED_DEPTH) (library/thunk leaves like memmove/memset excluded). Measured effect: HEVD now reports 11 pool-allocation-without-validation and 6 TOCTOU double-fetch findings (including the genuine TriggerDoubleFetch) where it previously reported none; ALSysIO64 and WinRing0x64 now report the ungated MmMapIoSpace privileged op.
  • heuristics: callee matching is now import-aware. Imported functions disassemble as call cs:__imp_<Name>, for which print_operand returns "cs:_imp" and CodeRefsFrom+get_func_name return None -- so every name-based check (copy-sink, validation, pool-alloc, privileged-op, IRQL, MDL, alloca, double-fetch probe) silently skipped imported functions. A new _callee_name() resolver strips the segment and __imp_ decoration so e.g. ExAllocatePoolWithTag, ProbeForRead and MmMapIoSpace match regardless of being local or imported. This both enables the pool/privilege checks and makes copy-validation correctly treat a nearby imported ProbeForRead as validation (HEVD unvalidated-copy 10 -> 8).
  • heuristics.check_double_fetch(): reads through a frame/stack register (rsp/rbp) are excluded (they are locals, never user pointers), removing a class of false positive exposed once callee scanning was enabled.

Added

  • heuristics.check_write_primitives(): decompiler-ctree detection of arbitrary-write primitives that are plain pointer stores (never a memcpy-family call, so the copy-validation check could not see them). *(*p) = c (double-dereference store, the canonical write-what-where) is HIGH; *p = *q (value read through one pointer stored through another) is MEDIUM, with memcpy/memmove-style copy routines excluded by name so the copy primitive itself is not flagged. Gated on Feature.IOCTL_DECOMPILER + HexRays. Verified: HEVD TriggerWriteNULL HIGH plus TriggerArbitraryWrite and the fake-object installers MEDIUM; ALSysIO64 surfaces its physical-memory write wrapper; beep stays clean. (The remaining item-7 sub-cases -- integer-overflow in a hand-rolled bounds check, e.g. HEVD's Size + 4 <= 0x800 -- need value-flow tracking and are left as a known gap; check_pool_alloc_trust already covers unvalidated allocation sizes.)
  • heuristics.check_use_after_free_global(): cross-function, global-pointer UAF detection. The existing register-tracking check_use_after_free() is intra-function and cannot model the canonical driver UAF where one IOCTL frees a global object pointer without nulling it and a different IOCTL later dereferences the dangling global. The new pass finds ExFreePool* calls whose argument is loaded directly from a global, confirms that global is not zeroed in the freeing function, and confirms it is read from another function -- emitting HIGH only when all three hold. Verified: HEVD reports both g_UseAfterFreeObjectNonPagedPool and ...Nx; beep/ALSysIO64/WinRing0x64 report none (no false positives).
  • heuristics.check_privileged_instructions(): flags privileged CPU instructions reachable from a dispatch handler -- port I/O (in/out/ins/outs), control/debug-register moves (mov cr*/mov dr*), descriptor-table loads (lgdt/lidt/lldt/ltr/lmsw) and cache/halt (invd/wbinvd/cli/sti/hlt). These are inline instructions, not calls, so the sink/callchain layer could never see them, yet out to an attacker-controlled port and mov cr* are canonical BYOVD hardware-access primitives. Severities in new config.PRIV_INSN_SEVERITY (out CRITICAL, in HIGH, ...). Verified: WinRing0x64 now flags 3 in + 3 out + hlt, ALSysIO64 7 in + 1 out; HEVD/beep have none.
  • PCI configuration-space access (HalGetBusDataByOffset HIGH / HalSetBusDataByOffset CRITICAL) added to config.DANGEROUS_SINKS, config.PRIVILEGED_SENSITIVE_OPS and the winapi flagged list, so the callchain tracer and privilege-gate check now surface arbitrary PCI config read/write. Verified: WinRing0x64 callchain now reports reaching HalSet/HalGetBusDataByOffset; ALSysIO64 reports HalGetBusDataByOffset.
  • config.HANDLER_SEED_DEPTH (default 4): call-edge depth the heuristic engine expands from each dispatcher to reach per-IOCTL handler bodies.
  • callchain.transitive_callees(start_eas, max_depth): shared bounded-BFS helper returning every function reachable from a set of start EAs over call/jump edges (inclusive). Gives heuristics one consistent notion of "the code a dispatcher actually reaches".

Fixed

  • heuristics.check_double_fetch(): eliminated the METHOD_BUFFERED false positives and stopped pairing mutually-exclusive sibling switch cases. The check previously flagged any mov reg, [base+off] re-read with no intervening Probe call, with no notion of whether the source was a user pointer -- so it fired on Irp->AssociatedIrp.SystemBuffer / IrpSp->Parameters kernel copies (5 findings on ALSysIO64, 6 on WinRing0x64, all bogus) while the one genuine double-fetch went unlabelled. Now: (1) the run loop only scans handlers reachable from a METHOD_NEITHER IOCTL (_user_pointer_tainted()), since only METHOD_NEITHER hands the driver a raw user pointer; (2) a CFG reachability check (_cfg_reachable()) requires the second read to lie on a path from the first, dropping sibling-case pairs; (3) the finding title is now "TOCTOU double-fetch". Verified: ALSysIO64 5->0 and WinRing0x64 6->0 false positives, beep stays silent.

  • device_name_finder._decode_symlink_arg(): symbolic-link target paths now decode instead of reporting "path could not be decoded" on essentially every driver. Two root causes: (1) the decoder read the operand with idc.get_strlit_contents(ea, -1, STRTYPE_C_16), which transcodes a wide string to UTF-8, then decoded that result a second time as UTF-16LE -- turning \DosDevices\X into mojibake that matched no device prefix. It now reads the raw bytes and decodes UTF-16LE once, cutting at the first NUL. (2) The backward walk from the IoCreateSymbolicLink call was capped at 30 instructions, but HEVD initialises the link name ~38 instructions before the call (the whole IoCreateDevice + MajorFunction[] setup sits in between); the window is now 64. The accept test also tightened from "backslash anywhere" to a leading backslash so a stray pointer byte cannot false-match. Verified: HEVD \DosDevices\HackSysExtremeVulnerableDriver, WinRing0x64 \DosDevices\WinRing0_1_2_0, ALSysIO64 \DosDevices\ALSysIO now recovered.

  • ACL (utils.find_device_create_calls) and symbolic-link (device_name_finder.find_symbolic_links) findings were emitted in exact duplicate on every driver. idautils.XrefsTo(ea, 0) can return more than one xref kind for a single call site, so a lone IoCreateDevice / IoCreateSymbolicLink call produced two identical findings (inflating the acl/symlink category counts and the LOW/INFO severity totals). Both loops now dedup on the call-site address (xr.frm), and reporting.Reporter.add() gained a content-identity guard that drops any finding identical to one already recorded (same category, title, ea, severity, detail) so this class of duplicate cannot recur from any module.

Added

  • ioctl_decoder.py scan_dispatchers(): now recovers IOCTL codes that never appear as immediate operands in the disassembly. Previously the dispatcher scan only matched literal immediates in cmp/sub/mov, so it missed every code the compiler hid inside a jump table (only the table base/bound remain as immediates) or a binary-search comparison tree (intermediate codes survive only as deltas). Two new collectors run per dispatcher and merge with the immediate scan through a single _emit_ioctl() validation/dedup funnel:
    • _collect_hexrays_consts() decompiles the dispatcher and reads switch-case labels and ==/!= comparison constants from the ctree. Comparison constants are anchored to a switch-selector variable when a switch is present (so an unrelated status == STATUS_* check is ignored); with no switch present (if-chain dispatcher) every comparison constant is taken. Gated on the new config.Feature.IOCTL_DECOMPILER flag + HexRays availability.
    • _collect_switch_cases() reads IDA's recovered switch metadata (get_switch_info + calc_switch_cases), excluding the default-jump group so the dense filler values between real cases are dropped. Works without the decompiler. The raw immediate scan is now a last resort, used only when both structured collectors recover nothing for a dispatcher. Measured recovery on the test corpus rose from 7/28 to 28/28 (HEVD) and 4/17 to 17/17 (ALSysIO64); WinRing0x64 (18) and beep (2) unchanged, all with no false positives (verified full-pipeline on IDA 7.6 SP1 and 8.4).

Changed

  • analysis.run_analysis(): the precise dispatcher scan now runs before find_ioctls(), and the fuzzy whole-binary IoControlCode text scan only runs as a fallback when the dispatcher scan found nothing. find_ioctls() can mistake data constants for IOCTLs (e.g. a misread 0x0032C004), so skipping it when the structured decode succeeds removes those false positives.
  • ioctl_decoder.py _is_valid_ctl_code(): now also rejects the 0xFFFFFFFF ((DWORD)-1 / INVALID_HANDLE_VALUE) sentinel, which is structurally a valid CTL_CODE but surfaces from == -1 checks inside dispatchers (observed in WinRing0x64).

Fixed

  • DriverBuddyReloaded.py make_comment(): fixed an AttributeError: module 'idc' has no attribute 'add_extra_cmt' crash that aborted plugin_t.run() whenever auto-analysis tried to write an IOCTL anterior comment. add_extra_cmt does not exist in idc on any supported build (it lives only in ida_lines/idaapi, unlike its siblings get_extra_cmt / del_extra_cmt / E_PREV, which idc does expose). Anterior-comment handling now routes through two new ida_compat helpers, get_anterior_cmt() and add_anterior_cmt(). The duplicate-guard now scans all anterior lines instead of only the first (E_PREV + 0), so a pre-existing IDA-placed anterior line can no longer push DBR's comment past the check and cause it to be re-appended on every re-run.

  • ioctl_decoder.py scan_dispatchers(): replaced range(block.start_ea, block.end_ea) with a while instr < block.end_ea: ... instr = idc.next_head(instr, block.end_ea) loop. The old byte-level iteration asked IDA to decode every interior byte of a multi-byte x64 instruction as if it were an instruction start; depending on IDA version, this could produce spurious mnemonics, incorrect finding EAs (pointing inside an instruction rather than at its start), or false-positive IOCTL findings from garbage operand values. Instruction-level iteration via idc.next_head() is the correct pattern (used by iter_text_matches() and all other walkers in the codebase).

  • wdm.py locate_ddc() experimental path: the xref filter previously required that the DDC candidate be called directly from DriverEntry's own instructions (reffunc.start_ea == driver_entry_address). Many real drivers initialise MajorFunction[] in a helper function called from DriverEntry; those DDC addresses were never added to ctx.ddc_addresses, so scan_dispatchers() silently skipped the entire dispatcher scan. The filter now builds the set of functions reachable in one call step from DriverEntry (entry_callees) and accepts any DDC whose xref comes from that set. Also deduplicates ddc_list with set() before walking xrefs to avoid processing the same candidate multiple times when the pattern matched more than once in the same function.

Added

  • tests/ida_smoke.py: extended with three optional check modes (T5-T7):

    • --golden <ref.json> (T5): compare findings against a reference JSON order-insensitively on (category, title, severity, code, method, access). Designed for beep.sys and WinRing0x64.sys regression guards.
    • --ioctl-count <N> (T6): assert that exactly N unique IOCTL codes are present after analysis (e.g. 17 for ALSysIO64.sys, 28 for HEVD.sys).
    • --expect-heuristic <pattern> (T7): assert at least one heuristic finding title contains the pattern (e.g. "TOCTOU" for HEVD.sys). Each check result is recorded in the output JSON under checks; the overall exit code is 0 only when all checks pass.
  • tests/test_dbr.py: four new pure-Python unit tests (T1-T4), bringing total to 27 checks.

    • T1: mocks idaapi.get_func, idc.prev_head, idc.print_insn_mnem, idc.get_name_ea_simple to simulate a GsDriverEntry stub ending with jmp real_entry; asserts check_for_fake_driver_entry() returns the real DriverEntry EA.
    • T2: five boundary calls to _is_valid_ctl_code(): verifies 0x00010000 (device_type=1) and 0x00222003/0x0022e004 are valid; 0x00000000 (device_type=0) and 0xC0000005 (STATUS_ACCESS_VIOLATION) are rejected.
    • T3: mocks idautils.FuncItems, idautils.CodeRefsFrom, ida_funcs.get_func_name, and idc.print_operand so that a synthetic handler calls KeRaiseIrql then ZwOpenProcess; asserts check_irql() emits an IRQL mismatch finding.
    • T4: pre-seeds Reporter with IOCTL 0x222003; runs scan_dispatchers() against a mocked FlowChart block that would emit the same code; asserts count stays at 1 (dedup by code value, not EA).
  • heuristics.py check_use_after_free(): use-after-free heuristic (N6). Forward-walks the basic-block CFG via idaapi.FlowChart; tracks the argument register (RCX on x64, ECX on x86) after each ExFreePool/ExFreePoolWithTag/ ExFreePool2 call; emits HIGH when that register is read before being overwritten by a write instruction. Set propagates across block successors. Gated on Feature.UAF_DETECT = True.

  • device_name_finder.py find_symbolic_links(): Symbolic link tracking (N4). Walks xrefs to IoCreateSymbolicLink; attempts to decode the target path by scanning backwards from each call site for UNICODE_STRING buffer references. Decoded paths are stored in ctx.symbolic_links; an INFO finding is emitted per call site regardless of whether the path was recovered. Gated on Feature.SYMLINK_TRACK = True.

  • utils.py AnalysisContext: added symbolic_links: list field (N4).

  • utils.py find_device_create_calls(): Device ACL audit (N3). Walks xrefs to IoCreateDevice (LOW: no security descriptor, world-accessible by default) and IoCreateDeviceSecure (scans the calling function for a UTF-16 SDDL string via IDA data-xrefs; MEDIUM if it contains a world SID -- WD, S-1-1-0, BU, or S-1-5-32-545 -- LOW if the SDDL cannot be statically recovered). Gated on Feature.ACL_AUDIT = True; wired into analysis.py before the callchain stage.

  • heuristics.py check_double_fetch(): TOCTOU/double-fetch heuristic (N1). Walks the instruction stream of each handler; groups mov reg, [src+offset] loads by (src_register, offset); flags any pair with 2+ occurrences that has no ProbeForRead/ProbeForWrite or copy-sink call between them (MEDIUM). Gated on Feature.TOCTOU_CHECK = True.

  • config.py: new feature flags TOCTOU_CHECK, ACL_AUDIT, SYMLINK_TRACK, UAF_DETECT; new function-name sets PROBE_FUNCS, DEVICE_CREATE_FUNCS, SYMLINK_FUNCS, FREE_POOL_FUNCS.

  • config.py Feature.validate(): startup classmethod that raises ValueError for incoherent feature-flag combinations. Currently checks that Feature.CALLCHAIN is not enabled when Feature.IOCTL_SCAN is disabled (callchain needs IOCTL findings as seeds). Called in DriverBuddyPlugin.init().

  • config.py Feature.IOCTL_SCAN = True: new flag that gates both find_ioctls() and scan_dispatchers() in analysis.py.

  • callchain.py trace(): progress line every 10 seeds so long runs are visible in the output log ([callchain] N/M handlers traced).

  • ioctl_decoder.py scan_dispatchers(): per-dispatcher progress line ([scan] dispatcher 0xXXXX (N/M)) before processing each entry point.

  • dump_pool_tags.py: extracted _collect_tags_for_imports(import_names, decode_fn) helper that owns the outer import-enumeration loop and per-call-site backward walk. find_pool_tags() and collect_fallback() now each supply only a small decode_fn closure that handles the tag-extraction logic specific to them.

  • analysis.py _stage() helper: wraps each analysis stage in try/except so a single stage crash does not abort the rest of the pipeline; the exception is logged via rep.info() and execution continues. populate_data_structures() and the IOCTL-discovery path are not wrapped (their failures are handled explicitly).

  • analysis.py: early-return guard after populate_data_structures() returns False; all downstream stages (callchain, heuristics, scoring, etc.) are now skipped rather than running against an empty function map.

  • wdf.py populate_wdf(): emits a warning when no segment contains the mdfLibrary UTF-16 string so analysts know the WDF classification is a fallback, not a confirmed WDF version detection.

Fixed

  • reporting.py / DriverBuddyReloaded.py: Reporter.remove_findings_at(ea) and Reporter.re_save() added. Both ResultsChooser and IOCTLChooser now carry CH_CAN_DEL and override OnDeleteLine() so rows can be removed interactively; deletion propagates to the Reporter's findings list and re-writes JSON/HTML. InvalidHandler.activate() now also removes the anterior comment, calls _last_rep.remove_findings_at(), and live-refreshes the IOCTL chooser window.

  • DriverBuddyReloaded.py make_comment(): IOCTL decode comments are now also written as anterior comments (ida_compat.add_anterior_cmt(pos, string)), making them visible in the HexRays decompiler pseudocode view. Non-repeatable disassembly-only comments (idc.set_cmt) were silently dropped by HexRays. A duplicate-guard prevents re-running decode from appending the same comment twice.

  • ioctl_decoder.py scan_dispatchers(): deduplication now keyed on IOCTL code value instead of instruction EA. The old already_seen = {f.ea for f in ...} set compared instruction addresses against instruction addresses, so the same code value at two different EAs produced two findings; a code seen by find_ioctls() at one EA was not suppressed by scan_dispatchers() finding the same code at a different EA.

  • ida_compat.py is_64bit(): added explicit IS_IDA9 guard before calling the removed get_inf_structure() API. Without the guard, reaching the fallback path on IDA 9.0 raises AttributeError instead of a clear RuntimeError. The normal path (via ida_ida.inf_is_64bit()) is unaffected on all supported IDA versions.

  • irp_mj.py _create_enum_legacy(): member-add loop now wrapped in try/except; any add_enum_member failure deletes the partially-created enum and returns None instead of leaving an orphaned enum in the IDA database.

  • ioctl_decoder.py find_ioctls(): operand parsed with int(raw, 16) when IDA returns a hex string (e.g. 0x222003); bare int(raw) raised ValueError and silently dropped the IOCTL. Occurs when op_dec() does not take effect before print_operand() is called.

  • wdm.py check_for_fake_driver_entry(): replaced byte-decrement backward walk (end_address -= 0x1) with idc.prev_head() calls. The old loop landed in the middle of multi-byte instructions and read garbage mnemonics, silently breaking fake-DriverEntry detection on most real binaries. Walk now returns the real DriverEntry address on hitting a jmp/call, or the original address if the walk limit (64 steps) or a BADADDR boundary is reached.

2.0 - 2026-06-22

Added

  • ida_compat.py: single compatibility layer for every version-divergent IDA API. Struct and type handling is version-branched here; no other module imports ida_struct or ida_enum directly. Minimum supported IDA version is 7.6; a clear warning is raised on older installs instead of a cryptic traceback (issue #27).
  • analysis.py: headless-callable analysis pipeline extracted from DriverBuddyPlugin.run(). Enables batch-mode and test-harness invocation without instantiating any UI hooks.
  • config.py: centralised tunables, severity definitions, feature flags (config.Feature), output-path helpers, and every function-name set used by heuristics, callchain, and scoring (DANGEROUS_SINKS, VALIDATION_FUNCS, PRIVILEGE_GATE_FUNCS, PRIVILEGED_SENSITIVE_OPS, IRQL_RAISING_FUNCS, MDL_USER_FUNCS, COPY_SINKS, ALLOCA_FUNCS, POOL_ALLOC_FUNCS).
  • reporting.py: shared Finding model and Reporter spine. All analysis modules emit findings via rep.add_finding() instead of duplicating every print() with a matching log_file.write().
  • callchain.py: heuristic BFS tracer from dispatch handlers to dangerous sinks; feeds the IOCTL risk scorer and all seven heuristics.
  • scoring.py: per-IOCTL risk scoring. Base severity derived from transfer method and access mode; bumped to Critical when a handler reaches a dangerous sink (METHOD_NEITHER + reachable sink => Critical).
  • heuristics.py: seven heuristic checks ported and extended from the Driver Buddy Revolutions fork: check_user_copy_validation, check_privilege_gate, check_irql, check_mdl, check_alloca, check_pool_alloc_trust, check_physical_mem_ref (BYOVD indicator via \Device\PhysicalMemory xrefs).
  • exports_audit.py: flags driver exports with zero internal code references (excluding DriverEntry / GsDriverEntry / start) as potential hidden entry points.
  • irp_mj.py: creates an IRP_MJ_FUNCTION enum in the IDA type database and annotates MajorFunction array assignments in DriverEntry (issue #25). When HexRays is loaded, also registers number_format_t (user_numforms) entries so the decompiler renders MajorFunction[IRP_MJ_CREATE] instead of MajorFunction[0], and adds per-assignment end-of-line comments.
  • poc.py: generates a severity-sorted DeviceIoControl PoC harness in C (ioctl_pocs.c) for all discovered IOCTLs.
  • IOCTLChooser: severity-colored IDA chooser window with Severity, Address, Code, Device type, Method, Access, and Function number columns. Double-click jumps to the dispatcher EA. Opened automatically after auto-analysis and via Ctrl+Alt+I; reopenable at any time without re-running analysis.
  • ResultsChooser: clickable findings window listing all findings by severity; double-click jumps to the relevant address. Opened via Ctrl+Alt+W.
  • JSON export (findings.json) and HTML report (report.html) written to the IDB directory at the end of each run.
  • scan_dispatchers() in ioctl_decoder.py: flow-chart brute-force scan of identified dispatcher entry points, complementing find_ioctls() for stripped or poorly-typed binaries where IDA has not applied IO_STACK_LOCATION struct types.
  • Dynamic NTSTATUS filter in ioctl_decoder.py: queries the live IDA NTSTATUS / _NTSTATUS enum; falls back to a minimal 21-entry hardcoded set. Result is cached per run.
  • config.Feature flags: every optional analysis stage can be disabled without touching pipeline logic. SEGMENT_OPCODE_SCAN defaults to False; all others default to True.
  • Right-click menu additions in the disassembly view: Decode All IOCTLs in Function, Show all IOCTLs, Show Findings, Invalid IOCTL (context-sensitive).
  • Hotkeys Ctrl+Alt+I (IOCTL window) and Ctrl+Alt+W (Findings window).
  • tests/ida_smoke.py: in-IDA batch script that runs run_analysis() and writes a JSON summary for the cross-version matrix runner.
  • tests/run_cross_version.ps1: PowerShell matrix runner over IDA 7.6 SP1, 8.4, and Free 9.3 against real .sys files; prints a coloured pass/fail table.
  • tests/test_dbr.py: 17-check IDA-free regression suite covering IOCTL decode, risk scoring, device_name_finder bytes/str handling, JSON/HTML/PoC generation, NTSTATUS fallback, and pool-tag collection. Simulate IDA 9.0 import paths with DBR_SDK=900.
  • IOCTL finding entries now include the address where each code was found (PR #28).
  • \??\ prefix added to device_name_finder search set; IDA Strings DB fallback with EA in findings (issue #30); segment-scan fallback reading IDA database bytes directly when the string DB returns nothing.
  • Register-propagated pool-tag fallback (collect_fallback()) in dump_pool_tags.py for drivers where the primary import scan finds nothing (issue #16).

Changed

  • IDA 7.6 through 9.3 supported on Python 3. Previously the WDF analysis path crashed at load time on any IDA 9.x install.
  • AnalysisContext dataclass in utils.py holds all per-run mutable state and is threaded through every analysis module; eliminates module-level mutable dicts that carried stale data between plugin re-runs.
  • All heuristic and scoring function-name lists centralised in config.py; modules import the sets by name.
  • Output files now land in the IDB directory (<IDB_DIR>/<DRIVER_NAME>-<DATE>-<TS>-<suffix>), replacing the previous fixed DriverBuddyReloaded_autoanalysis.txt in the working directory.
  • winapi.py: replaced overbroad Ob*/Rtl*/Mm*/Zw* prefix matches with curated exact-match entries, eliminating a large class of false-positive flagged function findings.
  • vulnerable_functions_lists/c.py list converted to frozenset for O(1) lookup.
  • poc.py: de-duplicated IOCTL iteration; extracted _build_c_source(); normalised to f-strings; prefers \\DosDevices\\ device path.
  • find_opcodes.py: renamed FindInstructions to find_instructions; fixed variable shadowing; explicit opcode import.
  • UiAction helper: registerAction -> register_action, unregisterAction -> unregister_action, menuPath -> menu_path.
  • ida-plugin.json migrated to the official Hex-Rays publishing schema.
  • wdm.py: dispatcher candidate search now excludes GsDriverEntry, _guard_xfg_dispatch_icall_nop, and other XFG/CFG stubs.
  • get_driver_id() now detects GsDriverEntry as a valid driver entry point in IDA 8.2+ (issue #31).
  • wdf.populate_wdf() now reads the K/U prefix from the mdfLibrary string to return KMDF, UMDF, or WDF instead of always defaulting to WDF (issue #29).

Removed

  • NTSTATUS.py: 204 hardcoded values replaced by dynamic IDA enum lookup.

Fixed

  • IDA 9.0 crash on WDF analysis (ida_struct removed in 9.0).
  • device_name_finder crash on Python 3: mmap byte indexing returns int, not bytes; the repeat-buffer filter was silently broken. Fixed with buf[0:1] slice and bytes literals.
  • device_name_finder dropping short device names (e.g. \Device\Beep) due to an over-strict prefix-length filter.
  • wdm.py no-op "...".format() statement, broken chained comparison (io_stack_reg in "+10h" in disasm), and copy/paste op_stroff operand index.
  • wdm.py using a 32-bit BADADDR sentinel; replaced with idaapi.BADADDR and added a MAX_WALK=256 guard against unbounded traversal.
  • exports_audit.py crash unpacking a 4-tuple from idautils.Entries().
  • 6-entry gap in the IOCTL device-type name table (indices 0x4A-0x4F).
  • irp_mj.py receiving GsDriverEntry (a /GS stub with no MajorFunction assignments) instead of the real DriverEntry; AnalysisContext.real_entry_addr now stores the correct address.
  • irp_mj.py HexRays user_numforms entries not surviving a save/restore round-trip: bit 23 of number_format_t.flags must be set for type_name to serialise; IDA silently dropped it without the flag.
  • analysis.py is False/is True identity comparisons on pipeline return values.
  • False-negative "Unable to find IOCTLs" message emitted before scan_dispatchers() had a chance to run; deferred until both strategies complete.
  • Occasional error from a branch not executed in the IOCTL decode path (PR #35).
  • AnalysisContext mutable state carried over between plugin re-runs in the same IDA session.
  • IOCTLTracker.remove_ioctl() used set.remove() (raises on missing key); replaced with set.discard().
  • find_all_ioctls() registered the same IDA action twice.

1.6 - 2022-08-09

Added

  • ZwTerminateProcess added to the dangerous functions list (PR #26).
  • Arbitrary memory read/write functions added to the dangerous functions list.

Fixed

  • IOCTL code table correction.

1.5 - 2022-05-07

Added

  • Additional WDF versions and WDF data/code separation (PR #24).

Fixed

  • is_driver() function fix.
  • Issue #15 (partial fix).

1.4 - 2022-04-25

Added

  • IOCTL device-type table expanded with entries from h0mbre/ioctl.py.
  • Function name in output for interesting cross-reference hits (PR #19).

Changed

  • IDA API upgrades: idc.Dword -> idc.get_wide_dword and related.

Fixed

  • Issue #22.
  • Issue #21.
  • Issue #15 (partial fix).
  • Issue #4: WDF driver structure implementation (PR #20).

1.3 - 2021-12-10

Added

  • Deprecated/banned function list expanded from Windows SDK dontuse.h and banned.h (PR #14).

Fixed

  • Bug where IOCTLs found via IoControlCode were not saved to the log file.
  • Issue #13.

1.2 - 2021-11-04

Added

  • Rtl* API entries to the dangerous functions list.
  • Arbitrary memory read/write function entries.

1.1 - 2021-10-27

Changed

  • Windows API functions reorganised into correct categories.

1.0 - 2021-10-22

Initial release.