
White paper
TL;DR: Questo PoC dimostra una cascata di Structured Exception Handling (SEH) a 3 stadi causata da una deliberata esecuzione disallineata in blocchi di byte grezzi la cui prima istruzione valida è una
div regche genera un fault con divisore zero. Ogni stadio di eccezione installa il successivo handler SEH, innesca una nuova divisione per zero disallineata ed esegue un payload osservabile benigno prima di ripristinare la catena SEH originale e terminare in modo pulito.
MOEW (Misaligned Opcode Exception Waterfall) è un campione di ricerca difensiva che mostra un'esecuzione controllata multi-stadio guidata da eccezioni su Windows x86/Wow64. Dimostra:
fs:[0]blob1, blob2, blob3)KiUserExceptionDispatcherRtlDispatchExceptionTutti i payload sono benigni:
%TEMP%Il PoC è volutamente reso innocuo. Nessun dato viene crittografato, modificato o distrutto.
div disallineato a offset controllati (ECX/EDX/EBX = 0)ORIGINAL_SEH)Il PoC usa funzionalità solo-nightly:
#![feature(asm_experimental_arch)]
#![feature(naked_functions)]
Installa i componenti necessari:
rustup toolchain install nightly
rustup target add i686-pc-windows-msvc --toolchain nightly
Poiché il PoC installa handler SEH personalizzati che non sono presenti nella tabella SAFESEH, è necessario istruire il linker per disabilitare la validazione SAFESEH.
Crea .cargo/config.toml:
[target.i686-pc-windows-msvc]
rustflags = [
"-C", "link-arg=/SAFESEH:NO",
]
Compila il binario:
cargo +nightly build --target i686-pc-windows-msvc --release
L'output si trova in:
target\i686-pc-windows-msvc\release\seh_waterfall.exe
Esegui:
seh_waterfall.exe
Flusso di controllo atteso:
Stage 0 → misaligned blob1 → Stage 1 handler
Stage 1 → misaligned blob2 → Stage 2 handler
Stage 2 → misaligned blob3 → Final handler
Final → restore SEH → exit
Artefatti visibili:
%TEMP%\moew_stage2.txt (Stadio 2)Su Windows a 32 bit, i record SEH formano una lista concatenata memorizzata in fs:[0]:
#[repr(C)]
struct SehRec {
next: *mut SehRec,
handler: usize,
}
Ogni handler usa la firma SEH standard:
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();
Usato per:
fs:[0] come ORIGINAL_SEH.fs:[0] con questo nuovo record.blob1 + 5, che decodifica come div ecx (dopo aver impostato ECX = 0).notepad.exe.blob2 + 3 → div edx (con EDX = 0).%TEMP%\moew_stage2.txt.blob3 + 3 → div ebx (con EBX = 0).calc.exe.ORIGINAL_SEH in 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 padding
}
}
mov eax, 0x10; div ecx; ret+5: div ecx (con 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 padding
}
}
push ebp; mov ebp, esp; div edx; ret+3: div edx (con 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 padding
}
}
push ebx; mov ebx, eax; div ebx; ret+3: div ebx (con EBX = 0 → #DE)Ogni stadio in fault rientra nella pipeline delle eccezioni in modalità utente di Windows:
KiUserExceptionDispatcher
→ RtlDispatchException
→ SEH chain walk (fs:[0])
→ MOEW handler
Tipica visualizzazione nel debugger:
seh_waterfall!blobX+offset
ntdll!KiUserExceptionDispatcher
ntdll!RtlDispatchException
seh_waterfall!stageN_handler
La cascata è interamente guidata da veri fault hardware e dispatch SEH; non vengono usate eccezioni sintetiche o finte.
Il file scritto nello Stadio 2 appare così:
MOEW Stage 2 Marker
-------------------
This file was written by the Stage 2 SEH handler
as a benign demonstration payload.
La sua presenza in %TEMP% serve come semplice prova osservabile che lo Stadio 2 è stato eseguito tramite la catena SEH.
Le estensioni potenziali includono:
L'implementazione completa del PoC è disponibile in questo repository (vedi src/main.rs).
Questo progetto è pensato per ricerca difensiva e formazione. Copyright <2025>
Con la presente viene concessa l'autorizzazione, a titolo gratuito, a chiunque ottenga una copia di questo software e dei relativi file di documentazione (il “Software”), di utilizzare il Software senza alcuna restrizione, inclusi, senza limitazione, i diritti di utilizzare, copiare, modificare, fondere, pubblicare, distribuire, concedere in sublicenza e/o vendere copie del Software, e di permettere alle persone a cui il Software è fornito di fare altrettanto, alle seguenti condizioni:
L'avviso di copyright qui sopra e questo avviso di autorizzazione dovranno essere inclusi in tutte le copie o porzioni sostanziali del Software.
IL SOFTWARE È FORNITO “COSÌ COM'È”, SENZA GARANZIA DI ALCUN TIPO, ESPRESSA O IMPLICITA, INCLUSO MA NON LIMITATO ALLE GARANZIE DI COMMERCIABILITÀ, IDONEITÀ PER UN PARTICOLARE SCOPO E NON VIOLAZIONE. IN NESSUN CASO GLI AUTORI O I TITOLARI DEL COPYRIGHT SARANNO RESPONSABILI PER QUALSIASI RIVENDICAZIONE, DANNO O ALTRA RESPONSABILITÀ, SIA IN UN'AZIONE DI CONTRATTO, ILLECITO O ALTRO, DERIVANTE DA, FUORI O IN CONNESSIONE CON IL SOFTWARE O L'USO O ALTRE OPERAZIONI NEL SOFTWARE.
Il campione MOEW reale che ha ispirato questo PoC non ripristinava fs:[0] al termine dell'esecuzione.
Al contrario, il suo stadio finale:
fs:[0]) con NULL o con un puntatore a memoria non valida.RtlDispatchException incontrasse un puntatore a handler non valido.unknown.unknown.Application Error del Visualizzatore eventi con offset di fault privi di significato.KiUserExceptionDispatcher,Questo passaggio di corruzione SEH distruttiva serve allo scopo anti-forense principale del campione: cancellare la catena causale e produrre un crash terminale non attribuibile.
A differenza del campione reale, questo PoC:
Cattura la testa SEH originale durante lo Stadio 0:
asm!("mov {old}, fs:[0]", old = out(reg) old_head);
ORIGINAL_SEH = old_head;
Ripristina la testa SEH originale nell'handler finale:
asm!("mov fs:[0], {p}", p = in(reg) ORIGINAL_SEH);
Termina in modo pulito tramite process::exit(0) invece di innescare un fault non gestito.
Di conseguenza, il PoC:
Application Error 1000 nel Visualizzatore eventi,Tuttavia—in modo critico—il PoC non elimina il comportamento di degradazione della telemetria guidata dalle eccezioni di MOEW.
Il PoC degrada davvero la telemetria nello stesso modo fondamentale:
Innesca deliberatamente molteplici fault hardware disallineati.
Produce molteplici eccezioni first-chance in rapida successione.
Costringe Windows a eseguire ripetutamente:
KiUserExceptionDispatcher
RtlDispatchException
→ custom handler
→ misaligned blob
→ hardware fault
Genera call stack non lineari e dominate dalle eccezioni.
Distorce la ricostruzione del flusso di controllo in debugger e EDR instradando l'esecuzione attraverso:
Pertanto il PoC riproduce fedelmente:
…evitando però il crash finale distruttivo.
Questo rende il PoC ideale per strumentazione e ricerca senza invocare l'intero payload anti-forense.
Poiché il PoC preserva il comportamento complessivo di MOEW meno il crash da corruzione SEH, è:
Il PoC modella la cascata di eccezioni (l'essenza di MOEW) rimuovendo al contempo la firma distruttiva finale. Dimostra che la degradazione della telemetria deriva non solo dalla corruzione SEH, ma dal modello stesso di flusso di controllo guidato dalle eccezioni.
Questa sezione formalizza le differenze comportamentali mantenendo chiaro lo scopo del PoC: dimostrare la cascata MOEW in una forma sicura e adatta alla ricerca, senza il crash finale anti-forense.
| Comportamento | Campione MOEW reale | Implementazione PoC |
|---|
| Cascata di opcode disallineati | ✔ | ✔ |
| Macchina a stati ricorsiva guidata da SEH | ✔ | ✔ |
| Call stack dominati dal dispatch eccezioni | ✔ | ✔ |
| Degradazione di telemetria / stack trace | ✔ | ✔ |
| Corruzione intenzionale della SEH | ✔ | ❌ |
| Puntatori SEH pendenti o non validi | ✔ | ❌ |
| Eccezione finale non gestita | ✔ | ❌ |
| Crash WER con “modulo sconosciuto” | ✔ | ❌ |
Ripristino pulito di fs:[0] | ❌ | ✔ |
| Terminazione pulita | ❌ | ✔ |