Necropolis v1 release

This commit is contained in:
2026-07-07 04:37:29 +01:00
commit cf9c51a3df
69 changed files with 15056 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
# Necropolis C2 Architecture
## Overview
Necropolis is a decentralised C2 framework inspired by Sliver (BishopFox/sliver),
built on libp2p (the modular peer-to-peer networking stack from Protocol Labs).
Instead of hosting centralised C2 servers on VPS/cloud infra that can be taken down,
blocked, or fingerprinted, necropolis uses the global libp2p DHT and GossipSub for
operator discovery, implant communications, and command relay. No central server,
no static IP, no DNS.
## Why libp2p Instead of HTTP/DNS/mTLS/WireGuard (Sliver's Approach)
| Feature | Sliver | Necropolis |
|---|---|---|
| Transport | mTLS, HTTP(S), DNS, WireGuard | libp2p (TCP, WS, WSS) |
| Server identity | Static IP / domain | PeerID (cryptographic) |
| Discovery | Hardcoded C2 endpoints | DHT + PubSub topic discovery |
| Resilience | Multiple listeners | Any peer can relay |
| Takedown | Block IP/domain | Unbounded: must Sybil the DHT |
| NAT traversal | Manual / WireGuard | AutoNAT + relay + hole-punching |
| Encryption | Per-binary asymmetric keys | libp2p noise/TLS + protobuf envelopes |
| Implant comms | Polling / long-poll / DNS ticks | Direct streams + persistent beacon stream |
## File Reference
```
necropolis-c2/
├── README.md
├── cmd/necropolis/main.go # Single binary entry point. Runs operator CLI by default.
├── server/core/
│ ├── run.go # Binary startup: parses flags, creates operator, launches CLI
│ ├── operator.go # Operator node: implant registry, command dispatch, DHT dead-drop
│ ├── cli.go # Interactive console: command history, implant selection, all commands
│ ├── generate.go # Implant build pipeline: cross-compile, garble, UPX, credential embedding
│ ├── generate_toolchain.go # Auto-install: Go, MinGit, garble downloads if missing on operator machine
│ ├── socks_proxy.go # SOCKS5 proxy server on operator side. Credential management.
│ └── embedsrc/embed.go # Embeds the full implant source tree for self-contained builds
├── implant/
│ ├── main.go # Implant entry point. Parses -peer and -wss flags.
│ └── core/
│ ├── agent.go # Implant agent: startup, beacon loop, discovery, streams, DHT polling
│ ├── agent_commands.go # All command handlers: ps, ls, cd, pwd, exec, shell, download, upload, kill, deadman
│ ├── agent_relay.go # Mesh routing: relay discovery, relay health tracking, circuit relay selection
│ ├── shell.go # Shell dispatch (platform-specific ConPTY/PTY)
│ ├── shell_windows.go # Windows ConPTY shell implementation
│ ├── shell_unix.go # Unix PTY shell implementation
│ ├── ps.go # Cross-platform process listing
│ ├── portfwd.go # TCP port forwarding through implant
│ ├── socks.go # SOCKS5 client on implant side
│ ├── antivm.go # VM detection core (scoring engine)
│ ├── antivm_windows.go # Windows-specific VM checks
│ ├── antivm_linux.go # Linux-specific VM checks
│ ├── antivm_darwin.go # macOS-specific VM checks
│ ├── antivm_stub.go # No-op stub when --antivm not used
│ ├── evasion_windows.go # Reflective PE loader for Zig DLL, sleep obfuscation
│ ├── evasion_stub.go # No-op stub for non-evasion builds
│ ├── embed_evasion.go # Embeds valak.dll as []byte for reflective loading
│ ├── embedded_pubkey.go # Generated stub: operator Ed25519 public key (replaced at build time)
│ ├── embedded_implant_key.go# Generated stub: implant Ed25519 private key (replaced at build time)
│ ├── embedded_boxpubkey.go # Generated stub: operator NaCl box public key (replaced at build time)
│ └── embedded_authtoken.go # Generated stub: 32-byte auth token (replaced at build time)
├── evasion/ # Zig evasion DLL source
│ ├── build.zig # Zig build configuration
│ ├── main.zig # DLL exports: init, patch_etw, patch_amsi, stomp_evasion
│ ├── syscall.zig # FreshyCalls + HAL's Gate syscall resolution, indirect dispatch (no stack spoofing)
│ ├── resolve.zig # PEB-based module/function resolution via ror13 hashes. No static imports.
│ ├── stomp.zig # Module stomping: copies DLL .text into signed Microsoft DLL, wipes headers
│ ├── etw.zig # ETW suppression: NtTraceControl → hardware breakpoint → RET patch fallback
│ ├── amsi.zig # AMSI bypass: hardware breakpoint on AmsiScanBuffer via VEH
│ ├── api.zig # Global function pointer storage for dynamically resolved Windows APIs
│ ├── pe.zig # PE header structure definitions
│ ├── win32.zig # Windows type definitions and constants
│ └── arch/hells_gate.s # x64 assembly stubs: syscall dispatch
├── pkg/
│ ├── transport/
│ │ ├── node.go # libp2p host wrapper: DHT, PubSub, relay, mDNS. Protocol ID constants.
│ │ ├── messenger.go # Envelope creation/signing/verification, topic management, replay protection
│ │ └── types.go # Message type constants (MsgTypeRegister, MsgTypePs, etc.)
│ └── cryptography/
│ ├── keys.go # Ed25519 key pairs, NaCl box encryption, auth token generation
│ └── keys_test.go # Crypto unit tests: encrypt/decrypt roundtrip, sign/verify, key persistence
├── protobuf/
│ ├── apb/necropolis.proto # Wire message definitions (Envelope, Z1-Z25)
│ ├── apb/necropolis.pb.go # Generated protobuf Go code
│ ├── cpb/common.proto # Common types (Response, Request, Process)
│ └── cpb/common.pb.go # Generated common types
├── docs/ # Architecture and design documentation
├── build.sh # Cross-platform release build script
├── go.mod / go.sum # Go module definition
└── .gitignore
```
## High-Level Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ libp2p Network (DHT + Relay) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Operator │ persistent │ Implant │ │ Implant │ │
│ │ (Client) │←──beacon─────│ (Agent) │ │ (Agent) │ │
│ │ │──command────→│ │ │ │ │
│ │ │ direct str │ │ │ │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────────────────┴──────────────┘ │
│ DHT discovery + relay circuits │
└─────────────────────────────────────────────────────────────┘
```
## Key Components
### 1. Operator Node (Client)
- Connects to libp2p network with a **PeerID** derived from an operator key
- Handles persistent beacon streams from implants (`/bc/1.0.0`)
- Sends commands to implants over direct libp2p streams (`/bc/1.0.0/cmd`)
- Opens direct libp2p streams for interactive sessions (shell, socks, portfwd)
### 2. Implant Node (Agent)
- Compiled with an **operator's public key** (embedded at build time)
- Connects to libp2p bootstrap peers or uses embedded peer list
- Opens persistent beacon stream to operator (`/bc/1.0.0`) with 5s keepalive
- Receives commands on direct streams (`/bc/1.0.0/cmd`) and via pubsub fallback
- Supports session mode (direct stream for shell, portfwd) over relay circuits
### 3. Relay Nodes
- Any libp2p peer can act as a relay (no cost, no registration)
- AutoNAT + relay protocol for NAT traversal
- No special server software — standard libp2p relays
## Communication Model
| Message Type | Transport | Pattern |
|---|---|---|
| Beacon / Heartbeat | Persistent direct stream (`/bc/1.0.0`) | Implant → Operator |
| Command dispatch | Direct stream (`/bc/1.0.0/cmd`) | Operator → Implant |
| Task result | Beacon stream or pubsub fallback | Implant → Operator |
| DHT dead-drop | DHT value store (`/necropolis/cmd/<id>/<nonce>`, 30s poll) | Operator → Implant |
| Interactive shell | Direct libp2p stream (`/x/sh/1.0.0`) | Bidirectional (Ctrl+] to exit) |
| File download | Direct stream | Implant → Operator |
| SOCKS / Portfwd | Direct stream (`/x/pf/1.0.0`) | Proxied through libp2p |
## Security Model
Each implant build embeds a unique Ed25519 keypair so the implant keeps the same PeerID
across restarts. All libp2p transports are encrypted (Noise XX or TLS 1.3). Envelopes are
signed with the sender's private key. Only the operator with the correct private key can
publish to `necropolis/<op-id>/commands`. Ephemeral session keys are used for direct streams.
## Evasion Architecture
When built with `--evasion`, the implant carries an embedded Zig DLL (`valak.dll`) compiled
from the `evasion/` directory. The DLL contains kernel evasion primitives: FreshyCalls SSN
extraction, indirect syscall dispatch, sleep obfuscation (Go-side PAGE_NOACCESS), AMSI
bypass via hardware breakpoint, ETW three-tier patching, and EDR callback removal.
At runtime, the implant loads the DLL reflectively from memory (no %TEMP% extraction, no
LoadLibrary call). A reflective PE loader in the evasion module maps the DLL's sections
into memory, resolves imports, applies relocations, and calls the entry point directly.
After loading, the DLL stomps its `.text` section into a legitimate signed Microsoft DLL's
memory range (e.g. `CryptoAPI.dll`, `dwrite.dll`) so the code executes from inside a
Microsoft-signed address range. The original allocation's PE headers (MZ, PE\0\0) are zeroed
so memory scanners find no orphaned PE signature.
| Evasion Layer | Implementation |
|---|---|---|
| Module stomping | `.text` copied into a signed Microsoft DLL range; original headers wiped |
| Sleep obfuscation (PAGE_NOACCESS) | DLL .text set PAGE_NOACCESS during idle via VirtualProtect; Go binary .text unaffected |
| Indirect syscalls | FreshyCalls SSN + random ntdll gadget (no call-stack spoofing) |
| ETW patch | Three-tier: NtTraceControl → HWBP (Dr0 + VEH) → RET patch fallback |
| AMSI bypass | VEH + DR0 hardware breakpoint on AmsiScanBuffer; SuspendThread/ResumeThread |
| EDR callback removal | SeDebugPrivilege escalation + NtSetInformationProcess(40) |
| Build tag | `-tags=evasion` embeds and activates Zig DLL |
+100
View File
@@ -0,0 +1,100 @@
# Evasion Chain
## Architecture
Evasion lives in two layers:
- **Go binary** — reflective PE loader, sleep obfuscation, cgocall bridge
- **Zig DLL** — syscall dispatch, API resolution, AMSI/ETW/EDR evasion, module stomping
## Loading Phase
### Reflective DLL Loading
How the Go loader reads PE headers, allocates memory via VirtualAlloc, copies sections, applies relocations, resolves imports, sets section protections, resolves exports, calls DllMain and init_evasion. No file on disk, no LoadLibrary callback.
## Initialization Phase
### init_evasion → syscall.init_syscall
- Walks PEB to find ntdll base
- Scans ntdll for `0F 05 C3` (syscall;ret) gadgets, stores addresses in pool
- Seeds PRNG
- Caches exception directory for HAL's Gate fallback
- Builds FreshyCalls table: walks ntdll export directory, collects Nt* exports,
sorts by RVA, SSN = position. Immune to inline hooks.
Reference: https://0xdbgman.github.io/posts/edr-internals-research-and-bypass/
- Scans ntdll for `41 FF E2` (jmp r10) gadget — used as CFG-safe IC neutralizer
### api.ensure()
- Resolves ~100 Windows API function pointers by walking PEB → module export tables
- kernel32, ntdll, advapi32, winhttp, bcrypt
- Used by evasion functions for non-syscall operations (VirtualProtect, LoadLibrary, etc.)
## Evasion Phase
### stomp_evasion
- Copies DLL .text into a signed Microsoft DLL (CryptoAPI/dwrite/msvcp_win)
- Changes target protection to RWX, copies, restores to RX
- Zeros original PE headers (DOS header + NT headers)
- Memory scanners see legitimate signed DLL code at execution addresses
- Base passed from Go (reflective DLL not in PEB module list)
### patch_etw (three-tier)
- Tier 1: NtTraceControl to stop EDR ETW providers (MDE, kernel-process, security-mitigations)
- Tier 2: Hardware breakpoint on EtwEventWrite via VEH + debug registers
- Tier 3: RET patch fallback — writes 0xC3 to EtwEventWrite's first byte
- On Windows 10/11 where NtTraceControl is forwarded to api-ms-win DLLs, extract_ssn skips Method 1 (forward RVA guard) and falls through to HAL's Gate Method 2
### patch_amsi
- Loads amsi.dll, resolves AmsiScanBuffer address
- Installs Vectored Exception Handler (VEH) as first-in-chain
- Sets DR0 hardware breakpoint to AmsiScanBuffer address
- Sets Dr7 to enable DR0 locally
- SuspendThread → SetThreadContext → ResumeThread (proper thread context modification per MS docs)
- On hit: VEH handler sets RAX=0 (AMSI_RESULT_CLEAN), sets RIP to return address, pops stack
- No bytes modified in amsi.dll — invisible to EDR tamper detection
### remove_edr_callbacks
- Escalates SeDebugPrivilege (best-effort, silently skips if fails)
- Calls NtSetInformationProcess with InfoClass=40 (ProcessInstrumentationCallback)
- CFG-aware: on CFG-enabled systems (Win10 1709+), kernel refuses null callback pointer.
Uses `jmp r10` gadget address instead — IC fires but immediately returns, neutralizing it.
Reference: https://cirosec.de/en/news/windows-instrumentation-callbacks-part-4/
- On non-CFG systems: sets Callback=NULL, removing the EDR's instrumentation callback
- Runs LAST in startup chain (the call itself is an IoC — but nothing follows it)
- Defanged on Windows 11 23H2+ (restricted to kernel-mode callers)
## Sleep Phase
### EvasionSleep (Go-side)
- Gated by dllLoaded flag (prevents protection before DLL fully initialized)
- Pre-saved .text bounds from section scan during loading
- VirtualProtect(DLL .text, PAGE_NOACCESS) — DLL code unreadable during idle
- time.Sleep(duration) — Go runtime, not DLL code
- VirtualProtect(restore original protection) — DLL executable again
- DLL .text at different address than Go binary .text — Go code executes fine during protected period
## Syscall Dispatch
### FreshyCalls: SSN Extraction
- Walks ntdll's export directory, collects all Nt* exports with real code addresses
- Sorts by RVA ascending — SSN = position in sorted order
- Immune to inline hooking: EDRs can patch stub bytes but cannot change linker RVA order
- HAL's Gate fallback: binary search exception directory for forwarded exports
not in ntdll's direct export table
- Reference: https://0xdbgman.github.io/posts/edr-internals-research-and-bypass/
### Indirect Syscall Execution
- Random gadget from g_syscall_addrs pool (0F 05 C3 in ntdll)
- hells_gate assembly: encrypts SSN, stores gadget address globally
- hell_descent assembly: decrypts SSN into eax, sets up r10 (first arg), jumps to gadget
- Gadget's `syscall` runs with eax=SSN — kernel processes it
- Gadget's `ret` returns to hell_descent caller, unwinding through normal call chain
- Never calls the hooked export stub — EDR sees a syscall from a random ntdll address
### No Call Stack Spoofing
- Go's cgocall requires clean stack returns
- NtContinue-based ROP chains corrupt Go's goroutine scheduler
- All dispatch is non-spoofed
## Key Fixes Applied
- PE struct offsets corrected (SizeOfImage at 56, not 36)
- imageImportDescriptor corrected to 20 bytes
- @intCast→ntstatus helper (usize to NTSTATUS via @bitCast)
- extract_ssn forwarded export guard (prevents @intCast panic)
- CONTEXT_DEBUG_REGISTERS = 0x100010 (was 0x10, missing AMD64 flag)
- SuspendThread/ResumeThread before SetThreadContext
- stomp_evasion(base) parameter from Go
- dllLoaded flag guards EvasionSleep
+181
View File
@@ -0,0 +1,181 @@
# Implant Design
## Overview
The necropolis implant is a lightweight Go binary that connects to the libp2p network and
communicates with its operator entirely through direct p2p streams, with pubsub as a
fallback. It has no hardcoded server IPs or DNS names — only a cryptographic reference
to the operator's public key.
## Directory Structure
```
implant/
├── main.go # Entry point
└── core/ # All implant runtime logic
├── agent.go # Core lifecycle, beacon loop, command dispatch, handlers
├── shell.go # Shell path selection (/bin/bash, cmd.exe, etc.)
├── shell_unix.go # PTY-based interactive shell (Linux/macOS)
├── shell_windows.go # ConPTY-based interactive shell (Windows)
├── portfwd.go # TCP port forwarding tunnel
├── socks.go # SOCKS proxy tunnel (identical to portfwd currently)
├── ps.go # Process listing (Linux /proc, Windows tasklist)
├── antivm.go # VM detection framework (cross-platform, build-tag gated)
├── antivm_linux.go # 50+ Linux-specific VM detection techniques
├── antivm_darwin.go # macOS-specific VM detection techniques
├── antivm_windows.go# Windows-specific VM detection techniques
├── antivm_stub.go # No-op stub when compiled without `-tags=antivm`
├── embed_evasion.go # Embeds valak.dll via //go:embed
├── evasion_windows.go # Evasion loader + sleep obfuscation
├── valak.dll # Zig evasion DLL (FreshyCalls, indirect syscalls, AMSI/ETW bypass, module stomping)
├── evasion_stub.go # No-op stub when compiled without `-tags=evasion`
├── agent_relay.go # Implant-to-implant relay advertisement and routing
├── embedded_pubkey.go # Generated: operator's public key embedded at build time
├── embedded_implant_key.go # Generated: implant's unique keypair embedded at build time
├── embedded_authtoken.go # Generated: operator-specific auth token embedded at build time
└── embedded_boxpubkey.go # Generated: NaCl box public key embedded at build time
```
## Build Process
1. Operator runs `necropolis generate --os linux --arch amd64` (from CLI or standalone)
2. `server/core/generate.go` unpacks the embedded source tree from `server/core/embedsrc/implant_src.tar.gz`
3. Injects the operator's public key and a fresh implant Ed25519 keypair into the source tree
4. Optionally applies build tags (`-tags=antivm`, `-tags=evasion`) and quiet-mode stubs
5. Cross-compiles with `CGO_ENABLED=0` for the target OS/arch
6. When `--evasion` is set, embeds `valak.dll` (1.5 MB Zig kernel evasion DLL) into the
binary via `//go:embed`. The DLL is loaded at runtime from memory and provides FreshyCalls
syscall dispatch, AMSI/ETW bypass, module stomping, EDR callback removal.
7. Applies garble obfuscation (`--obfuscate`), UPX compression (`--upx`), and strip (`--quiet`)
8. The resulting binary is self-contained — no source tree, no external dependencies
Each build generates a unique implant keypair. The implant keeps the same PeerID across restarts.
## Core Lifecycle
### Startup
1. Load operator's public key from embedded `embedded_pubkey.go`
2. Load implant's private key from embedded `embedded_implant_key.go`
3. Derive libp2p PeerID from implant public key
4. Initialise libp2p host with:
- TCP + WebSocket transports (no UDP/QUIC for sandbox compatibility)
- AutoNAT + relay client for NAT traversal
- DHT client for peer discovery
- GossipSub pubsub
5. If `--peer` flag is provided, connect to operator directly
6. If `--wss` flag is provided, connect via WebSocket Secure (TLS over TCP/443)
7. Start DHT discovery loop to find operator via rendezvous namespace
8. Start DHT dead-drop poll loop (`pollDHTCmdLoop`) — polls `/necropolis/cmd/<id>/<nonce>` every 30s for signed command envelopes
9. Once connected, open persistent beacon stream (`/bc/1.0.0`) to operator
10. Register via signed `Z1` (beacon register with system metadata)
11. Start beacon loop and cover traffic loop
### Main Loop
```
beaconLoop (every 10-15s):
Send Z1 (beacon register) on persistent /bc/1.0.0 stream
Sleep(interval + random(jitter))
streamKeepaliveLoop (every 5s):
Write MsgTypeCover on persistent stream to prevent relay idle timeout
coverTrafficLoop (every 4-7s):
Publish random noise to the beacon pubsub topic
discoverOperatorLoop (every 15s):
Find operator via DHT rendezvous
If beacon stream is nil, reconnect
pollDHTCmdLoop (every 30s):
Poll DHT dead-drop at /necropolis/cmd/<id>/<nonce> for signed command envelopes
Process through existing command handler
commandStream handler:
Read length-prefixed envelope from /bc/1.0.0/cmd stream
Verify Ed25519 signature against embedded operator pubkey
Dispatch by message type
Send result on persistent beacon stream
```
### Session Mode (Shell, Portfwd, SOCKS)
```
on incoming stream:
Read target/winsize from stream header
Establish local connection / PTY
Bidirectional io.Copy between libp2p stream and local resource
On disconnect or Ctrl+]: cleanup and return
```
## Command Handlers
All command handlers follow the same pattern:
1. Deserialise protobuf request from envelope
2. Execute operation (local filesystem, process execution, etc.)
3. Serialise protobuf result
4. Send result via `sendResult()` — tries persistent stream first, falls back to pubsub
| Handler | Protobuf | Operation |
|---|---|---|
| `handlePs` | Z12 → Z13 | List processes via `/proc` (Linux) or `tasklist` (Windows) |
| `handleLs` | Z16 → Z17 | Read directory entries |
| `handleCd` | Z19 → Z21 | `os.Chdir()` |
| `handlePwd` | Z20 → Z21 | `os.Getwd()` |
| `handleExecute` | Z14 → Z15 | `exec.CommandContext()` with optional output capture |
| `handleDownload` | Z22 → Z23 | `os.ReadFile()` with 100MB limit |
| `handleUpload` | Z24 → Z25 | `os.WriteFile()` with optional overwrite and 100MB limit |
| `handleKill` | (none) → (none) | Log stack trace, `os.Exit(0)` |
| `handleScreenshot` | Z2 → Z3 | Returns "not implemented" stub |
## Platform Support
| Feature | Windows | Linux | macOS |
|---|---|---|---|
| TCP transport | ✓ | ✓ | ✓ |
| WebSocket transport | ✓ | ✓ | ✓ |
| Process list | ✓ (tasklist) | ✓ (/proc) | ✓ (stub) |
| File ops | ✓ | ✓ | ✓ |
| Interactive shell | ✓ (ConPTY) | ✓ (PTY) | ✓ (PTY) |
| Port forwarding | ✓ | ✓ | ✓ |
| VM detection | 50+ checks | 50+ checks | 8 checks |
| Kernel evasion | ✓ (DLL) | — | — |
## Evasion System
When compiled with `--evasion`, the implant includes a Zig DLL (`valak.dll`) built from
`evasion/` in the source tree. The DLL is embedded via `//go:embed valak.dll` at compile
time and loaded from memory via reflective PE loader at runtime. After loading, the DLL
stomps its `.text` section into a legitimate signed Microsoft DLL (e.g. `CryptoAPI.dll`,
`dwrite.dll`), frees the original allocation, and wipes its PE headers. Once stomped, the
code executes from inside a Microsoft-signed address range with no orphaned PE signature.
It provides kernel-level evasion primitives ported from the Tenshu C2 framework:
| Technique | Implementation | Persistence |
|---|---|---|---|
| Module stomping | `.text` copied into signed MS DLL range; original headers wiped | On load |
| Image header wiping | MZ/PE\0\0 zeroed at original allocation after stomp | On load |
| Sleep obfuscation (PAGE_NOACCESS) | DLL .text set PAGE_NOACCESS via VirtualProtect during idle | Per sleep cycle |
| Indirect syscalls | FreshyCalls + HAL's Gate SSN extraction, random ntdll gadget | Available via DLL call |
| ETW patch | Three-tier: NtTraceControl → HWBP → RET patch | On init |
| AMSI bypass | DR0 hardware breakpoint + VEH with SuspendThread/ResumeThread | Installed on init |
| EDR callback removal | SeDebugPrivilege + NtSetInformationProcess(40) | On init |
If the DLL fails to load, evasion is skipped. No fallback to pure Go implementations — the DLL is the only evasion path.
## Transport Configuration
The implant explicitly disables libp2p's default transports and enables only:
1. **WebSocket** — primary transport, works through most proxies
2. **WebSocket Secure** — WSS is a dial address (`/dns4/host/tcp/443/wss/p2p/peerid`), not a separate transport layer; it uses the same WebSocket transport over TLS
3. **TCP** — fallback for direct connections
No UDP, no QUIC, no multicast. This maximises sandbox/container compatibility.
The implant always connects with `network.WithAllowLimitedConn` to work through relay
circuits when direct connections are unavailable.
When started with `-wss`, the implant connects via `/dns4/<host>/tcp/443/wss/p2p/<peer-id>`
before falling back to DHT discovery and dead-drop polling.
+18
View File
@@ -0,0 +1,18 @@
# IPFS & Decentralised Infrastructure Integration
## libp2p (Used)
| Feature | Use in necropolis |
|---|---|
| **Peer Identity** | Cryptographic identity (Ed25519) for operator, implants, relays |
| **GossipSub** | PubSub topics for command/beacon messaging |
| **DHT** | Peer discovery, relay discovery, content routing |
| **AutoNAT** | NAT status detection |
| **Circuit Relay** | Relay connections when direct dial fails |
| **Hole Punching** | Direct p2p connections through NAT |
| **Stream Multiplexing** | Multiple concurrent channels per connection |
| **Noise/TLS Security** | Encrypted, authenticated transports |
## IPFS (Not Yet Implemented)
IPFS-based exfiltration was planned via the now-removed Z11 proto message but was never implemented. Screenshot capture remains a stub.
+74
View File
@@ -0,0 +1,74 @@
# Mesh Routing
## How It Works
Every Necropolis node (operator and implants) runs a circuit relay v2 service via
`libp2p.EnableRelayService()`. This means any node can relay encrypted traffic between
any two peers on the network. There is no distinction between "relay nodes" and
"client nodes". Every node is both.
Routing works in layers:
1. **Direct stream**. The implant opens a persistent `/bc/1.0.0` stream to the operator
via a direct libp2p connection. This is the primary path.
2. **PubSub fallback**. If the direct stream is unavailable, beacons and results are
published to GossipSub topics. Every node relays these topics (`Topic.Relay()`),
so even offline implants receive messages when they reconnect (GossipSub keeps the
last 10 heartbeats of history).
3. **DHT dead-drop**. When both direct streams and PubSub are unavailable, implants poll
the DHT dead-drop at `/necropolis/cmd/<id>/<nonce>` every 30s. The operator publishes
signed command envelopes to the DHT, and implants fetch them in nonce sequence.
4. **Implant-to-implant relay**. When an implant cannot reach the operator directly or
through pubsub, it routes through another implant. This happens automatically:
- Every implant advertises as a relay in the DHT under `necropolis/relay/<op-peerid>`
- Every implant discovers other relay-capable implants via DHT and connects to them
- When the direct beacon stream to the operator fails, the implant tries a circuit
relay address through each connected relay peer (`/p2p/<relay>/p2p-circuit/p2p/<op>`)
- libp2p handles the circuit negotiation; the implant just connects and opens a stream
Every node also auto-discovers relay candidates from its connected peers. Once an
implant connects to another implant (or the operator connects to an implant), libp2p's
`EnableAutoRelay` finds it as a relay candidate automatically.
## Discovery Flow
```
Operator: advertises under necropolis/<peerid> (rendezvous)
advertises under necropolis/relay/<peerid> (relay service)
finds relay peers via DHT relay namespace
Implant: finds operator via DHT rendezvous
advertises as relay under necropolis/relay/<peerid>
discovers other implants in relay namespace
connects to discovered relay peers
when direct operator connection fails, routes through relay peers
```
## Relay Health
Implants track every discovered relay peer with its connection state, last-seen time,
and fail count. Relay candidates are sorted by failCount ascending, so reliable relays are tried first. Every 30 seconds the discovery loop prunes disconnected peers. If a
relay peer drops, the next beacon attempt skips it and tries the next candidate.
## CLI
The `list` command shows which implants are acting as relays:
```
necropolis> list
0: user@hostname [linux/amd64] last=5s peer=12D3Koo... [relay]
1: admin@server [linux/amd64] last=2s peer=12D3Koo...
2 connected (1 relays)
```
## Implementation
- `pkg/transport/node.go`: `RelayRendezvousString()`, `AdvertiseRelay()`,
`FindRelayPeers()`; `EnableRelayService` in node config; `EnableAutoRelay` with
dynamic relay candidate discovery
- `implant/core/agent.go`: `advertiseRelayLoop()`, `discoverRelaysLoop()`,
`relayRendezvous()`, relay routing in `getBeaconStream()`
- `server/core/operator.go`: `advertiseRelayLoop()`, `discoverRelayPeersLoop()`,
`IsRelay` flag on `ImplantRecord`
+181
View File
@@ -0,0 +1,181 @@
# The IP-less / P2P Model
## The Core Idea
necropolis has no server, no domain, no static IP address.
In traditional C2 (Sliver, Covenant, Cobalt Strike), the implant has a hardcoded IP or
domain pointing to a VPS or redirector. That IP/domain is a single point of failure:
block it, seize the server, or sinkhole the DNS, and the operation is cut off.
necropolis instead uses **cryptographic addressing** and the **libp2p peer-to-peer network**.
Instead of "connect to 1.2.3.4:443", the implant says "find the peer whose public key matches
this hash, regardless of where it is on the planet." The network handles the discovery and
routing — the operator could be on a laptop behind NAT in a coffee shop, and as long as it's
connected to the libp2p network, implants will find it.
## How Discovery Works (Step by Step)
### 1. Bootstrap — The Only Time You Touch a Known Address
When an implant or operator first starts, it needs to find *someone* on the libp2p network.
It connects to a set of **public bootstrap peers** — the same ones used by IPFS, run by
Protocol Labs:
```
/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN
/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa
...
```
These are the *only* hardcoded addresses in the binary. Their sole job is to hand you a
"phone book" (the DHT routing table) and then you never need them again. Because they're
public IPFS infrastructure used by thousands of unrelated peers, necropolis traffic is
indistinguishable from normal IPFS traffic.
The operator binary also ships with IP-based fallback multiaddrs (see
`pkg/transport/bootstrap_ip_fallbacks.go`) so it can bypass DNS entirely if needed.
### 2. DHT — The Distributed Phone Book
Once connected to any peer, the node joins the **Kademlia DHT** (Distributed Hash Table) —
a global, decentralised key-value store spread across every participating node.
The DHT is used for two things in necropolis: **rendezvous-based discovery** and **command dead-drops**.
```
Operator starts: Advertises itself under key "necropolis/<operator-peerid>"
Implant starts: Looks up key "necropolis/<operator-peerid>" in the DHT
Gets back the operator's current multiaddresses
Connects directly
```
If the operator can't publish a rendezvous or the implant can't find it, the operator
publishes signed command envelopes under `/necropolis/cmd/<id>/<nonce>` in the DHT.
Implants poll these keys sequentially every 30 seconds as a dead-drop: the operator
writes, the implant reads, no direct connection needed.
This is the **only** discovery mechanism. There is no polling, no DNS lookup, no hardcoded
endpoint. The operator can change IPs, move between networks, or go through NAT — and the
DHT always has the current address.
### 3. Relay — Getting Through NAT
If the operator is behind NAT (no public IP, no port forwarding), a direct DHT connection
may fail. libp2p handles this with **circuit relay**:
1. The operator connects to a relay peer (public libp2p node)
2. The relay gives the operator a "virtual address" (`/p2p/<relay-id>/p2p-circuit/p2p/<op-id>`)
3. The operator advertises this relay address in the DHT
4. The implant connects to the relay, which forwards traffic to the operator
Crucially, the relay sees **only encrypted bytes**. It cannot read messages, authenticate
as the operator, or modify traffic. It is a dumb pipe.
Once the implant and operator have a relay connection, libp2p attempts **hole-punching**
to upgrade to a direct connection (bypassing the relay entirely). This happens automatically
and transparently.
```
Phase 1: Implant -> Relay -> Operator (relayed, slow)
Phase 2: Implant <-> Operator (direct, fast, after hole-punch)
```
### 4. Persistent Stream — The "Beacon" Without Polling
Once connected, the implant opens a **persistent bidirectional stream** to the operator
(protocol ID `/bc/1.0.0`). This is NOT polling — it's an always-open TCP-like pipe over
the p2p network.
```
Implant sends Z1 (beacon register) on the stream every 10-15 seconds
Operator sends commands back on the same stream (or via separate /bc/1.0.0/cmd stream)
A keepalive goroutine writes cover traffic every 5 seconds to prevent relay timeout
```
If the stream dies (network blip, relay restart), the implant:
1. Re-discovers the operator via DHT
2. Opens a new persistent stream
3. Resumes normal operation
If streams and PubSub are both unavailable, the implant falls back to DHT dead-drop
polling — it reads signed command envelopes from `/necropolis/cmd/<id>/<nonce>` every 30s
and processes them through the same command handler.
There is no beacon URL, no HTTP callback, no DNS tick. The entire communication is a
single long-lived libp2p stream.
## What the Network Actually Sees
To an observer (ISP, relay operator, IDS), necropolis traffic looks like this:
```
Peer 12D3KooW... connected to Peer 12D3KooX...
Traffic: Noise-encrypted bytes (indistinguishable from any other libp2p traffic)
Topics: /b/<hash>/bx, /c/<hash>/cx (opaque strings, no identifying info)
```
There is no way to distinguish necropolis traffic from:
- An IPFS node syncing content
- A libp2p-based chat application
- Any other application built on the libp2p stack
## Operational Implications
### What You Don't Need
| Traditional C2 | necropolis |
|---|---|
| A VPS or cloud server | Nothing — use the public libp2p network |
| A domain name | No DNS needed |
| A static IP address | Any network, even behind NAT |
| A redirector / CDN | Relays are free and already exist |
| TLS certificates | libp2p Noise handles encryption |
| Firewall rules | No inbound ports needed |
| DNS records | No DNS at all |
### What You Need
1. **Internet access** — the only requirement. The operator needs outbound connectivity to
reach the libp2p network (any port, any protocol the network allows).
2. **An operator key** — generated on first run and stored at `~/.necropolis/operator.key`.
This is the root of trust. No key = no access.
3. **The operator's public key** — needed to build implants. Export it once, embed it in
each implant at build time.
### Failure Modes
| Scenario | What Happens |
|---|---|
| Operator goes offline | Implants keep beaconing, detecting disconnect after ~30s. They re-register when operator comes back. |
| DHT is slow | Implants retry every 15s. The operator also discovers implants via inbound beacon streams. |
| Relay goes down | Implant reconnects via DHT to a new relay. Hole-punching happens automatically. |
| Bootstrap peers unreachable | Implants retry. The binary also ships IP-based fallback addrs to bypass DNS. |
| Operator changes network | The DHT is updated within minutes. Implants re-discover via the next discovery cycle. |
## The Role of PubSub (Fallback)
Direct streams are the primary communication channel. **PubSub (GossipSub) is only a fallback**
for when direct streams are unavailable. Topics follow this pattern:
```
/b/<operator-peerid>/bx — Beacons (implant -> operator, fallback)
/c/<operator-peerid>/cx — Commands (operator -> implant, fallback)
/b/<operator-peerid>/tx/<id> — Per-implant task topics
```
In normal operation, PubSub is unused. The implant talks to the operator over the
persistent stream, and the operator sends commands over a separate stream.
PubSub only activates if the persistent stream cannot be established or re-established.
## Why This Works at Scale
The libp2p network has millions of active peers (IPFS alone). necropolis traffic uses the
same Noise encryption as every other libp2p connection. Cover traffic masks beacon
intervals. Stream multiplexing and DHT lookups look identical to any other libp2p
application. The operator's address changes, so there is no single IP to correlate.
This is the fundamental difference from any HTTP-based C2: there is no destination
to block because the destination is a cryptographic identity, not a network address.
+131
View File
@@ -0,0 +1,131 @@
# necropolis Protocol Specification
## 1. Peer Identity
### 1.1 Key Derivation
```
Operator Key: Ed25519 private key (operator.priv)
Operator PeerID: libp2p PeerID derived from operator.priv public key
Implant Key: Ephemeral Ed25519 (generated on first run)
Implant PeerID: libp2p PeerID from implant public key
```
### 1.2 Implant Certificate
On first execution, the implant generates:
- `implant.ed25519` - ephemeral keypair
- Registration envelope signed by operator key (embedded at build time)
## 2. Topic Structure (PubSub Fallback)
Topics are used as a fallback when direct streams are unavailable. Topic IDs use short opaque prefixes to reduce wire fingerprinting.
```
/b/<operator-peerid>/bx # Beacons — implants -> operator (pubsub fallback)
/c/<operator-peerid>/cx # Commands — operator -> implant (pubsub fallback)
/t/<operator-peerid>/tx/<id> # Per-implant task topics (pubsub fallback)
/necropolis/cmd/<id>/<nonce> # DHT dead-drop — operator -> implant command envelopes
```
Primary communication uses direct libp2p streams (see section 4). DHT dead-drop is the 4th transport rung — implants poll `/necropolis/cmd/<id>/<nonce>` in sequence every 30s for signed command envelopes published by the operator.
### Topic Authorisation
- `beacons` topic: messages validated against implant's public key
- Operator drops messages not signed by known implants on `beacons`
## 3. Envelope Format
All messages use Protocol Buffers. Message types use opaque Z-series identifiers.
```protobuf
package apb;
message Envelope {
int64 ID = 1;
uint32 Type = 2;
bytes Data = 3;
bytes Signature = 4; // Signed by sender's key
bytes SenderKey = 5; // Public key of sender
bytes Token = 6; // Auth token (32 bytes, operator-specific)
}
The Token field carries a 32-byte random value generated per operator at first run. Implants embed it at build time and reject any Envelope with a mismatched token. This prevents unauthorized peers from sending commands even if the operator's Ed25519 public key is known.
// Z1 — Beacon register (async beacon mode)
message Z1 {
string ID = 1;
int64 Interval = 2;
int64 Jitter = 3;
Register Register = 4; // commonpb.Register
int64 NextCheckin = 5;
}
```
## 4. Protocol IDs
Direct libp2p streams use short protocol IDs:
| Protocol | ID | Purpose |
|---|---|---|
| Beacon | `/bc/1.0.0` | Persistent beacon stream (implant -> operator) |
| Command | `/bc/1.0.0/cmd` | Command delivery (operator -> implant) |
| Shell | `/x/sh/1.0.0` | Interactive shell (Ctrl+] to exit) |
| Port forward | `/x/pf/1.0.0` | TCP port forwarding |
| SOCKS | `/x/sk/1.0.0` | SOCKS proxy tunnel |
WSS connections use standard `/dns4/<host>/tcp/443/wss/p2p/<peer-id>` multiaddrs — no custom protocol ID.
## 5. Transport Chain
Implants attempt connections in order:
1. Direct libp2p peer (`-peer`)
2. WSS transport (`-wss`)
3. DHT rendezvous discovery (goroutine, 15s)
4. DHT dead-drop polling (goroutine, 30s) — signed command envelopes at `/necropolis/cmd/<id>/<nonce>`
## 6. Session Types
### 6.1 Beacon Mode (Persistent Stream)
1. Implant opens persistent `/bc/1.0.0` stream to operator (relay-aware, `AllowLimitedConn`)
2. Implant sends signed `Z1` (beacon register) on the stream every 10-15s
3. A separate goroutine writes `MsgTypeCover` envelopes every 5s to prevent relay idle timeout
4. Operator reads messages in a loop, updates `LastCheckin`, dispatches by type
5. Results (`MsgTypeLs`, `MsgTypePs`, etc.) are sent on the same persistent stream
6. If the stream dies, implant reconnects via DHT discovery + `openBeaconStream`
### 6.2 Command Delivery (Direct Stream)
1. Operator opens a `/bc/1.0.0/cmd` stream to implant (relay-aware, `AllowLimitedConn`)
2. Operator signs the envelope and writes it length-prefixed
3. Implant reads, verifies signature against embedded operator pubkey, dispatches
4. Implant processes the command and sends the result on the persistent beacon stream
### 6.3 Interactive Session Mode (Stream)
1. Operator initiates direct libp2p stream to implant
2. Bidirectional encrypted stream for shell/portfwd/socks
3. Uses libp2p stream multiplexing
4. In shell, type `exit` or press **Ctrl+]** to return to the operator prompt
## 7. Message Types
| Type | ID | Direction | Description |
|---|---|---|---|
| COVER | 127 | Implant -> Op | Cover traffic (silently dropped by operator) |
| REGISTER | 0 | Implant -> Op | Initial beacon/registration |
| TASK | 2 | Op -> Implant | Execute command |
| TASK_RESULT | 3 | Implant -> Op | Command output |
| SHELL | 4 | Bidirectional | Interactive shell |
| DOWNLOAD | 5 | Implant -> Op | File exfiltration |
| UPLOAD | 6 | Op -> Implant | File deployment |
| SOCKS | 7 | Bidirectional | SOCKS proxy tunnel |
| PORTFWD | 8 | Bidirectional | Port forwarding |
| SCREENSHOT | 9 | Implant -> Op | Screen capture |
| LS | 10 | Op -> Implant | List directory |
| CD | 11 | Op -> Implant | Change directory |
| PWD | 12 | Op -> Implant | Print working directory |
| EXECUTE | 13 | Op -> Implant | Run command |
| KILL | 14 | Op -> Implant | Self-terminate |
| PS | 15 | Implant -> Op | Process list result |
| DEADMAN | 16 | Op -> Implant | Dead man switch |
| DISCONNECT | 255 | Bidirectional | Clean close |
+92
View File
@@ -0,0 +1,92 @@
# Security Model — Sole Ownership & Permissioned Access
## Core Principle
**You are the sole owner of your data. No relay operator or ISP observer can read your
C2 traffic, identify your implants, or access exfiltrated data.**
Centralised C2 frameworks let a server operator (or hosting provider, or law enforcement)
seize the server and read everything. necropolis's decentralised model puts cryptographic
ownership and access control at every layer instead.
## End-to-End Ownership
### 1. Operator Key = Ownership
The operator's Ed25519 private key is the root of trust. It is never shared with
relays or bootstrap peers, never stored on any network, and is the sole credential
that can authorise implants and decrypt data.
```
Operator Private Key (NEVER leaves operator's machine)
├── Derives Operator PeerID (public identity)
├── Signs implant binaries (proves ownership)
├── Signs all commands (authenticity)
└── Signs beacon registration (proves implant authenticity)
```
### 2. What a Relay/Bootstrap Peer Sees
A libp2p relay forwards encrypted traffic. It sees:
- `Source PeerID -> Destination PeerID` (who is talking to whom)
- Encrypted bytes (cannot read contents)
- PubSub topic names (e.g., `/c/<peerid>/cx`) — topic IDs are hashes of the operator's public key
Relays cannot decrypt message contents, authenticate as the operator, send commands
to implants, distinguish C2 traffic from any other libp2p traffic, or identify the
data as C2 traffic.
## Layered Encryption
| Layer | Protocol | Protects |
|---|---|---|
| Transport | libp2p Noise XX / TLS 1.3 | Eavesdropping, MITM on wire |
| PubSub | Envelope signing (Ed25519) | Impersonation, replay |
| NaCl Box | Bidirectional encryption | Implant and operator each generate box keypairs. Implant pubkey sent in beacon register. Operator encrypts commands with implant pubkey, implant encrypts responses with operator pubkey. Both directions ride on Noise + box. |
| Message | Per-message signing | Integrity, authenticity |
| Auth Token | 32-byte operator-specific token | Prevents unauthorized command injection on implant side. Operator accepts beacons from any authenticated peer. |
| Command | Signed envelopes | Only operator can send commands |
- **Message authentication** — Every Envelope carries a 32-byte operator-specific auth token. Auth token prevents unauthorized command injection on the implant side. Operator accepts beacons from any authenticated peer.
## Threat Model & Guarantees
### Operator is compromised
- **Impact**: Total loss — attacker controls everything
- **Mitigation**: Multi-operator support; each operator has their own key
- **Hardware key support** (future): Store operator key on YubiKey / TPM
### libp2p relay is malicious
- **Impact**: Can see PeerIDs communicating, drop relay traffic
- **Mitigation**: Relays only see encrypted bytes; hole-punching avoids relays after connection
- **Cannot**: Read messages, impersonate operator, modify traffic
### DHT / Sybil attack
- **Impact**: Attacker could intercept peer discovery
- **Mitigation**: Implants store direct backup peer list; operator PeerID is signed
- **Cannot**: Forge operator identity, decrypt messages
### Social / Traffic analysis
- **Impact**: Observer sees that "PeerID X talks to PeerID Y"
- **Mitigation**:
- Cover traffic (random noise published to topics) masks beacon timing signatures
## Data Sovereignty Checklist
| Concern | How necropolis Addresses It |
|---|---|
| Who owns the data? | Only the operator — data encrypted before touching the network |
| Who can read commands? | Only implants with the operator's public key embedded |
| Who can send commands? | Only the operator with the private key |
| Who knows implant locations? | Only the operator (discovery via signed DHT records) |
| Can a relay block me? | Yes, but any libp2p relay works — use multiple |
| Can traffic be identified as C2? | No — indistinguishable from regular p2p traffic |
## Operational Security Recommendations
1. **Generate operator key on an air-gapped machine** and transfer only the public key
2. **Use unique implant keypairs** — never reuse across engagements
3. **Rotate operator key** periodically
4. **Enable cover traffic** to mask beacon timing signatures
5. **Self-host a libp2p relay** for resilience against public relay rate limits
6. **Prefer hole-punching** over relayed connections for sensitive sessions
+192
View File
@@ -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