Unwinder 提供了对 SilentMoonWalk 技术的完整武器化实现,使得能够在 Rust 中获得完整且稳定的调用栈欺骗(call stack spoofing)。
该技术具有以下特性:
感谢 SilentMoonWalk 技术的创造者:
当然,还要特别感谢 namazso 的 Twitter 帖子,正是它启发了整个项目。
通过将以下行添加到你的 cargo.toml 中,并在 release 模式下编译,即可将此 crate 导入到你的项目中:
[dependencies]
unwinder = "=0.1.4"
此 crate 的主要功能已封装在两个宏中:
call_function!() 宏允许以干净的调用栈运行任意函数。indirect_syscall!() 宏以干净的调用栈执行指定的(间接)系统调用。要使用这些宏中的任意一个,需要导入 std::ffi::c_void 数据类型。
这两个宏都会返回一个 *mut c_void,可用于获取被执行的函数所返回的值。更详细的信息请参阅示例部分。
该宏用于以干净的调用栈调用任何所需函数。 该宏需要以下参数:
usize、isize 或指针传递。该宏用于以干净的调用栈执行任何所需的间接系统调用。 该宏需要以下参数:
为了向这两个宏传递不同类型的参数,必须考虑以下事项:
usize 的基本数据类型(u8-u64、i8-i64、bool 等)都可以直接传递给宏。&str 和 String)必须作为指针传递。ptr::null()、ptr::null_mut() 等)作为 0 传递(无论是 u8、u16、i32 还是其他类型)。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);
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 返回的值,即目标进程的新句柄。
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。
欺骗过程可以任意次串联,而不会出现异常的调用栈大小增长。执行流程也将得到保留。下面的代码是一个示例:
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 的帧开始。

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

这是 SilentMoonWalk 的一种调用栈欺骗替代方案,允许在程序执行期间保持干净的调用栈。该技术的主要思想是:模块内的每个被调用函数负责处理先前压入的返回地址,在运行时找到一个与要欺骗的返回地址具有相同帧大小的合法函数。一旦找到具有相同帧大小的合法函数,就会计算出其中的一个偏移量,并使用最终地址来替换最后一个返回地址,从而隐藏调用栈中的任何异常条目并使其保持可展开(unwindable)。原始返回地址由 unwinder 存储,并在执行返回指令之前将其移回栈中的正确位置,从而允许程序继续正常流程。
这是一个实验性功能,尽管已经完全可用,但仍处于开发和研究中,因此如果你决定将该技术集成到代码中,请务必进行测试。
要使用栈替换功能,你应该将以下行添加到你的 cargo.toml 中,并在 release 模式下编译:
[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 这对宏以及如何向它们传递预期的参数。
该宏用于在栈替换期间,以干净的调用栈调用当前模块之外的任何所需函数。 该宏需要以下参数:
usize、isize 或指针传递。该宏用于在栈替换期间,以干净的调用栈执行任何所需的间接系统调用。 该宏需要以下参数:
我认为通过一个实际示例来展示这些宏的用法是最好的方式。假设我们正在创建一个将被反射注入到内存中的 dll。这个 dll 将导出两个函数 ExportA 和 ExportB,因此我们将这两个函数视为模块的入口点。它们都必须在开头调用 start_stack_replacement 宏,并且还必须在返回之前调用相反的 end_replacement 宏。start_stack_replacement 宏期望以模块基地址作为参数,或者如果你在运行时不知道该地址,可以传递 0,宏会尝试自行计算出来。
#[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 宏之间:
#[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(间接系统调用)宏来执行。
#[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
}
由于这是一个正在开发中的功能,有一些事项必须注意:
start_stack_replace 宏。目前,它无法自行找到该地址(将在下一次更新中解决)。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 个参数。如果使用此功能时出现任何错误,请向我报告。