CIS490/samples
max b809e1e26e auto_fetch_samples: pick Linux i386 ELF; manifest matches theZoo
User caught it: I shipped the theZoo path without running it
end-to-end. A real fetch on the Pi exposed two bugs:

1. Family-name matcher was substring-strict. "Cryptolocker-class"
   wouldn't match the dir "CryptoLocker_22Jan2014" because "-class"
   isn't in the dir name. Now expands to a sequence of tokens
   (full, head-of-dash, head-of-dot, head-of-underscore) and tries
   each. First match wins.

2. Extraction picker was "largest non-text" — a bad heuristic for
   theZoo, where each Linux.* zip often contains MULTIPLE binaries
   for different platforms (Linux i386, x86-64, ARM, FreeBSD, sometimes
   even Windows PE). The largest is rarely the i386 Linux ELF that
   would actually run on Metasploitable2. Now sniffs ELF magic bytes
   in stdlib and tiers:
     1. Linux i386 ELF (largest first)
     2. any other ELF (best-effort, may not execute)
     3. largest non-text (Wine fallback)

Verified end-to-end on the Pi against a real theZoo clone (~500 MB,
263 family dirs, 2026-05-01 fresh pull):

  linux-encoder-ransomware  → ELF 32-bit Intel i386 SYSV (278 KB)
  linux-wirenet-rat         → ELF 32-bit Intel i386 SYSV (64 KB)
  linux-rex-ransomware      → ELF 32-bit Intel i386 SYSV Go (7.6 MB)
  linux-neurevt-bot         → ELF 32-bit Intel i386 SYSV (3.0 MB)
  linux-earthkrahang-apt    → ELF 32-bit Intel i386 GNU/Linux (5.8 MB)

5/5 picks are runnable Linux i386 ELFs. Manifest rewrites in place
add source/sha256/url; meta.sample.kind goes to "real" automatically.

Manifest rewritten:
  - Old families (XMRig, Mirai, Cryptolocker-class, Dridex, Kovter,
    Reverse-Shell) → mostly absent from theZoo's Linux catalog or
    matched the wrong arch.
  - New families chosen against a verified theZoo presence list:
    Linux.Encoder, Linux.Wirenet, Ransomware.Rex, Neurevt,
    EarthKrahang.
  - XMRig + Kovter remain as mimic-only fallbacks (theZoo lacks a
    runnable Linux i386 binary for these; orchestrator falls back
    to the mimic profile).

Tests added (tests/test_auto_fetch_samples.py): 13 cases covering
ELF magic detection (i386 accepted, FreeBSD/x86-64/ARM/PE32/text
all rejected), family-token expansion (the "-class" suffix bug),
extraction picker (prefers Linux i386 over larger non-Linux ELFs),
manifest in-place rewrite preserves mode + skips entries that
already have sha256.

What's still NOT verified end-to-end (requires a lab host with
KVM x86):
  - Metasploitable2 boot under QEMU
  - vsftpd_234_backdoor exploit fire via msfrpcd
  - chunked binary upload through a real shell session
  - real binary executing inside a Metasploitable2 guest

The Pi is ARM64 — can't run Metasploitable2. install-tier-3-4.sh's
verify step (run_tier3_demo.py) covers all four on a real lab host;
deploy verifies on first run there.

171/171 tests pass.
2026-05-01 03:28:26 -05:00
..
__init__.py Collectors 2/4/5 + fleet runner + sample manifest + Tier-3 setup scripts 2026-04-30 00:02:27 -05:00
manifest.py Close out the open issues: bridge pcap wiring, perf collector, Tier-4 2026-04-30 00:17:49 -05:00
manifest.toml auto_fetch_samples: pick Linux i386 ELF; manifest matches theZoo 2026-05-01 03:28:26 -05:00
README.md Close out the deployment-readiness gaps 2026-04-30 00:31:55 -05:00

samples/

