10 KiB
Operator Design
Architecture
The "server" is not a traditional server — it's an operator node that participates in the libp2p network as a peer. There is no central infrastructure, no VPS requirement, and no listening port that needs to be exposed to the internet.
┌──────────────────────────────────────────────────────────┐
│ Operator Node Process │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ server/core/operator.go │ │
│ │ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │ │
│ │ │ RunCLI() │ │ Implant │ │ handlers │ │ │
│ │ │ (liner) │ │ Registry │ │ (ps, ls, exec, │ │ │
│ │ │ │ │ │ │ cd, pwd, d/l, │ │ │
│ │ │ │ │ │ │ upload, shell, │ │ │
│ │ │ │ │ │ │ portfwd, socks,│ │ │
│ │ │ │ │ │ │ kill, deadman) │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────────┬───────┘ │ │
│ │ │ │ │ │ │
│ │ └──────────────┴─────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────┴──────┐ │ │
│ │ │ transport │ │ │
│ │ │ (libp2p) │ │ │
│ │ └─────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ server/core/generate.go │ │
│ │ Cross-compile implants with embedded keys │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
│
libp2p Network
Directory Structure
server/
└── core/
├── run.go # Entry point: load keys, create operator, start CLI
├── operator.go # Core operator logic (708 lines)
│ ├── ImplantRecord # In-memory implant metadata
│ ├── Operator # Main struct: keys, node, messenger, implant registry
│ ├── NewOperator() # Constructor — creates libp2p node, messenger
│ ├── Start() # Connects to network, starts DHT, listens for beacons
│ ├── handleBeaconStream # Persistent stream reader from implants
│ ├── handleMessage # Message dispatch by type
│ ├── sendCommandToImplant # Sends signed command via direct stream
│ ├── OpenShell() # Interactive PTY shell via direct libp2p stream
│ ├── Portfwd() # TCP port forwarding through implant
│ ├── Ls/Cd/Pwd/Ps/Execute/Download/Upload/Kill/DeadMan
│ └── disconnectCheckLoop # Detects silent implants via heartbeat timeout
├── cli.go # Readline-based interactive console (liner)
│ ├── RunCLI() # Main loop — parser/dispatch
│ ├── commandHelp # Help text for all commands
│ └── saveHistory/shortenStr
├── generate.go # Implant build system (470 lines)
│ ├── RunGenerate() # CLI flag parser
│ ├── BuildImplant() # Key generation, source extraction, cross-compilation
│ ├── prepareBuildDir() # Extracts embedded source, injects keys
│ ├── ensureGo() # Auto-installs Go if missing
│ ├── ensureGarble() # Auto-installs garble if missing
│ └── writeQuietStub() # Daemonise stub for --quiet mode
├── socks_proxy.go # SOCKS5 proxy through implant (397 lines)
│ ├── SocksStart/Stop/List
│ ├── handleSocksConn # Full SOCKS5 handshake (auth, connect, relay)
│ ├── LoadSocksCreds/SaveSocksCreds
│ └── pickImplantPeerID/pickRandomImplant
└── embedsrc/
└── embed.go # Embeds implant_src.tar.gz via Go 1.16 embed
Key Responsibilities
1. Implant Discovery & Registration
The operator does NOT actively discover implants. Instead:
- The operator advertises itself in the DHT under the rendezvous key
necropolis/<operator-peerid> - Implants find the operator via DHT lookup and open a persistent beacon stream
- The operator also accepts inbound beacon streams (
/bc/1.0.0) from any peer - When a direct stream can't be established, the operator publishes signed command
envelopes to the DHT dead-drop at
/necropolis/cmd/<id>/<nonce>— implants poll this every 30s - Each
Z1(beacon register) is signature-verified against the sender's public key - Implants are tracked in an in-memory
map[string]*ImplantRecordkeyed by PeerID
Beacon Stream Flow:
Implant opens /bc/1.0.0 stream ──→ Operator accepts in handleBeaconStream
└── Loop:
read length-prefixed protobuf
verify signature
dispatch by envelope.Type
update LastCheckin
2. Implant Tracking
Each ImplantRecord stores:
type ImplantRecord struct {
Name, Hostname, UUID, Username string
UID, GID, OS, Arch string
PID int32
PeerID, Version, ActiveC2 string
Locale string
LastCheckin time.Time
Interval, Jitter time.Duration
PublicKey crypto.PubKey
Disconnected bool
}
A background goroutine (disconnectCheckLoop) runs every 30 seconds. Any implant whose
LastCheckin exceeds Interval + Jitter + 30s is marked Disconnected = true.
3. Command Dispatch
Commands are sent over direct libp2p streams (/bc/1.0.0/cmd) — not over pubsub.
Operator CLI Operator core Implant
│ │ │
│ exec("whoami") │ │
│ ─────────────────→ │ │
│ │ Z14{Path:"whoami"} │
│ │ sign envelope │
│ │ open /bc/1.0.0/cmd │
│ │ ───────────────────→ │
│ │ │ exec.Command("whoami")
│ │ │ capture output
│ │ Z15{Stdout:"root\n"} │
│ │ ←─────────────────── │
│ "command sent" │ │
│ ← result printed via │ │
│ handleExecuteResult() │ │
4. Interactive Sessions
Shell, port forwarding, and SOCKS all use direct libp2p streams with bidirectional
io.Copy. These bypass the envelope/signing layer entirely — once the stream is
established, raw bytes flow in both directions.
| Feature | Protocol ID | How It Works |
|---|---|---|
| Shell | /x/sh/1.0.0 |
Operator sends winsize, implant starts PTY, bidirectional I/O |
| Portfwd | /x/pf/1.0.0 |
Operator sends target address, implant dials TCP, relay |
| SOCKS | /x/sk/1.0.0 |
Operator handles SOCKS5 handshake locally, sends target over stream |
Shell is terminated by typing exit or pressing Ctrl+] (0x1d byte), which triggers an
escape in the shellEscaper reader on the operator side.
5. Implant Generation
The operator binary is self-contained — it embeds the entire implant source tree as
implant_src.tar.gz via Go's //go:embed. At generation time:
- The embedded tarball is extracted to a temp directory
- The operator's public key and a fresh implant keypair are written as Go source files
- Optional build tags are added (antivm, evasion, quiet mode)
- When
--evasionis set, the embeddedvalak.dll(Zig kernel evasion DLL) is compiled into the implant binary via//go:embed. At runtime the DLL is loaded from memory and provides FreshyCalls syscall dispatch, AMSI/ETW bypass, module stomping, AMSI bypass via hardware breakpoint, and ETW patching. go build(orgarble build) cross-compiles for the target- UPX compression and ELF stripping are applied as post-processing
SOCKS5 Proxy
The operator can act as a SOCKS5 proxy, routing traffic through an implant:
SOCKS client (browser) ←→ Operator (SOCKS5 handler) ←libp2p stream→ Implant ←→ Target
Features:
- Username/password authentication (SHA-256 hashed, saved to
~/.necropolis/socks.json) - Random implant selection per request
- Multiple simultaneous proxy instances on different ports