Skip to content

RemiKG/stomped

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

34 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Stomped

A junior reads the timestamp and closes the case. Stomped reads the journal underneath it and reopens the one the attacker backdated.

Stomped is an autonomous DFIR agent that hunts timestomping and rename/move laundering on the SANS SIFT Workstation. It runs in a Linux terminal, extends Protocol SIFT, and is driven by Claude Code. It runs the textbook $STANDARD_INFORMATION‑vs‑$FILE_NAME stamp check, distrusts its own clean result, and reads the $UsnJrnl:$J change journal underneath the file — ordering events by the monotonic USN (each record's ever‑increasing offset in $J), a clock the attacker cannot forge. It catches its own false negative live, tags every finding CONFIRMED / INFERRED / CONTRADICTED with a traceable tool execution behind it, and physically cannot touch the original evidence.

  • Architecture pattern: Pattern B — Custom MCP Server. Seven typed, read‑only forensic functions. No execute_shell, no write sink, no O_RDWR.
  • Agentic engine: Claude Code, extending Protocol SIFT (OpenClaw‑compatible seam).
  • No web app, no browser, no dashboard. The "UI" is the terminal: the live execution view, the cream‑paper report, the JSONL audit log, and the diagram below.

Stomped architecture — Pattern B, trust boundaries marked


The one mechanic — the self‑correcting, evidence‑integrity triage loop

  enumerate_mft → triage a "clean-but-active" candidate
  mft_si_fn(inode)      →  $SI == $FN to the nanosecond  →  naive check NEGATIVE  ("clean")
        ↓  "a clean check isn't a clean file"  (the agent distrusts its own result)
  usn_events(inode)     →  $UsnJrnl logs FileCreate + BasicInfoChange + Rename at a
                           USN ordered months AFTER the claimed creation  →  ⊥ CONTRADICTION
        ↓  SELF-CORRECTION (supersedes the naive step)
  reconstruct_timeline([inode], order_by=USN)  →  true creation window, USN-anchored
  logfile_attr_changes + vol_mft_memory         →  corroborate (deterministic confidence)
        →  CONFIRMED / INFERRED / CONTRADICTED, each with a ◦ receipt

It earns trust by being wrong on camera first, then catching itself — on data it does not control. A judge can re‑point it at any inode (--verdict-inode N) and a fixed script gets a different answer than it hard‑coded.

Verified, real, on the ROCBA Standard Forensic Case

The intruder downloaded Sysinternals SDelete (a secure‑wipe anti‑forensics tool), timestomped it to read as 2018‑11‑15, renamed it into the Recycle Bin, and deleted it. Stomped's run (inode 472521, $R9531FE.exeC:\Users\fredr\Downloads\SDelete\sdelete.exe):

mft_si_fn(472521)  →  $SI == $FN == 2018-11-15 12:33:42.000000000  →  naive check NEGATIVE
usn_events(472521) →  USN 4042552952  2020-11-14 03:46:51  FileCreate     ← true birth
                      USN 4060428920  2020-11-14 13:41:19  BasicInfoChange ← the timestomp
                      USN 4060433360  2020-11-14 13:41:24  Rename → $R9531FE.exe (deleted)
verdict            →  timestomp + rename-laundering; true creation 2020-11-14, backdated ~2 years

The naive check cleared the intruder's own anti‑forensics tool. The journal — which no user‑mode tool controls — reopened it.


Architecture — how the code fits together

repo/
├── mcp_server/                 ← Pattern B: the typed read-only MCP server
│   ├── server.py               stdlib JSON-RPC (stdio) MCP transport; per-call receipts
│   ├── tools.py                the 7 typed functions + reconstruct_timeline (verdict-maker)
│   ├── safety.py               the ARCHITECTURAL read-only boundary (allowlist, no shell)
│   ├── confidence.py           deterministic tri-state (1 layer→0.55, 2→0.80, +memory→0.94)
│   ├── config.py               env-driven config — no hardcoded paths/secrets
│   └── ntfs/                   bounded-struct parsers over court-vetted tools
│       ├── sleuthkit.py        istat / fls / icat / fsstat (offset-pinned, read-only)
│       ├── mft.py              $SI vs $FN MACB + clean-but-active enumeration (MFTECmd)
│       ├── usn.py              $UsnJrnl:$J index, USN-ordered, per-inode filter
│       ├── logfile.py          $LogFile second-layer corroboration (RCRD scan)
│       ├── mftscan.py          Volatility 3 disk-vs-RAM MFT cross-check
│       └── carve.py            residual USN_RECORD_V2 carving (rotated-$J fallback)
├── stomped/                    ← the orchestrator (drives Claude Code, renders deliverables)
│   ├── cli.py                  the `stomped` command (seam, mount, run, render)
│   ├── mount.py                ewfmount read-only + pre/post SHA-256 (the integrity proof)
│   ├── engine.py               headless Claude Code driver; agent gets ONLY the 7 tools
│   ├── logio.py                merges the model token stream + MCP receipts → execution log
│   ├── report.py               cream-paper report.md/.html + accuracy.md + dataset.md
│   ├── theme.py                the dark-editorial live terminal view
│   └── prompts/system.md       the agent extension (distrust the clean check; tri-state; receipts)
├── tests/spoliation/           ← the spoliation fuzz harness (Criterion-3 bypass test)
├── sample-evidence/            ← a bundled, clean-room, REAL NTFS triptych (real $UsnJrnl)
├── docs/                       ← the architecture diagram, brand, and example runs
├── LICENSE                     ← MIT
└── pyproject.toml              ← zero runtime Python deps (stdlib-only MCP transport)

The two ARCHITECTURAL guardrails (Ember on the diagram) vs the PROMPT one (dashed)

Boundary Kind Where What stops spoliation
Typed read‑only surface ARCHITECTURAL mcp_server/safety.py + tools.py The agent's entire universe of action is 7 read‑only functions. No execute_shell, no write, no mount, no O_RDWR. Bash/Write/Edit are disallowed in the engine. The dangerous verb is not in the surface.
Read‑only evidence ARCHITECTURAL ewfmount ro + immutable E01 + vboxsf ro mount The E01 is a sealed container opened read‑only; the host mount is ro. Any write → EROFS. Verified: touch /mnt/evidence/...Read-only file system.
Tri‑state tagging discipline PROMPT stomped/prompts/system.md Shapes judgment ("distrust the clean check"), not capability. We do not over‑claim it.

Both architectural layers must fail before a byte changes. We tested the boundary (tests/spoliation/): every mutator argument + a prompt‑injection ordering the agent to "remount read‑write and repair the MFT" degrades to read‑only or errors; 0/N reach evidence; pre‑run SHA‑256 == post‑run SHA‑256.


Install (on the SANS SIFT Workstation)

Stomped runs where the read‑only forensic tools are. It needs only the standard SIFT toolset and Claude Code — no Python dependencies (the MCP transport is hand‑written stdlib).

# On the SIFT Workstation (Ubuntu). Requires: The Sleuth Kit, plaso, Volatility 3 (`vol`),
# bulk_extractor, MFTECmd (dotnet), and Claude Code (`claude`) on PATH.
git clone <this-repo> stomped && cd stomped
export PATH="$PWD/bin:$PATH"            # or: pip install -e .   (gives the `stomped` command)
export ANTHROPIC_API_KEY=$(cat ~/.anthropic_key)   # never hardcoded, never committed

Tools are auto‑discovered; override any with STOMPED_ISTAT, STOMPED_VOL, STOMPED_MFTECMD, etc. Volatility 3 is invoked as vol.


Run it

stomped <evidence> [options]

  <evidence>              an E01 / raw image / volume / directory   [env: STOMPED_EVIDENCE]
  --memory <path>         paired memory image for the disk-MFT vs RAM-MFT cross-check
  --max-iterations <N>    HARD stop on the self-correcting loop. default 12. never unbounded.
  --min-layers <N>        journal layers that must agree before CONFIRMED. default 2.
  --mode ro               the only mode. `--mode rw` is a hard rejection, not a switch.
  --hash <algo|none>      pre/post evidence hash to prove zero writes. default sha256.
  --report <dir>          report + audit-log destination. default ./stomped-cases/<case>/
  --engine <name>         claude-code (default) | openclaw
  --verdict-inode <N>     verdict any single inode a judge names
  --triptych              the held-out clean / naive / laundered demo (bundled sample)

The one command a tired analyst types:

stomped /mnt/evidence/standard-forensic-case/rocba-cdrive.e01 \
        --memory /mnt/evidence/standard-forensic-case/Rocba-Memory.raw \
        --max-iterations 12 --report ./cases/rocba

Try it out (reproduce the demo)

Zero‑setup (the bundled sample — runs anywhere the SIFT tools exist):

stomped --triptych --report ./cases/triptych

This runs the identical, genuine analysis on sample-evidence/stomped-sample.ntfs — a small, clean‑room, real NTFS volume with a real $UsnJrnl:$J. The agent verdicts three files live and differently: clean_report.txt → CLEAN, naive_stub.exe ($SI≠$FN) → CONFIRMED naive timestomp, payload.exe ($SI==$FN, laundered via rename) → CONFIRMED. (Ground truth in sample-evidence/README.md.)

The real case (on the SIFT Workstation, read‑only):

# verdict the locked inode a judge can re-check:
stomped /mnt/evidence/standard-forensic-case/rocba-cdrive.e01 --verdict-inode 472521
# or run the full autonomous triage:
stomped /mnt/evidence/standard-forensic-case/rocba-cdrive.e01 --max-iterations 12 --report ./cases/rocba

Every run writes, under --report: report.md + report.html (the cream‑paper memo), execution-log.jsonl (timestamps + token usage; every finding traces to its tool execution; supersedes_step marks the self‑correction), accuracy.md (false positives, what we could not confirm, the spoliation result + pre/post hashes), dataset.md, and hashes.txt. See docs/example-run-*/ for captured real runs.


What is REAL (and what is honest about its limits)

  • Real evidence, real tools, real findings. ewfmount, SleuthKit, MFTECmd, plaso, Volatility 3, bulk_extractor — all execute against the actual image. The journal index is a bounded one‑time parse of the real $UsnJrnl:$J; nothing in the analysis is mocked.
  • The self‑correction is genuine. The deciding USN is unknowable until the real $J is parsed. The agent reasons, hits the contradiction, and recovers — never scripted.
  • Honest confidence. On rocba, the SDelete finding lands INFERRED (0.55), not CONFIRMED — because $LogFile's ring had wrapped past the record by the capture date, so only one journal layer survived. Stomped says so and recommends escalation. On the triptych, where both journals survive, it reaches CONFIRMED (0.80). Confidence is a fixed function of corroboration, never the model's mood.
  • False positives we refuse to assert. $SI<$FN fires on ~208k innocent files (registry hives, browser JSON, bulk update rewrites); Office installer files carry 2002 dates; installers produce whole‑second (uSecZeros) stamps. A high score is a reason to triage, never a verdict — the journal decides.
  • Out of scope: a kernel‑level stomp that also forges $UsnJrnl. Stomped catches the dominant user‑mode class (timestomp / SetMace / nTimetools + rename laundering).

License

MIT — see LICENSE. Built on, and extending, Protocol SIFT.

100% read‑only · zero writes to evidence · confirmed, inferred, or contradicted — never just asserted.

About

Stomped — autonomous DFIR agent that catches timestomping by reading the $UsnJrnl journal the attacker can't forge. Custom MCP Server, read-only by architecture.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors