use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use dashmap::DashMap;
use crate::{
emulation::{
memory::MemoryProtection,
runtime::{
hook::{HookContext, PreHookResult},
Hook, HookManager, HookPriority,
},
thread::EmulationThread,
tokens::{self, native_addresses},
EmValue,
},
metadata::token::Token,
Result,
};
#[derive(Clone, Debug)]
pub struct NativeFunctionRegistry {
address_to_name: Arc<DashMap<u64, Arc<str>>>,
token_to_name: Arc<DashMap<u32, Arc<str>>>,
next_address: Arc<AtomicU64>,
next_token_id: Arc<AtomicU64>,
}
impl NativeFunctionRegistry {
pub fn new() -> Self {
Self {
address_to_name: Arc::new(DashMap::new()),
token_to_name: Arc::new(DashMap::new()),
next_address: Arc::new(AtomicU64::new(native_addresses::PROC_ADDRESS_BASE)),
next_token_id: Arc::new(AtomicU64::new(1)),
}
}
pub fn register_proc(&self, name: &str) -> u64 {
let name: Arc<str> = Arc::from(name);
for entry in self.address_to_name.iter() {
if *entry.value() == name {
return *entry.key();
}
}
let addr = self
.next_address
.fetch_add(native_addresses::PROC_ADDRESS_PAGE, Ordering::Relaxed);
self.address_to_name.insert(addr, name);
addr
}
pub fn lookup_by_address(&self, addr: u64) -> Option<Arc<str>> {
self.address_to_name
.get(&addr)
.map(|v| Arc::clone(v.value()))
}
pub fn allocate_token(&self, name: &str) -> Token {
let name: Arc<str> = Arc::from(name);
for entry in self.token_to_name.iter() {
if *entry.value() == name {
return tokens::native::token_for_id(*entry.key());
}
}
let id = self.next_token_id.fetch_add(1, Ordering::Relaxed);
self.token_to_name.insert(id as u32, name);
tokens::native::token_for_id(id as u32)
}
pub fn lookup_address_by_name(&self, name: &str) -> Option<u64> {
for entry in self.address_to_name.iter() {
if entry.value().as_ref() == name {
return Some(*entry.key());
}
}
None
}
pub fn lookup_by_token(&self, token: Token) -> Option<Arc<str>> {
let id = token.value() & 0x0000_FFFF;
self.token_to_name.get(&id).map(|v| Arc::clone(v.value()))
}
}
pub fn register(manager: &HookManager, registry: &NativeFunctionRegistry) -> Result<()> {
register_virtual_protect(manager)?;
register_virtual_alloc(manager)?;
register_virtual_free(manager)?;
register_get_module_handle(manager)?;
register_get_proc_address(manager, registry)?;
register_load_library(manager)?;
register_is_debugger_present(manager)?;
register_check_remote_debugger_present(manager)?;
register_get_current_process(manager)?;
register_get_current_thread(manager)?;
Ok(())
}
fn register_virtual_protect(manager: &HookManager) -> Result<()> {
manager.register(
Hook::new("native-virtual-protect")
.with_priority(HookPriority::HIGH)
.match_native("kernel32", "VirtualProtect")
.pre(|ctx, thread| {
let args = ctx.args;
if args.len() < 4 {
return PreHookResult::Bypass(Some(EmValue::I32(0)));
}
let lp_address = match &args[0] {
EmValue::UnmanagedPtr(addr) if *addr != 0 => *addr,
EmValue::NativeInt(addr) if *addr > 0 => (*addr).cast_unsigned(),
EmValue::NativeUInt(addr) if *addr > 0 => *addr,
_ => return PreHookResult::Bypass(Some(EmValue::I32(0))),
};
#[allow(clippy::cast_possible_truncation)]
let dw_size = match &args[1] {
EmValue::I32(size) if *size > 0 => (*size).cast_unsigned() as usize,
EmValue::NativeInt(size) if *size > 0 => (*size).cast_unsigned() as usize,
EmValue::NativeUInt(size) if *size > 0 => *size as usize,
_ => return PreHookResult::Bypass(Some(EmValue::I32(0))),
};
#[allow(clippy::cast_possible_truncation)]
let fl_new_protect = match &args[2] {
EmValue::I32(p) => (*p).cast_unsigned(),
EmValue::NativeInt(p) => (*p).cast_unsigned() as u32,
EmValue::NativeUInt(p) => *p as u32,
_ => return PreHookResult::Bypass(Some(EmValue::I32(0))),
};
let space = thread.address_space();
if !space.is_valid(lp_address) || !space.is_valid(lp_address + dw_size as u64 - 1) {
return PreHookResult::Bypass(Some(EmValue::I32(0)));
}
let new_protection = MemoryProtection::from_windows(fl_new_protect);
let old_protection = space
.set_protection(lp_address, dw_size, new_protection)
.unwrap_or(MemoryProtection::READ_EXECUTE);
let old_protect_windows = old_protection.to_windows();
let old_protect_value = EmValue::I32(old_protect_windows.cast_signed());
match &args[3] {
EmValue::ManagedPtr(ptr)
if thread
.store_through_pointer(ptr, old_protect_value)
.is_err() =>
{
return PreHookResult::Bypass(Some(EmValue::I32(0)));
}
EmValue::UnmanagedPtr(addr) if *addr != 0 => {
let old_protect_bytes = old_protect_windows.to_le_bytes();
if thread
.address_space()
.write(*addr, &old_protect_bytes)
.is_err()
{
return PreHookResult::Bypass(Some(EmValue::I32(0)));
}
}
EmValue::NativeInt(addr) if *addr > 0 => {
let old_protect_bytes = old_protect_windows.to_le_bytes();
if thread
.address_space()
.write((*addr).cast_unsigned(), &old_protect_bytes)
.is_err()
{
return PreHookResult::Bypass(Some(EmValue::I32(0)));
}
}
EmValue::NativeUInt(addr) if *addr > 0 => {
let old_protect_bytes = old_protect_windows.to_le_bytes();
if thread
.address_space()
.write(*addr, &old_protect_bytes)
.is_err()
{
return PreHookResult::Bypass(Some(EmValue::I32(0)));
}
}
_ => {
}
}
PreHookResult::Bypass(Some(EmValue::I32(1)))
}),
)?;
Ok(())
}
fn register_virtual_alloc(manager: &HookManager) -> Result<()> {
manager.register(
Hook::new("native-virtual-alloc")
.with_priority(HookPriority::HIGH)
.match_native("kernel32", "VirtualAlloc")
.pre(|ctx, thread| {
let args = ctx.args;
#[allow(clippy::cast_possible_truncation)]
let size = match args.get(1) {
Some(EmValue::I32(size)) if *size > 0 => (*size).cast_unsigned() as usize,
Some(EmValue::NativeInt(size)) if *size > 0 => (*size).cast_unsigned() as usize,
Some(EmValue::NativeUInt(size)) if *size > 0 => *size as usize,
_ => return PreHookResult::Bypass(Some(EmValue::NativeInt(0))),
};
#[allow(clippy::cast_possible_truncation)]
let alloc_type = match args.get(2) {
Some(EmValue::I32(t)) if *t != 0 => (*t).cast_unsigned(),
Some(EmValue::NativeInt(t)) if *t != 0 => (*t).cast_unsigned() as u32,
_ => return PreHookResult::Bypass(Some(EmValue::NativeInt(0))),
};
if (alloc_type & 0x3000) == 0 {
return PreHookResult::Bypass(Some(EmValue::NativeInt(0)));
}
match thread.address_space().alloc_unmanaged(size) {
Ok(addr) => PreHookResult::Bypass(Some(EmValue::NativeInt(addr.cast_signed()))),
Err(_) => PreHookResult::Bypass(Some(EmValue::NativeInt(0))),
}
}),
)?;
Ok(())
}
fn register_virtual_free(manager: &HookManager) -> Result<()> {
manager.register(
Hook::new("native-virtual-free")
.with_priority(HookPriority::HIGH)
.match_native("kernel32", "VirtualFree")
.pre(|ctx, thread| {
let args = ctx.args;
if args.is_empty() {
return PreHookResult::Bypass(Some(EmValue::I32(0)));
}
let lp_address = match &args[0] {
EmValue::UnmanagedPtr(addr) if *addr != 0 => *addr,
EmValue::NativeInt(addr) if *addr > 0 => (*addr).cast_unsigned(),
EmValue::NativeUInt(addr) if *addr > 0 => *addr,
_ => return PreHookResult::Bypass(Some(EmValue::I32(0))),
};
if let Some(free_type) = args.get(2) {
#[allow(clippy::cast_possible_truncation)]
let ft = match free_type {
EmValue::I32(t) => (*t).cast_unsigned(),
EmValue::NativeInt(t) => (*t).cast_unsigned() as u32,
_ => 0,
};
if (ft & 0x8000) != 0 {
#[allow(clippy::cast_possible_truncation)]
let dw_size = args
.get(1)
.and_then(|v| match v {
EmValue::I32(s) => Some((*s).cast_unsigned()),
EmValue::NativeInt(s) => Some((*s).cast_unsigned() as u32),
_ => None,
})
.unwrap_or(0);
if dw_size != 0 {
return PreHookResult::Bypass(Some(EmValue::I32(0)));
}
}
}
if !thread.address_space().is_valid(lp_address) {
return PreHookResult::Bypass(Some(EmValue::I32(0)));
}
PreHookResult::Bypass(Some(EmValue::I32(1)))
}),
)?;
Ok(())
}
fn register_get_module_handle(manager: &HookManager) -> Result<()> {
manager.register(
Hook::new("native-get-module-handle-a")
.with_priority(HookPriority::HIGH)
.match_native("kernel32", "GetModuleHandleA")
.pre(|ctx, _thread| {
let is_null = ctx.args.first().is_none_or(|v| {
v.is_null()
|| matches!(v, EmValue::NativeInt(0))
|| matches!(v, EmValue::NativeUInt(0))
|| matches!(v, EmValue::UnmanagedPtr(0))
});
if is_null {
PreHookResult::Bypass(Some(EmValue::NativeInt(
native_addresses::CURRENT_MODULE,
)))
} else {
PreHookResult::Bypass(Some(EmValue::NativeInt(native_addresses::OTHER_MODULE)))
}
}),
)?;
manager.register(
Hook::new("native-get-module-handle-w")
.with_priority(HookPriority::HIGH)
.match_native("kernel32", "GetModuleHandleW")
.pre(|ctx, _thread| {
let is_null = ctx.args.first().is_none_or(|v| {
v.is_null()
|| matches!(v, EmValue::NativeInt(0))
|| matches!(v, EmValue::NativeUInt(0))
|| matches!(v, EmValue::UnmanagedPtr(0))
});
if is_null {
PreHookResult::Bypass(Some(EmValue::NativeInt(
native_addresses::CURRENT_MODULE,
)))
} else {
PreHookResult::Bypass(Some(EmValue::NativeInt(native_addresses::OTHER_MODULE)))
}
}),
)?;
Ok(())
}
fn register_get_proc_address(
manager: &HookManager,
registry: &NativeFunctionRegistry,
) -> Result<()> {
let registry = registry.clone();
manager.register(
Hook::new("native-get-proc-address")
.with_priority(HookPriority::HIGH)
.match_native("kernel32", "GetProcAddress")
.pre(move |ctx, thread| {
let func_name = ctx
.args
.get(1)
.and_then(|arg| match arg {
EmValue::ObjectRef(href) => {
thread.heap().get_string(*href).ok().map(|s| s.to_string())
}
_ => None,
})
.unwrap_or_else(|| "unknown".to_string());
let addr = registry.register_proc(&func_name);
log::debug!("GetProcAddress({func_name}) → 0x{addr:X}");
PreHookResult::Bypass(Some(EmValue::NativeInt(addr as i64)))
}),
)?;
Ok(())
}
fn register_load_library(manager: &HookManager) -> Result<()> {
let load_library_handler =
|_ctx: &HookContext<'_>, _thread: &mut EmulationThread| -> PreHookResult {
PreHookResult::Bypass(Some(EmValue::NativeInt(native_addresses::LOADED_LIBRARY)))
};
manager.register(
Hook::new("native-load-library-a")
.with_priority(HookPriority::HIGH)
.match_native("kernel32", "LoadLibraryA")
.pre(load_library_handler),
)?;
manager.register(
Hook::new("native-load-library-w")
.with_priority(HookPriority::HIGH)
.match_native("kernel32", "LoadLibraryW")
.pre(load_library_handler),
)?;
manager.register(
Hook::new("native-load-library")
.with_priority(HookPriority::HIGH)
.match_native("kernel32", "LoadLibrary")
.pre(load_library_handler),
)?;
Ok(())
}
fn register_is_debugger_present(manager: &HookManager) -> Result<()> {
manager.register(
Hook::new("native-is-debugger-present")
.with_priority(HookPriority::HIGHEST) .match_native("kernel32", "IsDebuggerPresent")
.pre(|_ctx, _thread| {
PreHookResult::Bypass(Some(EmValue::I32(0)))
}),
)?;
Ok(())
}
fn register_check_remote_debugger_present(manager: &HookManager) -> Result<()> {
manager.register(
Hook::new("native-check-remote-debugger-present")
.with_priority(HookPriority::HIGHEST)
.match_native("kernel32", "CheckRemoteDebuggerPresent")
.pre(|ctx, thread| {
let args = ctx.args;
if args.len() < 2 {
return PreHookResult::Bypass(Some(EmValue::I32(0)));
}
match &args[0] {
EmValue::NativeInt(-1) => {} EmValue::NativeInt(h) if *h > 0 => {}
EmValue::NativeUInt(h) if *h > 0 => {}
EmValue::UnmanagedPtr(h) if *h > 0 => {}
_ => return PreHookResult::Bypass(Some(EmValue::I32(0))),
}
let output_addr = match &args[1] {
EmValue::UnmanagedPtr(addr) if *addr != 0 => *addr,
EmValue::NativeInt(addr) if *addr > 0 => (*addr).cast_unsigned(),
EmValue::NativeUInt(addr) if *addr > 0 => *addr,
_ => return PreHookResult::Bypass(Some(EmValue::I32(0))),
};
let false_bytes = 0u32.to_le_bytes();
let space = thread.address_space();
if !space.is_valid(output_addr) {
return PreHookResult::Bypass(Some(EmValue::I32(0)));
}
if space.write(output_addr, &false_bytes).is_err() {
return PreHookResult::Bypass(Some(EmValue::I32(0)));
}
PreHookResult::Bypass(Some(EmValue::I32(1)))
}),
)?;
Ok(())
}
fn register_get_current_process(manager: &HookManager) -> Result<()> {
manager.register(
Hook::new("native-get-current-process")
.with_priority(HookPriority::HIGH)
.match_native("kernel32", "GetCurrentProcess")
.pre(|_ctx, _thread| {
PreHookResult::Bypass(Some(EmValue::NativeInt(-1)))
}),
)?;
Ok(())
}
fn register_get_current_thread(manager: &HookManager) -> Result<()> {
manager.register(
Hook::new("native-get-current-thread")
.with_priority(HookPriority::HIGH)
.match_native("kernel32", "GetCurrentThread")
.pre(|_ctx, _thread| {
PreHookResult::Bypass(Some(EmValue::NativeInt(-2)))
}),
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use crate::{
emulation::{
runtime::{HookContext, HookManager, HookOutcome},
tokens::native_addresses,
EmValue,
},
metadata::{token::Token, typesystem::PointerSize},
test::emulation::create_test_thread,
};
use super::{register, NativeFunctionRegistry};
fn create_native_context<'a>(dll: &'a str, function: &'a str) -> HookContext<'a> {
HookContext::native(Token::new(0x06000001), dll, function, PointerSize::Bit64)
}
#[test]
fn test_native_hooks_registered() {
let manager = HookManager::new();
register(&manager, &NativeFunctionRegistry::new()).unwrap();
assert!(manager.len() >= 12);
}
#[test]
fn test_is_debugger_present_hook() {
let manager = HookManager::new();
register(&manager, &NativeFunctionRegistry::new()).unwrap();
let mut thread = create_test_thread();
let context = create_native_context("kernel32", "IsDebuggerPresent").with_args(&[]);
let outcome = manager.execute(&context, &mut thread, |_| None).unwrap();
assert!(
matches!(outcome, HookOutcome::Handled(Some(EmValue::I32(0)))),
"IsDebuggerPresent should return 0"
);
}
#[test]
fn test_get_current_process_hook() {
let manager = HookManager::new();
register(&manager, &NativeFunctionRegistry::new()).unwrap();
let mut thread = create_test_thread();
let context = create_native_context("kernel32", "GetCurrentProcess").with_args(&[]);
let outcome = manager.execute(&context, &mut thread, |_| None).unwrap();
assert!(
matches!(outcome, HookOutcome::Handled(Some(EmValue::NativeInt(-1)))),
"GetCurrentProcess should return -1"
);
}
#[test]
fn test_get_module_handle_null() {
let manager = HookManager::new();
register(&manager, &NativeFunctionRegistry::new()).unwrap();
let mut thread = create_test_thread();
let args = [EmValue::NativeInt(0)];
let context = create_native_context("kernel32", "GetModuleHandleA").with_args(&args);
let outcome = manager.execute(&context, &mut thread, |_| None).unwrap();
assert!(
matches!(
outcome,
HookOutcome::Handled(Some(EmValue::NativeInt(native_addresses::CURRENT_MODULE)))
),
"GetModuleHandleA(null) should return base address"
);
}
}