Files
Necropolis-C2/docs/server-operator-design.md
T
2026-07-07 04:50:23 +01:00

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:

  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:

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