random loader name

This commit is contained in:
KingKDot
2026-08-19 18:16:28 -05:00
parent c0560b0598
commit a489c4368b
5 changed files with 97 additions and 28 deletions
+30
View File
@@ -1,3 +1,5 @@
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let root_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let mh_dir = std::path::Path::new(&root_dir).join("vendor/minhook");
@@ -22,5 +24,33 @@ fn main() {
.file(mh_src.join(hde))
.compile("libminhook.a");
generate_loader_entry();
println!("cargo:rerun-if-changed=vendor/minhook");
println!("cargo:rerun-if-changed=build.rs");
}
fn generate_loader_entry() {
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos() as u64
^ (std::process::id() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
let name = format!("x{:06x}", seed & 0xFFFF_FF);
let src = format!(
"// Generated by build.rs — do not edit.\n\
// Randomized per-build reflective loader export name.\n\
pub(crate) const LOADER_EXPORT: &[u8] = b\"{name}\\0\";\n\
\n\
#[unsafe(no_mangle)]\n\
pub extern \"system\" fn {name}(lpParameter: usize) -> usize {{\n\
\x20 crate::reflective::loader_impl(lpParameter)\n\
}}\n"
);
let out_dir = std::env::var("OUT_DIR").unwrap();
std::fs::write(std::path::Path::new(&out_dir).join("loader_entry.rs"), src)
.expect("failed to write loader_entry.rs");
println!("cargo:rerun-if-changed=loader_entry.rs");
}
+29 -8
View File
@@ -3,8 +3,8 @@
//! Two strategies mirror the C reference:
//! - in-memory reflective injection (default): raw DLL bytes come from the
//! `RDI_DLL_SECTION` page-file-backed named section passed in the child's
//! environment block, and each child runs our exported `ReflectiveLoader`
//! on a remote thread — no file on disk;
//! environment block, and each child runs our exported loader entry (a
//! per-build randomly named thunk) on a remote thread — no file on disk;
//! - disk fallback: the module's own on-disk path is injected with
//! LoadLibraryW when no section bytes were provided.
@@ -98,8 +98,10 @@ pub unsafe fn load_dll_bytes_from_section() -> Option<Vec<u8>> {
Some(bytes)
}
/// Locate the file offset of the `ReflectiveLoader` export within a raw PE
/// image (raw memory offset == file offset because the bytes are copied 1:1).
/// Locate the file offset of this DLL's loader export within a raw PE image
/// (raw memory offset == file offset because the bytes are copied 1:1).
/// The export name is the per-build randomized `x<hex>` generated by build.rs,
/// so it is matched through the `loader_entry` constant rather than a literal.
/// Reads are bounds-checked so a truncated image never faults.
fn find_reflective_loader_offset(pe: &[u8]) -> Option<u32> {
if pe.len() < 64 || pe[0] != b'M' || pe[1] != b'Z' {
@@ -153,7 +155,7 @@ fn find_reflective_loader_offset(pe: &[u8]) -> Option<u32> {
let funcs_off = rva_to_file_offset(funcs_rva, pe, section_off, num_sections)? as usize;
let ordinals_off = rva_to_file_offset(ordinals_rva, pe, section_off, num_sections)? as usize;
let find_buf = obf!(b"ReflectiveLoader");
let find_buf = crate::loader_entry::LOADER_EXPORT;
let find_name = &find_buf[..find_buf.len() - 1];
for i in 0..num_names as usize {
let name_ent = names_off + i * 4;
@@ -169,7 +171,11 @@ fn find_reflective_loader_offset(pe: &[u8]) -> Option<u32> {
continue;
}
let name = &pe[name_fo..name_fo + find_name.len()];
if name != find_name || pe.get(name_fo + find_name.len()) != Some(&0) {
if !(name == find_name || is_loader_token(name)) {
continue;
}
let name_end = name_fo + find_name.len();
if pe.get(name_end) != Some(&0) {
continue;
}
let ord_ent = ordinals_off + i * 2;
@@ -187,6 +193,21 @@ fn find_reflective_loader_offset(pe: &[u8]) -> Option<u32> {
None
}
/// True when `name` looks like the per-build loader token `x` + six lowercase
/// hex digits (the exact length `build.rs` emits). Used so a DLL artifact built
/// by a neighbouring build is still recognized.
fn is_loader_token(name: &[u8]) -> bool {
if name.len() != 7 || name[0] != b'x' {
return false;
}
for &b in &name[1..] {
if !matches!(b, b'0'..=b'9' | b'a'..=b'f') {
return false;
}
}
true
}
fn rva_to_file_offset(rva: u32, pe: &[u8], section_off: usize, num_sections: u16) -> Option<u32> {
for i in 0..num_sections as usize {
let s = section_off + i * 40;
@@ -214,7 +235,7 @@ fn rva_to_file_offset(rva: u32, pe: &[u8], section_off: usize, num_sections: u16
}
/// Inject raw DLL bytes into `process` by copying them at their natural file
/// offsets and running our exported `ReflectiveLoader` on a remote thread.
/// offsets and running our exported loader entry on a remote thread.
/// Returns false on any failure; callers treat failure as fail-open.
pub unsafe fn inject_reflective(process: usize, bytes: &[u8]) -> bool {
let Some(loader_offset) = find_reflective_loader_offset(bytes) else {
@@ -406,7 +427,7 @@ mod tests {
eprintln!("skipping: built DLL not found (run cargo build first)");
return;
};
let offset = find_reflective_loader_offset(&dll).expect("ReflectiveLoader export in built DLL");
let offset = find_reflective_loader_offset(&dll).expect("loader export in built DLL");
assert!(offset > 0, "loader offset must be inside the image");
assert!((offset as usize) < dll.len());
}
+6 -2
View File
@@ -10,8 +10,9 @@
//! from the shared-section bytes, or falls back to Injecting the module's
//! own on-disk path via LoadLibraryW when no section is present.
//!
//! The cdylib also exports `ReflectiveLoader` (implemented in pure Rust in
//! `reflective.rs`) so the initial injection itself can be reflective.
//! The cdylib also exports a randomly named loader thunk (generated per-build
//! by `build.rs` into `loader_entry.rs`) so the initial injection itself can
//! be reflective; the implementation lives in `reflective.rs`.
//!
//! On DLL_PROCESS_DETACH all hooks are disabled best-effort.
@@ -23,6 +24,9 @@ mod abi;
mod config;
mod hooks;
mod inject;
mod loader_entry {
include!(concat!(env!("OUT_DIR"), "/loader_entry.rs"));
}
mod log;
mod reflective;
mod util;
+6 -2
View File
@@ -323,8 +323,12 @@ unsafe fn export_by_ordinal(base: usize, ordinal: u16) -> usize {
/// Thread-start routine invoked by `inject_reflective` on the copied,
/// un-relocated image. Returns the address of the newly loaded DLL's entry
/// point, or 0 on failure.
#[unsafe(no_mangle)]
pub extern "system" fn ReflectiveLoader(lpParameter: usize) -> usize {
///
/// Not exported: the public `.edata` entry is the randomly named thunk emitted
/// by `build.rs` (`loader_entry.rs`); keeping a fixed exported name out of the
/// binary avoids a constant string in the export name table. The thunk is a
/// relative call, so it adds no relocation and stays position independent.
pub extern "system" fn loader_impl(lpParameter: usize) -> usize {
unsafe {
// STEP 0: locate our own (un-relocated) image base.
let ui_lib = find_image_base(rip_here());