Direct syscall framework for Rust on Windows x86_64. Bypasses usermode API hooks (EDR/AV) by avoiding ntdll stubs entirely or restoring them from clean copies.
syscall!proc macro with per-call method selection, up to 12 args, compile-time function name hashing- 4 syscall methods:
direct,indirect,unhook,perunsfart - Halo's Gate SSN resolution even when stubs are hooked
no_stdruntime, no CRT dependency, usable in shellcode loaders- SSN cache (export table walked once per function, O(1) after)
- KnownDlls cache (
\KnownDlls\ntdll.dllmapped once, reused for all unhook ops)
| Method | Syntax | What happens | Return address points to |
|---|---|---|---|
direct |
syscall!(NtFoo, ...) |
Inline syscall instruction in your code |
Your module |
indirect |
syscall!(indirect, NtFoo, ...) |
call to a syscall;ret gadget inside ntdll |
ntdll |
unhook |
syscall!(unhook, NtFoo, ...) |
Restore stub from KnownDlls, then direct syscall | Your module |
perunsfart |
syscall!(perunsfart, NtFoo, ...) |
JIT unhook, call through clean stub, re-hook | ntdll |
direct is the default. Fast, no dependencies. Use when return-address checks aren't a concern.
indirect is for when EDR checks if the syscall instruction originates from ntdll. The gadget address is cached after first lookup.
unhook permanently restores a hooked stub so your code or other code in the process can call through ntdll normally again.
perunsfart is for when you need the call to look 100% legitimate (return address in ntdll) but don't want to leave the stub unhooked, since EDR periodic scans would notice.
[dependencies]
callghost = { git = "https://github.com/PatchRequest/CallGhost", branch = "master" }use callghost::syscall;
// Allocate memory via direct syscall, no ntdll involved
let mut base: *mut core::ffi::c_void = core::ptr::null_mut();
let mut size: usize = 4096;
let status = syscall!(NtAllocateVirtualMemory,
-1isize, // NtCurrentProcess
&mut base,
0usize,
&mut size,
0x3000u32, // MEM_COMMIT | MEM_RESERVE
0x04u32, // PAGE_READWRITE
);
// Same call via indirect syscall (return address points to ntdll)
let status = syscall!(indirect, NtAllocateVirtualMemory,
-1isize, &mut base, 0usize, &mut size, 0x3000u32, 0x04u32);
// Perun's Fart: JIT unhook, call through ntdll, re-hook
let status = syscall!(perunsfart, NtCreateThreadEx,
&mut thread, 0x1FFFFFu32, core::ptr::null_mut::<u8>(),
process, entry_point, core::ptr::null_mut::<u8>(),
0u32, 0usize, 0usize, 0usize, core::ptr::null_mut::<u8>());// Unhook a single function
unsafe { callghost::__private::unhook_stub(callghost::__private::fnv1a(b"NtWriteVirtualMemory")) };
// Unhook ALL of ntdll's .text section
unsafe { callghost::__private::unhook_all() };
// Release the cached KnownDlls mapping when done
unsafe { callghost::__private::release_clean_ntdll() };callghost/ Facade crate, re-exports macro + runtime
callghost-macros/ Proc macro crate, generates inline asm per call site
callghost-runtime/ no_std runtime: PEB walking, SSN resolution, unhook infra
- Compile time:
syscall!hashes the function name (FNV-1a) and generates method-specific inline assembly - First call: Walks PEB, finds ntdll, walks export table, resolves SSN, caches it
- Subsequent calls: SSN from cache, direct/indirect asm or function pointer call
- Hooked stubs: Halo's Gate sorts all Nt*/Zw* stubs by RVA, finds clean neighbors, calculates SSN by offset
- Unhook: Maps
\KnownDlls\ntdll.dll(cached), copies clean bytes over hooked stub
cargo test -- --test-threads=1
19 tests covering hash correctness, stub cleanliness verification across 7 functions, SSN uniqueness, gadget location verification, hook proof (calling through a hooked stub returns the sentinel value), all 4 methods bypassing hooks, Halo's Gate with hooked neighbors, permanent unhook with SSN consistency, unhook_all restoring 3 hooked stubs, Perun's Fart succeeding while re-hooking, and full interchangeability (alloc+write+free with every method).
Single-threaded execution required because hook tests temporarily modify ntdll stubs.
Windows x86_64, Rust stable 1.85+ (edition 2024).