random loader name
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let root_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
let root_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||||
let mh_dir = std::path::Path::new(&root_dir).join("vendor/minhook");
|
let mh_dir = std::path::Path::new(&root_dir).join("vendor/minhook");
|
||||||
@@ -22,5 +24,33 @@ fn main() {
|
|||||||
.file(mh_src.join(hde))
|
.file(mh_src.join(hde))
|
||||||
.compile("libminhook.a");
|
.compile("libminhook.a");
|
||||||
|
|
||||||
|
generate_loader_entry();
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=vendor/minhook");
|
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:
|
//! Two strategies mirror the C reference:
|
||||||
//! - in-memory reflective injection (default): raw DLL bytes come from the
|
//! - in-memory reflective injection (default): raw DLL bytes come from the
|
||||||
//! `RDI_DLL_SECTION` page-file-backed named section passed in the child's
|
//! `RDI_DLL_SECTION` page-file-backed named section passed in the child's
|
||||||
//! environment block, and each child runs our exported `ReflectiveLoader`
|
//! environment block, and each child runs our exported loader entry (a
|
||||||
//! on a remote thread — no file on disk;
|
//! 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
|
//! - disk fallback: the module's own on-disk path is injected with
|
||||||
//! LoadLibraryW when no section bytes were provided.
|
//! LoadLibraryW when no section bytes were provided.
|
||||||
|
|
||||||
@@ -98,8 +98,10 @@ pub unsafe fn load_dll_bytes_from_section() -> Option<Vec<u8>> {
|
|||||||
Some(bytes)
|
Some(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Locate the file offset of the `ReflectiveLoader` export within a raw PE
|
/// Locate the file offset of this DLL's loader export within a raw PE image
|
||||||
/// image (raw memory offset == file offset because the bytes are copied 1:1).
|
/// (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.
|
/// Reads are bounds-checked so a truncated image never faults.
|
||||||
fn find_reflective_loader_offset(pe: &[u8]) -> Option<u32> {
|
fn find_reflective_loader_offset(pe: &[u8]) -> Option<u32> {
|
||||||
if pe.len() < 64 || pe[0] != b'M' || pe[1] != b'Z' {
|
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 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 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];
|
let find_name = &find_buf[..find_buf.len() - 1];
|
||||||
for i in 0..num_names as usize {
|
for i in 0..num_names as usize {
|
||||||
let name_ent = names_off + i * 4;
|
let name_ent = names_off + i * 4;
|
||||||
@@ -169,7 +171,11 @@ fn find_reflective_loader_offset(pe: &[u8]) -> Option<u32> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let name = &pe[name_fo..name_fo + find_name.len()];
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
let ord_ent = ordinals_off + i * 2;
|
let ord_ent = ordinals_off + i * 2;
|
||||||
@@ -187,6 +193,21 @@ fn find_reflective_loader_offset(pe: &[u8]) -> Option<u32> {
|
|||||||
None
|
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> {
|
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 {
|
for i in 0..num_sections as usize {
|
||||||
let s = section_off + i * 40;
|
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
|
/// 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.
|
/// Returns false on any failure; callers treat failure as fail-open.
|
||||||
pub unsafe fn inject_reflective(process: usize, bytes: &[u8]) -> bool {
|
pub unsafe fn inject_reflective(process: usize, bytes: &[u8]) -> bool {
|
||||||
let Some(loader_offset) = find_reflective_loader_offset(bytes) else {
|
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)");
|
eprintln!("skipping: built DLL not found (run cargo build first)");
|
||||||
return;
|
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 > 0, "loader offset must be inside the image");
|
||||||
assert!((offset as usize) < dll.len());
|
assert!((offset as usize) < dll.len());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@
|
|||||||
//! from the shared-section bytes, or falls back to Injecting the module's
|
//! from the shared-section bytes, or falls back to Injecting the module's
|
||||||
//! own on-disk path via LoadLibraryW when no section is present.
|
//! own on-disk path via LoadLibraryW when no section is present.
|
||||||
//!
|
//!
|
||||||
//! The cdylib also exports `ReflectiveLoader` (implemented in pure Rust in
|
//! The cdylib also exports a randomly named loader thunk (generated per-build
|
||||||
//! `reflective.rs`) so the initial injection itself can be reflective.
|
//! 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.
|
//! On DLL_PROCESS_DETACH all hooks are disabled best-effort.
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@ mod abi;
|
|||||||
mod config;
|
mod config;
|
||||||
mod hooks;
|
mod hooks;
|
||||||
mod inject;
|
mod inject;
|
||||||
|
mod loader_entry {
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/loader_entry.rs"));
|
||||||
|
}
|
||||||
mod log;
|
mod log;
|
||||||
mod reflective;
|
mod reflective;
|
||||||
mod util;
|
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,
|
/// Thread-start routine invoked by `inject_reflective` on the copied,
|
||||||
/// un-relocated image. Returns the address of the newly loaded DLL's entry
|
/// un-relocated image. Returns the address of the newly loaded DLL's entry
|
||||||
/// point, or 0 on failure.
|
/// 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 {
|
unsafe {
|
||||||
// STEP 0: locate our own (un-relocated) image base.
|
// STEP 0: locate our own (un-relocated) image base.
|
||||||
let ui_lib = find_image_base(rip_here());
|
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 {
|
func reflectiveInject(hProcess uintptr, dllBytes []byte) error {
|
||||||
loaderOffset, err := findReflectiveLoaderOffset(dllBytes)
|
loaderOffset, err := findReflectiveLoaderOffset(dllBytes)
|
||||||
if err != nil {
|
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(
|
remoteBase, _, _ := procVirtualAllocEx.Call(
|
||||||
hProcess,
|
hProcess,
|
||||||
@@ -1736,14 +1736,36 @@ func findReflectiveLoaderOffset(pe []byte) (uint32, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
name := readCString(pe, nameFileOff)
|
name := readCString(pe, nameFileOff)
|
||||||
if contains(name, "ReflectiveLoader") {
|
if isBackstageLoaderExport(name) {
|
||||||
ordinal := binary.LittleEndian.Uint16(pe[ordinalsOff+i*2 : ordinalsOff+i*2+2])
|
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])
|
funcRVA := binary.LittleEndian.Uint32(pe[funcsOff+uint32(ordinal)*4 : funcsOff+uint32(ordinal)*4+4])
|
||||||
return rvaToFileOffset(funcRVA, pe, sectionOff, numberOfSections), nil
|
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 {
|
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])
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user