Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
Unwinder — 调用栈伪装工具,适用于 Rust | Kitploit
工具/GitHubGitHub/kudaes/unwinder
IDS/IPS规避后渗透利用红队Payload 开发对抗性攻击
GitHubkudaes/unwinder

Unwinder

调用栈伪装工具,适用于 Rust

查看仓库
3843641年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

目录

  • SilentMoonWalk
    • 描述
    • 致谢
    • 用法
      • call_function!() 宏
      • indirect_syscall!() 宏
      • 参数传递
    • 示例
      • 调用 kernel32.dll!Sleep()
      • 调用 kernel32.dll!OpenProcess()
      • 以间接系统调用方式调用 NtDelayExecution()
      • 串联宏调用
    • 注意事项
      • 初始帧
      • PoC
  • 栈替换
    • 技术描述
    • 使用方法
    • 实际示例
    • 备注

SilentMoonWalk

描述

Unwinder 提供了对 SilentMoonWalk 技术的完整武器化实现,使得能够在 Rust 中获得完整且稳定的调用栈欺骗(call stack spoofing)。

该技术具有以下特性:

  • 支持运行任意函数,最多可带 11 个参数。
  • 支持运行间接系统调用(无需额外堆分配),最多可带 11 个参数。
  • 该 crate 允许获取通过它调用的函数所返回的值。
  • 欺骗过程可以任意次串联,而不会增加调用栈的大小。
  • 在欺骗过程中使用 TLS 以提高效率。
  • 使用 dinvoke_rs 来执行该 crate 所需的任何 Windows API 调用。

致谢

感谢 SilentMoonWalk 技术的创造者:

  • KlezVirus
  • Waldo-IRC
  • Trickster0

当然,还要特别感谢 namazso 的 Twitter 帖子,正是它启发了整个项目。

用法

通过将以下行添加到你的 cargo.toml 中,并在 release 模式下编译,即可将此 crate 导入到你的项目中:

root@kitploit:~
[dependencies]
unwinder = "=0.1.4"

此 crate 的主要功能已封装在两个宏中:

  • call_function!() 宏允许以干净的调用栈运行任意函数。
  • indirect_syscall!() 宏以干净的调用栈执行指定的(间接)系统调用。

要使用这些宏中的任意一个,需要导入 std::ffi::c_void 数据类型。

这两个宏都会返回一个 *mut c_void,可用于获取被执行的函数所返回的值。更详细的信息请参阅示例部分。

call_function 宏

该宏用于以干净的调用栈调用任何所需函数。 该宏需要以下参数:

  • 第一个参数是在欺骗调用栈后要调用的内存地址。该参数应作为 usize、isize 或指针传递。
  • 第二个参数是一个 bool,指示是否保留启动函数的帧。如果不确定,请将其设置为 false,这总能保证良好的调用栈。
  • 后续参数是在调用栈被欺骗后要发送给函数的参数。

indirect_syscall 宏

该宏用于以干净的调用栈执行任何所需的间接系统调用。 该宏需要以下参数:

  • 第一个参数是一个字符串,包含你要执行其系统调用的 NT 函数的名称。
  • 第二个参数是一个 bool,指示是否保留启动函数的帧。如果不确定,请将其设置为 false,这总能保证良好的调用栈。
  • 后续参数是发送给 NT 函数的参数。

参数传递

为了向这两个宏传递不同类型的参数,必须考虑以下事项:

  • 任何可以转换为 usize 的基本数据类型(u8-u64、i8-i64、bool 等)都可以直接传递给宏。
  • 大小为 8、16、32 或 64 位的结构体和联合体,按照相同大小的整数进行传递。
  • 大小超过 64 位的结构体和联合体必须作为指针传递。
  • 字符串(&str 和 String)必须作为指针传递。
  • 空指针(ptr::null()、ptr::null_mut() 等)作为 0 传递(无论是 u8、u16、i32 还是其他类型)。
  • 当前不支持浮点数和双精度参数。
  • 任何其他数据类型都必须作为指针传递。

示例

调用 Sleep

root@kitploit:~
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);

调用 OpenProcess

root@kitploit:~
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);

请注意,该宏返回一个 *mut c_void,由于两种数据类型的大小相同,可以将其直接转换为 HANDLE。这样就能访问 OpenProcess 返回的值,即目标进程的新句柄。

以间接系统调用方式调用 NtDelayExecution

root@kitploit:~
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);

请注意,该宏返回一个 *mut c_void,可用于获取 NtDelayExecution 返回的 NTSTATUS。

