use dashmap::DashMap;
use iced_x86::{BlockEncoder, BlockEncoderOptions, Decoder, DecoderOptions, Instruction, InstructionBlock, SpecializedFormatter, SpecializedFormatterTraitOptions, code_asm::*};
use std::{ffi::CString, sync::{Arc, OnceLock}};
use winapi::um::memoryapi::WriteProcessMemory;
#[cfg(target_os = "windows")]
use winapi::{ctypes::c_void, um::{libloaderapi::GetModuleHandleA, memoryapi::{VirtualAlloc, VirtualProtect, VirtualQuery}, processthreadsapi::GetCurrentProcess, winnt::{HANDLE, MEM_COMMIT, MEM_FREE, MEM_RESERVE, MEMORY_BASIC_INFORMATION, PAGE_EXECUTE_READWRITE}, wow64apiset::IsWow64Process}};
#[macro_export]
macro_rules! assemble_1 {
($obj:ident, nop, $count:expr ; $($rest:tt)*) => {{
for _ in 0..$count {
$obj.nop().expect("Failed assembling instruction!");
}
assemble_1!($obj, $($rest)*);
}};
($obj:ident, $instr:tt $($dst:expr)? $(, $src:expr)* ; $($rest:tt)* ) => {
$obj.$instr($($dst)? $(, $src)*).expect("Failed assembling instruction!");
assemble_1!($obj, $($rest)*)
};
($obj:ident, $instr:tt $size:tt ptr [ $dst:expr ] $(, $src:tt)* ; $($rest:tt)* ) => {
$obj.$instr(paste::paste!([<$size _ptr>] )($dst) $(, $src)*).expect("Failed assembling instruction!");
assemble_1!($obj, $($rest)*)
};
($obj:ident, $instr:tt $dst:expr $(, $size:tt ptr [ $src:expr ])? ; $($rest:tt)* ) => {
$obj.$instr($dst $(, paste::paste!([<$size _ptr>])($src))?).expect("Failed assembling instruction!");
assemble_1!($obj, $($rest)*)
};
($obj:ident, $labelname:tt : $($rest:tt)* ) => {
$obj.set_label(&mut $labelname).expect("Failed assembling instruction!");
assemble_1!($obj, $($rest)*)
};
($obj:ident, ) => {};
}
#[macro_export]
macro_rules! assemble {
($($tt:tt)*) => {
std::sync::Arc::new(move |assembler: &mut iced_x86::code_asm::CodeAssembler| {
use iced_x86::code_asm::*;
use hook_king::assemble_1;
hook_king::assemble_1!(assembler,
$($tt)*
);
})
};
}
const PAGE_SIZE: usize = 0x1000;
const SAFETY: usize = 0x10;
struct MyTraitOptions;
impl SpecializedFormatterTraitOptions for MyTraitOptions {}
#[derive(Clone)]
pub struct HookInfo {
name: String,
address: usize,
typ: HookType,
assembly: Arc<dyn Fn(&mut CodeAssembler) + Send + Sync + 'static>,
org_bytes: Vec<u8>,
org_nops: usize,
jmp_size: usize,
jumping_address: usize,
enabled: bool,
}
impl std::fmt::Debug for HookInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("HookInfo").field("name", &self.name).field("address", &format!("{:016X?}", &self.address)).field("typ", &self.typ).field("assembly", &opcode_display(arch(), self.assembly.as_ref())).finish() }
}
impl Default for HookInfo {
fn default() -> Self {
Self {
name: String::new(),
address: 0,
typ: HookType::default(),
assembly: Arc::new(|_: &mut CodeAssembler| {}),
org_bytes: Vec::new(),
org_nops: 0,
jmp_size: 0,
jumping_address: 0,
enabled: true,
}
}
}
impl HookInfo {
pub fn new(name: &str, address: usize, typ: HookType, assembly: Arc<dyn Fn(&mut CodeAssembler) + Send + Sync + 'static>) -> Self { Self { name: name.to_string(), address, typ, assembly, ..Default::default() } }
pub fn enable(&mut self, hook_king: &HookKing) {
let address = self.address;
let mem_address = self.jumping_address;
let jmp_size = get_jmp_size(self.address, self.jumping_address);
if self.org_nops > 0 {
let nops = hook_king.get_nop_bytes(self.org_nops);
hook_king.place_bytes(address, nops).unwrap();
}
let offset = get_jump_offset(address, mem_address) as usize;
let jmp_bytes = hook_king.get_jump_bytes(jmp_size, offset);
hook_king.place_bytes(address, jmp_bytes).unwrap();
self.enabled = true;
}
pub fn disable(&mut self, hook_king: &HookKing) {
hook_king.place_bytes(self.address, self.org_bytes.clone()).unwrap();
self.enabled = false;
}
pub fn name(&self) -> &str { &self.name }
pub fn address(&self) -> usize { self.address }
pub fn jumping_address(&self) -> usize { self.jumping_address }
}
#[derive(Clone, Default, Debug)]
pub struct OwnedMem {
pub address: usize,
pub size: usize,
pub used: usize,
}
impl OwnedMem {
fn inc_used(&mut self, size: usize) -> Result<(), Box<dyn std::error::Error>> {
if self.used + size < self.size {
self.used += size;
Ok(())
} else {
Err("The used size has exceeded the available size".into())
}
}
fn is_nearby(&self, address: usize) -> bool {
const RANGE: usize = i32::MAX as usize; self.address.abs_diff(address) <= RANGE
}
fn is_mem_enough(&self, required_size: usize) -> bool {
let available = self.size.saturating_sub(self.used + SAFETY);
required_size <= available
}
fn check_in_mem(address: usize, required_size: usize, owned_mems: &DashMap<usize, OwnedMem>) -> Option<usize> {
for entry in owned_mems.iter() {
let (key, value) = entry.pair();
if value.is_nearby(address) && value.is_mem_enough(required_size) {
return Some(*key);
}
}
None
}
pub fn address(&self) -> usize { self.address }
pub fn size(&self) -> usize { self.size }
pub fn used(&self) -> usize { self.used }
}
#[derive(Clone, Debug, Default)]
struct InstructionsInfo {
pub address: usize,
pub opcodes: Vec<String>,
pub instrs: Vec<Instruction>,
pub bytes: Vec<u8>,
pub nops: usize,
}
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub enum ProcessId {
Windows(*mut std::ffi::c_void),
Linux(u32),
}
unsafe impl Send for ProcessId {}
unsafe impl Sync for ProcessId {}
impl Default for ProcessId {
#[cfg(target_os = "windows")]
fn default() -> Self { ProcessId::Windows(std::ptr::null_mut()) }
#[cfg(target_os = "linux")]
fn default() -> Self { ProcessId::Linux(0) }
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum HookLookup {
Name(String),
Address(usize),
Index(usize),
}
impl Default for HookLookup {
fn default() -> Self { HookLookup::Index(0) }
}
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub enum MemLookup {
Address(usize),
Index(usize),
}
impl Default for MemLookup {
fn default() -> Self { MemLookup::Index(0) }
}
#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)]
pub enum HookType {
#[default]
Patch,
Detour,
DetourNoOrg,
}
#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)]
enum AttachType {
#[default]
Internal,
External,
}
#[derive(Clone, Debug, Default)]
pub struct HookKing {
process: ProcessId,
attach_typ: AttachType,
owned_mems: DashMap<usize, OwnedMem>,
owned_mems_address: DashMap<usize, usize>,
hooks: DashMap<usize, HookInfo>,
hooks_name: DashMap<String, usize>,
hooks_address: DashMap<usize, usize>,
}
impl HookKing {
pub fn new(process: Option<ProcessId>) -> Self {
let process = if let Some(proc) = process { proc } else { Self::current_process() };
Self { process, ..Default::default() }
}
fn current_process() -> ProcessId {
#[cfg(target_os = "windows")]
{
ProcessId::Windows(unsafe { GetCurrentProcess() as _ })
}
#[cfg(target_os = "linux")]
{
let pid = process::id();
ProcessId::Linux(pid)
}
}
pub fn change_process(&mut self, process: Option<ProcessId>) {
if process.is_some() {
self.attach_typ = AttachType::External;
self.process = process.unwrap()
} else {
self.attach_typ = AttachType::Internal;
self.process = Self::current_process();
}
}
pub fn get_hook(&self, lookup: HookLookup) -> Option<HookInfo> {
match lookup {
HookLookup::Name(name) => self.hooks_name.get_mut(&name).and_then(|idx| self.hooks.get_mut(idx.value()).map(|h| h.clone())),
HookLookup::Address(address) => self.hooks_address.get_mut(&address).and_then(|idx| self.hooks.get_mut(idx.value()).map(|h| h.clone())),
HookLookup::Index(index) => self.hooks.get_mut(&index).map(|h| h.clone()),
}
}
pub fn get_mem(&self, lookup: MemLookup) -> Option<OwnedMem> {
match lookup {
MemLookup::Address(address) => self.owned_mems_address.get_mut(&address).and_then(|idx| self.owned_mems.get_mut(idx.value()).map(|h| h.clone())),
MemLookup::Index(index) => self.owned_mems.get_mut(&index).map(|h| h.clone()),
}
}
fn add_hook(&mut self, hook: HookInfo) {
self.hooks.insert(self.hooks.len(), hook.clone());
self.hooks_name.insert(hook.name.clone(), self.hooks_name.len());
self.hooks_address.insert(hook.address, self.hooks_address.len());
}
fn add_owned_mem(&mut self, owned_mem: OwnedMem) {
self.owned_mems.insert(self.owned_mems.len(), owned_mem.clone());
self.owned_mems_address.insert(owned_mem.address, self.owned_mems.len());
}
fn update_hook(&mut self, hook: HookInfo, index: usize) -> Result<(), Box<dyn std::error::Error>> {
if self.hooks.contains_key(&index) {
self.hooks.entry(index).and_modify(|hk| {
hk.name = hook.name;
hk.address = hook.address;
hk.typ = hook.typ;
hk.assembly = hook.assembly;
});
Ok(())
} else {
Err("Failed to update hook, incorrect index".into())
}
}
fn update_owned_mem(&mut self, owned_mem: OwnedMem, index: usize) -> Result<(), Box<dyn std::error::Error>> {
if self.owned_mems.contains_key(&index) {
self.owned_mems.entry(index).and_modify(|mem| {
mem.address = owned_mem.address;
mem.size = owned_mem.size;
mem.used = owned_mem.used;
});
Ok(())
} else {
Err("Failed to update owned memory, incorrect index".into())
}
}
pub unsafe fn hook(&mut self, hook_info: HookInfo) -> Result<(), Box<dyn std::error::Error>> {
let mut hook_info = hook_info;
let name = hook_info.name.clone();
let address = hook_info.address;
let hook_type = hook_info.typ;
let assembly = hook_info.assembly.as_ref();
let architecture = arch();
let module_base = module_base(None);
if name.len() < 3 {
panic!("Give the hook a proper name. Name must be no less than 3 characters!")
}
let mut mbi = unsafe { std::mem::zeroed::<MEMORY_BASIC_INFORMATION>() };
if unsafe { VirtualQuery(address as *const _, &mut mbi as *mut _, std::mem::size_of::<MEMORY_BASIC_INFORMATION>()) } == 0 || mbi.State == 0 {
panic!("Not a valid address! {address:#X}");
}
if hook_type == HookType::Patch {
unprotect(address);
let bytes = assemble(address, architecture, assembly)?;
let mut instr_info = instruction_info(address, bytes.len(), architecture);
get_required_nops(&mut instr_info, 0);
let nop_bytes = self.get_nop_bytes(instr_info.nops - bytes.len());
self.place_bytes(address, nop_bytes)?;
self.place_bytes(address, bytes)?;
return Ok(());
}
let required_size = estimate_required_size(address, architecture, &assembly)?;
let mem_index = OwnedMem::check_in_mem(address, required_size, &self.owned_mems);
let mut mem = if let Some(index) = mem_index { self.get_mem(MemLookup::Index(index)).unwrap() } else { alloc(module_base)? };
let bytes = assemble(address, architecture, assembly)?;
self.insert_bytes(address, architecture, &mut mem, &mut hook_info, bytes.clone())?;
if let Some(index) = mem_index {
self.update_owned_mem(mem.clone(), index)?;
} else {
self.add_owned_mem(mem.clone());
}
self.add_hook(hook_info.clone());
Ok(())
}
fn place_bytes(&self, address: usize, bytes: Vec<u8>) -> Result<(), Box<dyn std::error::Error>> {
match self.attach_typ {
AttachType::Internal => {
let address = address as *mut u8;
let address = unsafe { std::slice::from_raw_parts_mut(address, bytes.len()) };
for index in 0..bytes.len() {
(*address)[index] = bytes[index];
}
Ok(())
}
AttachType::External => {
#[cfg(target_os = "windows")]
{
let ProcessId::Windows(handle) = self.process else {
return Err("Invalid process handle for Windows".into());
};
if handle.is_null() {
return Err("Null process handle".into());
}
let size = bytes.len();
if size == 0 {
return Ok(()); }
let mut bytes_written = 0;
let result = unsafe { WriteProcessMemory(handle as _, address as *mut c_void, bytes.as_ptr() as *const c_void, size, &mut bytes_written) };
if result == 0 {
let error = std::io::Error::last_os_error();
return Err(format!("WriteProcessMemory failed: {}", error).into());
}
if bytes_written != size {
return Err(format!("Only wrote {} of {} bytes", bytes_written, size).into());
}
Ok(())
}
#[cfg(target_os = "linux")]
{
Err("Linux implementation not provided".into())
}
}
}
}
fn insert_bytes(&self, src_address: usize, architecture: u32, owned_mem: &mut OwnedMem, hook_info: &mut HookInfo, bytes: Vec<u8>) -> Result<(InstructionsInfo, usize), Box<dyn std::error::Error>> {
let dst_address = owned_mem.address + owned_mem.used;
let jmp_size = get_jmp_size(dst_address, src_address);
let rva_mem = get_jump_offset(src_address, dst_address);
unprotect(src_address);
let mut org_instr_info = instruction_info(src_address, jmp_size, architecture);
get_required_nops(&mut org_instr_info, jmp_size);
let mut ret_address_jump = dst_address as usize + bytes.len() + org_instr_info.bytes.len();
if hook_info.typ == HookType::DetourNoOrg {
ret_address_jump = dst_address as usize + bytes.len();
}
if dst_address != 0 {
let rva_ret_jmp = get_ret_jump(src_address, dst_address, jmp_size, hook_info.typ.clone(), &mut org_instr_info, bytes.len());
let hook_jmp = self.get_jump_bytes(jmp_size, rva_mem as usize);
self.place_bytes(src_address, hook_jmp).unwrap();
let nops = self.get_nop_bytes(org_instr_info.nops);
self.place_bytes(src_address + jmp_size, nops).unwrap();
self.place_bytes(dst_address, bytes.clone())?;
if hook_info.typ == HookType::Detour {
self.place_bytes(dst_address + bytes.len(), org_instr_info.bytes.clone())?;
owned_mem.inc_used(org_instr_info.bytes.len()).unwrap();
}
let ret_jmp_bytes = self.get_jump_bytes(jmp_size, rva_ret_jmp);
self.place_bytes(ret_address_jump, ret_jmp_bytes).unwrap();
owned_mem.inc_used(bytes.len()).unwrap();
owned_mem.inc_used(jmp_size).unwrap();
}
hook_info.jumping_address = dst_address;
hook_info.org_bytes = org_instr_info.bytes.clone();
hook_info.org_nops = org_instr_info.nops;
hook_info.jmp_size = jmp_size;
Ok((org_instr_info, dst_address))
}
fn get_nop_bytes(&self, length: usize) -> Vec<u8> {
let mut bytes = Vec::new();
if length != 0 {
for _ in 0..length {
bytes.push(0x90);
}
}
bytes
}
fn get_jump_bytes(&self, jmp_size: usize, offset: usize) -> Vec<u8> {
let mut bytes = Vec::new();
if jmp_size == 5 {
bytes.push(0xE9);
let mut v = offset;
for _ in 0..4 {
bytes.push(v as u8);
v >>= 8;
}
} else if jmp_size == 14 {
let prefix = [0xFF, 0x25, 0x00, 0x00, 0x00, 0x00];
bytes.extend_from_slice(&prefix);
let mut v = offset;
for _ in 0..8 {
bytes.push(v as u8);
v >>= 8;
}
}
bytes
}
}
fn process() -> HANDLE { unsafe { GetCurrentProcess() } }
fn module_base(module: Option<&str>) -> usize {
let module_name = match module {
Some(name) => {
match CString::new(name) {
Ok(c_str) => c_str.as_ptr(),
Err(_) => std::ptr::null(), }
}
None => std::ptr::null(), };
let handle = unsafe { GetModuleHandleA(module_name) };
handle as usize
}
fn alloc(address: usize) -> Result<OwnedMem, Box<dyn std::error::Error>> {
let mut memory_info: MEMORY_BASIC_INFORMATION = unsafe { std::mem::zeroed() };
let mut current_address = address;
let mut attempts = 0;
let mut new_mem = OwnedMem::default();
while attempts < 100000 {
let mem_query = unsafe { VirtualQuery(current_address as *mut c_void, &mut memory_info as *mut MEMORY_BASIC_INFORMATION, size_of::<MEMORY_BASIC_INFORMATION>()) };
if mem_query > 0 {
if memory_info.State == MEM_FREE && memory_info.RegionSize >= PAGE_SIZE {
let alloc_mem = unsafe { VirtualAlloc(current_address as *mut c_void, PAGE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE) };
if !alloc_mem.is_null() {
let size = get_region_size(alloc_mem as usize).unwrap();
new_mem = OwnedMem { address: alloc_mem as usize, size, ..Default::default() };
break;
}
}
current_address = memory_info.BaseAddress as usize - memory_info.RegionSize;
} else {
current_address += PAGE_SIZE;
}
attempts += 1; }
Ok(new_mem)
}
fn estimate_required_size(address: usize, architecture: u32, assembly: impl Fn(&mut CodeAssembler)) -> Result<usize, Box<dyn std::error::Error>> {
let bytes = assemble(address, architecture, assembly)?.len();
Ok(bytes * 2)
}
fn unprotect(address: usize) {
let protection_size = 100;
let mut old_protection = 0;
let old_protection: *mut u32 = &mut old_protection;
unsafe { VirtualProtect(address as _, protection_size, PAGE_EXECUTE_READWRITE, old_protection) };
}
fn assemble(address: usize, architecture: u32, assembly: impl Fn(&mut CodeAssembler)) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut assembler = CodeAssembler::new(architecture).expect("Failed at constructing CodeAssembler");
assembly(&mut assembler);
let instructions = assembler.instructions();
let block = InstructionBlock::new(instructions, address as u64);
let result = BlockEncoder::encode(architecture, block, BlockEncoderOptions::DONT_FIX_BRANCHES).expect("Failed at encoding");
Ok(result.code_buffer)
}
fn instruction_info(address: usize, length: usize, architecture: u32) -> InstructionsInfo {
let bytes = unsafe { std::slice::from_raw_parts_mut(address as *mut u8, 100) };
let mut instrs_size = 0;
let mut opcodes = Vec::new();
let mut decoder = Decoder::with_ip(architecture, &bytes, address as u64, DecoderOptions::NONE);
let mut formatter = SpecializedFormatter::<MyTraitOptions>::new();
let mut formated_instr = String::new();
let mut instr = Instruction::default();
let mut instrs = Vec::new();
while decoder.can_decode() {
decoder.decode_out(&mut instr);
formated_instr.clear();
formatter.format(&instr, &mut formated_instr);
opcodes.push(formated_instr.clone());
instrs_size += instr.len();
instrs.push(instr);
if instrs_size >= length {
break;
}
}
let block = InstructionBlock::new(&instrs, address as u64 + bytes.len() as u64);
let instrs_bytes = BlockEncoder::encode(decoder.bitness(), block, BlockEncoderOptions::NONE).expect("Failed at encoding BlockEncoder").code_buffer;
return InstructionsInfo { address, opcodes, instrs, bytes: instrs_bytes, nops: 0 };
}
fn opcode_display(architecture: u32, assembly: impl Fn(&mut CodeAssembler)) -> Vec<String> {
let mut assembler = CodeAssembler::new(architecture).unwrap();
assembly(&mut assembler);
let mut formatter = SpecializedFormatter::<MyTraitOptions>::new();
let mut formated_instr = String::new();
let mut opcodes = Vec::new();
for instr in assembler.instructions() {
formated_instr.clear();
formatter.format(&instr, &mut formated_instr);
opcodes.push(formated_instr.clone());
}
opcodes
}
fn get_required_nops(instr_info: &mut InstructionsInfo, jmp_size: usize) {
let mut length = 0;
for instr in instr_info.instrs.iter() {
length += instr.len();
if length >= jmp_size {
break;
}
}
instr_info.nops = length.abs_diff(jmp_size);
}
fn get_ret_jump(src_address: usize, dst_address: usize, jmp_size: usize, hook_type: HookType, instr_info: &mut InstructionsInfo, bytes_len: usize) -> usize {
get_required_nops(instr_info, jmp_size);
let rva_dst;
let mut rva_ret_jmp = src_address;
if jmp_size == 5 {
if src_address < (dst_address as usize) {
rva_dst = dst_address as usize - src_address - jmp_size;
rva_ret_jmp = rva_dst + instr_info.bytes.len() + jmp_size + instr_info.nops + 1;
if hook_type == HookType::DetourNoOrg {
rva_ret_jmp = rva_dst + jmp_size + instr_info.nops + 1;
}
} else {
rva_dst = src_address - dst_address as usize + jmp_size - 1;
rva_ret_jmp = rva_dst - bytes_len - instr_info.bytes.len() - jmp_size + instr_info.nops + 1;
if hook_type == HookType::DetourNoOrg {
rva_ret_jmp = rva_dst - bytes_len - jmp_size + instr_info.nops + 1;
}
}
} else if jmp_size == 14 {
rva_ret_jmp = src_address + jmp_size + instr_info.nops;
}
rva_ret_jmp
}
fn get_jmp_size(src_address: usize, dst_address: usize) -> usize {
let distance = dst_address.abs_diff(src_address);
if distance > i32::MAX as usize { 14 } else { 5 }
}
fn get_jump_offset(src_address: usize, dst_address: usize) -> isize {
let jmp_size = get_jmp_size(src_address, dst_address);
let src_address = src_address as isize;
let dst_address = dst_address as isize;
if jmp_size == 14 {
return dst_address;
}
dst_address - (src_address + jmp_size as isize)
}
static ARCHITECTURE: OnceLock<u32> = OnceLock::new();
fn arch() -> u32 { *ARCHITECTURE.get_or_init(|| get_architecture(process()).unwrap_or(64)) }
fn get_architecture(handle: winapi::um::winnt::HANDLE) -> Result<u32, Box<dyn std::error::Error>> {
let mut is_wow64 = 0;
let result = unsafe { IsWow64Process(handle, &mut is_wow64) };
if result == 0 {
return Err("Couldn't get the architecture".into());
}
Ok(if is_wow64 == 0 { 64 } else { 32 })
}
fn get_region_size(address: usize) -> Result<usize, Box<dyn std::error::Error>> {
let mut mem_info = unsafe { std::mem::zeroed() };
let result = unsafe { VirtualQuery(address as *mut _, &mut mem_info, std::mem::size_of::<MEMORY_BASIC_INFORMATION>()) };
if result > 0 { Ok(mem_info.RegionSize) } else { Err("Failed to retrieve region size".into()) }
}