From f0fd8b7c8ca026436a81c4736d682f618c31c33a Mon Sep 17 00:00:00 2001 From: ApfelTeeSaft <91074565+ApfelTeeSaft@users.noreply.github.com> Date: Thu, 8 Jan 2026 08:01:05 +0100 Subject: [PATCH] Initial Release --- Cargo.toml | 22 ++++ README.md | 55 ++++++++++ src/arch/arm32.rs | 65 ++++++++++++ src/arch/arm64.rs | 67 +++++++++++++ src/arch/mod.rs | 52 ++++++++++ src/arch/x64.rs | 118 ++++++++++++++++++++++ src/arch/x86.rs | 120 ++++++++++++++++++++++ src/diagnostics.rs | 37 +++++++ src/eat.rs | 154 ++++++++++++++++++++++++++++ src/hook.rs | 114 +++++++++++++++++++++ src/iat.rs | 178 +++++++++++++++++++++++++++++++++ src/lib.rs | 172 ++++++++++++++++++++++++++++++++ src/manager.rs | 244 +++++++++++++++++++++++++++++++++++++++++++++ src/safety.rs | 13 +++ src/status.rs | 23 +++++ src/trampoline.rs | 86 ++++++++++++++++ src/veh.rs | 157 +++++++++++++++++++++++++++++ src/windows.rs | 122 +++++++++++++++++++++++ 18 files changed, 1799 insertions(+) create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/arch/arm32.rs create mode 100644 src/arch/arm64.rs create mode 100644 src/arch/mod.rs create mode 100644 src/arch/x64.rs create mode 100644 src/arch/x86.rs create mode 100644 src/diagnostics.rs create mode 100644 src/eat.rs create mode 100644 src/hook.rs create mode 100644 src/iat.rs create mode 100644 src/lib.rs create mode 100644 src/manager.rs create mode 100644 src/safety.rs create mode 100644 src/status.rs create mode 100644 src/trampoline.rs create mode 100644 src/veh.rs create mode 100644 src/windows.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..baf2ce3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tetanus" +version = "1.0.0" +edition = "2026" + +[lib] +name = "tetanus" +path = "src/lib.rs" + +[dependencies] +iced-x86 = { version = "1.21.0", default-features = false, features = ["decoder", "encoder"] } +windows-sys = { version = "0.52.0", features = [ + "Win32_Foundation", + "Win32_System_Diagnostics_Debug", + "Win32_System_LibraryLoader", + "Win32_System_Memory", + "Win32_System_SystemServices", + "Win32_System_Threading" +] } + +[features] +default = [] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f66889f --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Tetanus + +## Windows Detouring Library in Rust + +### x86 · x64 · ARM · ARM64 + +**Tetanus** is a lightweight **Windows user-mode function hooking library written in Rust**, providing **inline detours and related hooking techniques** across all major Windows architectures. + +Inspired by **MinHook**, Tetanus reimplements its core behavior in **idiomatic Rust**, with added **ARM support** and a modular, safety-focused design. + +--- + +## Features + +* Inline detours with trampolines (MinHook-like) +* Enable / disable hooks at runtime +* Original function access via trampoline +* Architecture-aware instruction patching +* Safe for use in injected or external DLLs + +### Supported Architectures + +* x86 (32-bit) +* x64 (64-bit) +* ARM32 (Windows on ARM) +* ARM64 (Windows on ARM64) + +### Hooking Techniques + +* Inline detours +* IAT (Import Address Table) hooking +* EAT (Export Address Table) hooking +* VEH (Vectored Exception Handler) hooks + +--- + +## Design Goals + +* Correctness over cleverness +* Explicit and well-documented `unsafe` code +* Clear hook lifecycle management +* No MinHook source reuse + +--- + +## License + +**BSD 2-Clause License** + +--- + +## Disclaimer + +This is a low-level library. Incorrect usage can crash the target process. +Use responsibly. \ No newline at end of file diff --git a/src/arch/arm32.rs b/src/arch/arm32.rs new file mode 100644 index 0000000..49e460c --- /dev/null +++ b/src/arch/arm32.rs @@ -0,0 +1,65 @@ +use std::ffi::c_void; +use std::slice; + +use crate::arch::{ArchHook, DetourPlan, TrampolineLayout}; +use crate::status::TetanusStatus; + +pub struct Arm32; + +impl Arm32 { + fn make_abs_jump(detour: *const c_void) -> Vec { + let mut bytes = Vec::with_capacity(8); + bytes.extend_from_slice(&0xE51FF004u32.to_le_bytes()); + bytes.extend_from_slice(&(detour as u32).to_le_bytes()); + bytes + } + + fn make_trampoline_jump(back: *const c_void) -> Vec { + let mut bytes = Vec::with_capacity(8); + bytes.extend_from_slice(&0xE51FF004u32.to_le_bytes()); + bytes.extend_from_slice(&(back as u32).to_le_bytes()); + bytes + } +} + +impl ArchHook for Arm32 { + fn plan_detour(_target: *const c_void, detour: *const c_void) -> Result { + let detour_bytes = Self::make_abs_jump(detour); + Ok(DetourPlan { + patch_size: detour_bytes.len(), + detour_bytes, + }) + } + + fn build_trampoline( + target: *const c_void, + trampoline: *mut c_void, + original: &[u8], + ) -> Result { + unsafe { + std::ptr::copy_nonoverlapping(original.as_ptr(), trampoline as *mut u8, original.len()); + } + let back = unsafe { (target as *const u8).add(original.len()) }; + let offset = original.len(); + let exit_size = Self::write_trampoline_exit( + unsafe { (trampoline as *mut u8).add(offset) }, + back as *const c_void, + )?; + Ok(TrampolineLayout { + size: offset + exit_size, + exit_offset: offset, + exit_size, + }) + } + + fn write_trampoline_exit( + trampoline: *mut u8, + target: *const c_void, + ) -> Result { + let jump = Self::make_trampoline_jump(target); + unsafe { + slice::from_raw_parts_mut(trampoline, jump.len()).copy_from_slice(&jump); + } + Ok(jump.len()) + } +} diff --git a/src/arch/arm64.rs b/src/arch/arm64.rs new file mode 100644 index 0000000..4723c11 --- /dev/null +++ b/src/arch/arm64.rs @@ -0,0 +1,67 @@ +use std::ffi::c_void; +use std::slice; + +use crate::arch::{ArchHook, DetourPlan, TrampolineLayout}; +use crate::status::TetanusStatus; + +pub struct Arm64; + +impl Arm64 { + fn make_abs_jump(detour: *const c_void) -> Vec { + let mut bytes = Vec::with_capacity(16); + bytes.extend_from_slice(&0x58000071u32.to_le_bytes()); + bytes.extend_from_slice(&0xD61F0220u32.to_le_bytes()); + bytes.extend_from_slice(&(detour as u64).to_le_bytes()); + bytes + } + + fn make_trampoline_jump(back: *const c_void) -> Vec { + let mut bytes = Vec::with_capacity(16); + bytes.extend_from_slice(&0x58000071u32.to_le_bytes()); + bytes.extend_from_slice(&0xD61F0220u32.to_le_bytes()); + bytes.extend_from_slice(&(back as u64).to_le_bytes()); + bytes + } +} + +impl ArchHook for Arm64 { + fn plan_detour(_target: *const c_void, detour: *const c_void) -> Result { + let detour_bytes = Self::make_abs_jump(detour); + Ok(DetourPlan { + patch_size: detour_bytes.len(), + detour_bytes, + }) + } + + fn build_trampoline( + target: *const c_void, + trampoline: *mut c_void, + original: &[u8], + ) -> Result { + unsafe { + std::ptr::copy_nonoverlapping(original.as_ptr(), trampoline as *mut u8, original.len()); + } + let back = unsafe { (target as *const u8).add(original.len()) }; + let offset = original.len(); + let exit_size = Self::write_trampoline_exit( + unsafe { (trampoline as *mut u8).add(offset) }, + back as *const c_void, + )?; + Ok(TrampolineLayout { + size: offset + exit_size, + exit_offset: offset, + exit_size, + }) + } + + fn write_trampoline_exit( + trampoline: *mut u8, + target: *const c_void, + ) -> Result { + let jump = Self::make_trampoline_jump(target); + unsafe { + slice::from_raw_parts_mut(trampoline, jump.len()).copy_from_slice(&jump); + } + Ok(jump.len()) + } +} diff --git a/src/arch/mod.rs b/src/arch/mod.rs new file mode 100644 index 0000000..e99909e --- /dev/null +++ b/src/arch/mod.rs @@ -0,0 +1,52 @@ +use std::ffi::c_void; + +use crate::status::TetanusStatus; + +pub struct DetourPlan { + pub patch_size: usize, + pub detour_bytes: Vec, +} + +pub struct TrampolineLayout { + pub size: usize, + pub exit_offset: usize, + pub exit_size: usize, +} + +pub trait ArchHook { + fn plan_detour(target: *const c_void, detour: *const c_void) -> Result; + fn build_trampoline( + target: *const c_void, + trampoline: *mut c_void, + original: &[u8], + ) -> Result; + fn write_trampoline_exit( + trampoline: *mut u8, + target: *const c_void, + ) -> Result; +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub mod x86; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub mod x64; +#[cfg(target_arch = "arm")] +pub mod arm32; +#[cfg(target_arch = "aarch64")] +pub mod arm64; + +pub fn arch_plan(target: *const c_void, detour: *const c_void) -> Result { + #[cfg(target_arch = "x86")] + return x86::X86::plan_detour(target, detour); + #[cfg(target_arch = "x86_64")] + return x64::X64::plan_detour(target, detour); + #[cfg(target_arch = "arm")] + return arm32::Arm32::plan_detour(target, detour); + #[cfg(target_arch = "aarch64")] + return arm64::Arm64::plan_detour(target, detour); + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "arm", target_arch = "aarch64")))] + { + let _ = (target, detour); + Err(TetanusStatus::UnsupportedArchitecture) + } +} diff --git a/src/arch/x64.rs b/src/arch/x64.rs new file mode 100644 index 0000000..6d73a44 --- /dev/null +++ b/src/arch/x64.rs @@ -0,0 +1,118 @@ +use std::ffi::c_void; +use std::slice; + +use iced_x86::{Decoder, DecoderOptions, Encoder, Instruction, OpKind}; + +use crate::arch::{ArchHook, DetourPlan, TrampolineLayout}; +use crate::status::TetanusStatus; + +pub struct X64; + +impl X64 { + fn required_patch_size(target: *const c_void, min_len: usize) -> Result { + let mut decoder = unsafe { + Decoder::new(64, slice::from_raw_parts(target as *const u8, 64), DecoderOptions::NONE) + }; + let mut total = 0usize; + while total < min_len { + let instr = decoder.decode(); + if instr.is_invalid() { + return Err(TetanusStatus::InstructionDecodeFailed); + } + total += instr.len(); + } + Ok(total) + } + + fn make_abs_jmp(detour: *const c_void, patch_size: usize) -> Vec { + let mut bytes = Vec::with_capacity(patch_size); + bytes.extend_from_slice(&[0x48, 0xB8]); + bytes.extend_from_slice(&(detour as u64).to_le_bytes()); + bytes.extend_from_slice(&[0xFF, 0xE0]); + while bytes.len() < patch_size { + bytes.push(0x90); + } + bytes + } + + fn relocate_instruction( + encoder: &mut Encoder, + instr: &mut Instruction, + ip: u64, + ) -> Result<(), TetanusStatus> { + if instr.is_ip_rel_memory_operand() { + let target = instr.ip_rel_memory_address(); + instr.set_ip_rel_memory_address(target); + } + if instr.is_near_branch() { + let target = instr.near_branch_target(); + instr.set_near_branch64(target); + } + if instr.op0_kind() == OpKind::NearBranch64 || instr.op0_kind() == OpKind::NearBranch32 { + let target = instr.near_branch_target(); + instr.set_near_branch64(target); + } + encoder + .encode(instr, ip) + .map_err(|_| TetanusStatus::InstructionDecodeFailed)?; + Ok(()) + } +} + +impl ArchHook for X64 { + fn plan_detour(target: *const c_void, detour: *const c_void) -> Result { + let patch_size = Self::required_patch_size(target, 12)?; + let detour_bytes = Self::make_abs_jmp(detour, patch_size); + Ok(DetourPlan { + patch_size, + detour_bytes, + }) + } + + fn build_trampoline( + target: *const c_void, + trampoline: *mut c_void, + original: &[u8], + ) -> Result { + let mut decoder = Decoder::new(64, original, DecoderOptions::NONE); + decoder.set_ip(target as u64); + let mut encoder = Encoder::new(64); + encoder.set_buffer(unsafe { slice::from_raw_parts_mut(trampoline as *mut u8, 256) }); + let mut written = 0usize; + let mut ip = trampoline as u64; + while decoder.can_decode() && written < original.len() { + let mut instr = decoder.decode(); + if instr.is_invalid() { + return Err(TetanusStatus::InstructionDecodeFailed); + } + Self::relocate_instruction(&mut encoder, &mut instr, ip)?; + let len = encoder.len(); + ip += len as u64; + written = decoder.position(); + } + let exit_offset = encoder.len(); + let exit_size = Self::write_trampoline_exit( + unsafe { (trampoline as *mut u8).add(exit_offset) }, + unsafe { (target as *const u8).add(original.len()) as *const c_void }, + )?; + Ok(TrampolineLayout { + size: exit_offset + exit_size, + exit_offset, + exit_size, + }) + } + + fn write_trampoline_exit( + trampoline: *mut u8, + target: *const c_void, + ) -> Result { + unsafe { + *trampoline = 0x48; + *trampoline.add(1) = 0xB8; + std::ptr::copy_nonoverlapping((target as u64).to_le_bytes().as_ptr(), trampoline.add(2), 8); + *trampoline.add(10) = 0xFF; + *trampoline.add(11) = 0xE0; + } + Ok(12) + } +} diff --git a/src/arch/x86.rs b/src/arch/x86.rs new file mode 100644 index 0000000..6775c4c --- /dev/null +++ b/src/arch/x86.rs @@ -0,0 +1,120 @@ +use std::ffi::c_void; +use std::slice; + +use iced_x86::{Decoder, DecoderOptions, Encoder, Instruction, OpKind}; + +use crate::arch::{ArchHook, DetourPlan, TrampolineLayout}; +use crate::status::TetanusStatus; + +pub struct X86; + +impl X86 { + fn required_patch_size(target: *const c_void, min_len: usize) -> Result { + let mut decoder = unsafe { + Decoder::new(32, slice::from_raw_parts(target as *const u8, 64), DecoderOptions::NONE) + }; + let mut total = 0usize; + while total < min_len { + let instr = decoder.decode(); + if instr.is_invalid() { + return Err(TetanusStatus::InstructionDecodeFailed); + } + total += instr.len(); + } + Ok(total) + } + + fn make_jmp(detour: *const c_void, origin: *const c_void, patch_size: usize) -> Vec { + let mut bytes = Vec::with_capacity(patch_size); + let origin = origin as usize; + let detour = detour as usize; + let rel = detour.wrapping_sub(origin + 5) as i32; + bytes.push(0xE9); + bytes.extend_from_slice(&rel.to_le_bytes()); + while bytes.len() < patch_size { + bytes.push(0x90); + } + bytes + } + + fn relocate_instruction( + encoder: &mut Encoder, + instr: &mut Instruction, + ip: u64, + ) -> Result<(), TetanusStatus> { + if instr.is_ip_rel_memory_operand() { + let target = instr.ip_rel_memory_address(); + instr.set_ip_rel_memory_address(target); + } + if instr.is_near_branch() { + let target = instr.near_branch_target(); + instr.set_near_branch64(target); + } + if instr.op0_kind() == OpKind::NearBranch64 || instr.op0_kind() == OpKind::NearBranch32 { + let target = instr.near_branch_target(); + instr.set_near_branch64(target); + } + encoder + .encode(instr, ip) + .map_err(|_| TetanusStatus::InstructionDecodeFailed)?; + Ok(()) + } +} + +impl ArchHook for X86 { + fn plan_detour(target: *const c_void, detour: *const c_void) -> Result { + let patch_size = Self::required_patch_size(target, 5)?; + let detour_bytes = Self::make_jmp(detour, target, patch_size); + Ok(DetourPlan { + patch_size, + detour_bytes, + }) + } + + fn build_trampoline( + target: *const c_void, + trampoline: *mut c_void, + original: &[u8], + ) -> Result { + let mut decoder = Decoder::new(32, original, DecoderOptions::NONE); + decoder.set_ip(target as u64); + let mut encoder = Encoder::new(32); + encoder.set_buffer(unsafe { slice::from_raw_parts_mut(trampoline as *mut u8, 128) }); + let mut written = 0usize; + let mut ip = trampoline as u64; + while decoder.can_decode() && written < original.len() { + let mut instr = decoder.decode(); + if instr.is_invalid() { + return Err(TetanusStatus::InstructionDecodeFailed); + } + Self::relocate_instruction(&mut encoder, &mut instr, ip)?; + let len = encoder.len(); + ip += len as u64; + written = decoder.position(); + } + let exit_offset = encoder.len(); + let exit_size = Self::write_trampoline_exit( + unsafe { (trampoline as *mut u8).add(exit_offset) }, + unsafe { (target as *const u8).add(original.len()) as *const c_void }, + )?; + Ok(TrampolineLayout { + size: exit_offset + exit_size, + exit_offset, + exit_size, + }) + } + + fn write_trampoline_exit( + trampoline: *mut u8, + target: *const c_void, + ) -> Result { + let jump_origin = trampoline as usize; + let jump_target = target as usize; + let rel = jump_target.wrapping_sub(jump_origin + 5) as i32; + unsafe { + *trampoline = 0xE9; + std::ptr::copy_nonoverlapping(rel.to_le_bytes().as_ptr(), trampoline.add(1), 4); + } + Ok(5) + } +} diff --git a/src/diagnostics.rs b/src/diagnostics.rs new file mode 100644 index 0000000..b7ea4ff --- /dev/null +++ b/src/diagnostics.rs @@ -0,0 +1,37 @@ +use std::ffi::c_void; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::hook::HookState; + +static LOG_ENABLED: AtomicBool = AtomicBool::new(false); + +#[derive(Clone, Debug)] +pub struct HookInfo { + pub target: *const c_void, + pub detour: *const c_void, + pub state: HookState, + pub priority: i32, +} + +pub fn set_logging(enabled: bool) { + LOG_ENABLED.store(enabled, Ordering::Relaxed); +} + +pub fn log(message: &str) { + if LOG_ENABLED.load(Ordering::Relaxed) { + eprintln!("[tetanus] {message}"); + } +} + +pub fn architecture() -> &'static str { + #[cfg(target_arch = "x86")] + return "x86"; + #[cfg(target_arch = "x86_64")] + return "x64"; + #[cfg(target_arch = "arm")] + return "arm32"; + #[cfg(target_arch = "aarch64")] + return "arm64"; + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "arm", target_arch = "aarch64")))] + "unknown" +} diff --git a/src/eat.rs b/src/eat.rs new file mode 100644 index 0000000..8e7f8fe --- /dev/null +++ b/src/eat.rs @@ -0,0 +1,154 @@ +use std::collections::HashMap; +use std::ffi::{c_void, CStr}; +use std::sync::{Mutex, OnceLock}; + +use windows_sys::Win32::System::Memory::VirtualProtect; +use windows_sys::Win32::System::SystemServices::{ + IMAGE_DIRECTORY_ENTRY_EXPORT, IMAGE_DOS_HEADER, IMAGE_EXPORT_DIRECTORY, IMAGE_NT_HEADERS32, + IMAGE_NT_HEADERS64, PAGE_READWRITE, +}; + +use crate::status::TetanusStatus; + +struct EatEntry { + address: *mut u32, + original_rva: u32, +} + +static EAT_HOOKS: OnceLock>> = OnceLock::new(); + +fn eat_hooks() -> &'static Mutex> { + EAT_HOOKS.get_or_init(|| Mutex::new(HashMap::new())) +} + +pub unsafe fn hook_eat( + module: *const c_void, + export_name: &str, + detour: *const c_void, + original: *mut *const c_void, +) -> TetanusStatus { + if module.is_null() || detour.is_null() || original.is_null() { + return TetanusStatus::InvalidParameter; + } + let (entry_ptr, entry_rva, export_base, export_dir_size) = match find_eat_entry(module, export_name) { + Ok(result) => result, + Err(status) => return status, + }; + let key = (module as usize, export_name.to_string()); + let mut guard = eat_hooks().lock().unwrap(); + if guard.contains_key(&key) { + return TetanusStatus::AlreadyCreated; + } + let original_addr = (module as usize + entry_rva as usize) as *const c_void; + *original = original_addr; + let detour_rva = (detour as usize).wrapping_sub(module as usize) as u32; + let mut old = 0u32; + let ok = VirtualProtect( + entry_ptr as *mut c_void, + std::mem::size_of::(), + PAGE_READWRITE, + &mut old, + ); + if ok == 0 { + return TetanusStatus::MemoryProtectFailed; + } + *entry_ptr = detour_rva; + let _ = VirtualProtect( + entry_ptr as *mut c_void, + std::mem::size_of::(), + old, + &mut old, + ); + guard.insert( + key, + EatEntry { + address: entry_ptr, + original_rva: entry_rva, + }, + ); + if is_forwarder(entry_rva, export_base, export_dir_size) { + return TetanusStatus::InvalidParameter; + } + TetanusStatus::Ok +} + +pub unsafe fn unhook_eat(module: *const c_void, export_name: &str) -> TetanusStatus { + if module.is_null() { + return TetanusStatus::InvalidParameter; + } + let key = (module as usize, export_name.to_string()); + let mut guard = eat_hooks().lock().unwrap(); + let entry = match guard.remove(&key) { + Some(entry) => entry, + None => return TetanusStatus::NotCreated, + }; + let mut old = 0u32; + let ok = VirtualProtect( + entry.address as *mut c_void, + std::mem::size_of::(), + PAGE_READWRITE, + &mut old, + ); + if ok == 0 { + return TetanusStatus::MemoryProtectFailed; + } + *entry.address = entry.original_rva; + let _ = VirtualProtect( + entry.address as *mut c_void, + std::mem::size_of::(), + old, + &mut old, + ); + TetanusStatus::Ok +} + +unsafe fn find_eat_entry( + module: *const c_void, + export_name: &str, +) -> Result<(*mut u32, u32, usize, u32), TetanusStatus> { + let base = module as *const u8; + let dos = &*(base as *const IMAGE_DOS_HEADER); + if dos.e_magic != 0x5A4D { + return Err(TetanusStatus::InvalidParameter); + } + let nt_headers = base.add(dos.e_lfanew as usize); + let signature = *(nt_headers as *const u32); + if signature != 0x00004550 { + return Err(TetanusStatus::InvalidParameter); + } + let magic = *(nt_headers.add(24) as *const u16); + let (export_rva, export_size) = if magic == 0x20B { + let nt = &*(nt_headers as *const IMAGE_NT_HEADERS64); + let dir = nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT as usize]; + (dir.VirtualAddress, dir.Size) + } else { + let nt = &*(nt_headers as *const IMAGE_NT_HEADERS32); + let dir = nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT as usize]; + (dir.VirtualAddress, dir.Size) + }; + if export_rva == 0 || export_size == 0 { + return Err(TetanusStatus::NotCreated); + } + let export_dir = &*(base.add(export_rva as usize) as *const IMAGE_EXPORT_DIRECTORY); + let names = base.add(export_dir.AddressOfNames as usize) as *const u32; + let ordinals = base.add(export_dir.AddressOfNameOrdinals as usize) as *const u16; + let functions = base.add(export_dir.AddressOfFunctions as usize) as *mut u32; + for i in 0..export_dir.NumberOfNames { + let name_rva = *names.add(i as usize); + let name_ptr = base.add(name_rva as usize) as *const i8; + let name = CStr::from_ptr(name_ptr); + if name.to_string_lossy() == export_name { + let ordinal = *ordinals.add(i as usize) as usize; + let func_rva_ptr = functions.add(ordinal); + let func_rva = *func_rva_ptr; + return Ok((func_rva_ptr, func_rva, export_rva as usize, export_size)); + } + } + Err(TetanusStatus::NotCreated) +} + +fn is_forwarder(rva: u32, export_base: usize, export_size: u32) -> bool { + let rva = rva as usize; + let export_end = export_base + export_size as usize; + rva >= export_base && rva < export_end +} diff --git a/src/hook.rs b/src/hook.rs new file mode 100644 index 0000000..1fa55a6 --- /dev/null +++ b/src/hook.rs @@ -0,0 +1,114 @@ +use std::ffi::c_void; +use std::ptr; +use std::slice; + +use crate::status::TetanusStatus; +use crate::trampoline::{plan_detour, Trampoline}; +use crate::windows::{flush_icache, protect_rw, restore_protect}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HookState { + Created, + Enabled, + Disabled, +} + +pub struct Hook { + target: *const c_void, + detour: *const c_void, + trampoline: Trampoline, + trampoline_ptr: *const c_void, + original_return: *const c_void, + original: Vec, + detour_patch: Vec, + state: HookState, +} + +impl Hook { + pub unsafe fn create( + target: *const c_void, + detour: *const c_void, + original_out: *mut *const c_void, + ) -> Result { + if target.is_null() || detour.is_null() || original_out.is_null() { + return Err(TetanusStatus::InvalidParameter); + } + let (patch_size, detour_patch) = plan_detour(target, detour)?; + let original = slice::from_raw_parts(target as *const u8, patch_size).to_vec(); + let (trampoline, trampoline_ptr) = Trampoline::create(target, &original)?; + let original_return = (target as *const u8).add(original.len()) as *const c_void; + *original_out = trampoline_ptr; + Ok(Self { + target, + detour, + trampoline, + trampoline_ptr, + original_return, + original, + detour_patch, + state: HookState::Created, + }) + } + + pub fn target(&self) -> *const c_void { + self.target + } + + pub fn detour(&self) -> *const c_void { + self.detour + } + + pub fn original_return(&self) -> *const c_void { + self.original_return + } + + pub fn state(&self) -> HookState { + self.state + } + + pub fn enable(&mut self) -> Result<(), TetanusStatus> { + if self.state == HookState::Enabled { + return Err(TetanusStatus::Enabled); + } + unsafe { + let ptr = self.target as *mut c_void; + let old = protect_rw(ptr, self.detour_patch.len())?; + ptr::copy_nonoverlapping(self.detour_patch.as_ptr(), ptr as *mut u8, self.detour_patch.len()); + restore_protect(ptr, self.detour_patch.len(), old)?; + flush_icache(self.target, self.detour_patch.len())?; + } + self.state = HookState::Enabled; + Ok(()) + } + + pub fn disable(&mut self) -> Result<(), TetanusStatus> { + if self.state == HookState::Disabled || self.state == HookState::Created { + return Err(TetanusStatus::Disabled); + } + unsafe { + let ptr = self.target as *mut c_void; + let old = protect_rw(ptr, self.original.len())?; + ptr::copy_nonoverlapping(self.original.as_ptr(), ptr as *mut u8, self.original.len()); + restore_protect(ptr, self.original.len(), old)?; + flush_icache(self.target, self.original.len())?; + } + self.state = HookState::Disabled; + Ok(()) + } + + pub fn set_exit_target(&self, target: *const c_void) -> Result<(), TetanusStatus> { + self.trampoline.patch_exit(target)?; + Ok(()) + } + + pub fn set_state(&mut self, state: HookState) { + self.state = state; + } + + pub fn destroy(self) -> Result<(), TetanusStatus> { + if self.state == HookState::Enabled { + return Err(TetanusStatus::Enabled); + } + Ok(()) + } +} diff --git a/src/iat.rs b/src/iat.rs new file mode 100644 index 0000000..d9c4d36 --- /dev/null +++ b/src/iat.rs @@ -0,0 +1,178 @@ +use std::collections::HashMap; +use std::ffi::{c_void, CStr}; +use std::sync::{Mutex, OnceLock}; + +use windows_sys::Win32::System::Memory::VirtualProtect; +use windows_sys::Win32::System::SystemServices::{ + IMAGE_DIRECTORY_ENTRY_IMPORT, IMAGE_DOS_HEADER, IMAGE_IMPORT_BY_NAME, IMAGE_IMPORT_DESCRIPTOR, + IMAGE_NT_HEADERS32, IMAGE_NT_HEADERS64, IMAGE_ORDINAL_FLAG32, IMAGE_ORDINAL_FLAG64, + PAGE_READWRITE, +}; + +use crate::status::TetanusStatus; + +struct IatEntry { + address: *mut usize, + original: usize, +} + +static IAT_HOOKS: OnceLock>> = OnceLock::new(); + +fn iat_hooks() -> &'static Mutex> { + IAT_HOOKS.get_or_init(|| Mutex::new(HashMap::new())) +} + +pub unsafe fn hook_iat( + module: *const c_void, + import_name: &str, + detour: *const c_void, + original: *mut *const c_void, +) -> TetanusStatus { + if module.is_null() || detour.is_null() || original.is_null() { + return TetanusStatus::InvalidParameter; + } + let (entry_ptr, entry_value) = match find_iat_entry(module, import_name) { + Ok(result) => result, + Err(status) => return status, + }; + let key = (module as usize, import_name.to_string()); + let mut guard = iat_hooks().lock().unwrap(); + if guard.contains_key(&key) { + return TetanusStatus::AlreadyCreated; + } + let mut old = 0u32; + let ok = VirtualProtect( + entry_ptr as *mut c_void, + std::mem::size_of::(), + PAGE_READWRITE, + &mut old, + ); + if ok == 0 { + return TetanusStatus::MemoryProtectFailed; + } + *entry_ptr = detour as usize; + let _ = VirtualProtect( + entry_ptr as *mut c_void, + std::mem::size_of::(), + old, + &mut old, + ); + *original = entry_value as *const c_void; + guard.insert( + key, + IatEntry { + address: entry_ptr, + original: entry_value, + }, + ); + TetanusStatus::Ok +} + +pub unsafe fn unhook_iat(module: *const c_void, import_name: &str) -> TetanusStatus { + if module.is_null() { + return TetanusStatus::InvalidParameter; + } + let key = (module as usize, import_name.to_string()); + let mut guard = iat_hooks().lock().unwrap(); + let entry = match guard.remove(&key) { + Some(entry) => entry, + None => return TetanusStatus::NotCreated, + }; + let mut old = 0u32; + let ok = VirtualProtect( + entry.address as *mut c_void, + std::mem::size_of::(), + PAGE_READWRITE, + &mut old, + ); + if ok == 0 { + return TetanusStatus::MemoryProtectFailed; + } + *entry.address = entry.original; + let _ = VirtualProtect( + entry.address as *mut c_void, + std::mem::size_of::(), + old, + &mut old, + ); + TetanusStatus::Ok +} + +unsafe fn find_iat_entry( + module: *const c_void, + import_name: &str, +) -> Result<(*mut usize, usize), TetanusStatus> { + let base = module as *const u8; + let dos = &*(base as *const IMAGE_DOS_HEADER); + if dos.e_magic != 0x5A4D { + return Err(TetanusStatus::InvalidParameter); + } + let nt_headers = base.add(dos.e_lfanew as usize); + let signature = *(nt_headers as *const u32); + if signature != 0x00004550 { + return Err(TetanusStatus::InvalidParameter); + } + let magic = *(nt_headers.add(24) as *const u16); + let (import_rva, import_size, is_64) = if magic == 0x20B { + let nt = &*(nt_headers as *const IMAGE_NT_HEADERS64); + let dir = nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT as usize]; + (dir.VirtualAddress, dir.Size, true) + } else { + let nt = &*(nt_headers as *const IMAGE_NT_HEADERS32); + let dir = nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT as usize]; + (dir.VirtualAddress, dir.Size, false) + }; + if import_rva == 0 || import_size == 0 { + return Err(TetanusStatus::NotCreated); + } + let mut descriptor = base.add(import_rva as usize) as *const IMAGE_IMPORT_DESCRIPTOR; + while (*descriptor).Name != 0 { + let name_ptr = base.add((*descriptor).Name as usize) as *const i8; + let _dll_name = CStr::from_ptr(name_ptr); + let mut thunk = if (*descriptor).OriginalFirstThunk != 0 { + base.add((*descriptor).OriginalFirstThunk as usize) + } else { + base.add((*descriptor).FirstThunk as usize) + }; + let mut iat = base.add((*descriptor).FirstThunk as usize); + loop { + if is_64 { + let thunk_data = thunk as *const u64; + let val = *thunk_data; + if val == 0 { + break; + } + let is_ordinal = (val & IMAGE_ORDINAL_FLAG64 as u64) != 0; + if !is_ordinal { + let name_ptr = base.add(val as usize) as *const IMAGE_IMPORT_BY_NAME; + let name = CStr::from_ptr((*name_ptr).Name.as_ptr() as *const i8); + if name.to_string_lossy() == import_name { + let iat_entry = iat as *mut usize; + return Ok((iat_entry, *iat_entry)); + } + } + thunk = thunk.add(std::mem::size_of::()); + iat = iat.add(std::mem::size_of::()); + } else { + let thunk_data = thunk as *const u32; + let val = *thunk_data; + if val == 0 { + break; + } + let is_ordinal = (val & IMAGE_ORDINAL_FLAG32) != 0; + if !is_ordinal { + let name_ptr = base.add(val as usize) as *const IMAGE_IMPORT_BY_NAME; + let name = CStr::from_ptr((*name_ptr).Name.as_ptr() as *const i8); + if name.to_string_lossy() == import_name { + let iat_entry = iat as *mut usize; + return Ok((iat_entry, *iat_entry)); + } + } + thunk = thunk.add(std::mem::size_of::()); + iat = iat.add(std::mem::size_of::()); + } + } + descriptor = descriptor.add(1); + } + Err(TetanusStatus::NotCreated) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..dc54580 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,172 @@ +//! Tetanus: a Windows user-mode inline hooking framework inspired by MinHook. +//! +//! # Safety +//! +//! Inline hooks overwrite instructions in the target function. All unsafe blocks +//! in this crate are narrowly scoped to Windows API calls or raw pointer writes +//! needed to patch code safely. + +mod arch; +mod diagnostics; +mod hook; +mod manager; +mod safety; +mod status; +mod trampoline; +mod windows; +mod iat; +mod eat; +mod veh; + +pub use status::TetanusStatus; +pub use diagnostics::{HookInfo, set_logging, architecture, log}; + +use std::ffi::c_void; + +/// Initialize the global hook manager. +pub fn initialize() -> TetanusStatus { + manager::initialize() +} + +/// Uninitialize the global hook manager. +pub fn uninitialize() -> TetanusStatus { + manager::uninitialize() +} + +/// Create a hook and return a trampoline pointer for the original function. +/// +/// # Safety +/// +/// - `target` and `detour` must be valid function pointers. +/// - `original` must be a valid pointer to receive the trampoline. +/// - The caller must ensure the target code is safe to patch. +pub unsafe fn create_hook( + target: *const c_void, + detour: *const c_void, + original: *mut *const c_void, +) -> TetanusStatus { + manager::create_hook(target, detour, original) +} + +/// Create a hook with an explicit priority for chaining. +/// +/// Higher priority hooks run first. Hooks with equal priority preserve creation order. +/// +/// # Safety +/// +/// - `target` and `detour` must be valid function pointers. +/// - `original` must be a valid pointer to receive the trampoline. +/// - The caller must ensure the target code is safe to patch. +pub unsafe fn create_hook_with_priority( + target: *const c_void, + detour: *const c_void, + original: *mut *const c_void, + priority: i32, +) -> TetanusStatus { + manager::create_hook_with_priority(target, detour, original, priority) +} + +/// Enable the hook for a specific target. +/// +/// # Safety +/// +/// The target must be a valid hook created by `create_hook`. +pub unsafe fn enable_hook(target: *const c_void) -> TetanusStatus { + manager::enable_hook(target) +} + +/// Disable the hook for a specific target. +/// +/// # Safety +/// +/// The target must be a valid hook created by `create_hook`. +pub unsafe fn disable_hook(target: *const c_void) -> TetanusStatus { + manager::disable_hook(target) +} + +/// Enable all created hooks. +pub fn enable_all_hooks() -> TetanusStatus { + manager::enable_all_hooks() +} + +/// Disable all created hooks. +pub fn disable_all_hooks() -> TetanusStatus { + manager::disable_all_hooks() +} + +/// Return a snapshot of registered hooks for diagnostics. +pub fn hook_snapshot() -> Vec { + manager::hook_snapshot() +} + +/// Validate internal hook state for diagnostics. +pub fn validate_hooks() -> TetanusStatus { + manager::validate_hooks() +} + +/// Hook a function through a module's Import Address Table. +/// +/// # Safety +/// +/// The caller must ensure the module and target import are valid. +pub unsafe fn hook_iat( + module: *const c_void, + import_name: &str, + detour: *const c_void, + original: *mut *const c_void, +) -> TetanusStatus { + iat::hook_iat(module, import_name, detour, original) +} + +/// Restore a previously hooked IAT entry. +/// +/// # Safety +/// +/// The caller must ensure the module and import name match a previous hook. +pub unsafe fn unhook_iat(module: *const c_void, import_name: &str) -> TetanusStatus { + iat::unhook_iat(module, import_name) +} + +/// Hook a function through a module's Export Address Table. +/// +/// # Safety +/// +/// The caller must ensure the module and export name are valid. +pub unsafe fn hook_eat( + module: *const c_void, + export_name: &str, + detour: *const c_void, + original: *mut *const c_void, +) -> TetanusStatus { + eat::hook_eat(module, export_name, detour, original) +} + +/// Restore a previously hooked EAT entry. +/// +/// # Safety +/// +/// The caller must ensure the module and export name match a previous hook. +pub unsafe fn unhook_eat(module: *const c_void, export_name: &str) -> TetanusStatus { + eat::unhook_eat(module, export_name) +} + +/// Enable a VEH-based hook for a target address. +/// +/// # Safety +/// +/// The target must be a valid executable address. +pub unsafe fn enable_veh_hook( + target: *const c_void, + detour: *const c_void, +) -> TetanusStatus { + veh::enable_veh_hook(target, detour) +} + +/// Disable a VEH-based hook for a target address. +/// +/// # Safety +/// +/// The target must be a valid executable address. +pub unsafe fn disable_veh_hook(target: *const c_void) -> TetanusStatus { + veh::disable_veh_hook(target) +} diff --git a/src/manager.rs b/src/manager.rs new file mode 100644 index 0000000..b5e6086 --- /dev/null +++ b/src/manager.rs @@ -0,0 +1,244 @@ +use std::collections::HashMap; +use std::ffi::c_void; +use std::sync::{Mutex, OnceLock}; + +use crate::diagnostics::HookInfo; +use crate::hook::{Hook, HookState}; +use crate::status::TetanusStatus; +use crate::safety; + +struct HookEntry { + hook: Hook, + priority: i32, +} + +struct HookChain { + entries: Vec, + enabled: bool, +} + +struct HookManager { + initialized: bool, + hooks: HashMap, +} + +impl HookManager { + fn new() -> Self { + Self { + initialized: false, + hooks: HashMap::new(), + } + } +} + +static MANAGER: OnceLock> = OnceLock::new(); + +fn manager() -> &'static Mutex { + MANAGER.get_or_init(|| Mutex::new(HookManager::new())) +} + +pub fn initialize() -> TetanusStatus { + let mut guard = manager().lock().unwrap(); + if guard.initialized { + return TetanusStatus::AlreadyInitialized; + } + safety::install_panic_hook(disable_all_hooks); + guard.initialized = true; + TetanusStatus::Ok +} + +pub fn uninitialize() -> TetanusStatus { + let mut guard = manager().lock().unwrap(); + if !guard.initialized { + return TetanusStatus::NotInitialized; + } + for (_, chain) in guard.hooks.drain() { + if chain.enabled { + return TetanusStatus::Enabled; + } + } + guard.initialized = false; + TetanusStatus::Ok +} + +pub unsafe fn create_hook( + target: *const c_void, + detour: *const c_void, + original: *mut *const c_void, +) -> TetanusStatus { + create_hook_with_priority(target, detour, original, 0) +} + +pub unsafe fn create_hook_with_priority( + target: *const c_void, + detour: *const c_void, + original: *mut *const c_void, + priority: i32, +) -> TetanusStatus { + let mut guard = manager().lock().unwrap(); + if !guard.initialized { + return TetanusStatus::NotInitialized; + } + let key = target as usize; + match Hook::create(target, detour, original) { + Ok(hook) => { + let chain = guard.hooks.entry(key).or_insert_with(|| HookChain { + entries: Vec::new(), + enabled: false, + }); + if chain.entries.iter().any(|entry| entry.hook.detour() == detour) { + return TetanusStatus::AlreadyCreated; + } + chain.entries.push(HookEntry { hook, priority }); + if let Err(status) = rebuild_chain(chain) { + return status; + } + TetanusStatus::Ok + } + Err(status) => status, + } +} + +pub unsafe fn enable_hook(target: *const c_void) -> TetanusStatus { + let mut guard = manager().lock().unwrap(); + if !guard.initialized { + return TetanusStatus::NotInitialized; + } + let key = target as usize; + let chain = match guard.hooks.get_mut(&key) { + Some(chain) => chain, + None => return TetanusStatus::NotCreated, + }; + if chain.enabled { + return TetanusStatus::Enabled; + } + if let Some(first) = chain.entries.first_mut() { + match first.hook.enable() { + Ok(()) => { + chain.enabled = true; + for entry in chain.entries.iter_mut() { + entry.hook.set_state(HookState::Enabled); + } + TetanusStatus::Ok + } + Err(status) => status, + } + } else { + TetanusStatus::NotCreated + } +} + +pub unsafe fn disable_hook(target: *const c_void) -> TetanusStatus { + let mut guard = manager().lock().unwrap(); + if !guard.initialized { + return TetanusStatus::NotInitialized; + } + let key = target as usize; + let chain = match guard.hooks.get_mut(&key) { + Some(chain) => chain, + None => return TetanusStatus::NotCreated, + }; + if !chain.enabled { + return TetanusStatus::Disabled; + } + if let Some(first) = chain.entries.first_mut() { + match first.hook.disable() { + Ok(()) => { + chain.enabled = false; + for entry in chain.entries.iter_mut() { + entry.hook.set_state(HookState::Disabled); + } + TetanusStatus::Ok + } + Err(status) => status, + } + } else { + TetanusStatus::NotCreated + } +} + +pub fn enable_all_hooks() -> TetanusStatus { + let mut guard = manager().lock().unwrap(); + if !guard.initialized { + return TetanusStatus::NotInitialized; + } + for chain in guard.hooks.values_mut() { + if chain.enabled { + continue; + } + if let Some(first) = chain.entries.first_mut() { + if let Err(status) = first.hook.enable() { + return status; + } + chain.enabled = true; + for entry in chain.entries.iter_mut() { + entry.hook.set_state(HookState::Enabled); + } + } + } + TetanusStatus::Ok +} + +pub fn disable_all_hooks() -> TetanusStatus { + let mut guard = manager().lock().unwrap(); + if !guard.initialized { + return TetanusStatus::NotInitialized; + } + for chain in guard.hooks.values_mut() { + if !chain.enabled { + continue; + } + if let Some(first) = chain.entries.first_mut() { + if let Err(status) = first.hook.disable() { + return status; + } + chain.enabled = false; + for entry in chain.entries.iter_mut() { + entry.hook.set_state(HookState::Disabled); + } + } + } + TetanusStatus::Ok +} + +pub fn hook_snapshot() -> Vec { + let guard = manager().lock().unwrap(); + let mut info = Vec::new(); + for chain in guard.hooks.values() { + for entry in chain.entries.iter() { + info.push(HookInfo { + target: entry.hook.target(), + detour: entry.hook.detour(), + state: entry.hook.state(), + priority: entry.priority, + }); + } + } + info +} + +pub fn validate_hooks() -> TetanusStatus { + let guard = manager().lock().unwrap(); + if !guard.initialized { + return TetanusStatus::NotInitialized; + } + for chain in guard.hooks.values() { + if chain.entries.is_empty() { + return TetanusStatus::InvalidParameter; + } + } + TetanusStatus::Ok +} + +fn rebuild_chain(chain: &mut HookChain) -> Result<(), TetanusStatus> { + chain.entries.sort_by(|a, b| b.priority.cmp(&a.priority)); + for idx in 0..chain.entries.len() { + let exit_target = if idx + 1 < chain.entries.len() { + chain.entries[idx + 1].hook.detour() + } else { + chain.entries[idx].hook.original_return() + }; + chain.entries[idx].hook.set_exit_target(exit_target)?; + } + Ok(()) +} diff --git a/src/safety.rs b/src/safety.rs new file mode 100644 index 0000000..fe65752 --- /dev/null +++ b/src/safety.rs @@ -0,0 +1,13 @@ +use std::sync::OnceLock; + +static PANIC_HOOK_INSTALLED: OnceLock<()> = OnceLock::new(); + +pub fn install_panic_hook(unhook: fn()) { + PANIC_HOOK_INSTALLED.get_or_init(|| { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + unhook(); + previous(info); + })); + }); +} diff --git a/src/status.rs b/src/status.rs new file mode 100644 index 0000000..20cf0ba --- /dev/null +++ b/src/status.rs @@ -0,0 +1,23 @@ +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TetanusStatus { + Ok = 0, + AlreadyInitialized = 1, + NotInitialized = 2, + AlreadyCreated = 3, + NotCreated = 4, + Enabled = 5, + Disabled = 6, + UnsupportedArchitecture = 7, + MemoryAllocFailed = 8, + MemoryProtectFailed = 9, + InstructionDecodeFailed = 10, + InvalidParameter = 11, + UnknownError = 12, +} + +impl TetanusStatus { + pub fn is_ok(self) -> bool { + matches!(self, TetanusStatus::Ok) + } +} diff --git a/src/trampoline.rs b/src/trampoline.rs new file mode 100644 index 0000000..890113e --- /dev/null +++ b/src/trampoline.rs @@ -0,0 +1,86 @@ +use std::ffi::c_void; + +use crate::arch::arch_plan; +use crate::status::TetanusStatus; +use crate::windows::ExecutableMemory; + +pub struct Trampoline { + memory: ExecutableMemory, + size: usize, + exit_offset: usize, + exit_size: usize, +} + +impl Trampoline { + pub fn create( + target: *const c_void, + original: &[u8], + ) -> Result<(Self, *const c_void), TetanusStatus> { + let memory = ExecutableMemory::allocate_guarded(256)?; + let tramp_ptr = memory.as_ptr(); + let layout = build_arch_trampoline(target, tramp_ptr, original)?; + Ok(( + Self { + memory, + size: layout.size, + exit_offset: layout.exit_offset, + exit_size: layout.exit_size, + }, + tramp_ptr as *const c_void, + )) + } + + pub fn size(&self) -> usize { + self.size + } + + pub fn patch_exit(&self, target: *const c_void) -> Result<(), TetanusStatus> { + let exit_ptr = unsafe { (self.memory.as_ptr() as *mut u8).add(self.exit_offset) }; + let written = write_arch_exit(exit_ptr, target)?; + if written != self.exit_size { + return Err(TetanusStatus::InstructionDecodeFailed); + } + Ok(()) + } +} + +fn build_arch_trampoline( + target: *const c_void, + trampoline: *mut c_void, + original: &[u8], +) -> Result { + #[cfg(target_arch = "x86")] + return crate::arch::x86::X86::build_trampoline(target, trampoline, original); + #[cfg(target_arch = "x86_64")] + return crate::arch::x64::X64::build_trampoline(target, trampoline, original); + #[cfg(target_arch = "arm")] + return crate::arch::arm32::Arm32::build_trampoline(target, trampoline, original); + #[cfg(target_arch = "aarch64")] + return crate::arch::arm64::Arm64::build_trampoline(target, trampoline, original); + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "arm", target_arch = "aarch64")))] + { + let _ = (target, trampoline, original); + Err(TetanusStatus::UnsupportedArchitecture) + } +} + +fn write_arch_exit(trampoline: *mut u8, target: *const c_void) -> Result { + #[cfg(target_arch = "x86")] + return crate::arch::x86::X86::write_trampoline_exit(trampoline, target); + #[cfg(target_arch = "x86_64")] + return crate::arch::x64::X64::write_trampoline_exit(trampoline, target); + #[cfg(target_arch = "arm")] + return crate::arch::arm32::Arm32::write_trampoline_exit(trampoline, target); + #[cfg(target_arch = "aarch64")] + return crate::arch::arm64::Arm64::write_trampoline_exit(trampoline, target); + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "arm", target_arch = "aarch64")))] + { + let _ = (trampoline, target); + Err(TetanusStatus::UnsupportedArchitecture) + } +} + +pub fn plan_detour(target: *const c_void, detour: *const c_void) -> Result<(usize, Vec), TetanusStatus> { + let plan = arch_plan(target, detour)?; + Ok((plan.patch_size, plan.detour_bytes)) +} diff --git a/src/veh.rs b/src/veh.rs new file mode 100644 index 0000000..355bd24 --- /dev/null +++ b/src/veh.rs @@ -0,0 +1,157 @@ +use std::collections::HashMap; +use std::ffi::c_void; +use std::ptr; +use std::sync::{Mutex, OnceLock}; + +use windows_sys::Win32::Foundation::EXCEPTION_CONTINUE_EXECUTION; +use windows_sys::Win32::Foundation::EXCEPTION_CONTINUE_SEARCH; +use windows_sys::Win32::System::Diagnostics::Debug::{ + AddVectoredExceptionHandler, RemoveVectoredExceptionHandler, EXCEPTION_POINTERS, +}; +use windows_sys::Win32::System::Memory::VirtualProtect; +use windows_sys::Win32::System::SystemServices::EXCEPTION_BREAKPOINT; +use windows_sys::Win32::System::SystemServices::PAGE_EXECUTE_READWRITE; + +use crate::status::TetanusStatus; + +struct VehEntry { + detour: *const c_void, + original: u8, +} + +static VEH_STATE: OnceLock>> = OnceLock::new(); +static VEH_HANDLE: OnceLock> = OnceLock::new(); + +fn veh_state() -> &'static Mutex> { + VEH_STATE.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn veh_handle() -> &'static Mutex<*mut c_void> { + VEH_HANDLE.get_or_init(|| Mutex::new(ptr::null_mut())) +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +unsafe extern "system" fn veh_handler(info: *mut EXCEPTION_POINTERS) -> i32 { + if info.is_null() { + return EXCEPTION_CONTINUE_SEARCH; + } + let record = (*info).ExceptionRecord; + if record.is_null() { + return EXCEPTION_CONTINUE_SEARCH; + } + if (*record).ExceptionCode != EXCEPTION_BREAKPOINT { + return EXCEPTION_CONTINUE_SEARCH; + } + let addr = (*record).ExceptionAddress as usize; + let state = veh_state().lock().unwrap(); + let entry = match state.get(&addr) { + Some(entry) => entry, + None => return EXCEPTION_CONTINUE_SEARCH, + }; + let context = (*info).ContextRecord; + if context.is_null() { + return EXCEPTION_CONTINUE_SEARCH; + } + #[cfg(target_arch = "x86")] + { + (*context).Eip = entry.detour as u32; + } + #[cfg(target_arch = "x86_64")] + { + (*context).Rip = entry.detour as u64; + } + EXCEPTION_CONTINUE_EXECUTION +} + +pub unsafe fn enable_veh_hook(target: *const c_void, detour: *const c_void) -> TetanusStatus { + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] + { + let _ = (target, detour); + return TetanusStatus::UnsupportedArchitecture; + } + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + if target.is_null() || detour.is_null() { + return TetanusStatus::InvalidParameter; + } + let mut state = veh_state().lock().unwrap(); + let key = target as usize; + if state.contains_key(&key) { + return TetanusStatus::AlreadyCreated; + } + install_handler(); + let byte_ptr = target as *mut u8; + let original = *byte_ptr; + let mut old = 0u32; + let ok = VirtualProtect( + target as *mut c_void, + 1, + PAGE_EXECUTE_READWRITE, + &mut old, + ); + if ok == 0 { + return TetanusStatus::MemoryProtectFailed; + } + *byte_ptr = 0xCC; + let _ = VirtualProtect(target as *mut c_void, 1, old, &mut old); + state.insert(key, VehEntry { detour, original }); + TetanusStatus::Ok + } +} + +pub unsafe fn disable_veh_hook(target: *const c_void) -> TetanusStatus { + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] + { + let _ = target; + return TetanusStatus::UnsupportedArchitecture; + } + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + if target.is_null() { + return TetanusStatus::InvalidParameter; + } + let mut state = veh_state().lock().unwrap(); + let key = target as usize; + let entry = match state.remove(&key) { + Some(entry) => entry, + None => return TetanusStatus::NotCreated, + }; + let mut old = 0u32; + let ok = VirtualProtect( + target as *mut c_void, + 1, + PAGE_EXECUTE_READWRITE, + &mut old, + ); + if ok == 0 { + return TetanusStatus::MemoryProtectFailed; + } + *(target as *mut u8) = entry.original; + let _ = VirtualProtect(target as *mut c_void, 1, old, &mut old); + if state.is_empty() { + uninstall_handler(); + } + TetanusStatus::Ok + } +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +fn install_handler() { + let mut handle = veh_handle().lock().unwrap(); + if handle.is_null() { + unsafe { + *handle = AddVectoredExceptionHandler(1, Some(veh_handler)) as *mut c_void; + } + } +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +fn uninstall_handler() { + let mut handle = veh_handle().lock().unwrap(); + if !handle.is_null() { + unsafe { + RemoveVectoredExceptionHandler(*handle as *const c_void); + } + *handle = ptr::null_mut(); + } +} diff --git a/src/windows.rs b/src/windows.rs new file mode 100644 index 0000000..dd22b15 --- /dev/null +++ b/src/windows.rs @@ -0,0 +1,122 @@ +use std::ffi::c_void; +use std::ptr; + +use windows_sys::Win32::Foundation::GetLastError; +use windows_sys::Win32::System::Memory::{ + VirtualAlloc, VirtualFree, VirtualProtect, MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, + PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_NOACCESS, PAGE_READWRITE, +}; +use windows_sys::Win32::System::SystemServices::SYSTEM_INFO; +use windows_sys::Win32::System::SystemServices::GetSystemInfo; +use windows_sys::Win32::System::Threading::FlushInstructionCache; + +use crate::status::TetanusStatus; + +pub struct ExecutableMemory { + ptr: *mut c_void, + size: usize, +} + +impl ExecutableMemory { + pub fn allocate(size: usize) -> Result { + unsafe { + let ptr = VirtualAlloc( + ptr::null_mut(), + size, + MEM_COMMIT | MEM_RESERVE, + PAGE_EXECUTE_READWRITE, + ); + if ptr.is_null() { + return Err(TetanusStatus::MemoryAllocFailed); + } + Ok(Self { ptr, size }) + } + } + + pub fn allocate_guarded(size: usize) -> Result { + unsafe { + let page_size = system_page_size(); + let rounded = ((size + page_size - 1) / page_size) * page_size; + let alloc_size = rounded + page_size; + let ptr = VirtualAlloc( + ptr::null_mut(), + alloc_size, + MEM_COMMIT | MEM_RESERVE, + PAGE_EXECUTE_READWRITE, + ); + if ptr.is_null() { + return Err(TetanusStatus::MemoryAllocFailed); + } + let guard_ptr = (ptr as *mut u8).add(rounded) as *mut c_void; + let mut old = 0u32; + let ok = VirtualProtect(guard_ptr, page_size, PAGE_NOACCESS, &mut old); + if ok == 0 { + VirtualFree(ptr, 0, MEM_RELEASE); + return Err(TetanusStatus::MemoryProtectFailed); + } + Ok(Self { ptr, size: rounded }) + } + } + + pub fn as_ptr(&self) -> *mut c_void { + self.ptr + } + + pub fn size(&self) -> usize { + self.size + } +} + +impl Drop for ExecutableMemory { + fn drop(&mut self) { + unsafe { + if !self.ptr.is_null() { + VirtualFree(self.ptr, 0, MEM_RELEASE); + } + } + } +} + +pub unsafe fn protect_rw(ptr: *mut c_void, size: usize) -> Result { + let mut old = 0u32; + let ok = VirtualProtect(ptr, size, PAGE_READWRITE, &mut old); + if ok == 0 { + return Err(TetanusStatus::MemoryProtectFailed); + } + Ok(old) +} + +pub unsafe fn protect_rx(ptr: *mut c_void, size: usize) -> Result { + let mut old = 0u32; + let ok = VirtualProtect(ptr, size, PAGE_EXECUTE_READ, &mut old); + if ok == 0 { + return Err(TetanusStatus::MemoryProtectFailed); + } + Ok(old) +} + +pub unsafe fn restore_protect(ptr: *mut c_void, size: usize, old: u32) -> Result<(), TetanusStatus> { + let mut _discard = 0u32; + let ok = VirtualProtect(ptr, size, old, &mut _discard); + if ok == 0 { + return Err(TetanusStatus::MemoryProtectFailed); + } + Ok(()) +} + +pub unsafe fn flush_icache(ptr: *const c_void, size: usize) -> Result<(), TetanusStatus> { + let ok = FlushInstructionCache(0, ptr, size); + if ok == 0 { + let _ = GetLastError(); + return Err(TetanusStatus::UnknownError); + } + Ok(()) +} + +fn system_page_size() -> usize { + unsafe { + let mut info = SYSTEM_INFO::default(); + GetSystemInfo(&mut info); + info.dwPageSize as usize + } +}