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
+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());
}
}