Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
Unwinder — Call stack spoofing for Rust | Kitploit
Tools/GitHubGitHub/kudaes/unwinder
IDS/IPS EvasionPost-ExploitationRed TeamingPayload DevelopmentAdversarial Attack
GitHubkudaes/unwinder

Unwinder

Call stack spoofing for Rust

View Repository
38436161 year agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

Content

  • SilentMoonWalk
    • Description
    • Credits
    • Usage
      • call_function!() macro
      • indirect_syscall!() macro
      • Parameter passing
    • Examples
      • Calling kernel32.dll!Sleep()
      • Calling kernel32.dll!OpenProcess()
      • Calling NtDelayExecution() as indirect syscall
      • Concatenate macro calls
    • Considerations
      • Initial frame
      • PoC
  • Stack replacement
    • Description
    • Usage
    • Practical example
    • Remarks

SilentMoonWalk

Description

Unwinder provides a full weaponization of SilentMoonWalk technique, allowing to obtain complete and stable call stack spoofing in Rust.

This technique comes with the following characteristics:

  • Support to run any arbitrary function with up to 11 parameters.
  • Support to run indirect syscalls (no additional heap allocations) with up to 11 parameters.
  • The crate allows to retrieve the value returned by the functions called through it.
  • The spoofing process can be concatenated any number of times without increasing the call stack size.
  • TLS is used to increase efficiency during the spoofing process.
  • dinvoke_rs is used to make any Windows API call required by the crate.

Credits

kudos to the creators of the SilentMoonWalk technique:

  • KlezVirus
  • Waldo-IRC
  • Trickster0

And of course a huge shoutout to namazso for the Twitter thread that inspired this whole project.

Usage

Import this crate into your project by adding the following line to your cargo.toml and compile on release mode:

[dependencies]
unwinder = "=0.1.4"

The main functionality of this crate has been wrapped in two macros:

  • The call_function!() macro allows to run any arbitrary function with a clean call stack.
  • The indirect_syscall!() macro executes the specified (indirect) syscall with a clean call stack.

To use any of these macros it is required to import std::ffi::c_void data type.

Both macros return a *mut c_void that can be used to retrieve the value returned by the function executed. More detailed information in the examples section.

call_function macro

This macro is used to call any desired function with a clean call stack. The macro expects the following parameters:

  • The first parameter is the memory address to call after spoofing the call stack. This parameter should be passed as a usize, isize or a pointer.
  • The second parameter is a bool indicating whether or not keep the start function frame. If you are not sure about this, set it to false which always guarantees a good call stack.
  • The following parameters are those arguments to send to the function once the call stack has been spoofed.

indirect_syscall macro

This macro is used to perform any desired indirect syscall with a clean call stack. The macro expects the following parameters:

  • The first parameter is a string that contains the name of the NT function whose syscall you want to execute.
  • The second parameter is a bool indicating whether or not keep the start function frame. If you are not sure about this, set it to false which always guarantees a good call stack.
  • The following parameters are those arguments to send to the NT function.

Parameter passing

In order to pass arguments of different types to these two macros, the following considerations must be taken into account:

  • Any basic data type that can be converted to usize (u8-u64, i8-i64, bool, etc.) can be passed directly to the macros.
  • Structs and unions of size 8, 16, 32, or 64 bits are passed as if they were integers of the same size.
  • Structures and unions with a size larger than 64 bits must be passed as a pointer.
  • Strings (&str and String) must be passed as a pointer.
  • Null pointers (ptr::null(), ptr::null_mut(), etc. ) are passed as a 0 (no matter if it is u8, u16, i32 or any other).
  • Floating-point and double-precision parameters are not currently supported.
  • Any other data type must be passed as a pointer.

Examples

Calling Sleep

let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let sleep = dinvoke_rs::dinvoke::get_function_address(k32, "Sleep"); // Memory address of kernel32.dll!Sleep() 
let miliseconds = 1000i32;
unwinder::call_function!(sleep, false, miliseconds);

Calling OpenProcess

let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll"); 
let open_process: isize = dinvoke_rs::dinvoke::get_function_address(k32, "Openprocess");
let desired_access: u32 = 0x1000;
let inherit = 0i32;
let pid = 20628i32;
let handle = unwinder::call_function!(open_process, false, desired_access, inherit, pid); // returns *mut c_void
let handle: HANDLE = std::mem::transmute(handle);
println!("Handle id: {:x}", handle.0);

Notice that the macro returns a *mut c_void that can be directly converted to a HANDLE since both data types has the same size. This allows to access to the value returned by OpenProcess, which is the new handle to the target process.

Calling NtDelayExecution as indirect syscall

let large = 0x8000000000000000 as u64; // Sleep indefinitely
let large: *mut i64 = std::mem::transmute(&large);
let alertable = false;
let ntstatus = unwinder::indirect_syscall!("NtDelayExecution", false, alertable, large); // returns *mut c_void
println!("ntstatus: {:x}", ntstatus as i32);

Notice that the macro returns a *mut c_void that can be used to retrieve the NTSTATUS returned by NtDelayExecution.

Concatenate macro calls

The spoofing process can be concatenated any number of times without an abnormal call stack size increment. The execution flow will be preserved as well. The following code is an example of this:

fn main()
{
	function_a();
}

fn function_a()
{
	unsafe
	{
		let func_b = function_b as usize;
		call_function!(func_b, false);
		println!("function_a done.");
	}
}

fn function_b()
{
	unsafe
	{
		let func_c = function_c as usize;
		call_function!(func_c, false);
		println!("function_b done.")
	}
}

fn function_c()
{
	unsafe
	{
		let large = 0x0000000000000000 as u64; // Don't sleep so we return to function_b, allowing to check the execution flow preservation.
		let large: *mut i64 = std::mem::transmute(&large);
		let alertable = false;
		let ntstatus = unwinder::indirect_syscall!("NtDelayExecution", false, alertable, large);
		println!("ntstatus: {:x}", (ntstatus as usize) as i32); //NTSTATUS is a i32, although that second casting is not really required in this case.
	}
}

Considerations

Initial frame

If you set the second parameter to true (both macros), the spoofing process will try to keep the thread start address' frame in the call stack to increase legitimacy.

Call stack spoofed keeping the main module.

Sometimes, the thread's start function does not perform a call to a subsequent function (e.g. a jmp instruction is executed instead), meaning there is not return address pushed to the stack. In that scenario (and also if you set that second parameter to false), the spoofed call stack will start at BaseThreadInitThunk's frame.

Download Tool