Initial Release

This commit is contained in:
ApfelTeeSaft
2026-01-08 08:01:05 +01:00
parent 5fd01e0736
commit f0fd8b7c8c
18 changed files with 1799 additions and 0 deletions
+22
View File
@@ -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 = []
+55
View File
@@ -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.
+65
View File
@@ -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<u8> {
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<u8> {
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<DetourPlan, TetanusStatus> {
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<TrampolineLayout, TetanusStatus> {
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<usize, TetanusStatus> {
let jump = Self::make_trampoline_jump(target);
unsafe {
slice::from_raw_parts_mut(trampoline, jump.len()).copy_from_slice(&jump);
}
Ok(jump.len())
}
}
+67
View File
@@ -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<u8> {
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<u8> {
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<DetourPlan, TetanusStatus> {
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<TrampolineLayout, TetanusStatus> {
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<usize, TetanusStatus> {
let jump = Self::make_trampoline_jump(target);
unsafe {
slice::from_raw_parts_mut(trampoline, jump.len()).copy_from_slice(&jump);
}
Ok(jump.len())
}
}
+52
View File
@@ -0,0 +1,52 @@
use std::ffi::c_void;
use crate::status::TetanusStatus;
pub struct DetourPlan {
pub patch_size: usize,
pub detour_bytes: Vec<u8>,
}
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<DetourPlan, TetanusStatus>;
fn build_trampoline(
target: *const c_void,
trampoline: *mut c_void,
original: &[u8],
) -> Result<TrampolineLayout, TetanusStatus>;
fn write_trampoline_exit(
trampoline: *mut u8,
target: *const c_void,
) -> Result<usize, TetanusStatus>;
}
#[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<DetourPlan, TetanusStatus> {
#[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)
}
}
+118
View File
@@ -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<usize, TetanusStatus> {
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<u8> {
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<DetourPlan, TetanusStatus> {
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<TrampolineLayout, TetanusStatus> {
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<usize, TetanusStatus> {
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)
}
}
+120
View File
@@ -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<usize, TetanusStatus> {
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<u8> {
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<DetourPlan, TetanusStatus> {
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<TrampolineLayout, TetanusStatus> {
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<usize, TetanusStatus> {
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)
}
}
+37
View File
@@ -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"
}
+154
View File
@@ -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<Mutex<HashMap<(usize, String), EatEntry>>> = OnceLock::new();
fn eat_hooks() -> &'static Mutex<HashMap<(usize, String), EatEntry>> {
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::<u32>(),
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::<u32>(),
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::<u32>(),
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::<u32>(),
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
}
+114
View File
@@ -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<u8>,
detour_patch: Vec<u8>,
state: HookState,
}
impl Hook {
pub unsafe fn create(
target: *const c_void,
detour: *const c_void,
original_out: *mut *const c_void,
) -> Result<Self, TetanusStatus> {
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(())
}
}
+178
View File
@@ -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<Mutex<HashMap<(usize, String), IatEntry>>> = OnceLock::new();
fn iat_hooks() -> &'static Mutex<HashMap<(usize, String), IatEntry>> {
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::<usize>(),
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::<usize>(),
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::<usize>(),
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::<usize>(),
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::<u64>());
iat = iat.add(std::mem::size_of::<u64>());
} 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::<u32>());
iat = iat.add(std::mem::size_of::<u32>());
}
}
descriptor = descriptor.add(1);
}
Err(TetanusStatus::NotCreated)
}
+172
View File
@@ -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<HookInfo> {
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)
}
+244
View File
@@ -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<HookEntry>,
enabled: bool,
}
struct HookManager {
initialized: bool,
hooks: HashMap<usize, HookChain>,
}
impl HookManager {
fn new() -> Self {
Self {
initialized: false,
hooks: HashMap::new(),
}
}
}
static MANAGER: OnceLock<Mutex<HookManager>> = OnceLock::new();
fn manager() -> &'static Mutex<HookManager> {
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<HookInfo> {
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(())
}
+13
View File
@@ -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);
}));
});
}
+23
View File
@@ -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)
}
}
+86
View File
@@ -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<crate::arch::TrampolineLayout, TetanusStatus> {
#[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<usize, TetanusStatus> {
#[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<u8>), TetanusStatus> {
let plan = arch_plan(target, detour)?;
Ok((plan.patch_size, plan.detour_bytes))
}
+157
View File
@@ -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<Mutex<HashMap<usize, VehEntry>>> = OnceLock::new();
static VEH_HANDLE: OnceLock<Mutex<*mut c_void>> = OnceLock::new();
fn veh_state() -> &'static Mutex<HashMap<usize, VehEntry>> {
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();
}
}
+122
View File
@@ -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<Self, TetanusStatus> {
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<Self, TetanusStatus> {
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<u32, TetanusStatus> {
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<u32, TetanusStatus> {
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
}
}