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());
@@ -1590,9 +1590,9 @@ func loadLibraryInject(hProcess uintptr, dllPath string) error {
func reflectiveInject(hProcess uintptr, dllBytes []byte) error {
loaderOffset, err := findReflectiveLoaderOffset(dllBytes)
if err != nil {
return fmt.Errorf("failed to find ReflectiveLoader: %v", err)
return fmt.Errorf("failed to find loader export: %v", err)
}
log.Printf("backstage inject: ReflectiveLoader at offset 0x%x", loaderOffset)
log.Printf("backstage inject: loader at offset 0x%x", loaderOffset)
remoteBase, _, _ := procVirtualAllocEx.Call(
hProcess,
@@ -1736,14 +1736,36 @@ func findReflectiveLoaderOffset(pe []byte) (uint32, error) {
}
name := readCString(pe, nameFileOff)
if contains(name, "ReflectiveLoader") {
if isBackstageLoaderExport(name) {
ordinal := binary.LittleEndian.Uint16(pe[ordinalsOff+i*2 : ordinalsOff+i*2+2])
funcRVA := binary.LittleEndian.Uint32(pe[funcsOff+uint32(ordinal)*4 : funcsOff+uint32(ordinal)*4+4])
return rvaToFileOffset(funcRVA, pe, sectionOff, numberOfSections), nil
}
}
return 0, fmt.Errorf("ReflectiveLoader export not found")
return 0, fmt.Errorf("loader export not found")
}
// isBackstageLoaderExport reports whether the given PE export name is the
// BackstageInjection reflective loader entry. The loader entry is exported
// under a per-build random name of the form "x" + six lowercase hex digits
// (generated by the DLL's build.rs), so a fixed string cannot be used to
// find it. The legacy fixed name is still accepted so DLL artifacts produced
// by older builds keep working.
func isBackstageLoaderExport(name string) bool {
if name == "ReflectiveLoader" {
return true
}
if len(name) != 7 || name[0] != 'x' {
return false
}
for i := 1; i < 7; i++ {
c := name[i]
if !(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') {
return false
}
}
return true
}
func rvaToFileOffset(rva uint32, pe []byte, sectionOff uint32, numSections uint16) uint32 {
@@ -1778,15 +1800,3 @@ func readCString(data []byte, offset uint32) string {
}
return string(data[offset:end])
}
func contains(s, substr string) bool {
if len(substr) > len(s) {
return false
}
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}