obfuscation
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! Redirect configuration read from environment variables on attach.
|
||||
|
||||
use crate::abi::GetEnvironmentVariableW;
|
||||
use crate::{obf16};
|
||||
|
||||
const ENV_BUFFER_WCHARS: u32 = 2048;
|
||||
|
||||
@@ -10,20 +11,14 @@ pub struct HookConfig {
|
||||
pub replace: Vec<u16>,
|
||||
}
|
||||
|
||||
const RDI_SEARCH_PATH: &str = "RDI_SEARCH_PATH";
|
||||
const RDI_REPLACE_PATH: &str = "RDI_REPLACE_PATH";
|
||||
pub const RDI_DLL_SECTION: &str = "RDI_DLL_SECTION";
|
||||
pub const RDI_DLL_SIZE: &str = "RDI_DLL_SIZE";
|
||||
|
||||
pub fn load() -> HookConfig {
|
||||
HookConfig {
|
||||
search: read_env(RDI_SEARCH_PATH),
|
||||
replace: read_env(RDI_REPLACE_PATH),
|
||||
search: read_env(&obf16!(b"RDI_SEARCH_PATH")),
|
||||
replace: read_env(&obf16!(b"RDI_REPLACE_PATH")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_env(name: &str) -> Vec<u16> {
|
||||
let name = name.encode_utf16().chain(core::iter::once(0)).collect::<Vec<u16>>();
|
||||
pub fn read_env(name: &[u16]) -> 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);
|
||||
|
||||
@@ -5,7 +5,46 @@ use std::slice;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use minhook::MinHook;
|
||||
mod mh {
|
||||
//! Thin FFI over the linked MinHook C engine. We bypass the `minhook`
|
||||
//! crate's wrapper type so its panic/format strings are never emitted.
|
||||
|
||||
use core::ffi::{c_int, c_void};
|
||||
|
||||
pub const OK: c_int = 0;
|
||||
const ALL_HOOKS: *mut c_void = -1isize as *mut c_void;
|
||||
|
||||
unsafe extern "system" {
|
||||
fn MH_Initialize() -> c_int;
|
||||
fn MH_CreateHook(target: *mut c_void, detour: *mut c_void, original: *mut *mut c_void)
|
||||
-> c_int;
|
||||
fn MH_EnableHook(target: *mut c_void) -> c_int;
|
||||
fn MH_RemoveHook(target: *mut c_void) -> c_int;
|
||||
fn MH_DisableHook(target: *mut c_void) -> c_int;
|
||||
}
|
||||
|
||||
pub fn init() {
|
||||
let _ = unsafe { MH_Initialize() };
|
||||
}
|
||||
|
||||
pub fn disable_all() {
|
||||
let _ = unsafe { MH_DisableHook(ALL_HOOKS) };
|
||||
}
|
||||
|
||||
pub fn create_hook(target: *mut c_void, detour: *mut c_void) -> (c_int, *mut c_void) {
|
||||
let mut original: *mut c_void = core::ptr::null_mut();
|
||||
let status = unsafe { MH_CreateHook(target, detour, &mut original) };
|
||||
(status, original)
|
||||
}
|
||||
|
||||
pub fn enable_hook(target: *mut c_void) -> c_int {
|
||||
unsafe { MH_EnableHook(target) }
|
||||
}
|
||||
|
||||
pub fn remove_hook(target: *mut c_void) {
|
||||
let _ = unsafe { MH_RemoveHook(target) };
|
||||
}
|
||||
}
|
||||
|
||||
use crate::abi::{
|
||||
self, CreateProcessWFn, IoStatusBlock, NtCreateFileFn, NtDeleteFileFn, NtOpenFileFn,
|
||||
@@ -15,7 +54,7 @@ use crate::abi::{
|
||||
use crate::config::HookConfig;
|
||||
use crate::inject;
|
||||
use crate::util::{self, UNICODE_STRING_MAX_WCHARS};
|
||||
use crate::dbg_log;
|
||||
use crate::{dbg_log, obf, obf16};
|
||||
|
||||
static HOOKS_CONFIG: OnceLock<HookConfig> = OnceLock::new();
|
||||
|
||||
@@ -374,7 +413,7 @@ pub extern "system" fn detour_create_process_w(
|
||||
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"
|
||||
"cpr child pid={child_pid} created but hooks NOT active — skipping injection"
|
||||
);
|
||||
} else {
|
||||
let h_process = unsafe { (*lp_process_information).hProcess };
|
||||
@@ -382,17 +421,17 @@ pub extern "system" fn detour_create_process_w(
|
||||
unsafe { inject::inject_reflective(h_process, bytes) }
|
||||
} else if let Some(path) = inject::our_path() {
|
||||
if path.is_empty() {
|
||||
dbg_log!("CreateProcessW child pid={child_pid} injection skipped: our_path empty");
|
||||
dbg_log!("cpr child pid={child_pid} injection skipped: our_path empty");
|
||||
false
|
||||
} else {
|
||||
unsafe { inject::inject_library(h_process, path) }
|
||||
}
|
||||
} else {
|
||||
dbg_log!("CreateProcessW child pid={child_pid} injection skipped: no dll bytes or path");
|
||||
dbg_log!("cpr child pid={child_pid} injection skipped: no dll bytes or path");
|
||||
false
|
||||
};
|
||||
dbg_log!(
|
||||
"CreateProcessW child pid={child_pid} app='{}' inject={injected} mode={} suspended_forced={}",
|
||||
"cpr child pid={child_pid} app='{}' inject={injected} mode={} suspended_forced={}",
|
||||
display_cmd(lp_application_name, lp_command_line),
|
||||
if inject::dll_bytes().is_some() { "reflective" } else { "loadlibrary" },
|
||||
!was_suspended,
|
||||
@@ -405,7 +444,7 @@ pub extern "system" fn detour_create_process_w(
|
||||
}
|
||||
} else {
|
||||
dbg_log!(
|
||||
"CreateProcessW failed or no proc info (result={result}) — no injection"
|
||||
"cpr failed or no proc info (result={result}) — no injection"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -447,30 +486,27 @@ fn install_one(
|
||||
);
|
||||
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
|
||||
}
|
||||
let (status, orig) = mh::create_hook(target as *mut c_void, detour as *mut c_void);
|
||||
if status != mh::OK {
|
||||
dbg_log!(
|
||||
"hook create failed for '{}': status={}",
|
||||
String::from_utf8_lossy(&proc_name[..proc_name.len().saturating_sub(1)]),
|
||||
status
|
||||
);
|
||||
return false;
|
||||
}
|
||||
let status = mh::enable_hook(target as *mut c_void);
|
||||
if status == mh::OK {
|
||||
store.store(orig as usize, Ordering::SeqCst);
|
||||
true
|
||||
} else {
|
||||
mh::remove_hook(target as *mut c_void);
|
||||
dbg_log!(
|
||||
"hook enable failed for '{}': status={}",
|
||||
String::from_utf8_lossy(&proc_name[..proc_name.len().saturating_sub(1)]),
|
||||
status
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,32 +519,30 @@ pub fn install(cfg: HookConfig) {
|
||||
redirect_on,
|
||||
);
|
||||
let _ = HOOKS_CONFIG.set(cfg);
|
||||
mh::init();
|
||||
|
||||
let ntdll = util::wide_zstr("ntdll.dll");
|
||||
let kernel32 = util::wide_zstr("kernel32.dll");
|
||||
let ntdll = obf16!(b"ntdll.dll");
|
||||
let kernel32 = obf16!(b"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 create = install_one(&ntdll, &obf!(b"NtCreateFile"), detour_nt_create_file as *const () as usize, &ORIG_NT_CREATE_FILE);
|
||||
let open = install_one(&ntdll, &obf!(b"NtOpenFile"), detour_nt_open_file as *const () as usize, &ORIG_NT_OPEN_FILE);
|
||||
let delete = install_one(&ntdll, &obf!(b"NtDeleteFile"), detour_nt_delete_file as *const () as usize, &ORIG_NT_DELETE_FILE);
|
||||
let set_info = install_one(&ntdll, &obf!(b"NtSetInformationFile"), detour_nt_set_information_file as *const () as usize, &ORIG_NT_SET_INFORMATION_FILE);
|
||||
let query_attrs = install_one(&ntdll, &obf!(b"NtQueryAttributesFile"), detour_nt_query_attributes_file as *const () as usize, &ORIG_NT_QUERY_ATTRIBUTES_FILE);
|
||||
let query_full = install_one(&ntdll, &obf!(b"NtQueryFullAttributesFile"), detour_nt_query_full_attributes_file as *const () as usize, &ORIG_NT_QUERY_FULL_ATTRIBUTES_FILE);
|
||||
let cpr = install_one(&kernel32, &obf!(b"CreateProcessW"), 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,
|
||||
);
|
||||
dbg_log!("install: f0={} f1={} f2={} f3={} f4={} f5={} f6={}",
|
||||
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)");
|
||||
dbg_log!("install: hooks active (both create and open live)");
|
||||
} else {
|
||||
unsafe {
|
||||
let _ = MinHook::disable_all_hooks();
|
||||
}
|
||||
mh::disable_all();
|
||||
ORIG_NT_CREATE_FILE.store(0, Ordering::SeqCst);
|
||||
ORIG_NT_OPEN_FILE.store(0, Ordering::SeqCst);
|
||||
ORIG_NT_DELETE_FILE.store(0, Ordering::SeqCst);
|
||||
@@ -521,9 +555,7 @@ pub fn install(cfg: HookConfig) {
|
||||
}
|
||||
|
||||
pub fn remove() {
|
||||
unsafe {
|
||||
let _ = MinHook::disable_all_hooks();
|
||||
}
|
||||
mh::disable_all();
|
||||
unsafe {
|
||||
abi::Sleep(50);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use std::sync::OnceLock;
|
||||
|
||||
use crate::abi::{self, GetModuleFileNameW};
|
||||
use crate::dbg_log;
|
||||
use crate::{obf, obf16};
|
||||
|
||||
static OUR_PATH: OnceLock<Vec<u16>> = OnceLock::new();
|
||||
static DLL_BYTES: OnceLock<Vec<u8>> = OnceLock::new();
|
||||
@@ -51,23 +52,23 @@ fn dll_path_from_hinst(h_instance: usize) -> Vec<u16> {
|
||||
/// DLL bytes out of it. Returns None (without touching the section) when the
|
||||
/// parent did not provide it — the loadlibrary-mode disk fallback then applies.
|
||||
pub unsafe fn load_dll_bytes_from_section() -> Option<Vec<u8>> {
|
||||
let name = crate::config::read_env(crate::config::RDI_DLL_SECTION);
|
||||
let size_raw = crate::config::read_env(crate::config::RDI_DLL_SIZE);
|
||||
let name = crate::config::read_env(&obf16!(b"RDI_DLL_SECTION"));
|
||||
let size_raw = crate::config::read_env(&obf16!(b"RDI_DLL_SIZE"));
|
||||
if name.is_empty() || size_raw.is_empty() {
|
||||
dbg_log!("section: no RDI_DLL_SECTION/RDI_DLL_SIZE env — reflective child inject disabled");
|
||||
dbg_log!("section: no section env — reflective child inject disabled");
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut size: usize = 0;
|
||||
for &c in &size_raw {
|
||||
if !(0x30..=0x39).contains(&c) {
|
||||
dbg_log!("section: RDI_DLL_SIZE not ASCII digits — reflective child inject disabled");
|
||||
dbg_log!("section: size not ASCII digits — reflective child inject disabled");
|
||||
return None;
|
||||
}
|
||||
size = size.saturating_mul(10).saturating_add((c - 0x30) as usize);
|
||||
}
|
||||
if size == 0 {
|
||||
dbg_log!("section: RDI_DLL_SIZE is 0 — reflective child inject disabled");
|
||||
dbg_log!("section: size is 0 — reflective child inject disabled");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -152,7 +153,8 @@ 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_name = b"ReflectiveLoader";
|
||||
let find_buf = obf!(b"ReflectiveLoader");
|
||||
let find_name = &find_buf[..find_buf.len() - 1];
|
||||
for i in 0..num_names as usize {
|
||||
let name_ent = names_off + i * 4;
|
||||
if name_ent + 4 > pe.len() {
|
||||
@@ -216,7 +218,7 @@ fn rva_to_file_offset(rva: u32, pe: &[u8], section_off: usize, num_sections: u16
|
||||
/// 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 {
|
||||
dbg_log!("inject-reflective: ReflectiveLoader export not found in DLL bytes");
|
||||
dbg_log!("inject-reflective: loader export not found in DLL bytes");
|
||||
return false;
|
||||
};
|
||||
dbg_log!("inject-reflective: loader offset=0x{loader_offset:x} size={}", bytes.len());
|
||||
@@ -288,15 +290,15 @@ pub unsafe fn inject_library(process: usize, path: &[u16]) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
let kernel32 = crate::util::wide_zstr("kernel32.dll");
|
||||
let kernel32 = obf16!(b"kernel32.dll");
|
||||
let k32 = unsafe { abi::GetModuleHandleW(kernel32.as_ptr()) };
|
||||
if k32 == 0 {
|
||||
dbg_log!("inject: kernel32 not loaded — abort");
|
||||
dbg_log!("inject: system dll not loaded — abort");
|
||||
return false;
|
||||
}
|
||||
let load_library_w = unsafe { abi::GetProcAddress(k32, c"LoadLibraryW".as_ptr().cast()) };
|
||||
let load_library_w = unsafe { abi::GetProcAddress(k32, obf!(b"LoadLibraryW").as_ptr()) };
|
||||
if load_library_w == 0 {
|
||||
dbg_log!("inject: LoadLibraryW not found — abort");
|
||||
dbg_log!("inject: load-fn not found — abort");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#![allow(clippy::missing_safety_doc)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
mod obf;
|
||||
mod abi;
|
||||
mod config;
|
||||
mod hooks;
|
||||
|
||||
@@ -12,12 +12,13 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::abi;
|
||||
use crate::{obf16};
|
||||
|
||||
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 name = obf16!(b"BackstageInjectionDebug");
|
||||
let mut buf = [0u16; 8];
|
||||
unsafe {
|
||||
let len = abi::GetEnvironmentVariableW(name.as_ptr(), buf.as_mut_ptr(), buf.len() as u32);
|
||||
@@ -102,28 +103,48 @@ fn open_log() -> Option<usize> {
|
||||
}
|
||||
|
||||
fn log_paths() -> Option<(Vec<u16>, Vec<u16>, Vec<u16>)> {
|
||||
let env_name = crate::util::wide_zstr("TEMP");
|
||||
let env_name = obf16!(b"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 prefix = &temp[..len as usize];
|
||||
let base = concat_z(prefix, &obf16!(b"\\Overlord"));
|
||||
let dir = concat_z(prefix, &obf16!(b"\\Overlord\\backstage"));
|
||||
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"),
|
||||
);
|
||||
let mut file = prefix.to_vec();
|
||||
file.extend_from_slice(&obf16!(b"\\Overlord\\backstage\\BackstageInjection-debug-"));
|
||||
append_pid(&mut file, pid);
|
||||
file.extend_from_slice(&obf16!(b".log"));
|
||||
file.push(0);
|
||||
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);
|
||||
fn concat_z(prefix: &[u16], suffix: &[u16]) -> Vec<u16> {
|
||||
let mut v = Vec::with_capacity(prefix.len() + suffix.len() + 1);
|
||||
v.extend_from_slice(prefix);
|
||||
v.extend(suffix.encode_utf16());
|
||||
v.extend_from_slice(suffix);
|
||||
v.push(0);
|
||||
v
|
||||
}
|
||||
|
||||
fn append_pid(v: &mut Vec<u16>, mut pid: u32) {
|
||||
if pid == 0 {
|
||||
v.push(0x0030); // '0'
|
||||
return;
|
||||
}
|
||||
let mut digits = [0u16; 10];
|
||||
let mut n = 0;
|
||||
while pid > 0 {
|
||||
digits[n] = 0x0030 + (pid % 10) as u16;
|
||||
pid /= 10;
|
||||
n += 1;
|
||||
}
|
||||
while n > 0 {
|
||||
n -= 1;
|
||||
v.push(digits[n]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//! Build-time string obfuscation.
|
||||
//!
|
||||
//! `obf!` and `obf16!` XOR-encode string literals at compile time so the
|
||||
//! plaintext never appears as a contiguous byte sequence in the binary's
|
||||
//! `.rodata`. The runtime expansion decrypts into a fresh, NUL-terminated
|
||||
//! stack buffer allocated at each call site.
|
||||
//!
|
||||
//! - `obf!` produces a `[u8; N]` buffer (N = length + trailing NUL). Use for
|
||||
//! ANSI strings handed to `GetProcAddress` and friends.
|
||||
//! - `obf16!` produces a `[u16; N]` buffer (wide). Use for module names, env
|
||||
//! var names, and paths handed to `GetModuleHandleW`,
|
||||
//! `GetEnvironmentVariableW`, and `CreateFileW`.
|
||||
//!
|
||||
//! Each decrypted byte is committed with a `write_volatile` store, and the XOR
|
||||
//! key is passed through `core::hint::black_box` so LLVM cannot fold the
|
||||
//! decrypt and promote the fully constant stack buffer back into `.rodata` as
|
||||
//! a plaintext global string table.
|
||||
|
||||
pub const XOR_KEY8: u8 = 0x3C;
|
||||
pub const XOR_KEY16: u16 = 0x4D7A;
|
||||
|
||||
pub const fn xor8<const N: usize>(src: &[u8], key: u8) -> [u8; N] {
|
||||
let mut out = [0u8; N];
|
||||
let n = if N < src.len() { N } else { src.len() };
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
out[i] = src[i] ^ key;
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub const fn xor16<const N: usize>(src: &[u8], key: u16) -> [u16; N] {
|
||||
let mut out = [0u16; N];
|
||||
let n = if N < src.len() { N } else { src.len() };
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
out[i] = (src[i] as u16) ^ key;
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! obf {
|
||||
($lit:literal) => {{
|
||||
const SRC: &[u8] = $lit;
|
||||
const LEN: usize = SRC.len() + 1;
|
||||
const ENC: [u8; LEN] = $crate::obf::xor8(SRC, $crate::obf::XOR_KEY8);
|
||||
let k = core::hint::black_box($crate::obf::XOR_KEY8);
|
||||
let mut buf = [0u8; LEN];
|
||||
let mut i = 0;
|
||||
while i < LEN - 1 {
|
||||
unsafe {
|
||||
(buf.as_mut_ptr() as *mut u8)
|
||||
.add(i)
|
||||
.write_volatile(ENC[i] ^ k);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
buf
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! obf16 {
|
||||
($lit:literal) => {{
|
||||
const SRC: &[u8] = $lit;
|
||||
const LEN: usize = SRC.len() + 1;
|
||||
const ENC: [u16; LEN] = $crate::obf::xor16(SRC, $crate::obf::XOR_KEY16);
|
||||
let k = core::hint::black_box($crate::obf::XOR_KEY16);
|
||||
let mut buf = [0u16; LEN];
|
||||
let mut i = 0;
|
||||
while i < LEN - 1 {
|
||||
unsafe {
|
||||
(buf.as_mut_ptr() as *mut u16)
|
||||
.add(i)
|
||||
.write_volatile(ENC[i] ^ k);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
buf
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{xor8, xor16, XOR_KEY8, XOR_KEY16};
|
||||
|
||||
#[test]
|
||||
fn byte_roundtrip_is_nul_terminated() {
|
||||
let s = obf!(b"ntdll.dll");
|
||||
assert_eq!(&s[..], b"ntdll.dll\0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_encoded_bytes_are_not_plaintext() {
|
||||
const ENC: [u8; 10] = xor8(b"ntdll.dll", XOR_KEY8);
|
||||
assert_ne!(&ENC[..9], b"ntdll.dll");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_roundtrip_is_nul_terminated() {
|
||||
let s = obf16!(b"kernel32.dll");
|
||||
let mut expect: Vec<u16> = b"kernel32.dll".iter().map(|&c| c as u16).collect();
|
||||
expect.push(0);
|
||||
assert_eq!(&s[..], &expect[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_encoded_bytes_are_not_plaintext() {
|
||||
const ENC: [u16; 12] = xor16(b"kernel32.dll", XOR_KEY16);
|
||||
let plain: Vec<u16> = b"kernel32.dll".iter().map(|&c| c as u16).collect();
|
||||
assert_ne!(&ENC[..11], &plain[..]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//! Build-time string obfuscation.
|
||||
//!
|
||||
//! `obf!` and `obf16!` XOR-encode string literals at compile time so the
|
||||
//! plaintext never appears as a contiguous byte sequence in the binary's
|
||||
//! `.rodata`. The runtime expansion decrypts into a fresh, NUL-terminated
|
||||
//! stack buffer allocated at each call site.
|
||||
//!
|
||||
//! - `obf!` produces a `[u8; N]` buffer (N = length + trailing NUL). Use for
|
||||
//! ANSI strings handed to `GetProcAddress` and friends.
|
||||
//! - `obf16!` produces a `[u16; N]` buffer (wide). Use for module names, env
|
||||
//! var names, and paths handed to `GetModuleHandleW`,
|
||||
//! `GetEnvironmentVariableW`, and `CreateFileW`.
|
||||
//!
|
||||
//! Each decrypted byte is committed with a `write_volatile` store. That keeps
|
||||
//! LLVM from folding the decrypt loop and promoting the fully constant stack
|
||||
//! buffer back into `.rodata` as a plaintext global string table.
|
||||
|
||||
pub const XOR_KEY8: u8 = 0x3C;
|
||||
pub const XOR_KEY16: u16 = 0x4D7A;
|
||||
|
||||
pub const fn xor8<const N: usize>(src: &[u8], key: u8) -> [u8; N] {
|
||||
let mut out = [0u8; N];
|
||||
let n = if N < src.len() { N } else { src.len() };
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
out[i] = src[i] ^ key;
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub const fn xor16<const N: usize>(src: &[u8], key: u16) -> [u16; N] {
|
||||
let mut out = [0u16; N];
|
||||
let n = if N < src.len() { N } else { src.len() };
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
out[i] = (src[i] as u16) ^ key;
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! obf {
|
||||
($lit:literal) => {{
|
||||
const SRC: &[u8] = $lit;
|
||||
const LEN: usize = SRC.len() + 1;
|
||||
const ENC: [u8; LEN] = $crate::obf::xor8(SRC, $crate::obf::XOR_KEY8);
|
||||
let mut buf = [0u8; LEN];
|
||||
let mut i = 0;
|
||||
while i < LEN - 1 {
|
||||
unsafe {
|
||||
(buf.as_mut_ptr() as *mut u8)
|
||||
.add(i)
|
||||
.write_volatile(ENC[i] ^ $crate::obf::XOR_KEY8);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
buf
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! obf16 {
|
||||
($lit:literal) => {{
|
||||
const SRC: &[u8] = $lit;
|
||||
const LEN: usize = SRC.len() + 1;
|
||||
const ENC: [u16; LEN] = $crate::obf::xor16(SRC, $crate::obf::XOR_KEY16);
|
||||
let mut buf = [0u16; LEN];
|
||||
let mut i = 0;
|
||||
while i < LEN - 1 {
|
||||
unsafe {
|
||||
(buf.as_mut_ptr() as *mut u16)
|
||||
.add(i)
|
||||
.write_volatile(ENC[i] ^ $crate::obf::XOR_KEY16);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
buf
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{xor8, xor16, XOR_KEY8, XOR_KEY16};
|
||||
|
||||
#[test]
|
||||
fn byte_roundtrip_is_nul_terminated() {
|
||||
let s = obf!(b"ntdll.dll");
|
||||
assert_eq!(&s[..], b"ntdll.dll\0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_encoded_bytes_are_not_plaintext() {
|
||||
const ENC: [u8; 10] = xor8(b"ntdll.dll", XOR_KEY8);
|
||||
assert_ne!(&ENC[..9], b"ntdll.dll");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_roundtrip_is_nul_terminated() {
|
||||
let s = obf16!(b"kernel32.dll");
|
||||
let mut expect: Vec<u16> = b"kernel32.dll".iter().map(|&c| c as u16).collect();
|
||||
expect.push(0);
|
||||
assert_eq!(&s[..], &expect[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_encoded_bytes_are_not_plaintext() {
|
||||
const ENC: [u16; 12] = xor16(b"kernel32.dll", XOR_KEY16);
|
||||
let plain: Vec<u16> = b"kernel32.dll".iter().map(|&c| c as u16).collect();
|
||||
assert_ne!(&ENC[..11], &plain[..]);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,6 @@ 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();
|
||||
@@ -35,10 +31,9 @@ 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) {
|
||||
let dev_pfx = crate::obf16!(b"\\Device\\");
|
||||
let pfx = &dev_pfx[..dev_pfx.len() - 1];
|
||||
if path.len() >= pfx.len() && ci_eq(&path[..pfx.len()], pfx) {
|
||||
return (path, 0);
|
||||
}
|
||||
(path, 0)
|
||||
|
||||
Reference in New Issue
Block a user