3 个版本
0.1.2 | 2024 年 5 月 19 日 |
---|---|
0.1.1 | 2024 年 1 月 20 日 |
0.1.0 | 2023 年 11 月 28 日 |
#9 in #call-stack
198 个月下载量
在 shelter 中使用
120KB
1.5K SLoC
内容
SilentMoonWalk
描述
Unwinder 为 SilentMoonWalk 技术提供了完整的武器化,允许在 Rust 中实现完整且稳定的调用栈欺骗。
该技术具有以下特点
- 支持运行带有最多 11 个参数的任何任意函数。
- 支持运行带有最多 11 个参数的间接系统调用(无需额外的堆分配)。
- 该库允许检索通过它调用的函数返回的值。
- 欺骗过程可以连接任意次数,而不会增加调用栈大小。
- 在欺骗过程中使用 TLS 以提高效率。
- 使用 dinvoke_rs 来执行库所需的任何 Windows API 调用。
致谢
感谢 SilentMoonWalk 技术的创造者
当然,还要向 namazso 表示敬意,他的 Twitter 线程 启发了这个项目的整个构思。
用法
通过在您的 cargo.toml
中添加以下行将此 crate 导入到您的项目中,并在 release
模式下编译
[dependencies]
unwinder = "0.1.2"
此 crate 的主要功能已封装在两个宏中
call_function!()
宏允许以干净的调用栈运行任何任意函数。indirect_syscall!()
宏在干净的调用栈上执行指定的(间接)系统调用。
要使用这些宏之一,需要导入 std::ffi::c_void
数据类型。
这两个宏都返回一个 *mut c_void
,可用于检索执行函数返回的值。更详细的信息请参阅示例部分。
call_function 宏
此宏用于调用任何所需的函数并带有干净的调用栈。该宏期望以下参数
- 第一个参数是在欺骗调用栈之后调用的内存地址。此参数应作为
usize
、isize
或一个指针传递。 - 第二个参数是一个布尔值,表示是否保留起始函数帧。如果您不确定这一点,请将其设置为 false,这始终可以保证良好的调用栈。
- 以下参数是在欺骗调用栈后发送给函数的参数。
间接syscall宏
此宏用于执行任何所需的间接syscall,具有干净的调用栈。宏期望以下参数
- 第一个参数是一个包含您要执行的syscall的NT函数名称的字符串。
- 第二个参数是一个布尔值,表示是否保留起始函数帧。如果您不确定这一点,请将其设置为 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
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
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: *mut c_void = unwinder::call_function!(open_process, false, desired_access, inherit, pid);
let handle: HANDLE = std::mem::transmute(handle);
println!("Handle id: {:x}", handle.0);
注意,该宏返回一个 *mut c_void
,它可以直接转换为 HANDLE
,因为这两种数据类型具有相同的大小。这允许访问 OpenProcess
返回的值,即目标进程的新句柄。
作为间接syscall调用NtDelayExecution
let large = 0x8000000000000000 as u64; // Sleep indefinitely
let large: *mut i64 = std::mem::transmute(&large);
let alertable = false;
let ntstatus: *mut c_void = unwinder::indirect_syscall!("NtDelayExecution", false, alertable, large);
println!("ntstatus: {:x}", ntstatus as usize);
注意,该宏返回一个 *mut c_void
,可以用来检索 NTSTATUS
,它是 NtDelayExecution
返回的。
连接宏调用
欺骗过程可以多次拼接,而不会导致调用栈大小异常增加。执行流程也将得到保留。以下代码是一个示例
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
为了测试该技术的实现,使用了带有标志 /threads
的 PE-sieve。测试结果显示,在使用该程序的功能时,通过检查调用栈并不能发现有效载荷的存在。如图二所示,当不使用回溯器时,可以检测到有效载荷。
堆栈替换
技术描述
这是一种替代 SilentMoonWalk 的调用栈欺骗技术,可以在程序执行期间保持干净的调用栈。该技术背后的主要思想是,模块内部被调用的每个函数都负责处理之前压入的返回地址,在运行时找到一个与要欺骗的返回地址具有相同帧大小的合法函数。一旦找到具有相同帧大小的合法函数,就会计算其中的偏移量,并使用最终的地址来替换最后的返回地址,从而隐藏调用栈中的任何异常条目,并保持其可展开性。原始的返回地址被 unwinder
存储,并在执行返回指令之前将其移回堆栈的正确位置,从而允许程序继续正常执行。
这是一个实验性功能,尽管它已经完全功能化,但仍处于开发和研究中,因此如果您决定将其集成到代码中,请确保测试您的代码。
如何使用它
要使用堆栈替换功能,您应该在您的 cargo.toml
中添加以下行,并在 release
模式下编译
[dependencies]
unwinder = {version = "0.1.2", 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
此宏用于使用堆栈替换执行任何所需的间接系统调用,同时具有干净的调用堆栈。此宏期望以下参数
- 第一个参数是一个包含您要执行的syscall的NT函数名称的字符串。
- 以下参数是要发送到 NT 函数的参数。它们遵循 参数传递 部分中指定的相同规则。
示例
我认为通过实际示例展示这些宏的使用是最好的方法。假设我们正在创建一个将被 反射注入 到内存的 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::get_module_base_address("kernel32.dll");
let load_library = 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
+ 隐藏帧的组合。这仅在使用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个参数。
请报告使用此功能时可能出现的任何错误。
依赖项
134MB
~2M SLoC