
TL;DR: このPoCは、最初の有効命令がゼロ除数によるフォールト発生
div regである生バイトブロブへの意図的なミスアラインド実行によって引き起こされる3段階の構造化例外処理(SEH)カスケードを実証します。
各例外ステージは次のSEHハンドラをインストールし、新しいミスアラインド除算ゼロをトリガーし、元のSEHチェーンを復元してクリーンに終了する前に、良性の観測可能なペイロードを実行します。
MOEW(Misaligned Opcode Exception Waterfall)は、x86/Wow64 Windows上で制御された例外駆動型マルチステージ実行を示す防御研究用サンプルです。以下を実証します:
fs:[0]を介した手動SEHチェーン操作blob1、blob2、blob3)への意図的なミスアライメントKiUserExceptionDispatcherRtlDispatchExceptionすべてのペイロードは良性です:
%TEMP%にマーカーファイルを書き込みこのPoCは意図的に無害化されています。データの暗号化、変更、破壊は行われません。
div(ECX/EDX/EBX = 0)ORIGINAL_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
期待される制御フロー:
Stage 0 → misaligned blob1 → Stage 1 handler
Stage 1 → misaligned blob2 → Stage 2 handler
Stage 2 → misaligned blob3 → Final handler
Final → restore SEH → exit
可視の成果物:
%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 padding
}
}
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 padding
}
}
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 padding
}
}
push ebx; mov ebx, eax; div ebx; ret+3でのミスアラインド: div ebx(EBX = 0 → #DE)フォールトした各ステージはWindowsユーザーモード例外パイプラインに再入します:
KiUserExceptionDispatcher
→ RtlDispatchException
→ SEH chain walk (fs:[0])
→ MOEW handler
一般的なデバッガでの表示:
seh_waterfall!blobX+offset
ntdll!KiUserExceptionDispatcher
ntdll!RtlDispatchException
seh_waterfall!stageN_handler
このウォーターフォールは完全に本物のハードウェアフォールトとSEHディスパッチによって駆動されます。合成または偽の例外は使用されません。
ステージ2で書き込まれるファイルは次のようになります:
MOEW Stage 2 Marker
-------------------
This file was written by the Stage 2 SEH handler
as a benign demonstration payload.
%TEMP%内に存在することは、ステージ2がSEHチェーンを介して実行されたことのシンプルで観測可能な証明となります。
考えられる拡張は以下のとおりです:
完全なPoC実装はこのリポジトリにあります(src/main.rsを参照)。
このプロジェクトは防御研究と教育を目的としています。 Copyright <2025>
ここに、本ソフトウェアおよび関連文書ファイル(以下「本ソフトウェア」)の複製を取得するすべての人に対し、本ソフトウェアを無制限に扱うことを無償で許可します。これには、本ソフトウェアの使用、複製、変更、統合、公開、配布、サブライセンス、および/または販売の権利、ならびに本ソフトウェアを提供される者にそれを行うことを許可する権利が、以下の条件に従うことを条件として含まれます:
上記の著作権表示およびこの許可表示は、本ソフトウェアのすべてのコピーまたは主要部分に含まれるものとします。
本ソフトウェアは「現状のまま」提供され、明示または黙示を問わず、商品性、特定目的への適合性、および非侵害に関する保証を含むがこれらに限定されない、いかなる保証もありません。いかなる場合も、著作者または著作権保有者は、契約、不法行為、その他の行為であるかを問わず、本ソフトウェアまたは本ソフトウェアの使用もしくはその他の取引に起因または関連して発生するいかなる請求、損害、その他の責任についても責任を負いません。
このPoCの着想元となった実世界のMOEWサンプルは、実行終了時にfs:[0]を復元しませんでした。
その代わりに、その最終ステージは:
fs:[0])をNULLまたは無効なメモリへのポインタで上書きしました。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
→ custom handler
→ misaligned blob
→ hardware fault
非線形で例外が支配的なコールスタックを生成します。
実行を以下を経由させることで、デバッガやEDRでの制御フロー再構築を歪めます:
したがって、PoCは以下を忠実に再現します:
…を実現しつつ、破壊的な最終クラッシュを回避します。
これにより、PoCは完全なアンチフォレンジックペイロードを実行することなく、インストルメンテーションと研究に理想的です。
PoCはSEH破壊クラッシュを除いたMOEWの全体的な動作を保持しているため、次のようになります:
PoCは、最終的な破壊的シグネチャを除去しつつ、例外ウォーターフォール(MOEWの本質)をモデル化しています。
テレメトリ劣化がSEH破壊だけでなく、例外駆動型制御フローモデル自体からも生じることを実証しています。
このセクションは、PoCの目的を明確に保ちながら、動作の違いを形式化します:
アンチフォレンジックな最終クラッシュなしに、MOEWウォーターフォールを安全で研究に適した形式で実証すること。
| 動作 | 実際のMOEWサンプル | PoC実装 |
|---|
| ミスアラインドオペコードウォーターフォール | ✔ | ✔ |
| 再帰的SEH駆動ステートマシン | ✔ | ✔ |
| 例外ディスパッチが支配的なコールスタック | ✔ | ✔ |
| テレメトリ/スタックトレース劣化 | ✔ | ✔ |
| 意図的なSEH破壊 | ✔ | ❌ |
| 宙吊りまたは無効なSEHポインタ | ✔ | ❌ |
| 最終的な未処理例外 | ✔ | ❌ |
| WER「unknown module」クラッシュ | ✔ | ❌ |
fs:[0]のクリーンな復元 | ❌ | ✔ |
| クリーンな終了 | ❌ | ✔ |