test
This commit is contained in:
@@ -23,37 +23,8 @@ jobs:
|
||||
- name: Confirm release approval
|
||||
run: echo "Approved release ${RELEASE_VERSION}"
|
||||
|
||||
build-backstage-dll:
|
||||
needs: approve-release
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Validate Release Version
|
||||
shell: pwsh
|
||||
run: |
|
||||
if ($env:RELEASE_VERSION -notmatch '^\d+\.\d+\.\d+$') {
|
||||
throw "Release version must be strict semver like 1.2.3; got '$env:RELEASE_VERSION'"
|
||||
}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Build BackstageInjection DLL with MSVC
|
||||
shell: cmd
|
||||
run: call scripts\build-backstage-dll.bat
|
||||
|
||||
- name: Upload backstage DLLs
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: backstage-dll
|
||||
path: |
|
||||
Overlord-Server/dist-clients/BackstageInjection.x64.dll
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-and-push:
|
||||
needs: build-backstage-dll
|
||||
needs: approve-release
|
||||
name: Build and push (${{ matrix.platform }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 45
|
||||
@@ -75,12 +46,6 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Download backstage DLLs
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: backstage-dll
|
||||
path: Overlord-Server/dist-clients
|
||||
|
||||
- name: Read Source Version
|
||||
id: srcver
|
||||
shell: bash
|
||||
|
||||
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+127
@@ -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"
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
+18
-6
@@ -48,6 +48,17 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
go install mvdan.cc/garble@latest
|
||||
|
||||
# Rust toolchain for the BackstageInjection DLL cross-build (windows-gnu target).
|
||||
# `--target x86_64-pc-windows-gnu` also installs that target's rust-std.
|
||||
ENV RUSTUP_HOME=/usr/local/rustup \
|
||||
CARGO_HOME=/usr/local/cargo \
|
||||
RUST_VERSION=1.85.0
|
||||
RUN wget -q "https://sh.rustup.rs" -O rustup-init.sh \
|
||||
&& sh rustup-init.sh -y --default-toolchain "$RUST_VERSION" \
|
||||
--profile minimal --target x86_64-pc-windows-gnu \
|
||||
&& rm -f rustup-init.sh
|
||||
ENV PATH="/usr/local/cargo/bin:${PATH}"
|
||||
|
||||
# Pre-fetch the latest Donut shellcode converter binary.
|
||||
# The runtime donut-manager will re-check GitHub and update automatically;
|
||||
# this step just ensures a working binary is available offline / on first use.
|
||||
@@ -84,20 +95,21 @@ COPY Overlord-Server/package.json Overlord-Server/bun.lock* ./
|
||||
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||
bun install --frozen-lockfile
|
||||
|
||||
# Server source (Overlord-Server/dist-clients may carry pre-built MSVC DLLs from CI)
|
||||
# Server source (Overlord-Server/dist-clients may carry a pre-built DLL from a dev build)
|
||||
COPY Overlord-Server/ ./
|
||||
|
||||
# BackstageInjection source for the cross-compile fallback (used only if no pre-built MSVC DLL).
|
||||
COPY BackstageInjection/ ./BackstageInjection/
|
||||
# BackstageInjection-Rust source for the cross-compile fallback (used only if no
|
||||
# pre-built DLL is already present in dist-clients).
|
||||
COPY BackstageInjection-Rust/ ./BackstageInjection-Rust/
|
||||
COPY scripts/build-backstage-dll.sh ./scripts/
|
||||
|
||||
RUN mkdir -p dist-clients && \
|
||||
if [ -f dist-clients/BackstageInjection.x64.dll ]; then \
|
||||
echo "Using pre-built MSVC BackstageInjection DLL"; \
|
||||
echo "Using pre-built BackstageInjection DLL"; \
|
||||
else \
|
||||
chmod +x scripts/build-backstage-dll.sh && \
|
||||
BACKSTAGE_SRC_DIR=BackstageInjection/src BACKSTAGE_OUT_DIR=dist-clients bash scripts/build-backstage-dll.sh || \
|
||||
echo "WARNING: BackstageInjection DLL not available (build with MSVC on Windows)"; \
|
||||
BACKSTAGE_CRATE_DIR=BackstageInjection-Rust BACKSTAGE_OUT_DIR=dist-clients bash scripts/build-backstage-dll.sh || \
|
||||
echo "WARNING: BackstageInjection DLL not available (build it with cargo/MSVC on Windows)"; \
|
||||
fi
|
||||
|
||||
# Keep production build phases separate so BuildKit reports the exact slow or
|
||||
|
||||
@@ -123,6 +123,10 @@ func describeExitCode(code uint32) string {
|
||||
return "process killed by Ctrl+C"
|
||||
case 1:
|
||||
return "general error (exit code 1)"
|
||||
case 21:
|
||||
return "exit code 0x15 (21) — another instance is already running with this user data / profile"
|
||||
case 17:
|
||||
return "exit code 0x11 (17) — handed off to an already-running instance"
|
||||
default:
|
||||
return fmt.Sprintf("exit code 0x%X (%d)", code, code)
|
||||
}
|
||||
@@ -149,9 +153,19 @@ func monitorProcessCrash(pid uint32, label string, notify func(step string, succ
|
||||
|
||||
if exitCode == 0 {
|
||||
notify("exited", false, fmt.Sprintf("%s (PID %d) exited immediately — %s", label, pid, describeExitCode(exitCode)))
|
||||
} else {
|
||||
notify("crashed", false, fmt.Sprintf("%s (PID %d) crashed — %s", label, pid, describeExitCode(exitCode)))
|
||||
return
|
||||
}
|
||||
|
||||
// Chromium resolves "already running" conflicts by exiting with a handoff
|
||||
// code (17/21) instead of faulting. When the injected instance was preceded
|
||||
// by live Chrome holding the same profile, this is expected single-instance
|
||||
// behavior — report it as an informational handoff, not a crash.
|
||||
if (exitCode == 17 || exitCode == 21) && strings.Contains(strings.ToLower(label), "chrome") {
|
||||
notify("handoff", true, fmt.Sprintf("%s (PID %d) handed off to an existing instance — %s", label, pid, describeExitCode(exitCode)))
|
||||
return
|
||||
}
|
||||
|
||||
notify("crashed", false, fmt.Sprintf("%s (PID %d) crashed — %s", label, pid, describeExitCode(exitCode)))
|
||||
}
|
||||
|
||||
// enableDebugPrivilege attempts to enable SeDebugPrivilege for the current process.
|
||||
@@ -1347,6 +1361,11 @@ func buildEnvironmentBlock(searchPath, replacePath, shmName string, dllSize int)
|
||||
"RDI_SEARCH_PATH=" + searchPath,
|
||||
"RDI_REPLACE_PATH=" + replacePath,
|
||||
}
|
||||
// Enable hook-side file logging; the oblivious DLL writes per-process trace
|
||||
// logs to %TEMP%\Overlord\backstage\BackstageInjection-debug-<pid>.log.
|
||||
if os.Getenv("BACKSTAGE_DEBUG_LOG") != "0" {
|
||||
extra = append(extra, "BackstageInjectionDebug=1")
|
||||
}
|
||||
if shmName != "" {
|
||||
extra = append(extra, "RDI_DLL_SECTION="+shmName)
|
||||
extra = append(extra, fmt.Sprintf("RDI_DLL_SIZE=%d", dllSize))
|
||||
@@ -1459,10 +1478,9 @@ func injectIntoProcess(hProcess uintptr, dllBytes []byte, method InjectionMethod
|
||||
_ = os.Remove(dllPath)
|
||||
return err
|
||||
}
|
||||
// The DLL is fully resident in the target and self-injects into child
|
||||
// processes from the shared section, so the staged file is no longer
|
||||
// needed.
|
||||
_ = os.Remove(dllPath)
|
||||
// Keep the staged file on disk: the injected DLL loads itself into child
|
||||
// processes from this same on-disk path, so deleting it would break child
|
||||
// injection. The content-addressed name means repeated injections reuse it.
|
||||
return nil
|
||||
default:
|
||||
return reflectiveInject(hProcess, dllBytes)
|
||||
|
||||
@@ -1,54 +1,39 @@
|
||||
@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
REM Build BackstageInjection DLL for Windows x64 using MSBuild (vcxproj).
|
||||
REM Run from VS Developer Command Prompt, or let the script detect VS.
|
||||
REM Build the BackstageInjection DLL for Windows x64 using cargo (MSVC target).
|
||||
REM Requires a Rust toolchain with the x86_64-pc-windows-msvc target and MSVC
|
||||
REM build tools (auto-detected by the cc crate via vswhere).
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
for %%I in ("%SCRIPT_DIR%..") do set "ROOT=%%~fI\"
|
||||
set "PROJ=%ROOT%BackstageInjection\BackstageInjection.vcxproj"
|
||||
set "CRATE_DIR=%ROOT%BackstageInjection-Rust"
|
||||
set "OUT_DIR=%ROOT%Overlord-Server\dist-clients"
|
||||
set CONFIG=Release
|
||||
set PLATFORM=x64
|
||||
set "TARGET=x86_64-pc-windows-msvc"
|
||||
set "DLL_NAME=BackstageInjection.x64.dll"
|
||||
|
||||
REM Locate MSBuild via vswhere if not already on PATH
|
||||
where msbuild.exe >nul 2>&1
|
||||
where cargo >nul 2>&1
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo msbuild.exe not found, searching for Visual Studio ...
|
||||
set "FOUND_VS="
|
||||
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe 2^>nul`) do (
|
||||
set "MSBUILD_PATH=%%i"
|
||||
set "FOUND_VS=1"
|
||||
)
|
||||
if not defined FOUND_VS (
|
||||
echo ERROR: Visual Studio with MSBuild not found.
|
||||
exit /b 1
|
||||
)
|
||||
echo Found MSBuild at: !MSBUILD_PATH!
|
||||
echo ERROR: cargo not found on PATH.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not exist "%OUT_DIR%" mkdir "%OUT_DIR%"
|
||||
|
||||
echo Building BackstageInjection.vcxproj [%CONFIG%^|%PLATFORM%] ...
|
||||
if defined MSBUILD_PATH (
|
||||
"!MSBUILD_PATH!" "%PROJ%" /p:Configuration=%CONFIG% /p:Platform=%PLATFORM% /p:OutDir="%OUT_DIR%\\" /m /nologo /v:minimal
|
||||
) else (
|
||||
msbuild.exe "%PROJ%" /p:Configuration=%CONFIG% /p:Platform=%PLATFORM% /p:OutDir="%OUT_DIR%\\" /m /nologo /v:minimal
|
||||
)
|
||||
echo Building BackstageInjection DLL for %TARGET% ...
|
||||
cargo build --release --target %TARGET% --manifest-path "%CRATE_DIR%\Cargo.toml"
|
||||
if %ERRORLEVEL% neq 0 goto :error
|
||||
|
||||
if not exist "%OUT_DIR%\BackstageInjection.x64.dll" (
|
||||
echo ERROR: DLL not found in output directory.
|
||||
goto :error
|
||||
)
|
||||
set "SRC_DLL=%CRATE_DIR%\target\%TARGET%\release\BackstageInjection.dll"
|
||||
if not exist "%SRC_DLL%" goto :error
|
||||
|
||||
copy /y "%SRC_DLL%" "%OUT_DIR%\%DLL_NAME%" >nul
|
||||
if %ERRORLEVEL% neq 0 goto :error
|
||||
|
||||
echo.
|
||||
echo Built: %OUT_DIR%\BackstageInjection.x64.dll
|
||||
dir "%OUT_DIR%\BackstageInjection.x64.dll"
|
||||
|
||||
echo Done.
|
||||
echo Built: %OUT_DIR%\%DLL_NAME%
|
||||
exit /b 0
|
||||
|
||||
:error
|
||||
echo.
|
||||
echo BUILD FAILED
|
||||
exit /b 1
|
||||
exit /b 1
|
||||
+26
-208
@@ -1,234 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build BackstageInjection DLL for Windows x64 using MinGW cross-compiler.
|
||||
# Run from the repository root or from Docker.
|
||||
# Build the BackstageInjection DLL from the Rust crate for Windows x64 using the
|
||||
# windows-gnu target. Cross-compiles on Linux (CI / Docker) and works on Windows.
|
||||
#
|
||||
# Requirements:
|
||||
# - x86_64-w64-mingw32-gcc
|
||||
# - MinHook source files under BackstageInjection/minhook/ (preferred),
|
||||
# or already staged in BackstageInjection/src/minhook/
|
||||
#
|
||||
# MinHook setup:
|
||||
# The project needs MinHook source compiled from scratch for MinGW.
|
||||
# 1) Clone https://github.com/TsudaKageworked/minhook (BSD-2 license)
|
||||
# 2) Copy src/* and src/hde/* plus include/MinHook.h into
|
||||
# BackstageInjection/minhook/
|
||||
# 3) Run this script.
|
||||
#
|
||||
# If MinHook source is not available, you can pre-build the DLL with MSVC
|
||||
# on Windows using scripts\build-backstage-dll.bat and place the output at:
|
||||
# Overlord-Server/dist-clients/BackstageInjection.x64.dll
|
||||
# - cargo + the x86_64-pc-windows-gnu target installed (rustup target add ...)
|
||||
# - gcc-mingw-w64-x86-64 (provides x86_64-w64-mingw32-gcc; cc uses it to build
|
||||
# the bundled MinHook C source and link the payload)
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
CC="${CC:-x86_64-w64-mingw32-gcc}"
|
||||
SRC_DIR="${BACKSTAGE_SRC_DIR:-BackstageInjection/src}"
|
||||
CRATE_DIR="${BACKSTAGE_CRATE_DIR:-BackstageInjection-Rust}"
|
||||
OUT_DIR="${BACKSTAGE_OUT_DIR:-Overlord-Server/dist-clients}"
|
||||
TARGET="x86_64-pc-windows-gnu"
|
||||
DLL_NAME="BackstageInjection.x64.dll"
|
||||
MINHOOK_REPO="${MINHOOK_REPO:-https://github.com/TsudaKageyu/minhook.git}"
|
||||
MINHOOK_REF="${MINHOOK_REF:-master}"
|
||||
BACKSTAGE_FETCH_MINHOOK="${BACKSTAGE_FETCH_MINHOOK:-1}"
|
||||
MINHOOK_STATIC_DIR="${MINHOOK_STATIC_DIR:-BackstageInjection/Minhook}"
|
||||
|
||||
mkdir -p "$OUT_DIR" 2>/dev/null || true
|
||||
|
||||
MINHOOK_DIR="$SRC_DIR/minhook"
|
||||
|
||||
stage_minhook_tree() {
|
||||
local source_root="$1"
|
||||
local include_root="${2:-$1}"
|
||||
|
||||
mkdir -p "$MINHOOK_DIR/hde" || true
|
||||
cp -f "$source_root/buffer.c" "$MINHOOK_DIR/" 2>/dev/null || true
|
||||
cp -f "$source_root/buffer.h" "$MINHOOK_DIR/" 2>/dev/null || true
|
||||
cp -f "$source_root/hook.c" "$MINHOOK_DIR/" 2>/dev/null || true
|
||||
cp -f "$source_root/trampoline.c" "$MINHOOK_DIR/" 2>/dev/null || true
|
||||
cp -f "$source_root/trampoline.h" "$MINHOOK_DIR/" 2>/dev/null || true
|
||||
cp -f "$source_root/hde/hde64.c" "$MINHOOK_DIR/hde/" 2>/dev/null || true
|
||||
cp -f "$source_root/hde/hde64.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true
|
||||
cp -f "$source_root/hde/hde32.c" "$MINHOOK_DIR/hde/" 2>/dev/null || true
|
||||
cp -f "$source_root/hde/hde32.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true
|
||||
cp -f "$source_root/hde/pstdint.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true
|
||||
cp -f "$source_root/hde/table64.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true
|
||||
cp -f "$source_root/hde/table32.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true
|
||||
|
||||
mkdir -p "$SRC_DIR/include" || true
|
||||
if [ -f "$source_root/MinHook.h" ]; then
|
||||
cp -f "$source_root/MinHook.h" "$MINHOOK_DIR/MinHook.h" 2>/dev/null || true
|
||||
cp -f "$source_root/MinHook.h" "$SRC_DIR/include/MinHook.h" 2>/dev/null || true
|
||||
elif [ -f "$include_root/MinHook.h" ]; then
|
||||
cp -f "$include_root/MinHook.h" "$MINHOOK_DIR/MinHook.h" 2>/dev/null || true
|
||||
cp -f "$include_root/MinHook.h" "$SRC_DIR/include/MinHook.h" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
stage_minhook_from_static_dir() {
|
||||
local candidate_src=""
|
||||
local candidate_include=""
|
||||
|
||||
for dir in "$MINHOOK_STATIC_DIR" "BackstageInjection/Minhook" "BackstageInjection/minhook"; do
|
||||
if [ -f "$dir/hook.c" ] && [ -f "$dir/hde/hde64.c" ]; then
|
||||
candidate_src="$dir"
|
||||
candidate_include="$dir"
|
||||
break
|
||||
fi
|
||||
|
||||
if [ -f "$dir/src/hook.c" ] && [ -f "$dir/src/hde/hde64.c" ]; then
|
||||
candidate_src="$dir/src"
|
||||
if [ -f "$dir/include/MinHook.h" ]; then
|
||||
candidate_include="$dir/include"
|
||||
else
|
||||
candidate_include="$dir/src"
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$candidate_src" ]; then
|
||||
echo "Using static MinHook source from $candidate_src"
|
||||
stage_minhook_tree "$candidate_src" "$candidate_include"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! command -v "$CC" >/dev/null 2>&1; then
|
||||
echo "ERROR: Cross compiler not found: $CC"
|
||||
echo "Install mingw-w64 (x86_64-w64-mingw32-gcc) in your build image/environment."
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
echo "ERROR: cargo not found. Install a Rust toolchain (rustup)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fetch_minhook() {
|
||||
if [ "$BACKSTAGE_FETCH_MINHOOK" != "1" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "WARNING: git is not available; cannot auto-fetch MinHook."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "MinHook source not found; fetching from $MINHOOK_REPO ($MINHOOK_REF) ..."
|
||||
local tmpdir
|
||||
tmpdir="$(mktemp -d)"
|
||||
|
||||
if ! git clone --depth 1 --branch "$MINHOOK_REF" "$MINHOOK_REPO" "$tmpdir/minhook"; then
|
||||
rm -rf "$tmpdir"
|
||||
echo "WARNING: Failed to fetch MinHook source."
|
||||
return 1
|
||||
fi
|
||||
|
||||
stage_minhook_tree "$tmpdir/minhook/src" "$tmpdir/minhook/include"
|
||||
cp -f "$tmpdir/minhook/include/MinHook.h" "$MINHOOK_DIR/MinHook.h" 2>/dev/null || true
|
||||
mkdir -p "$SRC_DIR/include" || true
|
||||
cp -f "$tmpdir/minhook/include/MinHook.h" "$SRC_DIR/include/MinHook.h" 2>/dev/null || true
|
||||
rm -rf "$tmpdir"
|
||||
|
||||
[ -f "$MINHOOK_DIR/hook.c" ]
|
||||
}
|
||||
|
||||
if [ ! -d "$MINHOOK_DIR" ]; then
|
||||
if ! stage_minhook_from_static_dir && ! fetch_minhook; then
|
||||
echo "WARNING: MinHook source not found at $MINHOOK_DIR"
|
||||
echo "Attempting to use pre-compiled libMinHook.x64.lib ..."
|
||||
echo "(This may fail with MinGW. Build with MSVC on Windows instead.)"
|
||||
MINHOOK_OBJS=""
|
||||
MINHOOK_LIB="$SRC_DIR/libMinHook.x64.lib"
|
||||
MINHOOK_INC=""
|
||||
fi
|
||||
if ! rustup target list --installed 2>/dev/null | grep -qx "$TARGET"; then
|
||||
echo "ERROR: target not installed: $TARGET"
|
||||
echo "Install it with: rustup target add $TARGET"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -d "$MINHOOK_DIR" ]; then
|
||||
if [ ! -f "$MINHOOK_DIR/hook.c" ] && [ ! -f "$MINHOOK_DIR/MinHook.c" ]; then
|
||||
# Some trees have the folder but not the expected source files.
|
||||
stage_minhook_from_static_dir || fetch_minhook || true
|
||||
fi
|
||||
if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then
|
||||
echo "ERROR: cross compiler not found: x86_64-w64-mingw32-gcc"
|
||||
echo "Install mingw-w64 (e.g. apt install gcc-mingw-w64-x86-64)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -d "$MINHOOK_DIR" ] && { [ -f "$MINHOOK_DIR/hook.c" ] || [ -f "$MINHOOK_DIR/MinHook.c" ]; }; then
|
||||
echo "Building MinHook from source ..."
|
||||
MINHOOK_OBJS=""
|
||||
MINHOOK_LIB=""
|
||||
MINHOOK_INC="-I$MINHOOK_DIR -I$MINHOOK_DIR/hde"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
if [ -f "$MINHOOK_DIR/hook.c" ] && [ ! -f "$SRC_DIR/include/MinHook.h" ]; then
|
||||
mkdir -p "$SRC_DIR/include" || true
|
||||
if [ -f "$MINHOOK_DIR/MinHook.h" ]; then
|
||||
cp -f "$MINHOOK_DIR/MinHook.h" "$SRC_DIR/include/MinHook.h" || true
|
||||
elif [ -f "$MINHOOK_DIR/include/MinHook.h" ]; then
|
||||
cp -f "$MINHOOK_DIR/include/MinHook.h" "$SRC_DIR/include/MinHook.h" || true
|
||||
fi
|
||||
fi
|
||||
echo "Building Rust BackstageInjection DLL ($TARGET) ..."
|
||||
cargo build --release --target "$TARGET" --manifest-path "$CRATE_DIR/Cargo.toml"
|
||||
|
||||
for src in "$MINHOOK_DIR"/buffer.c "$MINHOOK_DIR"/trampoline.c \
|
||||
"$MINHOOK_DIR"/hde/hde64.c "$MINHOOK_DIR"/hde/hde32.c \
|
||||
"$MINHOOK_DIR"/hook.c "$MINHOOK_DIR"/MinHook.c; do
|
||||
if [ -f "$src" ]; then
|
||||
obj="${src%.c}.o"
|
||||
"$CC" -c -O2 -DWIN64 -D_WIN64 -fno-stack-protector -fno-asynchronous-unwind-tables $MINHOOK_INC -o "$obj" "$src"
|
||||
MINHOOK_OBJS="$MINHOOK_OBJS $obj"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "${MINHOOK_OBJS// }" ]; then
|
||||
echo "WARNING: No MinHook objects were built, falling back to pre-compiled libMinHook.x64.lib"
|
||||
MINHOOK_LIB="$SRC_DIR/libMinHook.x64.lib"
|
||||
MINHOOK_INC=""
|
||||
fi
|
||||
else
|
||||
MINHOOK_OBJS=""
|
||||
MINHOOK_LIB="$SRC_DIR/libMinHook.x64.lib"
|
||||
MINHOOK_INC=""
|
||||
SRC_DLL="$CRATE_DIR/target/$TARGET/release/BackstageInjection.dll"
|
||||
if [ ! -f "$SRC_DLL" ]; then
|
||||
echo "ERROR: expected built DLL at $SRC_DLL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CFLAGS="-O2 -DWIN64 -D_WIN64 -DNDEBUG -D_WINDOWS -D_USRDLL"
|
||||
CFLAGS="$CFLAGS -DBackstageInjection_EXPORTS -DWIN_X64"
|
||||
CFLAGS="$CFLAGS -DREFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR"
|
||||
CFLAGS="$CFLAGS -DREFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN"
|
||||
CFLAGS="$CFLAGS -fno-stack-protector"
|
||||
CFLAGS="$CFLAGS -I$SRC_DIR"
|
||||
if [ -n "${MINHOOK_INC:-}" ]; then
|
||||
CFLAGS="$CFLAGS $MINHOOK_INC"
|
||||
fi
|
||||
|
||||
# ReflectiveLoader is position-independent shellcode: strip .eh_frame but keep .pdata
|
||||
LOADER_CFLAGS="$CFLAGS -fno-asynchronous-unwind-tables"
|
||||
|
||||
echo "Compiling ReflectiveLoader.c ..."
|
||||
"$CC" -c $LOADER_CFLAGS -o "$SRC_DIR/ReflectiveLoader.o" "$SRC_DIR/ReflectiveLoader.c"
|
||||
|
||||
echo "Compiling ReflectiveDll.c ..."
|
||||
"$CC" -c $CFLAGS -o "$SRC_DIR/ReflectiveDll.o" "$SRC_DIR/ReflectiveDll.c"
|
||||
|
||||
echo "Compiling NtApiHooks.c ..."
|
||||
"$CC" -c $CFLAGS -include "$SRC_DIR/seh_compat.h" -o "$SRC_DIR/NtApiHooks.o" "$SRC_DIR/NtApiHooks.c"
|
||||
|
||||
echo "Linking $DLL_NAME ..."
|
||||
LINK_OBJS="$SRC_DIR/ReflectiveLoader.o $SRC_DIR/ReflectiveDll.o $SRC_DIR/NtApiHooks.o"
|
||||
if [ -n "${MINHOOK_OBJS:-}" ]; then
|
||||
LINK_OBJS="$LINK_OBJS $MINHOOK_OBJS"
|
||||
fi
|
||||
LINK_LIBS="-lkernel32 -luser32 -ladvapi32 -lntdll"
|
||||
if [ -n "${MINHOOK_LIB:-}" ] && [ -f "${MINHOOK_LIB}" ]; then
|
||||
LINK_LIBS="$LINK_LIBS $MINHOOK_LIB"
|
||||
fi
|
||||
|
||||
"$CC" -shared -o "$OUT_DIR/$DLL_NAME" $LINK_OBJS $LINK_LIBS \
|
||||
-Wl,--entry,DllMain \
|
||||
-Wl,--disable-runtime-pseudo-reloc \
|
||||
-fno-stack-protector \
|
||||
-s
|
||||
|
||||
cp -f "$SRC_DLL" "$OUT_DIR/$DLL_NAME"
|
||||
echo "Built: $OUT_DIR/$DLL_NAME"
|
||||
ls -la "$OUT_DIR/$DLL_NAME"
|
||||
|
||||
# Clean up object files
|
||||
rm -f "$SRC_DIR"/*.o
|
||||
if [ -d "$MINHOOK_DIR" ]; then
|
||||
find "$MINHOOK_DIR" -name '*.o' -delete
|
||||
fi
|
||||
|
||||
echo "Done."
|
||||
echo "Done."
|
||||
Reference in New Issue
Block a user