Catalog of malware (or behaviour-matched mimics) the fleet draws from. Sample binaries are NEVER committed to this repo.

What's here

manifest.toml      schema-checked catalog (loaded by samples/manifest.py)
manifest.py        loader + per-(host_id, slot, ep) deterministic selection
store/             SHA-256-pinned binary content (gitignored — never commit)
.bazaar.token      MalwareBazaar API key (mode 0600, gitignored)

Manifest schema

Each entry in manifest.toml:

[[sample]]
name        = "xmrig-cryptominer"   # unique within manifest, DNS-safe
family      = "XMRig"               # canonical family label for ML
category    = "cryptominer"         # one of: cryptominer, botnet, ransomware,
                                    # banking-trojan, fileless, rat, worm,
                                    # loader, wiper, other
profile     = "cpu-saturate"        # behaviour profile from
                                    # exploits/workloads.py — gates the
                                    # in-session shell workload when no
                                    # real binary is staged
description = "..."

# Optional — present iff this is a real binary the fetcher should pull:
sha256 = "abc123..."
source = "MalwareBazaar"
url    = "https://bazaar.abuse.ch/sample/abc123/"

The loader rejects unknown categories and duplicate names. See tests/test_fleet.py for the property tests covering selection distribution + catalog walkability.

"real" vs "mimic"

Sample.kind is "real" when sha256 is set, otherwise "mimic".

  • Mimic — the orchestrator runs the matching profile-shaped shell command (cpu-saturate / scan-and-dial / io-walk / bursty-c2 / low-and-slow / shell-resident) inside the guest. No real binary needed; useful right now for testing the dataset pipeline and as the realistic-but-safe envelope class the trainer expects.
  • Real — the orchestrator's Tier-3+ driver chunked-uploads samples/store/<sha256> into the shell session, sha256-verifies on the guest side, and execs it. Hash mismatch fail-stops the run; a tampered binary is never executed.

meta.sample.kind lands in every episode's meta.json, so trainers can stratify on it (the realistic-model path consumes only kind == "real" episodes by default).

Fetching a real binary

# 1. Register a (free) account at https://bazaar.abuse.ch and get the API key.
echo "<your-key>" > samples/.bazaar.token
chmod 0600 samples/.bazaar.token

# 2. Add an entry with sha256+source+url to manifest.toml.

# 3. Pull the binary into samples/store/<sha256>:
uv run python tools/fetch_sample.py <sha256>

Idempotent — re-running checks the staged copy's sha256 and skips the download if it already matches.

Per-(host, slot, episode) selection

manifest.py::SampleManifest.select(host_id, slot, episode_index) hashes those three into a uniform integer and indexes the catalog. Two lab hosts on the same slot pick different samples (collision rate ~1/N). A single host walks the whole catalog within ~len(manifest) episodes. No coordinator.

Safety rules

  • Only download to a lab host, never to a developer workstation. samples/store/ lives only there, gitignored, on a disk that is not auto-mounted elsewhere.
  • The lab host's br-malware bridge is host-only by design (no NAT, no route). Real malware running in the guest cannot call out unless the operator explicitly opens egress, which we don't.
  • Snapshot/revert (see EpisodeConfig.revert_at_* + qmp.savevm/ loadvm) means every fresh episode starts from a known-good baseline regardless of what the previous one did to the guest.
  • The fetcher verifies sha256 on download; the driver verifies again in-guest before exec. Both layers must match the manifest.

Adding a sample

  1. Pick a family + category from the closed enum above.
  2. Pick a profile from exploits/workloads.all_profiles(). If the sample's behaviour doesn't match any of the six existing shapes, add a new factory to exploits/workloads.py first, with tests.
  3. (Real-only) Compute sha256, fetch via tools/fetch_sample.py, verify the staged file's hash matches.
  4. Append the entry to manifest.toml.
  5. Run the test suite — the manifest loader's invariants catch typos.