test
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user