Necropolis v1 release
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
# 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:
|
||||
|
||||
1. The operator **advertises itself** in the DHT under the rendezvous key
|
||||
`necropolis/<operator-peerid>`
|
||||
2. Implants find the operator via DHT lookup and open a persistent beacon stream
|
||||
3. The operator also accepts inbound beacon streams (`/bc/1.0.0`) from any peer
|
||||
4. 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
|
||||
5. Each `Z1` (beacon register) is signature-verified against the sender's public key
|
||||
6. Implants are tracked in an in-memory `map[string]*ImplantRecord` keyed 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:
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
1. The embedded tarball is extracted to a temp directory
|
||||
2. The operator's public key and a fresh implant keypair are written as Go source files
|
||||
3. Optional build tags are added (antivm, evasion, quiet mode)
|
||||
4. When `--evasion` is set, the embedded `valak.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.
|
||||
5. `go build` (or `garble build`) cross-compiles for the target
|
||||
6. 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user