Automated, portable static analysis tool, built for production environments (SOC / Blue Team / DFIR). This Python script dissects Windows binaries without ever executing them, then offers an assisted reverse engineering phase with an interactive console.
Three PE families are handled with their own specifics: executables (.exe), libraries (.dll, .ocx, .cpl, .ax — imports and exports, sideloading) including .NET assemblies, and kernel drivers (.sys — WDM/KMDF capabilities, BYOVD cross-check against known vulnerable drivers).
Zero new dependency: only lief and r2pipe (+ rizin/radare2 on the system).
This is v7's structural change: static analysis and reverse engineering are now two distinct steps, with two separate JSON reports.
┌─────────────────────┐
binary.exe ───▶ │ PHASE 1 : STATIC │ ──▶ report_<name>_<hash8>_static.json
└─────────────────────┘ (+ .yar if --yara)
│
[?] Launch reverse? ──── no ──▶ clean stop (code 0)
│ yes
┌─────────────────────┐
│ PHASE 2 : REVERSE │
│ auto → interactive │ ──▶ report_<name>_<hash8>_reverse.json
└─────────────────────┘ (+ .yar generated in session)
Important points:
- The static report is written to disk BEFORE the reverse phase. If rizin crashes, if you hit Ctrl+C, or if you answer no, the static analysis work is already saved.
- The two JSON files are distinguishable by their
_static/_reversesuffix, in addition to the first 8 characters of the SHA256, which avoids collisions between samples sharing a name. - The reverse report references the static report (
linked_static_report) and recomputes the suspicion score by folding in the reverse phase's signals (suspicion_deltagives the before/after score). - On a Ctrl+C during a long rizin analysis, the partial reverse report is still written (
"interrupted": true, return code 130).
-
Metadata & IOCs
- MD5, SHA256, global entropy (packing detection).
- ASCII / UTF-16 strings, with automatic Base64 decoding and single-byte XOR brute-force (bounded window).
- IOCs: IPv4, IPv6, URLs, domains (filtered of false positives like
kernel32.dll), emails, crypto wallets (BTC/ETH), registry keys, suspicious Windows paths, PowerShell/CMD commands. extraction_statsblock indicating whether any caps were hit (truncated) — the IOC list is never silently incomplete.
-
Structural parsing (LIEF)
- PE header: architecture, entry point, ImpHash, Rich Header fingerprint.
- Compiled-in protections (ASLR, DEP/NX, CFG, SEH).
- Regular and delay-load imports, cross-referenced against the behavioral capability table.
- TLS callbacks (anti-sandbox), overlay, resources (version info, manifest, icon fingerprint).
- Recomputed PE checksum compared against the declared field — algorithm reimplemented from public documentation, to be validated on a real sample before operational use (see warning in the code).
- Best-effort Authenticode check (depends on the LIEF version).
- exe / dll / driver classification, plus .NET assembly detection.
- For a binary whose import table is silent (.NET, destroyed imports, dynamically-resolving driver), capabilities are recovered from strings and weighted down accordingly.
- Managed .NET capabilities (
DOTNET_MANAGED_CAPABILITIES): a .NET malware needs no P/Invoke at all to exfiltrate over HTTP, persist at startup, or encrypt files — it all goes through the BCL, whose type/method names live in plain sight in the metadata. - Signer identity extracted from the certificate (subject, issuer, validity) — a forged or stolen certificate is a major signal.
- Export analysis (DLL/EXE): names, unnamed exports (ordinals), forwarded exports, fingerprint to group variants together. An executable exposing exports is itself abnormal (pattern of a DLL disguised as a
.exe— found on an actual sample in the test set) and counts against it. - A DLL's execution surface: the exports Windows hosts call —
DllRegisterServer(regsvr32),ServiceMain(svchost),CPlApplet(control.exe), rundll32 conventions,ReflectiveLoader. - Proxy DLL detection: when the majority of exports are forwarded to another library, that's the signature of DLL sideloading.
- Packer detection via section signatures and a cluster of structural clues.
- Heuristic suspicion score (0-100) — a triage aid, not a substitute for manual analysis.
-
Kernel drivers (
.sys)- Reliable classification via import of a library loadable only in kernel mode (
ntoskrnl.exe,hal.dll,fltmgr.sys...) — far more robust than the on-disk extension or the subsystem alone, both of which are imitable. TheIMAGE_FILE_DLLheader flag is required as corroboration: a single forged import isn't enough to get a binary (mis)classified as a driver. - Dedicated kernel capabilities: notify routine tampering (
PsSetCreateProcessNotifyRoutineand its removal — the signature of a kernel-mode "EDR killer"), handle protection callbacks (ObRegisterCallbacks), arbitrary kernel memory access (MmMapIoSpace,ZwMapViewOfSection...). - BYOVD cross-check: the binary's name is compared against a list of signed but vulnerable drivers, actually abused in attacks (cross-referenced with the LOLDrivers project). Checked first against internal metadata (
OriginalFilename/InternalNamefrom VERSIONINFO, PDB path) — which renaming the file on disk doesn't change. nt_device_pathsIOC:\Device\.../\DosDevices\...paths on the kernel side,\\.\Nameform on the usermode side — this is how a loader talks to the vulnerable driver it just loaded.
- Reliable classification via import of a library loadable only in kernel mode (
Automatic pass:
- Xrefs to sensitive APIs (including via the IAT).
- Decompilation of the entry point and of the functions referencing the most distinct sensitive APIs, with the ranking exposed in
priority_functions(selection_reason: "api_xref"). - Complexity-based safety net: on a packed or obfuscated sample — the majority of the real test set —, the import table is nearly empty and the API signal barely picks out any function; the analyst previously received only the entry point, almost always just the unpacking stub. With remaining budget, the largest functions (by basic block count) fill out the selection (
selection_reason: "complexity_fallback"), size-capped so as not to pick a function doomed to fail (see below). - Lightweight CFG (blocks + edges) for these functions.
- Time-bounded decompilation, independent of the underlying engine: on an actual Go binary from the test set, a function with 550+ basic blocks (massive Go runtime inlining) had
pdgrunning for over 90s without ever finishing.r2ghidra.timeout— the native mechanism meant for this role — proved unreliable inside a container, making decompilation go silent altogether once set. An independent Python guard kills the underlying rizin process if a command exceeds its deadline (pdg,agfj, andaaaitself).
Interactive console — the rizin session stays open, so the aaa analysis is never replayed: this is where the real time savings happen on a large sample.
| Command | Effect |
|---|---|
l [filter] [n] |
List functions (sorted by size) |
top |
Recall the function ranking from the auto pass |
exp [filter] |
List exports, execution surface listed first |
d <func|0xADDR> |
Decompile and add to the report |
s <func|0xADDR> [n] |
Disassemble n instructions (default 80) |
g <func|0xADDR> |
Extract the CFG and add it to the report |
x <API> |
Cross-references to an API |
str [n] |
Strings from the data sections |
str all [n] |
Strings from the whole file (noisy: headers, code) |
pick <text> |
Mark a string as a signal for the YARA rule |
picks |
Show marked strings |
note <text> |
Timestamped analyst note |
y |
Generate a YARA rule (marked strings take priority) |
sum |
Session summary |
? / q |
Help / quit and write the JSON |
Everything you do in the console is logged in interactive_session: commands, UTC timestamps, decompilations, CFGs, xrefs, notes. Free analysis traceability.
- YARA rule (
--yara) from extracted IOCs/strings — to be manually refined. Validated during calibration with the officialyaraengine (yara rule.yar sample) against their source sample: the tool isn't required for analysis, useful for anyone wanting to reproduce that check. - VirusTotal hash lookup, disabled by default (an OPSEC choice). Prefer the
VT_API_KEYenvironment variable: a key passed as an argument is readable by any user on the machine viaps aux. - Multi-file/folder batch mode with parallelization (
--recursive,--workers).
Unchanged.
sudo apt update
sudo apt install python3 python3-venv git
git clone https://github.com/radareorg/radare2
radare2/sys/install.sh
r2pm -U
r2pm -ci r2ghidra
cd /path/to/the/project/folder
python3 -m venv env
source env/bin/activate
pip install -r requirements.txt
Golden rule, unchanged: activate the venv before every run (source env/bin/activate).
python3 win_malware_analyzer.py la_meuh.exe
python3 win_malware_analyzer.py la_meuh.exe --reverse skip
python3 win_malware_analyzer.py la_meuh.exe --reverse auto --no-interactive
python3 win_malware_analyzer.py /path/to/samples_folder --recursive
| Option | Effect |
|---|---|
-o, --output-dir DIR |
Output folder for reports (default: current folder) |
-r, --recursive |
Walks subfolders in batch mode |
--reverse ask|auto|skip |
ask (default): prompts after the static phase — auto: chains automatically — skip: static only |
--skip-reverse |
Legacy alias for --reverse skip |
--no-interactive |
Automatic reverse without opening the console |
--max-functions N |
Functions decompiled in addition to the entry point (default 15) |
--no-cfg |
Disables lightweight CFG extraction |
--yara |
Generates a .yar rule alongside the static report |
--vt-key KEY |
VirusTotal lookup (default: VT_API_KEY) |
--timeout S |
rizin / network timeout (default 30s) |
--workers N |
Parallel processes in batch mode |
-v / -vv |
Verbosity (INFO / DEBUG) |
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Target not found / no file to analyze |
2 |
Failure on at least one file |
130 |
Reverse interrupted from the keyboard (partial report is written) |
The prompt never shows if stdin isn't a TTY (cron, pipe, CI) nor in batch mode: ask then degrades to auto, and no worker touches stdin. Your existing scripted usages don't change.
Batch mode is determined by the nature of the target (a folder), not by the number of files found: a folder containing a single sample is still processed as a batch.
The tool handles inherently hostile binaries; several safeguards are in place:
- Symlinks refused. The target isn't followed if it's a link — resolve it before passing it as an argument. The refusal is applied upstream, so it also covers LIEF and rizin, which open the file by its path.
- FIFOs, devices, sockets and directories refused (opened with
O_NONBLOCKthenS_ISREGchecked). - rizin command injection. Symbol names come from the analyzed binary, i.e. from the attacker. Known functions are targeted by numeric address, never by name; manually entered targets are filtered through a strict allowlist that rejects rizin metacharacters (
@temporary seek,$variables,;, backticks, pipes...). Accepted consequence: a genuinely mangled symbol (MSVC, stdcall_foo@8) isn't reachable by name — use its address instead (d 0x401500). - TOCTOU. If the file changes between the static and reverse phases, the reverse phase is aborted: the report's hashes would no longer be trustworthy.
- Atomic report writing (temp file +
os.replace): no half-written JSON on a Ctrl+C, a full disk, or two batch workers landing on duplicate samples. - Anti-DoS bounds: caps on the number of strings analyzed, IOCs per category, session artifacts, and a time budget on IOC extraction. When a cap is hit, it's stated plainly in
extraction_stats.truncated. - Driver classification not spoofable by a single isolated signal. A forged import to
ntoskrnl.exe(never actually resolved) is no longer enough to pass an ordinary binary off as a kernel driver and thereby disable the scoring rules that apply to executables/DLLs — theIMAGE_FILE_DLLheader flag, which a real driver always carries, is required as corroboration. - Domain filters by boundary, not by substring. "Boilerplate" exclusions (PKI infrastructure, .NET XML namespaces, Go/Rust/Node package registries) check a label boundary rather than a plain
in: a C2 namedevil-w3.org-c2.netis no longer wrongly filtered out, andtempuri.org.attacker.netis no longer treated as harmless noise. - Analysis run in an isolated environment (recommended): although the tool never executes the sample, it does have LIEF and rizin parse its bytes — two parsers themselves exposed to hostile input. This project's real test set is systematically analyzed inside a disposable Docker container, network disabled.
A deliberately heterogeneous and difficult set: 6 real kernel drivers (.sys), one Go binary, several binaries signed with a valid certificate (signature-abuse technique), one Lazarus Group DLL, and the rest ordinary EXE/DLL (droppers, loaders, agents). Analyzed inside a disposable Docker container, network disabled, archive password infected.
| Score | Count | |
|---|---|---|
minimal (< 15) |
1 / 26 | see "what the score isn't" below |
low (15-34) |
7 / 26 | including one sample fully packed, zero resolved imports |
medium (35-59) |
12 / 26 | |
high (≥ 60) |
6 / 26 | including the 4 drivers with the richest kernel capabilities |
- All 6 drivers in the set are signed and correctly classified
driver(scores 17 to 100/100 depending on exposed kernel capabilities). Two files carrying the.sysextension but structurally ordinary GUI DLLs (no kernel-only import) remain correctly classifiedexecutable— non-regression verified after the anti-spoofing hardening of the classification. - Two confirmed false negatives, fixed mid-loop: a managed .NET malware with not a single P/Invoke (9/100 →
DOTNET_MANAGED_CAPABILITIESadded) and a DLL disguised as a.exe—DllMaindispatcher visible under reverse, two non-standard exports (7/100 → 17/100 after adding the "executable exposing exports" rule). - Auto-generated YARA rules validated by the official
yaraengine against their source sample: 26/26, across three rounds of fixes. - The only
minimalsample (6/100) is a .NET application whose strings reveal no offensive capability in the strict sense (name, resources and control flow of an ordinary utility): consistent with the documented limitation below — a payload whose behavior is entirely resolved elsewhere (a remote second stage, for instance) escapes static analysis by construction.
22 malicious (ransomware, droppers, loaders, RATs, signed Go payloads, packed agents, injection DLLs, .NET AMSI patchers) and 3 clean, including a signed Microsoft installer and a utility wrongly flagged by about twenty antivirus engines.
| Result | |
|---|---|
| Malicious, "high" | 3 / 21 |
| Malicious, "medium" or above | 18 / 21 |
| Clean binaries | 0, 9 and 29 / 100 |
Only one malicious sample scores below the highest-scoring clean binary. The remaining false negatives are a 7-Zip self-extractor, a Delphi packer, and a 37 MB Electron application whose only visible capability is BCryptEncrypt — the latter remains out of reach of purely static analysis.
What the score isn't. A triage-aid heuristic, not a verdict. A high score calls for manual analysis; a low score clears nothing. Every point is justified by a line in suspicion.reasons, precisely so the analyst can contest the reasoning.
- Format: PE only (
.exe,.dll,.ocx,.cpl,.sys). An MSI, an MSIX, or an archive comes back withscore: nullandlevel: non_applicable— the tool doesn't claim to analyze them. - The score depends on what the binary exposes. A payload whose capabilities are entirely resolved at runtime (or delivered by a remote second stage) can't be characterized statically; the tool then flags the packing rather than the behavior, or flags nothing at all if the binary itself shows nothing.
- Data read from the binary is attacker-controlled — section names, export names, certificate subject, declared import table. Heuristics relying on it are cross-checked (certificate issuer, redirection targets, addresses rather than symbol names, header flag corroborating an import for driver classification), but that cross-checking has its limits.
- Calibration reflects its test set. 25, then 26 binaries is a solid basis, not a guarantee. Families not yet covered — Rust infostealer, userland rootkit, MSIX container — haven't been tested.
- The known-vulnerable-driver list (BYOVD) is a curated list, not the full LOLDrivers database: a name match is a strong clue, its absence doesn't clear a driver unknown to the list.
- Authenticode signature is checked via LIEF, without cross-referencing the system's certificate store: a self-signed chain can be internally consistent. This is why the issuer is checked against a list of recognized authorities.
Static analysis tool: no sample is ever executed at any point. Reports are written with 0600 permissions — they contain code extracted from malicious binaries. VirusTotal lookup is disabled by default: enabling it sends the sample's fingerprint to a third party.