
TL;DR: 이 PoC는 의도적인 정렬이 어긋난 실행이 0으로 나누는
div reg명령어가 첫 번째 유효 명령어인 원시 바이트 블롭으로 이어져 발생하는 3단계 구조적 예외 처리(SEH) 연쇄를 보여줍니다.
각 예외 단계는 다음 SEH 핸들러를 설치하고, 새로운 정렬이 어긋난 0으로 나누기를 트리거하며, 무해한 관찰 가능 페이로드를 실행한 후 원래 SEH 체인을 복원하고 깔끔하게 종료됩니다.
MOEW(Misaligned Opcode Exception Waterfall)는 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
예상 제어 흐름:
Stage 0 → 잘못 정렬된 blob1 → Stage 1 핸들러
Stage 1 → 잘못 정렬된 blob2 → Stage 2 핸들러
Stage 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로 잘못 정렬하여 ECX = 0 설정 후 div ecx로 디코드됩니다.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 체인 워크 (fs:[0])
→ MOEW 핸들러
일반적인 디버거 보기:
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는:
Stage 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는 동일한 기본 방식으로 원격 측정을 확실히 저하시킵니다:
의도적으로 여러 개의 정렬이 어긋난 하드웨어 오류를 트리거합니다.
빠른 연속으로 여러 개의 first-chance 예외를 생성합니다.
Windows가 다음을 반복적으로 실행하도록 강제합니다:
KiUserExceptionDispatcher
RtlDispatchException
→ 사용자 정의 핸들러
→ 잘못 정렬된 블롭
→ 하드웨어 오류
비선형적이고 예외가 지배적인 호출 스택을 생성합니다.
디버거 및 EDR에서 제어 흐름 재구성을 왜곡하여 실행을 다음을 통해 라우팅합니다:
따라서 PoC는 다음을 충실히 재현합니다:
…단, 파괴적인 최종 충돌은 피합니다.
따라서 PoC는 전체 안티포렌식 페이로드를 호출하지 않고 계측 및 연구에 이상적입니다.
PoC는 SEH 손상 충돌을 제외한 MOEW의 전반적인 동작을 보존하기 때문에:
PoC는 최종 파괴적 서명을 제거하면서 예외 폭포(MOEW의 핵심)를 모델링합니다.
원격 측정 성능 저하가 SEH 손상뿐만 아니라 예외 기반 제어 흐름 모델 자체에서 발생함을 보여줍니다.
이 섹션은 PoC의 목적을 명확히 하면서 동작 차이를 공식화합니다:
안티포렌식 최종 충돌 없이 안전하고 연구 친화적인 형태로 MOEW 폭포를 시연하는 것.
| 동작 | 실제 MOEW 샘플 | PoC 구현 |
|---|
| 잘못 정렬된 opcode 폭포 | ✔ | ✔ |
| 재귀적 SEH 기반 상태 머신 | ✔ | ✔ |
| 예외 디스패치 호출 스택이 지배적 | ✔ | ✔ |
| 원격 측정 / 스택 추적 성능 저하 | ✔ | ✔ |
| 의도적인 SEH 손상 | ✔ | ❌ |
| 매달린 또는 유효하지 않은 SEH 포인터 | ✔ | ❌ |
| 최종 처리되지 않은 예외 | ✔ | ❌ |
| WER "알 수 없는 모듈" 충돌 | ✔ | ❌ |
fs:[0]의 깔끔한 복원 | ❌ | ✔ |
| 깔끔한 종료 | ❌ | ✔ |