Offline malware triage for infected Windows machines: reads raw NTFS and the registry hives itself, ranks candidate files by evidence, recovers the sample's bytes. Never mounts, executes or transmits anything.
- Runs on a live elevated system, from a WinRE console against a machine that will not boot, or against a disk image (
dd, VDI, VMDK with snapshot chains). - Parses
$MFT, the USN journal, Amcache, ShimCache, prefetch, PCA, UserAssist, scheduled tasks, run keys and Startup shortcuts, Defender's log and quarantine, the recycle bin, mark-of-the-web and PE structure (imphash, Rich header) in-process — no Windows API on the analysis path, so locked files and unmounted hives are read anyway. - Scores candidates as log-likelihood ratios over a per-machine prior; every evidence row is printed with its weight, and
malmathic explainshows the reasoning behind each. - Recovers the sample from the live file, Defender's quarantine, the recycle bin, a shadow copy, deleted
$MFTclusters, index slack, orphaned records, the unused tail of a reused$MFTrecord, or a record image still in$LogFile— and labels each recoveryVERIFIED,UNVERIFIEDorPARTIAL. - With
--deep, carves free space by digest, and for a file no artifact ever hashed, by the name the image carries inside itself (its debug directory or export table). - Verifies Authenticode and catalog signatures offline against pinned roots.
- Redacts reports for sharing (
--redact,malmathic redact). - Python bindings (
pymalmathic) for the parsers, PE analysis and image reading.
Download malmathic.exe for x86-64 or ARM64 from Releases: one static file, about 4 MB, no runtime.
From source: Rust 1.94+ with the MSVC toolchain, cargo build --release.
Reading an attached volume needs administrator rights (WinRE is already privileged). Reading an image needs none.
malmathic triage the Windows volume it finds; case in cases\ on the tool's drive
malmathic --out D:\case case directory of your choosing
malmathic --image disk.vmdk --out D:\case disk image; --out is required
malmathic --redact also write report.redacted.*
malmathic explain <feature> the weight behind a feature id
malmathic redact D:\case\report.json pseudonymise an existing report
Started by double-click (Run as administrator), it shows the case directory and waits for Enter or another path.
Exit codes: 0 — the analysis ran and the case was written (a negative result is a result); 1 — stopped early (not elevated, no Windows volume, refused placement) or a report could not be written; diag and redact use 2 for a usage error or a refused output path.
| Flag | |
|---|---|
-o, --out <DIR> |
Case directory. Default cases\<volume>-<time> on the drive holding the exe; required with --image. Refused when it already holds a result, lies inside the source tree or in the exe's own folder (for an exe in a drive root, that root itself), or — with --deep — on the volume being read. |
--overwrite-case |
Replace an existing case directory. |
--volume <V> |
Volume to analyze: a drive letter or part of a volume GUID. |
--image <FILE> |
Disk image instead of a device; detected by header, no privileges. Do not mount it first. |
--list-volumes |
List volumes and what was found on them (stderr), then exit. |
--list-snapshots |
With --image: print the VMDK snapshot chain, then exit. |
--no-samples |
Recover and hash, but write no malware into the case directory. |
--deep |
Also carve unallocated clusters for candidates with no bytes, matching a recorded hash or the name the image carries inside itself. Slow. |
--acquire-top <N> |
At most N top candidates to recover (default 10), only those over the reporting threshold. |
--verify-top <N> |
Verify code signatures this far down the list (default 200). |
--redact |
Also write report.redacted.txt and report.redacted.json. |
--json |
Print the JSON report to stdout instead of the text one. |
--quiet |
No per-stage progress on stderr. |
--pause, --no-pause |
Force or forbid waiting for Enter at the end (--no-pause also skips the case-directory question); by default only when the window would close. |
| Command | |
|---|---|
explain [FEATURE...] |
The weight table, or one row: meaning, the reasoning behind the weight, benign rate. |
redact <REPORT.JSON> [--out FILE] [--overwrite] [--keep-urls] |
Writes <name>.redacted.json and .txt: users become user1…, machines host1…, SID domains, volume ids and serials are renumbered, e-mail and IP addresses masked, URLs cut to their host, the case path dropped. Same name, same pseudonym throughout. |
diag mft [PATH] [--record N] [--children] |
One $MFT record with every ancestor; finds stale parent references. |
diag attribute-lists [--follow] |
Census of records carrying an $ATTRIBUTE_LIST. |
diag lzx-capture --out <FILE> [--overwrite] [--mount ROOT] [--all-algorithms] [--limit N] |
Capture Compact-OS streams beside their plaintext (WinRE). |
diag lzx-describe <FILE> |
Describe such a capture. |
| Path | |
|---|---|
report.txt, report.json |
Ranked candidates with evidence and weights, volume-level findings, coverage of what could not be read. |
report.redacted.txt, report.redacted.json |
Pseudonymised copies. Attach these to issues. |
sample/C<id>.bin |
Recovered bytes of a ranked candidate — live malware; your antivirus will quarantine it, which is expected. Absent with --no-samples. |
sample/unranked/ |
Recoveries the ranking cannot reach: deleted records that still hold a runlist, vanished scratch-directory executables, names from index slack, below-threshold carves. Capped at 64 files or 256 MB. |
- No process creation, library loading, mounting or network API in the shipped sources; the single
DeviceIoControlis a length query;Cargo.lockmatches an audited dependency list. - Image readers hold no writable handle; with
--deep, a case directory on the volume being read is refused. imphashand Rich-header hashes matchpefilerule for rule, frozen ordinal tables and packed-sample heuristics included; checked againstpefileby the Python tests.- Every parser survives truncated, self-referential, size-lying and all-zero input.
- Weights are expert estimates, not fitted to a labelled corpus; every report says so.
- The prior is one in the machine's candidate count: evidence that convicts on a small machine may not on a large one.
- Validly signed malware scores down; a stolen certificate defeats the strongest negative weight.
- A deleted file with no recorded hash can still be found by the name its own PE headers carry, but nothing then confirms the bytes are that file's rather than another copy of the same program.
- Attributes recovered from record slack or
$LogFileare bound to a path by a leftover name alone; a matching digest is what turns that into proof, and usually there is none. - Disk and registry only: no processes, memory or network, and no disinfection.
- On a live system a user-mode rootkit can filter what it reads; WinRE and images avoid that.
- Windows only.
cd bindings\python
pip install maturin pefile
maturin build --release
pip install target\wheels\pymalmathic-0.1.0-cp39-abi3-win_amd64.whl
import pymalmathic as mm
mm.parse_amcache(hive) # parse_shimcache, parse_prefetch, parse_tasks, parse_defender_log, parse_persistence, parse_recycle_bin, analyze_pe
mm.imphash(pe); mm.imports(pe); mm.rich_header(pe)
img = mm.Image("disk.vmdk"); img.list_dir("\\Users"); img.read_file(path, max_bytes=64 << 20)A separate cargo workspace, so PyO3 never enters the audited dependency tree. One abi3 wheel for CPython 3.9+, with type stubs. Parsers return observation dicts in the shape of report.json.
| Crate | |
|---|---|
mm-core |
Candidates, observations, paths, hashes, LZX and Xpress decoders |
mm-raw |
NTFS: $MFT, indexes and slack, USN journal, shadow copies, WOF |
mm-env |
Volumes, dd/VDI/VMDK images, snapshot chains, read-only file type, the Win32 layer |
mm-harvest |
Artifact parsers, PE, imphash, Rich header, mass-encryption detection |
mm-sign |
Authenticode and catalog verification, pinned roots |
mm-score |
Candidate graph, zones, features, weight table, incident window, baseline |
mm-report |
Report model, text rendering, redaction |
malmathic |
Pipeline, acquisition chain, CLI, diagnostics |
bindings/python |
pymalmathic |
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
The reasoning behind every weight is in crates/mm-score/rules/weights.toml. See CONTRIBUTING.md and SECURITY.md.
MIT — see LICENSE. Derived source and dependency licences, including the imphash ordinal tables taken from pefile (MIT, Ero Carrera), are listed in NOTICE.