update readme
This commit is contained in:
@@ -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 |
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
@@ -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