串联宏调用

欺骗过程可以任意次串联,而不会出现异常的调用栈大小增长。执行流程也将得到保留。下面的代码是一个示例:

root@kitploit:~
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.
	}
}

注意事项

初始帧

如果你将第二个参数设置为 true(两个宏都是如此),欺骗过程将尝试在调用栈中保留线程起始地址的帧,以增加合法性。

保留了主模块的调用栈欺骗。

有时,线程的启动函数不会执行 call 来调用后续函数(例如,执行的是 jmp 指令),这意味着没有返回地址被压入栈中。在这种场景下(以及如果你将该第二个参数设置为 false),被欺骗的调用栈将从 BaseThreadInitThunk 的帧开始。

不保留主模块时的调用栈欺骗。

PoC

为了测试该技术的实现,使用了 PE-sieve 并带有 /threads 标志。测试结果表明,当使用此 crate 的功能时,对调用栈的检查不会暴露 payload 的存在。从第二张图片可以看出,当不使用 unwinder 时,payload 会被检测到。

使用 unwinder 时的 PE-sieve 结果。 未使用 unwinder 时的 PE-sieve 结果。

栈替换

技术描述

这是 SilentMoonWalk 的一种调用栈欺骗替代方案,允许在程序执行期间保持干净的调用栈。该技术的主要思想是:模块内的每个被调用函数负责处理先前压入的返回地址,在运行时找到一个与要欺骗的返回地址具有相同帧大小的合法函数。一旦找到具有相同帧大小的合法函数,就会计算出其中的一个偏移量,并使用最终地址来替换最后一个返回地址,从而隐藏调用栈中的任何异常条目并使其保持可展开(unwindable)。原始返回地址由 unwinder 存储,并在执行返回指令之前将其移回栈中的正确位置,从而允许程序继续正常流程。

栈替换

这是一个实验性功能,尽管已经完全可用,但仍处于开发和研究中,因此如果你决定将该技术集成到代码中,请务必进行测试。

使用方法

要使用栈替换功能,你应该将以下行添加到你的 cargo.toml 中,并在 release 模式下编译:

root@kitploit:~
[dependencies]
unwinder = {version = "0.1.4", features = ["Experimental"]}

该功能的主要功能已封装在以下宏中:

  • start_stack_replacement!() / end_replacement!() 这对宏用于指示 unwinder 开始/结束栈替换过程。这两个宏必须在你代码的入口点调用(例如,在 dll 的导出函数中)。
  • replace_and_continue!() / restore!() 这对宏用于执行最后一个返回地址的替换/恢复。
  • 最后,replace_and_call!() / replace_and_syscall!() 这对宏用于在我们要调用当前模块之外的函数时执行栈替换(例如,使用 Windows API 或调用任何其他 dll 的代码)。这两个宏都将返回一个 *mut c_void,其中包含以这种方式调用的函数所返回的值(即,它们的操作方式与用于执行 SilentMoonWalk 的 call_function 和 indirect_syscall 宏相同)。

要使用这些宏,需要导入 std::ffi::c_void 数据类型。 使用这些宏的所有函数都应标注 #[no_mangle] 或 #[inline(never)] 属性,以防止 rust 编译器在优化过程中内联它们。

在深入一个实际示例展示如何使用所有这些内容之前,先快速了解一下 replace_and_call / replace_and_syscall 这对宏以及如何向它们传递预期的参数。

replace_and_call

该宏用于在栈替换期间,以干净的调用栈调用当前模块之外的任何所需函数。 该宏需要以下参数:

  • 第一个参数是要调用的函数的内存地址。该参数应作为 usize、isize 或指针传递。
  • 后续参数是发送给指定函数的参数。它们遵循 参数传递 部分中指定的相同规则。

replace_and_syscall

该宏用于在栈替换期间,以干净的调用栈执行任何所需的间接系统调用。 该宏需要以下参数:

  • 第一个参数是一个字符串,包含你要执行其系统调用的 NT 函数的名称。
  • 后续参数是发送给 NT 函数的参数。它们遵循 参数传递 部分中指定的相同规则。

实际示例

我认为通过一个实际示例来展示这些宏的用法是最好的方式。假设我们正在创建一个将被反射注入到内存中的 dll。这个 dll 将导出两个函数 ExportA 和 ExportB,因此我们将这两个函数视为模块的入口点。它们都必须在开头调用 start_stack_replacement 宏,并且还必须在返回之前调用相反的 end_replacement 宏。start_stack_replacement 宏期望以模块基地址作为参数,或者如果你在运行时不知道该地址,可以传递 0,宏会尝试自行计算出来。

