TL;DR: 此 PoC 演示了一个由故意未对齐执行进入原始字节 blob 而引发的 3 阶段结构化异常处理(SEH)级联,其中 blob 的第一条有效指令是除数为零的故障
div reg。
每个异常阶段安装下一个 SEH 处理程序,触发新的未对齐除零错误,并执行一个良性的可观察有效载荷,然后恢复原始 SEH 链并干净地终止。
MOEW(未对齐操作码异常瀑布)是一个防御性研究示例,展示了在 x86/Wow64 Windows 上受控的异常驱动多阶段执行。它演示了:
fs:[0] 手动操作 SEH 链blob1、blob2、blob3)KiUserExceptionDispatcherRtlDispatchException所有有效载荷都是良性的:
%TEMP% 中写入标记文件此 PoC 已被有意解除攻击性。不会加密、修改或销毁任何数据。
ECX/EDX/EBX = 0)的未对齐 divORIGINAL_SEH)此 PoC 使用仅 nightly 可用的功能:
#![feature(asm_experimental_arch)]
#![feature(naked_functions)]
安装必要的组件:
rustup toolchain install nightly
rustup target add i686-pc-windows-msvc --toolchain nightly
由于 PoC 安装了不在 SAFESEH 表中的自定义 SEH 处理程序,因此必须指示链接器禁用 SAFESEH 验证。
创建 .cargo/config.toml:
[target.i686-pc-windows-msvc]
rustflags = [
"-C", "link-arg=/SAFESEH:NO",
]
构建二进制文件:
cargo +nightly build --target i686-pc-windows-msvc --release
输出位于:
target\i686-pc-windows-msvc\release\seh_waterfall.exe
执行:
seh_waterfall.exe
预期的控制流:
阶段 0 → 未对齐 blob1 → 阶段 1 处理程序
阶段 1 → 未对齐 blob2 → 阶段 2 处理程序
阶段 2 → 未对齐 blob3 → 最终处理程序
最终 → 恢复 SEH → 退出
可见的人工产物:
%TEMP%\moew_stage2.txt 被创建(阶段 2)在 32 位 Windows 上,SEH 记录形成一个存储在 fs:[0] 的链表:
#[repr(C)]
struct SehRec {
next: *mut SehRec,
handler: usize,
}
每个处理程序使用标准的 SEH 签名:
extern "system" fn handler(
record: *mut u8,
frame: *mut u8,
context: *mut u8,
dispatcher: *mut u8,
) -> i32
static STAGE_COUNTER: AtomicU32 = AtomicU32::new(0);
static mut ORIGINAL_SEH: *mut SehRec = std::ptr::null_mut();
用于:
fs:[0] 保存为 ORIGINAL_SEH。fs:[0]。blob1 + 5,该地址解码为 div ecx(在设置 ECX = 0 之后)。notepad.exe。blob2 + 3 → div edx(EDX = 0)。%TEMP%\moew_stage2.txt。blob3 + 3 → div ebx(EBX = 0)。calc.exe。ORIGINAL_SEH 恢复到 fs:[0]。process::exit(0) 干净地退出进程。blob1#[unsafe(naked)]
pub extern "C" fn blob1() {
naked_asm! {
".byte 0xB8, 0x10, 0x00, 0x00, 0x00", // mov eax, 0x10
".byte 0xF7, 0xF1", // div ecx
".byte 0xC3", // ret
".byte 0x90, 0x90, 0x90", // nop 填充
}
}
mov eax, 0x10; div ecx; ret+5 处未对齐:div ecx(ECX = 0 → #DE)blob2#[unsafe(naked)]
pub extern "C" fn blob2() {
naked_asm! {
".byte 0x55", // push ebp
".byte 0x8B, 0xEC", // mov ebp, esp
".byte 0xF7, 0xF2", // div edx
".byte 0xC3", // ret
".byte 0x90, 0x90, 0x90", // nop 填充
}
}
push ebp; mov ebp, esp; div edx; ret+3 处未对齐:div edx(EDX = 0 → #DE)blob3#[unsafe(naked)]
pub extern "C" fn blob3() {
naked_asm! {
".byte 0x53", // push ebx
".byte 0x8B, 0xD8", // mov ebx, eax
".byte 0xF7, 0xF3", // div ebx
".byte 0xC3", // ret
".byte 0x90, 0x90, 0x90", // nop 填充
}
}
push ebx; mov ebx, eax; div ebx; ret+3 处未对齐:div ebx(EBX = 0 → #DE)每个故障阶段重新进入 Windows 用户模式异常管道:
KiUserExceptionDispatcher
→ RtlDispatchException
→ SEH 链遍历 (fs:[0])
→ MOEW 处理程序
典型的调试器视图:
seh_waterfall!blobX+偏移
ntdll!KiUserExceptionDispatcher
ntdll!RtlDispatchException
seh_waterfall!stageN_handler
瀑布完全由真实的硬件故障和 SEH 调度驱动;不使用合成或虚假异常。
阶段 2 写入的文件内容如下:
MOEW 阶段 2 标记
-------------------
该文件由阶段 2 SEH 处理程序写入
作为良性演示有效载荷。
其在 %TEMP% 中的存在作为一个简单、可观察的证据,表明阶段 2 已通过 SEH 链执行。
潜在的扩展包括:
完整的 PoC 实现可在本仓库中找到(参见 src/main.rs)。
本项目旨在用于防御性研究和教育。 Copyright <2025>
特此免费授予任何获得本软件及相关文档文件(“软件”)副本的人,不受限制地处理本软件的权利,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或出售本软件副本的权利,并允许获得本软件的人这样做,但须满足以下条件:
上述版权声明和本许可声明应包含在本软件的所有副本或重要部分中。
本软件按“原样”提供,不提供任何明示或默示的担保,包括但不限于适销性、特定用途适用性和非侵权性的担保。在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是合同行为、侵权行为还是其他行为,均由本软件或本软件的使用或其他交易引起或与之相关。
启发此 PoC 的真实世界 MOEW 样本在执行结束时没有恢复 fs:[0]。
相反,其最终阶段:
NULL 或指向无效内存的指针覆写 SEH 头(fs:[0])。RtlDispatchException 遇到无效的处理程序指针。unknown。unknown。Application Error 事件,带有无意义的故障偏移。KiUserExceptionDispatcher 调用,这种破坏性的 SEH 破坏步骤服务于样本的主要反取证目的:
消除因果链并产生无法归因的终端崩溃。
与真实样本不同,此 PoC:
在阶段 0 期间捕获原始 SEH 头:
asm!("mov {old}, fs:[0]", old = out(reg) old_head);
ORIGINAL_SEH = old_head;
在最终处理程序中恢复原始 SEH 头:
asm!("mov fs:[0], {p}", p = in(reg) ORIGINAL_SEH);
通过 process::exit(0) 干净地终止,而不是触发未处理的故障。
因此,PoC:
Application Error 1000 条目,然而——关键是——PoC 并没有消除 MOEW 的异常驱动遥测退化行为。
PoC 绝对以相同的基本方式降低遥测质量:
它故意触发多个未对齐的硬件故障。
它快速连续产生多个首次机会异常。
它强制 Windows 重复执行:
KiUserExceptionDispatcher
RtlDispatchException
→ 自定义处理程序
→ 未对齐 blob
→ 硬件故障
它生成非线性、以异常为主的调用栈。
它通过以下方式扭曲调试器和 EDR 中的控制流重建:
因此,PoC 忠实地再现了:
……同时避免了破坏性的最终崩溃。
这使得 PoC 非常适合仪器化和研究,而无需调用完整的反取证有效载荷。
由于 PoC 保留了 MOEW 的整体行为 减去 SEH 破坏崩溃,因此:
PoC 模拟了异常瀑布(MOEW 的本质),同时移除了最终的破坏性签名。
它证明遥测退化不仅仅源于 SEH 破坏,还源于异常驱动的控制流模型本身。