手动导入 API 更隐蔽,但写起来比通过 IAT 调用麻烦,同时为了隐蔽性,我们还会将动态导入的模块名和函数名编码或哈希。本文介绍借助 Rust 的宏系统,来实现上述两个需求,做到兼具隐蔽性与开发效率。
本文的代码是笔者从个人项目中抠出来的一个 Demo,只保留了核心部分,不包含复杂功能和错误处理等。完整 Demo 在 win32api-practice/dyn-import[1]。
本例向读者展示在武器化开发时,借助 Rust 的高阶语言设施,能够在具备等同于 C / C++ 底层能力的同时,实现一些零开销的 magic 功能。
首先是对动态导入的宏实现,该实现是受标准库启发:
macro_rules! dyn_import {
($module:literal ($calling_convention:literal): $(
$(#[$meta:meta])*
fn $symbol:ident($($argname:ident: $argtype:ty),*) -> $rettype:ty;
)*) => ($(
$(#[$meta])*
#[warn(non_snake_case)]
pub mod $symbol {
use super::*;
type F = unsafe extern $calling_convention fn($($argtype),*) -> $rettype;
static mut PTR: *const c_void = ptr::null();
unsafe fn get_f() -> *const c_void {
let ptr = winapi::GetProcAddress(winapi::LoadLibraryW(macros::wss_z!($module).as_ptr()), macros::ss_z!($symbol).as_ptr());
if !ptr.is_null() {
return ptr;
}
#[cfg(debug_assertions)]
panic!(stringify!($symbol));
#[cfg(not(debug_assertions))]
panic!();
}
#[allow(dead_code)]
#[allow(clippy::too_many_arguments)]
pub unsafe fn call($($argname: $argtype),*) -> $rettype {
if PTR.is_null() {
PTR = get_f();
}
mem::transmute::<*const c_void, F>(PTR)($($argname),*)
}
}
$(#[$meta])*
pub use $symbol::call as $symbol;
)*);
}
macro_rules
是示例宏(macros by example),功能相对受限。代码细节就不解释了,读者感兴趣可以自己研究。
上节的例子中的这段代码通过宏将模块名和符号名转为栈上的字节数组(该功能也可用于 shellcode 开发)。
winapi::GetProcAddress(winapi::LoadLibraryW(macros::wss_z!($module).as_ptr()), macros::ss_z!($symbol).as_ptr());
wss_z
的含义为 Wide Stack String with Zero terminated
,实现是:
/// Stack string in UTF16-LE terminated by null
#[proc_macro]
pub fn wss_z(item: TokenStream) -> TokenStream {
let tt: Vec<TokenTree> = item.into_iter().take(1).collect();
let mut s = tt[0]
.to_string()
.strip_prefix('"')
.unwrap()
.strip_suffix('"')
.unwrap()
.replace("\\\\", "\\")
.to_string();
s.push('\0');
let mut output = Vec::<String>::new();
s.to_string()
.as_bytes()
.iter()
.map(|&c| {
output.push(format!("0x{:x}_u16", c));
})
.for_each(mem::drop);
format!("[{}]", output.join(", ")).parse().unwrap()
}
该功能无法通过 macro_rules
实现,而是通过过程宏(proc macros)实现。过程宏以编译器插件形式工作,它可以直接操作语法分析后的 token。
这个宏获取的输入是模块名和符号名的 token,可以直接将其改为字节数组、或是一个哈希值。
在我实际使用中是将符号哈希,并结合自实现 GetProcAddress
使用的,读者如有需要可以自行改写:
let hmod = winapi::load_library(core::str::from_utf8_unchecked(¯os::ss!($module)));
let ptr = winapi::get_proc_address(hmod, winapi::Thunk::Hash(macros::hsh!($symbol))) {
使用 dyn_import 宏声明只需要标注模块名,调用约定,符号名和函数签名。
dyn_import! {
"kernel32" ("system"):
fn LocalAlloc(uflags: u32, ubytes: usize) -> isize;
fn GetModuleFileNameA(hmod: *const c_void, lpfilename: *mut u8, size: u32) -> u32;
}
dyn_import! {
"msvcrt" ("C"):
fn fwrite(ptr: *const u8, size: usize, nmemb: usize, f: *const FILE) -> usize;
fn fflush(f: *const FILE) -> i32;
}
除了导入函数,也可以导入变量,比如 _iob
。
dyn_import! {
"msvcrt":
sym<FILE> _iob;
}
通过 dyn_import 宏导入的函数的使用和普通函数没有区别。
let addr = LocalAlloc(LPTR, 1 << 10);
let stdout = _iob().offset(1);
let n = GetModuleFileNameA(ptr::null(), addr as _, 1 << 10);
let output = format!("alloc @ 0x{addr:x}\nstdout {stdout:?}\n");
fwrite(output.as_ptr(), output.len(), 1, stdout);
fwrite(addr as _, n as _, 1, stdout);
fflush(stdout);
输出为:
alloc @ 0x24d3c6a1a40
stdout 0x7fff82cdd1f0
\\Mac\Home\Downloads\win32api-practice\dyn-import\target\x86_64-pc-windows-gnu\debug\dyn-import.exe
dyn_import 宏展开后的效果是:
#[warn(non_snake_case)]
pub mod LocalAlloc {
use super::*;
type F = unsafe extern "system" fn(u32, usize) -> isize;
static mut PTR: *const c_void = ptr::null();
unsafe fn get_f() -> *const c_void {
let ptr =
winapi::GetProcAddress(winapi::LoadLibraryW([0x6b_u16, 0x65_u16,
0x72_u16, 0x6e_u16, 0x65_u16, 0x6c_u16, 0x33_u16, 0x32_u16,
0x0_u16].as_ptr()),
[0x4c_u8, 0x6f_u8, 0x63_u8, 0x61_u8, 0x6c_u8, 0x41_u8,
0x6c_u8, 0x6c_u8, 0x6f_u8, 0x63_u8, 0x0_u8].as_ptr());
if !ptr.is_null() { return ptr; }
::core::panicking::panic_fmt(::core::fmt::Arguments::new_v1(&["LocalAlloc"],
&[]));
}
#[allow(dead_code)]
#[allow(clippy :: too_many_arguments)]
pub unsafe fn call(uflags: u32, ubytes: usize) -> isize {
if PTR.is_null() { PTR = get_f(); }
mem::transmute::<*const c_void, F>(PTR)(uflags, ubytes)
}
}
pub use LocalAlloc::call as LocalAlloc;
[1]
win32api-practice/dyn-import: https://github.com/EddieIvan01/win32api-practice/tree/master/dyn-import