A small, defensive static triage helper for malware analysis. It reads a suspicious file's bytes and produces a Markdown, HTML, or JSON report with hashes, strings, extracted IOCs, PE structure (including deep details such as Rich header, TLS callbacks, delay imports, .NET detection, and the resource tree), YARA matches, YAML capability signatures with MITRE ATT&CK mapping, packer identification, single-byte-XOR hidden-string mining, and static indicators for analyst review. It can also triage a whole directory in batch mode. Optional extras carve embedded PEs out of files and memory dumps, unpack UPX, query capa/FLOSS, and export annotation scripts for IDA Pro and Ghidra.
It never executes, loads, or emulates the sample. Static analysis only.
- Hashes -- MD5, SHA1, SHA256, SHA512
- File identification -- by magic bytes, plus overall Shannon entropy
- Strings -- ASCII and UTF-16 extraction
- IOC extraction from strings:
URLs, IPv4 and IPv6 addresses, domains, e-mail addresses,
cryptocurrency wallet addresses, registry Run/RunOnce key
references, file paths. IPv6 candidates are validated
with the standard
ipaddressparser and reported in canonical form, so clock times, MAC addresses, and colon-joined fragments such as C++std::filesystem::pathare not reported as addresses. Bitcoin candidates are likewise only reported after checksum validation (legacy1.../3...addresses via Base58Check, SegWitbc1...addresses via the BIP-173 Bech32 / BIP-350 Bech32m checksum plus witness-program shape checks), alongside Ethereum (0x+ 40 hex) and Monero (95-character) address shapes. Litecoin addresses are validated the same way as Bitcoin's: legacyL.../M...candidates via their Base58Check checksum (version bytes 0x30/0x32, which also keeps them distinct from Bitcoin's 0x00/0x05) and SegWitltc1...candidates via the Bech32/Bech32m checksum under theltcHRP. Dogecoin addresses are validated the same way: legacyD...(P2PKH) and9.../A...(P2SH) candidates via their Base58Check checksum (version bytes 0x1E/0x16, which also keeps them distinct from Bitcoin's and Litecoin's version bytes). Tron addresses are validated the same way too: fixed-lengthT...candidates via their Base58Check checksum (version byte 0x41) -- relevant because USDT-TRC20 is a frequent payment rail in ransom notes and stealer configs. XRP classicr...addresses are validated the same way, except under Ripple's own Base58 alphabet (not Bitcoin's) with version byte 0x00, so a Bitcoin1...address never validates as XRP and vice versa. Cardano mainnetaddr1...payment addresses are validated the same way, via their Bech32 checksum plus header and payload-length checks (base addresses, 57 bytes; enterprise addresses, 29 bytes); testnetaddr_test1...addresses are not reported. StellarG...account addresses are validated the same way, via their StrKey encoding (Base32 decoding, version byte 48, and the CRC16-XModem checksum over version + key);S...secret seeds are deliberately never extracted. Polkadot / Kusama / generic Substrate SS58 account addresses are validated the same way, via their Blake2b checksum (first 2 bytes of Blake2b-512 overSS58PRE+ prefix + key) with a 1-byte network prefix (Polkadot 0, Kusama 2, generic Substrate 42) and a 32-byte key; other prefixes and key lengths are not reported. Algorand account addresses are validated the same way, via their Base32 encoding (a 32-byte public key plus a 4-byte checksum, the last 4 bytes of the SHA-512/256 hash of the key), 58 characters in total. Cosmos Hubcosmos1...account addresses are validated the same way, via their Bech32 checksum plus a 20-byte payload-length check (45 characters in total); other Cosmos-SDK chains with a different HRP are not reported - Suspicious API references -- grouped by behaviour (process injection, persistence, network, crypto, anti-analysis, dynamic resolution, execution)
- PE parsing (via pefile): architecture, compile timestamp, entry point, sections with per-section entropy and flags, import table, imphash, overlay size
- Deep PE details: Authenticode certificate-table presence and size (existence/size only, no chain validation), Rich header presence with checksum validation and compid entries, TLS callbacks, delay-load imports, .NET (CLR) detection -- managed assemblies are flagged with a pointer to decompilers instead of native analysis -- a resource-tree inventory (type, language, size, per-resource SHA256 prefix), and version information (CompanyName, FileVersion, ...) when present
- Hidden-string mining: every single-byte XOR key (0x01-0xFF) is tried and decoded strings that yield an IOC or a suspicious keyword are reported with their key and file offset (noise-filtered, capped list)
- Batch mode -- point the tool at a directory: files are collected
recursively, de-duplicated by SHA256, triaged individually, and an
indexreport summarises everything (per-sample links, aggregated IOC totals, signature statistics, ATT&CK totals). Files over a size limit (default 200 MiB,--max-file-size) are skipped and listed - HTML reports (
--format html) -- a self-contained single file with inline CSS and no external resources, with the same content as the Markdown report (which remains the default) - YARA scanning (via
yara-python) against the
bundled generic rules in
yara_rules/, or your own rule directory - YAML signature matching (
signatures.py+signatures/) -- human- readable capability signatures (imports, API references, substrings, regexes, section names/entropy, file type) with per-match evidence - Packer identification -- known packer section names (UPX, ASPack, VMProtect, Themida, ...), entry-point placement, and section entropy
- Static unpacking helpers (opt-in,
--unpack-out): overlay extraction, embedded-PE carving, and UPX decompression via the externalupxbinary followed by a before/after re-triage - Memory-dump mode (
--from-dump) -- carve every PE out of a dump, triage each one, and merge the results plus global IOCs into one report - Tool integrations (
integrations.py) -- optional capa capability scans and FLOSS string deobfuscation merged into the report, plus annotation-script export for IDA Pro and Ghidra - Static triage indicators -- observations such as high-entropy sections, RWX sections, overlay data, forged/zero PE timestamps, persistence strings, YARA and signature matches
Important: the indicators are prompts for analyst review, not a detection verdict. Packed installers, EDR products, debuggers, and other legitimate software routinely trigger several of them.
| Rule | Idea |
|---|---|
Process_Injection_API_Combination |
PE imports VirtualAllocEx + WriteProcessMemory + a remote-thread API |
High_Entropy_PE_Section |
A section with raw-data entropy > 7.2 (packing/encryption hint) |
Persistence_Run_Key_Strings |
Strings referencing the registry Run/RunOnce autostart keys |
AntiVM_Environment_Strings |
Multiple VM/sandbox artifact strings (VMware, VirtualBox, QEMU, ...) |
All rules are generic and defensive in intent: they surface behaviours worth a second look, in samples and in legitimate software alike.
Signatures describe capabilities in plain YAML -- no code -- and every match comes with the concrete evidence that triggered it:
- name: process_injection_capability
severity: high
category: capability
attack_id: T1055 # MITRE ATT&CK Enterprise technique
attack_name: Process Injection
conditions:
logic: all # or "any"; items may nest
items:
- api_refs: # import table OR string tokens
any: [VirtualAllocEx, WriteProcessMemory, CreateRemoteThread]
min_count: 2 # at least 2 of the listed namesSupported predicates: imports (import table only), api_refs (imports
or string tokens), strings (substrings), strings_regex, section_name,
min_section_entropy, and file_type. Name-like specs accept a string, a
list (any-of), or {any: [...], min_count: N} / {all: [...]} /
{contains: X}.
The bundled library (signatures/capabilities.yaml, 9 signatures) covers
process-injection capability, Run-key persistence, anti-VM artifacts,
anti-debug APIs, downloader behaviour, crypto-API usage, keylogging
capability, credential-store strings, and high-entropy (packed) sections.
Every bundled signature also carries an attack_id / attack_name
mapping to the closest MITRE ATT&CK Enterprise technique (e.g. process
injection -> T1055, Run keys -> T1547.001, anti-VM -> T1497.001,
debugger evasion -> T1622). Reports aggregate matches into an ATT&CK
summary table, and batch index reports total techniques across all
samples. A mapping is analyst guidance -- the closest technique for the
capability -- not proof the technique was exercised.
Where YAML signatures fit next to YARA and capa: YARA is byte-pattern matching, capa is deep semantic capability analysis over a disassembly. These signatures sit in between -- cheap, readable, string/import-level capability hints that run anywhere Python runs.
Packer identification runs automatically for PE files (section-name
fingerprints such as UPX0/UPX1, .aspack, .vmp0, plus entry-point
and entropy analysis, reported with a confidence level and reasons).
Extraction actions are opt-in via --unpack-out DIR:
- Overlay extraction -- overlay bytes are written to
overlay.binwith size, entropy, and SHA256. - Embedded-PE carving -- every valid PE embedded after offset 0 is
validated with pefile and written to
DIR/carved/with its offset, size, and SHA256. - UPX unpacking -- if the fingerprint says UPX and the
upxbinary is on PATH, a copy is decompressed into DIR and immediately re-triaged; the report compares size/entropy/packer before and after and links the fullunpacked_report.md. Withoutupx, this step is skipped with a note. (Other packers are identified, not automatically unpacked -- use the dump mode below or a dedicated unpacker for those.)
For memory dumps and process dumps there is --from-dump:
python triage.py mem.dmp --from-dump --unpack-out carved/ --out dump_report.mdEvery PE is carved out of the dump, each is triaged individually (hashes, PE structure, YARA + YAML signatures, indicators), and the report merges a per-PE summary table with IOCs extracted from the whole dump's strings.
Export annotation scripts that carry the triage results into your disassembler:
python triage.py sample.exe --export-ida annotate_ida.py
python triage.py sample.exe --export-ghidra annotate_ghidra.pyOpen the sample in IDA Pro, finish auto-analysis, then run the
generated script via File -> Script file.... In Ghidra, run the
generated script from the Script Manager or via
analyzeHeadless ... -postScript annotate_ghidra.py. Both scripts only
add comments (IOC strings, suspicious API references, and a signature
summary at the entry point / image base); they never patch the sample.
Optional external tools, merged into the report when present on PATH:
python triage.py sample.exe --run-capa # mandiant/capa capabilities
python triage.py sample.exe --run-floss # FLOSS deobfuscated stringscapa and FLOSS are not dependencies: if the binary is missing, the report notes the step as skipped. FLOSS emulates string-decoding stubs internally, so treat it with the same care as the sample itself and run it only inside the isolated VM.
Requires Python 3.9+.
git clone <this-repo>
cd malware-triage-toolkit
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtpefile, yara-python, and PyYAML are optional at runtime: if any is
missing, triage.py still runs and notes in the report which step was
skipped. The upx binary is only needed for the UPX unpacking action.
# Print a Markdown report to stdout (bundled YARA rules and YAML
# signatures are picked up automatically)
python triage.py /path/to/sample.exe
# Write the report to a file and also emit JSON
python triage.py /path/to/sample.exe --out report.md --json report.json
# Use your own YARA rules / YAML signatures
python triage.py /path/to/sample.exe --rules /path/to/rules \
--signatures /path/to/signatures
# Extraction actions: overlay, embedded-PE carving, UPX unpack + re-triage
python triage.py /path/to/sample.exe --unpack-out unpacked/
# Memory-dump mode: carve + triage every embedded PE, one combined report
python triage.py /path/to/mem.dmp --from-dump --unpack-out carved/ \
--out dump_report.md
# Hand results to IDA / Ghidra, and merge capa + FLOSS output
python triage.py /path/to/sample.exe --export-ida ida_notes.py \
--export-ghidra ghidra_notes.py --run-capa --run-floss
# Self-contained HTML report instead of Markdown
python triage.py /path/to/sample.exe --format html --out report.html
# Batch mode: triage a whole directory (recursive, SHA256 de-duplicated,
# per-sample reports plus an index report in the output directory)
python triage.py /path/to/samples/ --out batch_reports/ --json
# Batch mode with a tighter size guard (files over the limit are skipped
# and listed in the index report)
python triage.py /path/to/samples/ --max-file-size 50000000See examples/sample_report.md for a sample report (generated from the
harmless EICAR antivirus test file).
- Analyse samples only inside an isolated VM with no network access (or a tightly controlled fake network), no shared folders to the host, and no credentials you care about.
- This tool only reads files, but treat every sample as hostile: other tools in your workflow may not be as careful.
- Never run an unknown sample on your host or on a production machine "just to see what happens". That is what the isolated VM is for.
tests/make_samples.py builds three harmless test inputs (an EICAR test
file, a minimal hand-crafted PE containing IOC strings, and a plain text
file) and there is no real malware anywhere in this repository.
The full test suite (regression, YAML signature engine, packer
detection, carving, dump mode, script export, batch mode with
de-duplication and IOC aggregation, deep-PE fixtures -- hand-built
.NET/certificate/TLS/delay-import/resource/Rich-header PEs -- XOR
hidden-string recovery, ATT&CK mapping validation, HTML report checks,
and -- when upx and a mingw cross-compiler are available -- a real UPX
pack/unpack round-trip of a compiled "hello world" PE) runs with:
python tests/run_tests.pyMIT -- see LICENSE.