Skip to content

PatchRequest/CallGhost

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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_std runtime, no CRT dependency, usable in shellcode loaders
  • SSN cache (export table walked once per function, O(1) after)
  • KnownDlls cache (\KnownDlls\ntdll.dll mapped 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

[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>());

Standalone unhook

// 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() };

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

  1. Compile time: syscall! hashes the function name (FNV-1a) and generates method-specific inline assembly
  2. First call: Walks PEB, finds ntdll, walks export table, resolves SSN, caches it
  3. Subsequent calls: SSN from cache, direct/indirect asm or function pointer call
  4. Hooked stubs: Halo's Gate sorts all Nt*/Zw* stubs by RVA, finds clean neighbors, calculates SSN by offset
  5. 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).

About

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

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages