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