root@kitploit:~
#[no_mangle]
fn ExportedA(base_address: usize) -> bool
{
    unwinder::start_replacement!(base_address);
    ...
    unwinder::end_replacement!();

    true
}

#[no_mangle]
fn ExportedB() -> bool
{
    unwinder::start_replacement!(0);
    ...
    unwinder::end_replacement!();

    true
}

启动栈替换过程需要手动构造一个新的栈,该栈将一直使用到调用 end_replacement 宏为止。下图说明了底层发生的情况:

栈替换

虽然理论上没有必要从头开始创建一个新栈,但我决定以这种方式实现该过程,以确保稳定性并防止任何东西出错。

现在,假设我们的 ExportedA 函数多次调用另外两个内部函数。这两个内部函数负责替换/恢复原始的返回地址(该地址将指向 ExportedA 内的某个位置),除非我们处理它,否则会破坏调用栈。这个替换过程涉及将我们内部函数的代码包裹在 replace_and_continue 和 restore 宏之间:

root@kitploit:~
#[no_mangle]
fn ExportedA(base_address: usize) -> bool
{
    unwinder::start_replacement!(base_address);
    let ret_a = internal_a();
    let ret_b = internal_b(ret_a);
    unwinder::end_replacement!();

    ret_b
}

#[inline(never)] // This attribute is mandatory
fn internal_a() -> bool
{
    unwinder::replace_and_continue();
    ...
    unwinder::restore();
    
    some_value
} 

#[inline(never)] // This attribute is mandatory
fn internal_b(value: bool) -> bool
{
    unwinder::replace_and_continue();
    ...
    unwinder::restore();
    
    some_value
} 

最后,internal_a 和 internal_b 函数都使用了一些 Windows API 功能。为了保持可展开的调用栈,这些调用应通过 replace_and_call(普通调用)或 replace_and_syscall(间接系统调用)宏来执行。

root@kitploit:~
#[no_mangle] // This attribute is mandatory
fn ExportedA(base_address: usize) -> bool
{
    unwinder::start_replacement!(base_address);
    let ret_a = internal_a();
    let ret_b = internal_b(ret_a);
    unwinder::end_replacement!();

    ret_b
}

#[inline(never)] // This attribute is mandatory
fn internal_a() -> bool
{
    unwinder::replace_and_continue();
    ...
    let module_name = "advapi32.dll";
    let module_name = CString::new(module_name.to_string()).expect("");
    let module_name_ptr: *mut u8 = std::mem::transmute(module_name.as_ptr());
    let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
    let load_library = dinvoke_rs::dinvoke::get_function_address(k32, "LoadLibraryA");
    let ret = unwinder::replace_and_call!(load_library, module_name_ptr); // Load a dll with an unwindable call stack
    println!("advapi.dll base address: 0x{:x}", ret as usize);
    ...
    unwinder::restore();
    
    some_value
} 

#[inline(never)] // This attribute is mandatory
fn internal_b(value: bool) -> bool
{
    unwinder::replace_and_continue();
    ...
    let large = 0xFFFFFFFFFF676980 as u64; // Sleep one second
    let large: *mut i64 = std::mem::transmute(&large);
    let alertable = false;
    let ntstatus = unwinder::replace_and_syscall!("NtDelayExecution", alertable, large);
    println!("ntstatus: {:x}", ntstatus as usize);
    ...
    unwinder::restore();
    
    some_value
} 

备注

由于这是一个正在开发中的功能,有一些事项必须注意:

  • 如果你在加载过程中去除了 PE 的头部,你必须将模块的基地址传递给 start_stack_replace 宏。目前,它无法自行找到该地址(将在下一次更新中解决)。
  • 如果你想知道,栈替换使用了与 SilentMoonWalk 技术相同的 jmp rbx + 隐藏帧(concealment frame)组合。这只在使用 replace_and_call 和 replace_and_syscall 宏时发生,并计划在下次更新中进行更改。
  • replace_and_call 和 replace_and_syscall 宏都会返回一个 *mut c_void,可用于获取通过它们执行的函数所返回的值。这与 call_function 和 indirect_syscall 宏所描述的行为相同。
  • replace_and_call 和 replace_and_syscall 宏最多允许 11 个参数。

如果使用此功能时出现任何错误,请向我报告。

下载工具