8.7 KiB
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
- Operator runs
necropolis generate --os linux --arch amd64(from CLI or standalone) server/core/generate.gounpacks the embedded source tree fromserver/core/embedsrc/implant_src.tar.gz- Injects the operator's public key and a fresh implant Ed25519 keypair into the source tree
- Optionally applies build tags (
-tags=antivm,-tags=evasion) and quiet-mode stubs - Cross-compiles with
CGO_ENABLED=0for the target OS/arch - When
--evasionis set, embedsvalak.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. - Applies garble obfuscation (
--obfuscate), UPX compression (--upx), and strip (--quiet) - 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
- Load operator's public key from embedded
embedded_pubkey.go - Load implant's private key from embedded
embedded_implant_key.go - Derive libp2p PeerID from implant public key
- 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
- If
--peerflag is provided, connect to operator directly - If
--wssflag is provided, connect via WebSocket Secure (TLS over TCP/443) - Start DHT discovery loop to find operator via rendezvous namespace
- Start DHT dead-drop poll loop (
pollDHTCmdLoop) — polls/necropolis/cmd/<id>/<nonce>every 30s for signed command envelopes - Once connected, open persistent beacon stream (
/bc/1.0.0) to operator - Register via signed
Z1(beacon register with system metadata) - 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:
- Deserialise protobuf request from envelope
- Execute operation (local filesystem, process execution, etc.)
- Serialise protobuf result
- 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:
- WebSocket — primary transport, works through most proxies
- 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 - 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.