Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
CallGhost
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.
Features
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)
Methods
| 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 |
When to use what
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.
Usage
[]
= { = "https://github.com/PatchRequest/CallGhost", = "master" }
use syscall;
// Allocate memory via direct syscall, no ntdll involved
let mut base: *mut c_void = null_mut;
let mut size: usize = 4096;
let status = syscall!;
// Same call via indirect syscall (return address points to ntdll)
let status = syscall!;
// Perun's Fart: JIT unhook, call through ntdll, re-hook
let status = syscall!;
Standalone unhook
// Unhook a single function
unsafe ;
// Unhook ALL of ntdll's .text section
unsafe ;
// Release the cached KnownDlls mapping when done
unsafe ;
Architecture
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
How it works
- 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
Tests
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.
Requirements
Windows x86_64, Rust stable 1.85+ (edition 2024).