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
- 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-sortkey (severity rank; numeric EA,-1for 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-IDAResultsChooser(_SEVERITY_COLORS).
- Default finding order (
reporting.default_sort_key, now shared by the HTML report and theResultsChooserwindow): severity descending, then category priority so confirmedcallchainfindings -- provable user-input -> dangerous-sink paths -- lead their severity tier, ahead ofioctl/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.
wdm.find_majorfunction_dispatchers(): binary-wide scan for stores intoMajorFunction[IRP_MJ_DEVICE_CONTROL](+0E0h) /[..._INTERNAL_...](+0E8h), now the primary DDC source. It works for every driver type and finds the handler even when theMajorFunctionassignment lives in a helper rather thanDriverEntry, 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-guessfind_dispatch_function()become fallbacks used only when the store scan finds nothing.wdm.references_iocontrolcode(): corroboration predicate - true when a function loads the IRP'sIO_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 acrossgoto-shared case labels so a code that reaches its work through a shared handler (e.g. WinRing0's0x9C4060CC/D0port I/O) is still attributed.scoring.score()uses these to attribute danger to the specific IOCTL. - Driver-level
heuristicfinding "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 intests/test_dbr.py(79 total).
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 withctx.ddc_addressesempty 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 thatreferences_iocontrolcode()confirms actually reads the IoControlCode. This stops a CFG-misidentified library/CRT helper from leaking its internal constants as IOCTLs - RTCore64'sSepSddlGetAclForStringhad emitted0xCCCCCCCD(an unsigned divide-by-5 reciprocal magic), a0x6C416553pool 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), anddispatcher-wide(imprecise fallback). Only precise/in-case evidence can forceMETHOD_NEITHERto 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 common0xC00000xxerror range (incl.STATUS_INSUFFICIENT_RESOURCES0xC000009A /STATUS_INVALID_ACL0xC0000077) 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(): extendedexcluded_functionswith CRT/security thunks (__GSHandlerCheck,__GSHandlerCheckCommon) and now skipsFUNC_LIBcandidates, so a library helper is less likely to be mis-selected as a dispatcher (dbutil_2_3 previously enqueued__GSHandlerCheckCommon).
tests/run_cross_version.ps1: passed-S<script> <resultpath>- a-Svalue containing a space, which PowerShell'sStart-Processmangles - so every cell reportedno_result(the exact bugrun_golden.ps1was written to avoid). It now passes no-Sargument and globs the<idb>.smoke.jsonthatida_smoke.pyderives from the IDB. Cross-version smoke passes on IDA 7.6 SP1 and 8.4; IDA Free 9.3 cannot be driven headlessly (-Sbatch 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.
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.
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 forpython38.dll, interpreter switched to Python 3.10)from PyQt5 import QtCoreraisedImportError: DLL load failed while importing sip; the old broadexcept Exception -> return Trueswallowed 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 newida_kernwin.Formfallback (_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 viaconfig.Feature.validate(); its form string is generated from the shared_FEATURE_GROUPS/_TUNINGtables and was verified to compile headlessly in IDA 7.6.- (B1, B2, B3, B4, B7, B16)
heuristics.check_use_after_free(): rewritten on top ofregisters.py. Previously it (B1) matched the free via rawidc.print_operand, so an importedcall cs:__imp_ExFreePoolWithTagwas never recognised and the check never started; (B2) putmovin its write-set, so the canonicalmov rax, [rcx]dereference-after-free was treated as a kill and never flagged; (B3) killed freed state withop0.startswith(reg), somov ecx, edxdid not clear a freedrcx(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 oncmp 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 changesuse-after-freefindings (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 checktid in (None, BADADDR, -1)treated ordinal0as a valid type. Type ordinals are 1-based, so a non-positive result now also counts as "not found" (theNonetest 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 toFeature.validate()would not be enforced by the UI. The rules now live once inFeature._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 literal5instead of the namedidc.o_imm, an opaque magic number inconsistent with the rest of the codebase. Now useidc.o_imm. - (B5, B13)
ioctl_decoder.find_ioctls(): the fuzzyIoControlCodefallback scanner calledidc.op_dec()on every match to coerce the operand to decimal before reading it back as text.op_decis 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 withidc.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()andheuristics._user_pointer_tainted(): these handler-subtree reachability queries used the defaultCALLCHAIN_MAX_DEPTH, which is independent of theHANDLER_SEED_DEPTHat which handler bodies are discovered. Both are user-tunable, so loweringCALLCHAIN_MAX_DEPTHbelowHANDLER_SEED_DEPTHcould make attribution/taint shallower than discovery. They now reachmax(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 betweentrace()andtransitive_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_eavianext_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 viaidautils.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), andget_driver_idruns through it, falling back to an"unknown"driver type so IOCTL and heuristic analysis still proceed. - (N21)
wdf.populate_wdf(): the result ofidc.get_first_dref_to(idx - 2)was used directly inaddr + ptr_size + ...reads and then passed toida_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 derivedWdfFunctionspointer is invalid, and guards the K/U prefix read against the segment boundary. - (N24, N25)
wdm.define_ddc()(cosmetic struct-member labelling): theIO_STACK_LOCATION.OutputBufferLengthtestio_stack_reg + "+8" in disasmalso 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*_resolvedflags 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[regprefix. For a 2-character base register the slice ate the leading+("[r8+0E0h]"[4:] == "0E0h]"), soMajorFunction[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 aGsDriverEntrystub and aDriverEntry(orDriverEntry_0) resolved nondeterministically depending on PE layout. Now collects all matches and returns by a fixed preference (GsDriverEntry>DriverEntry>DriverEntry_0);GsDriverEntryis the true /GS entry point and is unwrapped downstream bycheck_for_fake_driver_entry. Two new regression checks. - (B10)
DriverBuddyReloaded.py(IOCTL row-delete / "Invalid IOCTL"): removing an IOCTL only calledidc.del_extra_cmt(ea, E_PREV + 0), which clears just the first anterior line.make_commentcan append several anterior lines (E_PREV, E_PREV+1, ...), so the rest leaked and stayed in the decompiler view. Newida_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-independentCTL_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 (andget_strlit_contentstranscodes 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_FUNCSgains theRtlUIntAdd/Sub/Multsafe-arithmetic family.FREE_POOL_FUNCSgainsIoFreeMdl(its freed pointer is the first argument, which the UAF register model tracks); lookaside frees andMmFreePagesFromMdlare deliberately excluded and documented, since their freed pointer is not the first argument / the MDL stays valid.PRIV_INSN_SEVERITYgains the descriptor/task-register storessidt/sgdt/sldt/str(KASLR-leak primitives);rdmsr/wrmsr/rdpmcare intentionally kept out (already scanned whole-binary viaOPCODES, so listing them here would double-report). Five new regression checks intests/test_dbr.py.
2.3.0 - 2026-06-29
DriverBuddyReloaded/signatures.py: single source of truth for all function-name sets, opcode lists, and severity maps previously scattered betweenconfig.pyandvulnerable_functions_lists/. Every heuristic, callchain, scoring, and utility module now imports fromsignaturesby name;config.pyholds 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 editingconfig.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.CALLCHAINwithoutIOCTL_SCAN) are rejected with an inline warning that keeps the dialog open. A "Reset to Defaults" button restores the values shipped inconfig.py(captured at import time, before any runtime mutations). Changes are session-scoped --config.pyon disk is never touched.DriverBuddyReloaded/custom.pypromoted to package root (wasvulnerable_functions_lists/custom.py); the now-empty directory is deleted.signatures.DEVICE_CREATE_UNSECURED_FUNCSandsignatures.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 flagsWdfDeviceCreate(KMDF device whose DACL is set out-of-band viaWdfDeviceInitAssignSDDLString/ INF). The symbolic-link finder (device_name_finder.find_symbolic_links) additionally coversIoCreateUnprotectedSymbolicLink(rated LOW: the link object has a NULL DACL, so any user can delete and redirect it) andWdfDeviceCreateSymbolicLink. Both checks now resolve names viactx.functions_map(a superset ofctx.imports_map) so WDF functions resolved as named subs are covered alongside the ntoskrnl imports.
callchain.transitive_callees():max_depthparameter now defaults toNoneand the real default (config.CALLCHAIN_MAX_DEPTH) is read at call time. The previousmax_depth=config.CALLCHAIN_MAX_DEPTHwas 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.pysection order rationalised: feature flags -- analysis tuning constants -- severity model -- IOCTL risk weights -- output paths. All function-name sets removed (now insignatures.py).Ctrl+Alt+Freassigned from the removed "Decode ALL IOCTLs in Function" to "Show Findings" (previouslyCtrl+Alt+W).UiAction.register_action()/unregister_action(): menu-path calls are now skipped whenmenu_pathis empty, fixing a silentFalsereturn that affected all hotkey-only actions.
heuristics.run()/analysis.py: theTOCTOU / double-fetchandUse-after-freesettings-UI checkboxes had no effect unlessHeuristicswas also enabled, because both checks lived insideheuristics.run()which only ran underFeature.HEURISTICS. The structural checks are now gated as a group onFeature.HEURISTICS, while double-fetch and use-after-free are gated independently onFeature.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 returnsTrueso analysis proceeds with the current config, instead of conflating the failure with a user cancel and silently aborting the run.
vulnerable_functions_lists/directory (c.py,winapi.py,opcode.py,__init__.py): content consolidated intosignatures.py;custom.pypromoted to package root.- "Decode ALL IOCTLs in Function":
find_all_ioctls(),track_ioctls(),decode_all_ioctls(),DecodeAllHandler, theCtrl+Alt+FUiAction, 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
- Golden-output regression for the four reference drivers (
beep,HEVD,ALSysIO64,WinRing0x64). The current pipeline output is captured astests/drivers/<driver>.golden.jsonand is the authoritative FP/FN baseline:tests/run_golden.ps1runs the full analysis on a pristine copy of each.i64and fails on any added finding (false positive), missing finding (false negative) or severity change.tests/ida_smoke.pynow derives its result path from the IDB and auto-discovers an adjacent<idb>.golden.json, so the runner needs no-Sarguments (which PowerShell'sStart-Processmangles when they contain a space).
- Heuristic tuning constants consolidated into
config.py:COPY_VALIDATION_LOOKBACK/COPY_VALIDATION_LOOKAHEAD(washeuristics._VALID_LOOKBACK/_VALID_LOOKAHEAD),UAF_GLOBAL_BACKWALK(was a literal16), andSYMLINK_DECODE_LOOKBACK(wasdevice_name_finder._SYMLINK_LOOKBACK). Values unchanged; behaviour-preserving.
- The
DeviceIoControlPoC harness generator (poc.py,ioctl_pocs.c,Feature.POC_HARNESS). All IOCTL data is already infindings.json/report.htmland the IOCTL window; the C skeleton added little value. - The per-decode
IOCTLs.txtfile 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 infindings.json/report.html. (pooltags.txt-- the WinDbg-format pool-tag dump -- andautoanalysis.txt-- the run diagnostic log -- are retained.) - Stale precedent artifacts under
tests/drivers/superseded by the committed goldens: the 2026-06-24*-findings.jsonand*-autoanalysis.txtfor 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
- 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 showedmemmoveand all 17 ALSysIO64 IOCTLs showedMmMapIoSpace, 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; callchainadditionally seeds the tracer from each handler, so it reports per-handler paths;scoringattributes sinks to the IOCTL's own handler when known (falling back to the dispatcher taggedsink_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 reachmemmove) + 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.
- the decompiler collector resolves the per-case handler function (first in-binary call in the
switch case body, skipping logging imports) and stores
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 ungatedMmMapIoSpace(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'sTrigger*/*IoctlHandlerfunctions) these checks scanned only the dispatcher prologue and emitted nothing.heuristics.run()now expands the seed set viacallchain.transitive_callees(..., config.HANDLER_SEED_DEPTH)(library/thunk leaves likememmove/memsetexcluded). Measured effect: HEVD now reports 11 pool-allocation-without-validation and 6 TOCTOU double-fetch findings (including the genuineTriggerDoubleFetch) where it previously reported none; ALSysIO64 and WinRing0x64 now report the ungatedMmMapIoSpaceprivileged op.heuristics: callee matching is now import-aware. Imported functions disassemble ascall cs:__imp_<Name>, for whichprint_operandreturns "cs:_imp" andCodeRefsFrom+get_func_namereturn 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,ProbeForReadandMmMapIoSpacematch regardless of being local or imported. This both enables the pool/privilege checks and makes copy-validation correctly treat a nearby importedProbeForReadas 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.
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 onFeature.IOCTL_DECOMPILER+ HexRays. Verified: HEVDTriggerWriteNULLHIGH plusTriggerArbitraryWriteand 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'sSize + 4 <= 0x800-- need value-flow tracking and are left as a known gap;check_pool_alloc_trustalready covers unvalidated allocation sizes.)heuristics.check_use_after_free_global(): cross-function, global-pointer UAF detection. The existing register-trackingcheck_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 findsExFreePool*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 bothg_UseAfterFreeObjectNonPagedPooland...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, yetoutto an attacker-controlled port andmov cr*are canonical BYOVD hardware-access primitives. Severities in newconfig.PRIV_INSN_SEVERITY(outCRITICAL,inHIGH, ...). Verified: WinRing0x64 now flags 3in+ 3out+hlt, ALSysIO64 7in+ 1out; HEVD/beep have none.- PCI configuration-space access (
HalGetBusDataByOffsetHIGH /HalSetBusDataByOffsetCRITICAL) added toconfig.DANGEROUS_SINKS,config.PRIVILEGED_SENSITIVE_OPSand 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".
-
heuristics.check_double_fetch(): eliminated the METHOD_BUFFERED false positives and stopped pairing mutually-exclusive sibling switch cases. The check previously flagged anymov reg, [base+off]re-read with no intervening Probe call, with no notion of whether the source was a user pointer -- so it fired onIrp->AssociatedIrp.SystemBuffer/IrpSp->Parameterskernel 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 withidc.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\Xinto 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 theIoCreateSymbolicLinkcall was capped at 30 instructions, but HEVD initialises the link name ~38 instructions before the call (the wholeIoCreateDevice+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\ALSysIOnow 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 loneIoCreateDevice/IoCreateSymbolicLinkcall produced two identical findings (inflating theacl/symlinkcategory counts and the LOW/INFO severity totals). Both loops now dedup on the call-site address (xr.frm), andreporting.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.
ioctl_decoder.pyscan_dispatchers(): now recovers IOCTL codes that never appear as immediate operands in the disassembly. Previously the dispatcher scan only matched literal immediates incmp/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 unrelatedstatus == STATUS_*check is ignored); with no switch present (if-chain dispatcher) every comparison constant is taken. Gated on the newconfig.Feature.IOCTL_DECOMPILERflag + 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).
analysis.run_analysis(): the precise dispatcher scan now runs beforefind_ioctls(), and the fuzzy whole-binaryIoControlCodetext scan only runs as a fallback when the dispatcher scan found nothing.find_ioctls()can mistake data constants for IOCTLs (e.g. a misread0x0032C004), so skipping it when the structured decode succeeds removes those false positives.ioctl_decoder.py_is_valid_ctl_code(): now also rejects the0xFFFFFFFF((DWORD)-1/INVALID_HANDLE_VALUE) sentinel, which is structurally a valid CTL_CODE but surfaces from== -1checks inside dispatchers (observed in WinRing0x64).
-
DriverBuddyReloaded.pymake_comment(): fixed anAttributeError: module 'idc' has no attribute 'add_extra_cmt'crash that abortedplugin_t.run()whenever auto-analysis tried to write an IOCTL anterior comment.add_extra_cmtdoes not exist inidcon any supported build (it lives only inida_lines/idaapi, unlike its siblingsget_extra_cmt/del_extra_cmt/E_PREV, whichidcdoes expose). Anterior-comment handling now routes through two newida_compathelpers,get_anterior_cmt()andadd_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.pyscan_dispatchers(): replacedrange(block.start_ea, block.end_ea)with awhile 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 viaidc.next_head()is the correct pattern (used byiter_text_matches()and all other walkers in the codebase). -
wdm.pylocate_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 initialiseMajorFunction[]in a helper function called from DriverEntry; those DDC addresses were never added toctx.ddc_addresses, soscan_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 deduplicatesddc_listwithset()before walking xrefs to avoid processing the same candidate multiple times when the pattern matched more than once in the same function.
-
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 underchecks; 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_simpleto simulate a GsDriverEntry stub ending withjmp real_entry; assertscheck_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, andidc.print_operandso that a synthetic handler callsKeRaiseIrqlthenZwOpenProcess; assertscheck_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).
- T1: mocks
-
heuristics.pycheck_use_after_free(): use-after-free heuristic (N6). Forward-walks the basic-block CFG viaidaapi.FlowChart; tracks the argument register (RCX on x64, ECX on x86) after eachExFreePool/ExFreePoolWithTag/ExFreePool2call; emits HIGH when that register is read before being overwritten by a write instruction. Set propagates across block successors. Gated onFeature.UAF_DETECT = True. -
device_name_finder.pyfind_symbolic_links(): Symbolic link tracking (N4). Walks xrefs toIoCreateSymbolicLink; attempts to decode the target path by scanning backwards from each call site for UNICODE_STRING buffer references. Decoded paths are stored inctx.symbolic_links; an INFO finding is emitted per call site regardless of whether the path was recovered. Gated onFeature.SYMLINK_TRACK = True. -
utils.pyAnalysisContext: addedsymbolic_links: listfield (N4). -
utils.pyfind_device_create_calls(): Device ACL audit (N3). Walks xrefs toIoCreateDevice(LOW: no security descriptor, world-accessible by default) andIoCreateDeviceSecure(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, orS-1-5-32-545-- LOW if the SDDL cannot be statically recovered). Gated onFeature.ACL_AUDIT = True; wired intoanalysis.pybefore the callchain stage. -
heuristics.pycheck_double_fetch(): TOCTOU/double-fetch heuristic (N1). Walks the instruction stream of each handler; groupsmov reg, [src+offset]loads by(src_register, offset); flags any pair with 2+ occurrences that has noProbeForRead/ProbeForWriteor copy-sink call between them (MEDIUM). Gated onFeature.TOCTOU_CHECK = True. -
config.py: new feature flagsTOCTOU_CHECK,ACL_AUDIT,SYMLINK_TRACK,UAF_DETECT; new function-name setsPROBE_FUNCS,DEVICE_CREATE_FUNCS,SYMLINK_FUNCS,FREE_POOL_FUNCS. -
config.pyFeature.validate(): startup classmethod that raisesValueErrorfor incoherent feature-flag combinations. Currently checks thatFeature.CALLCHAINis not enabled whenFeature.IOCTL_SCANis disabled (callchain needs IOCTL findings as seeds). Called inDriverBuddyPlugin.init(). -
config.pyFeature.IOCTL_SCAN = True: new flag that gates bothfind_ioctls()andscan_dispatchers()inanalysis.py. -
callchain.pytrace(): progress line every 10 seeds so long runs are visible in the output log ([callchain] N/M handlers traced). -
ioctl_decoder.pyscan_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()andcollect_fallback()now each supply only a smalldecode_fnclosure 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 viarep.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 afterpopulate_data_structures()returnsFalse; all downstream stages (callchain, heuristics, scoring, etc.) are now skipped rather than running against an empty function map. -
wdf.pypopulate_wdf(): emits a warning when no segment contains themdfLibraryUTF-16 string so analysts know theWDFclassification is a fallback, not a confirmed WDF version detection.
-
reporting.py/DriverBuddyReloaded.py:Reporter.remove_findings_at(ea)andReporter.re_save()added. BothResultsChooserandIOCTLChoosernow carryCH_CAN_DELand overrideOnDeleteLine()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.pymake_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.pyscan_dispatchers(): deduplication now keyed on IOCTL code value instead of instruction EA. The oldalready_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 byfind_ioctls()at one EA was not suppressed byscan_dispatchers()finding the same code at a different EA. -
ida_compat.pyis_64bit(): added explicitIS_IDA9guard before calling the removedget_inf_structure()API. Without the guard, reaching the fallback path on IDA 9.0 raisesAttributeErrorinstead of a clearRuntimeError. The normal path (viaida_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; anyadd_enum_memberfailure deletes the partially-created enum and returnsNoneinstead of leaving an orphaned enum in the IDA database. -
ioctl_decoder.pyfind_ioctls(): operand parsed withint(raw, 16)when IDA returns a hex string (e.g.0x222003); bareint(raw)raisedValueErrorand silently dropped the IOCTL. Occurs whenop_dec()does not take effect beforeprint_operand()is called. -
wdm.pycheck_for_fake_driver_entry(): replaced byte-decrement backward walk (end_address -= 0x1) withidc.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 realDriverEntryaddress on hitting ajmp/call, or the original address if the walk limit (64 steps) or aBADADDRboundary is reached.
2.0 - 2026-06-22
ida_compat.py: single compatibility layer for every version-divergent IDA API. Struct and type handling is version-branched here; no other module importsida_structorida_enumdirectly. 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 fromDriverBuddyPlugin.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: sharedFindingmodel andReporterspine. All analysis modules emit findings viarep.add_finding()instead of duplicating everyprint()with a matchinglog_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\PhysicalMemoryxrefs).exports_audit.py: flags driver exports with zero internal code references (excludingDriverEntry/GsDriverEntry/start) as potential hidden entry points.irp_mj.py: creates anIRP_MJ_FUNCTIONenum in the IDA type database and annotatesMajorFunctionarray assignments inDriverEntry(issue #25). When HexRays is loaded, also registersnumber_format_t(user_numforms) entries so the decompiler rendersMajorFunction[IRP_MJ_CREATE]instead ofMajorFunction[0], and adds per-assignment end-of-line comments.poc.py: generates a severity-sortedDeviceIoControlPoC 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 viaCtrl+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 viaCtrl+Alt+W.- JSON export (
findings.json) and HTML report (report.html) written to the IDB directory at the end of each run. scan_dispatchers()inioctl_decoder.py: flow-chart brute-force scan of identified dispatcher entry points, complementingfind_ioctls()for stripped or poorly-typed binaries where IDA has not appliedIO_STACK_LOCATIONstruct types.- Dynamic NTSTATUS filter in
ioctl_decoder.py: queries the live IDANTSTATUS/_NTSTATUSenum; falls back to a minimal 21-entry hardcoded set. Result is cached per run. config.Featureflags: every optional analysis stage can be disabled without touching pipeline logic.SEGMENT_OPCODE_SCANdefaults toFalse; all others default toTrue.- 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) andCtrl+Alt+W(Findings window). tests/ida_smoke.py: in-IDA batch script that runsrun_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.sysfiles; prints a coloured pass/fail table.tests/test_dbr.py: 17-check IDA-free regression suite covering IOCTL decode, risk scoring,device_name_finderbytes/str handling, JSON/HTML/PoC generation, NTSTATUS fallback, and pool-tag collection. Simulate IDA 9.0 import paths withDBR_SDK=900.- IOCTL finding entries now include the address where each code was found (PR #28).
\??\prefix added todevice_name_findersearch 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()) indump_pool_tags.pyfor drivers where the primary import scan finds nothing (issue #16).
- 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.
AnalysisContextdataclass inutils.pyholds 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 fixedDriverBuddyReloaded_autoanalysis.txtin the working directory. winapi.py: replaced overbroadOb*/Rtl*/Mm*/Zw*prefix matches with curated exact-match entries, eliminating a large class of false-positive flagged function findings.vulnerable_functions_lists/c.pylist converted tofrozensetfor O(1) lookup.poc.py: de-duplicated IOCTL iteration; extracted_build_c_source(); normalised to f-strings; prefers\\DosDevices\\device path.find_opcodes.py: renamedFindInstructionstofind_instructions; fixed variable shadowing; explicit opcode import.UiActionhelper:registerAction->register_action,unregisterAction->unregister_action,menuPath->menu_path.ida-plugin.jsonmigrated to the official Hex-Rays publishing schema.wdm.py: dispatcher candidate search now excludesGsDriverEntry,_guard_xfg_dispatch_icall_nop, and other XFG/CFG stubs.get_driver_id()now detectsGsDriverEntryas a valid driver entry point in IDA 8.2+ (issue #31).wdf.populate_wdf()now reads theK/Uprefix from themdfLibrarystring to returnKMDF,UMDF, orWDFinstead of always defaulting toWDF(issue #29).
NTSTATUS.py: 204 hardcoded values replaced by dynamic IDA enum lookup.
- IDA 9.0 crash on WDF analysis (
ida_structremoved in 9.0). device_name_findercrash on Python 3:mmapbyte indexing returnsint, notbytes; the repeat-buffer filter was silently broken. Fixed withbuf[0:1]slice andbytesliterals.device_name_finderdropping short device names (e.g.\Device\Beep) due to an over-strict prefix-length filter.wdm.pyno-op"...".format()statement, broken chained comparison (io_stack_reg in "+10h" in disasm), and copy/pasteop_stroffoperand index.wdm.pyusing a 32-bitBADADDRsentinel; replaced withidaapi.BADADDRand added aMAX_WALK=256guard against unbounded traversal.exports_audit.pycrash unpacking a 4-tuple fromidautils.Entries().- 6-entry gap in the IOCTL device-type name table (indices 0x4A-0x4F).
irp_mj.pyreceivingGsDriverEntry(a/GSstub with noMajorFunctionassignments) instead of the realDriverEntry;AnalysisContext.real_entry_addrnow stores the correct address.irp_mj.pyHexRaysuser_numformsentries not surviving a save/restore round-trip: bit 23 ofnumber_format_t.flagsmust be set fortype_nameto serialise; IDA silently dropped it without the flag.analysis.pyis False/is Trueidentity 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).
AnalysisContextmutable state carried over between plugin re-runs in the same IDA session.IOCTLTracker.remove_ioctl()usedset.remove()(raises on missing key); replaced withset.discard().find_all_ioctls()registered the same IDA action twice.
1.6 - 2022-08-09
ZwTerminateProcessadded to the dangerous functions list (PR #26).- Arbitrary memory read/write functions added to the dangerous functions list.
- IOCTL code table correction.
1.5 - 2022-05-07
- Additional WDF versions and WDF data/code separation (PR #24).
is_driver()function fix.- Issue #15 (partial fix).
1.4 - 2022-04-25
- IOCTL device-type table expanded with entries from h0mbre/ioctl.py.
- Function name in output for interesting cross-reference hits (PR #19).
- IDA API upgrades:
idc.Dword->idc.get_wide_dwordand related.
- Issue #22.
- Issue #21.
- Issue #15 (partial fix).
- Issue #4: WDF driver structure implementation (PR #20).
1.3 - 2021-12-10
- Deprecated/banned function list expanded from Windows SDK
dontuse.handbanned.h(PR #14).
- Bug where IOCTLs found via
IoControlCodewere not saved to the log file. - Issue #13.
1.2 - 2021-11-04
Rtl*API entries to the dangerous functions list.- Arbitrary memory read/write function entries.
1.1 - 2021-10-27
- Windows API functions reorganised into correct categories.
1.0 - 2021-10-22
Initial release.