SEH(Structured Exception Handler)

예외는 프로그램이 잘못된 메모리 주소를 참조하거나 0으로 나누는 등 정상적인 실행 흐름을 변경하는 상태를 의미한다. 예외 발생 시 마이크로소프트(Microsoft)는 프로그램에서 예외 처리 기능을 담당하는 함수(SEH, Structured Exception Handler, 구조적 예외 처리)를 만들었다.


예외 처리는 기본적으로 운영체제에서 지원하지만, 개발자가 소스 코드 상에서 __try, __except, __finally 키워드로 구현할 수 있다.


프로그램 실행 시 예외가 발생하면 미리 등록된 예외 핸들러(예외 처리 코드)를 호출해서 예외를 처리하고, 예외 처리 실패 시 다음 단계의 예외 핸들러를 호출해서 예외를 처리한다.

 

만약 프로그램에서 등록된 예외 핸들러가 예외를 처리할 수 없거나 예외 핸들러가 없으면 운영체제가 예외를 전달받아(맨 마지막에 등록된 운영체제 예외 핸들러) 사용자에게 문제 발생을 알린다.(오류 보고창)


예외 처리는 SEH 체인(연결 리스트, Linked List) 구조로 동작하며 개발자가 예외 처리 구문을 사용하지 않으면 기본적으로 1개의 예외 처리기가 등록된다.


__try{
     // 예외 발생 영역
     // ~~ 예외 발생 시 __except 코드로 이동
}
__except (EXCEPTION_EXECUTE_HANDLER){
     // 예외 처리 영역
     // 예외 발생 시 ~~ 코드 실행
}

윈도우 운영체제에서 발생하는 예외의 종류는 minwinbase.h에 정의되어 있다. 프로그램을 디버깅하거나 공격 코드 개발 시 자주 접하게 되는 예외 다섯 가지는 다음과 같다.


EXCEPTION_ACCESS_VIOLATION(0xC0000005L)

○ 존재하지 않거나 접근 권한이 없는 메모리 영역에 접근을 시도할 때 발생하는 예외


EXCEPTION_BREAKPOINT(0x80000003L)

○ 실행 코드에 브레이크 포인트가 설치되면 CPU가 해당 주소에 있는 명령어를 실행하려고 할 때 발생하는 예외


EXCEPTION_ILLEGAL_INSTRUCTION(0xC000001DL)

○ CPU가 해석할 수 없는 명령어를 만날 때 발생하는 예외


EXCEPTION_INT_DIVIDE_BY_ZERO(0xC0000094L)

○ 정수 나눗셈 연산에서 분모가 0인 경우(0으로 나누는 경우) 발생하는 예외


EXCEPTION_SINGLE_STEP(0x80000004L)

○ CPU가 Single Step(명령어 하나를 실행하고 멈춤) 모드로 전환되면 명령어 하나를 실행 후 발생하는 예외


/* minwinbase.h - 예외 종류 */
 
#define EXCEPTION_ACCESS_VIOLATION          STATUS_ACCESS_VIOLATION
#define EXCEPTION_DATATYPE_MISALIGNMENT     STATUS_DATATYPE_MISALIGNMENT
#define EXCEPTION_BREAKPOINT                STATUS_BREAKPOINT
#define EXCEPTION_SINGLE_STEP               STATUS_SINGLE_STEP
#define EXCEPTION_ARRAY_BOUNDS_EXCEEDED     STATUS_ARRAY_BOUNDS_EXCEEDED
#define EXCEPTION_FLT_DENORMAL_OPERAND      STATUS_FLOAT_DENORMAL_OPERAND
#define EXCEPTION_FLT_DIVIDE_BY_ZERO        STATUS_FLOAT_DIVIDE_BY_ZERO
#define EXCEPTION_FLT_INEXACT_RESULT        STATUS_FLOAT_INEXACT_RESULT
#define EXCEPTION_FLT_INVALID_OPERATION     STATUS_FLOAT_INVALID_OPERATION
#define EXCEPTION_FLT_OVERFLOW              STATUS_FLOAT_OVERFLOW
#define EXCEPTION_FLT_STACK_CHECK           STATUS_FLOAT_STACK_CHECK
#define EXCEPTION_FLT_UNDERFLOW             STATUS_FLOAT_UNDERFLOW
#define EXCEPTION_INT_DIVIDE_BY_ZERO        STATUS_INTEGER_DIVIDE_BY_ZERO
#define EXCEPTION_INT_OVERFLOW              STATUS_INTEGER_OVERFLOW
#define EXCEPTION_PRIV_INSTRUCTION          STATUS_PRIVILEGED_INSTRUCTION
#define EXCEPTION_IN_PAGE_ERROR             STATUS_IN_PAGE_ERROR
#define EXCEPTION_ILLEGAL_INSTRUCTION       STATUS_ILLEGAL_INSTRUCTION
#define EXCEPTION_NONCONTINUABLE_EXCEPTION  STATUS_NONCONTINUABLE_EXCEPTION
#define EXCEPTION_STACK_OVERFLOW            STATUS_STACK_OVERFLOW
#define EXCEPTION_INVALID_DISPOSITION       STATUS_INVALID_DISPOSITION
#define EXCEPTION_GUARD_PAGE                STATUS_GUARD_PAGE_VIOLATION
#define EXCEPTION_INVALID_HANDLE            STATUS_INVALID_HANDLE
#define EXCEPTION_POSSIBLE_DEADLOCK         STATUS_POSSIBLE_DEADLOCK

/* winnt.h - 예외 종류 */
 
#define STATUS_ACCESS_VIOLATION          ((DWORD   )0xC0000005L)
#define STATUS_DATATYPE_MISALIGNMENT     ((DWORD   )0x80000002L)
#define STATUS_BREAKPOINT                ((DWORD   )0x80000003L)
#define STATUS_SINGLE_STEP               ((DWORD   )0x80000004L)
#define STATUS_ARRAY_BOUNDS_EXCEEDED     ((DWORD   )0xC000008CL)
#define STATUS_FLOAT_DENORMAL_OPERAND    ((DWORD   )0xC000008DL)
#define STATUS_FLOAT_DIVIDE_BY_ZERO      ((DWORD   )0xC000008EL)
#define STATUS_FLOAT_INEXACT_RESULT      ((DWORD   )0xC000008FL)
#define STATUS_FLOAT_INVALID_OPERATION   ((DWORD   )0xC0000090L)
#define STATUS_FLOAT_OVERFLOW            ((DWORD   )0xC0000091L)
#define STATUS_FLOAT_STACK_CHECK         ((DWORD   )0xC0000092L) 
#define STATUS_FLOAT_UNDERFLOW           ((DWORD   )0xC0000093L)
#define STATUS_INTEGER_DIVIDE_BY_ZERO    ((DWORD   )0xC0000094L)
#define STATUS_INTEGER_OVERFLOW          ((DWORD   )0xC0000095L)
#define STATUS_PRIVILEGED_INSTRUCTION    ((DWORD   )0xC0000096L) 
#define STATUS_IN_PAGE_ERROR             ((DWORD   )0xC0000006L)
#define STATUS_ILLEGAL_INSTRUCTION       ((DWORD   )0xC000001DL)
#define STATUS_NONCONTINUABLE_EXCEPTION  ((DWORD   )0xC0000025L) 
#define STATUS_STACK_OVERFLOW            ((DWORD   )0xC00000FDL) 
#define STATUS_INVALID_DISPOSITION       ((DWORD   )0xC0000026L)
#define STATUS_GUARD_PAGE_VIOLATION      ((DWORD   )0x80000001L)  
#define STATUS_INVALID_HANDLE            ((DWORD   )0xC0000008L)

프로그램에서 예외가 발생하면 어떤 단계로 예외를 처리하는지 대략적으로 알아본다.


예외 발생 시 FS 세그먼트 레지스터를 참조하여 커널 영역의 TEB를 따라가지 않고, 스택에 형성된 SEH 체인을 참조하여 예외를 처리한다.


FS 세그먼트 레지스터는 사용자 영역(User Mode)의 TEB/TIB 정보를 가지고 있는 레지스터이다. FS 세그먼트 레지스터는 TEB의 시작 주소를 가리키고, TEB의 시작 주소는 TIB의 시작 주소를 가리킨다.

 

TIB의 시작 주소는 ExceptionList(_EXCEPTION_REGISTRATION_RECORD)를 가리킨다. 이 구조체는 next와 handler 멤버로 구성된 총 8바이트의 SEH 구조체(SEH 함수)이다.


next는 다음에 실행될 SEH 구조체(_EXCEPTION_REGISTRATION_RECORD)를 가리키며, handler는 실질적으로 예외를 처리할 주소를 가리킨다.


프로그램에서 예외가 발생하면 운영체제는 SEH 체인을 따라가면서 핸들러를 순차적으로 실행하고, 처리할 핸들러가 없으면 기본적으로 등록된 ntdll의 핸들러가 호출되어 사용자에게 문제 발생을 알리고 프로그램을 종료시킨다.


ntdll 핸들러는 마지막 체인을 의미하는 것으로 Next SEH가 0xFFFFFFFF 주소를 가리켜 예외 처리를 커널로 넘겨준다. 이는 개발자가 모든 예외를 처리할 수 없는 상황에 대비하여 프로그램이 안정적으로 동작하기 위함이다.


SEH 체인은 연결 리스트 형식으로 구성되어 있으며, TEB/TIB 구조체의 시작 주소에 SEH 구조체가 존재하므로 FS:[0x00], FS:[0x18]로 접근 가능한 것이다.


○ FS 세그먼트 레지스터는 커널 모드 영역에서 KPCR 구조체를 가리키고, 유저 모드 영역에서 TEB 구조체를 가리킴

○ FS:[0x00] : 현재 SEH 프레임 (사용자 영역)

○ FS:[0x18] : TIB의 선형 주소 (사용자 영역)

○ 32비트에서 SEH 멤버는 총 8바이트(멤버 당 4바이트)이고 64비트에서 SEH 멤버는 총 16바이트(멤버 당 8바이트)

○ 개발자가 예외 처리 함수를 등록하지 않더라도, 기본적으로 ntdll의 핸들러가 등록됨


SEH 체인

seh.cpp는 __try, __except 구문을 사용하여 예외를 처리하는 코드이다.


사용자가 입력한 데이터를 temp 배열에 복사하는 과정(인자 없이 프로그램 실행)에서 예외가 발생하면 ExceptionHandler() 함수(예외 처리를 위한 사용자 정의 함수)가 호출되어 예외를 처리하고 프로그램을 종료시킨다.


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <Windows.h>
 
int ExceptionHandler();
 
int main(int argc, char* argv[]){
         char temp[512];
 
         printf("Application Launched\n");
 
         __try{
                 strcpy(temp, argv[1]);
         }__except (ExceptionHandler()){
         
         }
 
         printf("Application End\n");
         return 0;
}
 
int ExceptionHandler(){
         printf("Exception\n");
         return 0;
}

seh.cpp를 컴파일(seh.exe)하여 WinDbg로 열면 인터럽트가 발생하여 프로그램이 정지 상태가 된다. 프로그램이 정지 된 상태에서 현재 로드 된 모듈과 프로그램의 시작/종료 위치를 확인할 수 있다.


○ File > Open Executable > 파일 이름 : seh.exe > 열기


프로그램 실행 전


프로그램이 로드 된 메모리 주소에서 기계어 코드(0x64A1)을 검색하면 예외 핸들러가 등록되어 있는 것을 확인할 수 있다.


0x64A100000000은 MOV EAX, DWORD PTR FS:[0] 명령어의 기계어 코드로 프로그램에서 예외 발생 시 SEH 체인을 참고하여 예외를 처리한다. 만약, 프로그램에서 해당 기계어 코드를 찾지 못하면 프로그램이 예외 핸들러를 가지고 있지 않는 것이다.


s 400000 405000 64 A1

○ 지정한 메모리 주소에서 특정 패턴 검색


.hh

○ 도움말 열기


.hh .dt

○ .dt 명령어에 대한 도움말 열기


fs:[0] 세그먼트 레지스터의 메모리 값을 확인하면 SEH 체인의 시작 위치(첫 번째 SEH 핸들러)가 0x0012fb40 주소를 가리키고 있다.


해당 주소를 따라가보면 Next SEH는 0x0012fcf0 주소를 가리키고 있고, 현재 SEH 핸들러(0x0012fb44)는 0x7790e0ed 주소를 가리키고 있다.


두 번째 SEH 핸들러(0x0012fcf0)를 따라가보면 Next SEH는 0xffffffff 주소를 가리키고 있고, 현재 SEH 핸들러(0x0x0012fbf4)는 0x7790e0ed 주소를 가리키고 있다. 이는 해당 핸들러가 마지막 체인인 것을 의미한다.


○ d : 메모리 값 확인


SEH 체인 구조 (1)


!exchain 명령어를 입력하면 SEH 구조를 좀 더 보기 쉽게 출력한다.


○ !exchain : SEH 구조 확인


SEH 체인 구조 (2)

프로그램이 시작되지 않는 상태에서 일반적인 SEH 구조를 가지고 있다. 이번에는 프로그램을 실행하여 SEH 구조를 확인해보자.


fs:[0] 세그먼트 레지스터의 메모리 값을 확인하면 첫 번째 SEH 핸들러가 0x0012ff38 주소를 가리키고 있다. 해당 주소를 따라가보면 Next SEH는 0x0012ff78 주소를 가리키고 있고, 현재 SEH 핸들러(0x0012ff3c)는 0x004018e9 주소를 가리키고 있다(Next SEH와 SEH 핸들러는 4바이트). 이 주소는 프로그램의 핸들러이다.


두 번째 SEH 핸들러(0x0012ff78)를 따라가보면 Next SEH는 0x0012ffc4 주소를 가리키고 있고, 현재 SEH 핸들러(0x0012ff7c)는 0x004018e9 주소를 가리키고 있다. 이 주소도 마찬가지로 프로그램의 핸들러이다.


마지막 SEH 핸들러(0x0012ffc4)를 따라가보면 Next SEH는 0xffffffff 주소를 가리키고 있고, 현재 SEH 핸들러(0x0012ffc8)는 0x7790e0ed 주소를 가리키고 있다. 이는 해당 체인이 마지막 체인이고, 운영체제 핸들러인 것을 의미한다.


이와 같이 예외 핸들러는 스택의 아랫단에 연결 리스트 구조로 구성되어 있다. 예외가 발생되면, 윈도우 ntdll.dll이 구동되고, 첫 번째 SEH 체인으로 이동해 예외를 처리한다.


만약 예외를 처리하지 못하면 두 번째 SEH 체인으로 이동해 예외를 처리한다. 더 이상 예외를 처리할 핸들러가 없으면 기본적으로 등록된 ntdll의 핸들러(0xffffffff)가 호출되어 예외를 처리한다.


○ g(F5) : 프로그램(디버거) 실행


프로그램 실행 후 SEH 체인 구조


fs:[0]은 SEH 구조체 정보가 저장되어 있으므로, fs:[0]을 이용해 SEH 구조체에 접근하는 방법을 알아본다.


fs 세그먼트 레지스터가 가리키는 위치(0x7ffde000)를 따라가보면 TEB 구조체 정보를 확인할 수 있고, 첫 번째 요소(0x00)는 TIB 구조체이다. fs 세그먼트 레지스터의 시작 위치(fs:[0])는 SEH 구조체가 등록되어 있다.


이는 fs:[0]은 TEB 구조체의 시작 주소와 TIB 구조체의 시작 주소가 동일하며, 주소를 따라가보면 SEH 구조체를 확인할 수 있다.


r fs

○ fs 세그먼트 레지스터 정보 표시


dg 3b

○ 지정한 셀렉터(3b)에 대한 세그먼트 디스크립트 정보 표시


dt 7ffde000 _TEB

○ 지정한 주소에서 _TEB 구조체 정보 표시


TEB 구조체


TIB 구조체 정보를 보면 첫 번째 요소(0x00)에 _EXCEPTION_REGISTRATION_RECORD(ExceptionList) 구조체가 존재한다. 이는 SEH 핸들러의 시작 위치를 가리킨다.


TIB 구조체

_EXCEPTION_REGISTRATION_RECORD 구조체는 Next SEH, SEH Handler 멤버를 가지고 있다. Next SEH는 _EXCEPTION_REGISTRATION_RECORD 구조체를 의미하는 것으로 다음에 실행할 SEH 핸들러 위치를 가리키고 있고, 연결 리스트로 구성된다.


SEH Handler는 except_handler 함수를 의미하는 것으로 실질적으로 예외를 처리할 위치를 가리킨다. 32비트 환경에서 각 멤버(Next SEH, SEH Handler)는 4바이트 * 2 = 8바이트의 크기를 갖고, 64비트 환경에서 각 멤버는 8바이트 * 2 = 16바이트 크기를 갖는다.


프로그램 실행 중 예외가 발생하면 예외를 처리하기 위해 새로운 스택 프레임(프로그램상의 스택과 예외 처리를 위한 스택이 다름)이 생성되고, 생성되어 있는 스택 프레임으로 예외를 처리한다.


만약, 예외 핸들러가 있으면 예외를 처리하고, 예외를 처리하지 못하면 다음 예외 핸들러로 이동하여 예외를 처리한다. 프로그램에 등록된 예외를 처리하지 못하면 기본적으로 등록된 ntdll의 운영체제 예외 핸들러가 호출되어 예외를 처리한다.


_EXCEPTION_REGISTRATION_RECORD 구조체


실질적으로 예외를 처리하는 SEH 핸들러의 함수는 _except_handler이다. 이 함수의 반환형은 _ EXCEPTION_DISPOSITION이며, 총 4개의 멤버로 상수 값을 반환한다.


_EXCEPTION_DISPOSITION 구조체


프로그램 실행 중 0으로 나누거나 잘못된 메모리를 참조하게 되면 예외가 발생한다. 예외가 발생하면 FS 세그먼트 레지스터에 저장되어 있는 _EXCEPTION_REGISTRATION_RECORD 구조체를 찾아가 _except_handler 함수를 호출한다.


이때, EXCEPTION_REGISTRATION_RECORD 구조체는 FS:[0x00], FS:[0x18]에 저장되어 있다. FS:[0x00]는 TIB 구조체의 첫 번째 멤버인 SEH 구조체의 주소가 저장되어 있고, FS:[0x18]은 TIB 구조체의 시작 주소가 저장되어 있다.


FS:[0x00]과 FS:[0x18]은 동일하므로 둘 중 하나를 이용해 접근해도 SEH 구조체를 찾아갈 수 있다.


FS:[0x18]을 만든 이유는 미래에 FS:[0x00]이 TIB(=TEB)가 아닌 값으로 바뀌어도, 해당하는 구조체의 0x18만 다시 TEB를 가리키게 한다면 fs:[ 00000018h]를 사용하는 API의 코드를 변경하지 않고도 호환성 있게 사용하기 위해 설정해 놓은 것이다.


위치 길이 설명
FS:[0x00] 4 현재 SEH 프레임
FS:[0x04] 4 스택 베이스 (높은 주소)
FS:[0x08] 4 스택 한계 (낮은 주소)
FS:[0x0C] 4 SubSystem Tib
FS:[0x10] 4 Fiber data
FS:[0x14] 4 임의 데이터 슬롯
FS:[0x18] 4 TIB 시작 주소

WinDbg로 fs:[0x00] 세그먼트 레지스터를 이용해 SEH 체인을 확인했다. 지금까지 확인한 구조체 정의를 살펴본다.


_NT_TIB 구조체의 첫 번째 멤버는 SEH 체인의 시작 주소를 가리키는 _EXCEPTION_REGISTRATION_RECORD *ExceptionList이고, 마지막 멤버는 자기 자신(_NT_TIB)을 가리키는 자기 참조 구조체이다.


/* winnt.h - _NT_TIB 구조체 정의 */
 
typedef struct _NT_TIB {
    struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
    PVOID StackBase;
    PVOID StackLimit;
    PVOID SubSystemTib;
#if defined(_MSC_EXTENSIONS)
    union {
        PVOID FiberData;
        DWORD Version;
    };
#else
    PVOID FiberData;
#endif
    PVOID ArbitraryUserPointer;
    struct _NT_TIB *Self;
} NT_TIB;
typedef NT_TIB *PNT_TIB;

_NT_TIB 구조체의 첫 번째 멤버이자 SEH 체인을 구성하는 구조체인 _EXCEPTION_REGISTRATION_RECORD는 연결 리스트로 구성되어 있다.


첫 번째 멤버는 _EXCEPTION_REGISTRATION_RECORD의 주소가 저장되어 있고, 두 번째 멤버는 실질적으로 예외를 처리하는 _except_handler 함수의 주소가 저장되어 있다.


/* winnt.h - _EXCEPTION_REGISTRATION_RECORD 구조체 정의 */
 
typedef struct _EXCEPTION_REGISTRATION_RECORD {
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;
 
typedef EXCEPTION_REGISTRATION_RECORD *PEXCEPTION_REGISTRATION_RECORD;

_except_handler 함수(SEH 핸들러)는 4개의 매개변수를 입력 받아 시스템에서 호출되는 콜백 함수이다. 매개변수는 예외와 관련된 정보가 저장되어 있고, EXCEPTION_DISPOSITION이라는 열거형(enum)으로 반환한다.


*_ExceptionRecord

○ 발생한 예외의 종류를 나타내는 예외 코드와 예외가 발생한 코드 주소 정보를 가지고 있음


_EstablisherFrame

○ EXCEPTION_REGISTRATION_RECORD 구조체 포인터로 SEH 체인의 시작 주소

○ 예외 발생 시 ESP 값을 저장

○ SEH 체인의 상위 SEH 핸들러를 가리키는 주소와 같음


*_ContextRecord

○ 예외가 발생할 당시의 레지스터 값을 저장(예외 처리기 백업)


_DispatcherContext

○ _DISPATCHER_CONTEXT 구조체를 가리킴(시스템 내부에서 사용)

○ 이 구조체는 winnt.h에 정의되어 있음


/* except.h - _except_handler 함수 정의 */
 
EXCEPTION_DISPOSITION __cdecl _except_handler (
    _In_ struct _EXCEPTION_RECORD *_ExceptionRecord,
    _In_ void * _EstablisherFrame,
    _Inout_ struct _CONTEXT *_ContextRecord,
    _Inout_ void * _DispatcherContext
    );

_except_handler 함수의 반환형인 _EXCEPTION_DISPOSITION 열거형은 다음과 같이 상수값을 반환한다.


SEH 핸들러에서 예외를 잘 처리했다면 ExceptionContinueExecution(0)을 반환하여 예외가 발생한 주소를 다시 실행한다.


만약 예외를 처리할 수 없다면 ExceptionContinueSearch(1)을 반환하여 SEH 체인에서 다음 SEH 핸들러에서 처리하도록 한다.


ExceptionContinueExecution(0)

○ 예외가 발생했던 주소를 다시 실행


ExceptionContinueSearch(1)

○ SEH 체인에서 다음 SEH 핸들러를 실행


ExceptionNestedException(2)

○ 운영체제 내부에서 사용


ExceptionCollidedUnwind(3)

○ 운영체제 내부에서 사용


/* except.h - _EXCEPTION_DISPOSITION 열거형 정의 */
 
typedef enum _EXCEPTION_DISPOSITION {
    ExceptionContinueExecution,
    ExceptionContinueSearch,
    ExceptionNestedException,
    ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;

_except_handler 함수의 첫 번째 멤버인 _EXCEPTION_RECORD 구조체는 6개의 매개 변수로 구성되어 있다.


ExceptionCode는 발생한 예외의 종류를 나타내는 예외 코드 값이 저장되어 있고, ExceptionAddress는 예외가 발생한 코드 주소가 저장되어 있다.


ExceptionCode

○ 발생한 예외의 종류를 나타내는 예외 코드가 저장


ExceptionFlags

○ 예외 발생 시 플래그가 저장되어 있으며, 값이 0이면 예외가 발생


*ExceptionRecord

○ 다른 처리되지 않은 예외 상황을 위한 ExceptionRecord 구조체의 주소 값을 저장


ExceptionAddress

○ 예외가 발생한 코드(CPU) 주소가 저장


NumberParameters

○ 예외 상황과 연관된 매개변수의 갯수가 저장


ExceptionInformation

○ 예외 상황을 명시하는 추가적인 인자의 배열


/* winnt.h - _EXCEPTION_RECORD 구조체 정의 */
 
typedef struct _EXCEPTION_RECORD {
    DWORD    ExceptionCode;
    DWORD ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID ExceptionAddress;
    DWORD NumberParameters;
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
    } EXCEPTION_RECORD;
 
typedef EXCEPTION_RECORD *PEXCEPTION_RECORD;

WinDbg로 연결리스트로 구성된 SEH 체인에 찾아가보고, 각 구조체 정의를 살펴보았다. 이번에는 이뮤니티 디버거를 실행하여 디버깅하면서 SEH에 대해 알아보자.


이뮤니티 디버거로 seh.exe 파일을 인자를 주지 않고 실행한다. 예외 상황 발생 현황을 분석하는 것이 목적이므로 인자를 주지 않아야 예외가 발생한다. 파일을 열면 엔트리 포인트(Entry Point)가 0x40139D이다.


스택 창을 보면 마지막 체인이 등록되어 있는 것을 확인할 수 있다. 스택 창의 SEH 체인과 덤프 창의 SEH 체인을 비교 분석하기 위해 덤프 창에서 Ctrl + G > fs:[0]을 입력하자.


Next SEH는 0xFFFFFFFF이고, SEH Handler는 0x7790E0ED이다. 현 상황으로 볼 때, 프로그램을 실행하기도 전에 운영체제의 기본 SEH 핸들러가 등록된다.


○ File > Open > seh.exe > 열기


마지막 SEH 체인 등록


Alt + S를 누르면 현재 등록된 SEH 체인을 확인할 수 있다. Address 필드에 저장되어 있는 주소를 따라가보면 Next SEH가 저장되어 있고, SE Handler 필드에 저장되어 있는 주소는 실질적으로 핸들러를 처리하는 주소이다.


SEH 체인


엔트리 포인트 바로 밑의 주소에 JMP seh._tmainCRTStartup 코드가 보이는데, 이 부분을 더블 클릭하면 어떤 위치로 이동하는지 확인할 수 있다. 0x4013A2 주소에서 F8를 눌러 이동해보자.


seh._tmainCRTStartup 점프 코드


0x401238 주소로 이동하니 0x40123F 주소에 SEH 함수를 호출하는 코드가 보인다. F7를 눌러 함수 내부로 들어가보자.


seh._SEH_prolog4 호출 코드


0x401890, 0x401895, 0x4018CE 주소의 코드가 실행되면 기존에 설치 된 SEH 체인에 또 다른 예외 처리기가 추가된다. 즉, EXCEPTION_REGISTRATION_RECORD 구조체에 자신의 EXCEPTION_REGISTRATION_RECORD 구조체를 연결하는 것이다.


PUSH seh._except_handler4

○ 예외 처리기 스택에 저장


PUSH DWORD PTR FS:[0]

○ 기존의 설치된 SEH 체인


MOV DWORD PTR FS:[0], EAX

○ 기존의 SEH 체인에 새로운 예외 처리기를 연결


SEH 체인 추가


0x4018CE 주소까지 실행한 후 덤프와 스택 창을 보면 SEH 체인의 시작 주소가 변경되었다. FS:[0] 세그먼트 레지스터는 0x12FF78(SEH 체인의 시작 주소) 주소를 가리킨다.


코드가 실행되면 마지막 체인 위에 새로운 예외 처리기가 등록된다.


추가된 SEH 체인의 시작 주소


SEH 체인을 보면 예외 처리기가 추가된 것을 확인할 수 있다.


SEH 체인


F8를 눌러 실행하다 보면 0x401330 주소에 메인 함수를 호출하는 코드가 보인다. 이 함수 내부로 들어가보자.


메인 함수 호출 코드


0x401005, 0x40100A, 0x401030 주소의 코드가 실행되면 기존에 설치 된 SEH 체인에 또 다른 예외 처리기가 추가된다. 이 예외 처리기는 프로그램에서 작성한 _try, _except 코드이다.


0x40101C ~ 0x401026 주소의 코드는 데이터 섹션에 저장된 쿠키값을 스택에 저장하는 코드이다. 0x401030 주소까지 실행해서 스택에 상황을 살펴보자.


메인 함수


프로그램에서 작성한 예외 처리기가 기존 SEH 체인에 추가되어 SEH 체인의 시작 주소가 0x12FF38로 변경되었다. 운영체제에서 기본적으로 등록 된 예외 처리기를 포함해서 총 3개의 예외 처리기가 등록되었다.


추가 된 SEH 체인의 시작 주소


SEH 체인을 보면 예외 처리기가 추가된 것을 확인할 수 있다.


SEH 체인

0x40107A 주소의 코드를 실행하면 EXCEPTION_ACCESS_VIOLATION 예외가 발생하여 0x401080 주소의 코드가 실행되지 않고, 디버거에게 제어권이 넘어간다.


해당 예외는 EDX에 저장된 메모리 주소(0x00)를 AL 레지스터로 복사하는데, 메모리 주소 0x00은 존재하지 않으므로 예외가 발생한 것이다.


0x40107A, 0x4018E9(예외 처리기)에 브레이크 포인트를 설정하고, 예외를 디버기(seh.exe)에게 돌려주기 위해 Shift+F8를 누르고, F9를 눌러 실행한다.


예외 발생 시 디버거를 멈추지 않고 디버거에게 예외를 돌려주는 옵션


Options > Debugging options > Exceptions

○ Ignore memory access violations in KERNEL32(v)

○ kernel32.dll 모듈에서 발생한 EXCEPTION_ACCESS_VIOLATION 예외 무시

○ 디폴트 옵션

○ Ignore (press to program) following exceptions(v)

○ EXCEPTION_BREAK_POINT, EXCEPTION_SINGLE_STEP, EXCEPTION_ACCESS_VIOLATION,



EXCEPTION_INT_DEVIDE_BY_ZERO, EXCEPTION_ILLEGAL_INSTRUCTION 예외 무시

○ Ignore also following custom exceptions or ranges

○ 그 외 예외를 사용자가 입력하여 예외 무시


예외 처리기에 브레이크 포인트 설정

○ Ctrl + G > 4018E9 > F2


예외 발생


0x4018E9 주소에서 브레이크 포인트가 걸렸다. 이 주소는 예외 처리기로 _except_handler() 콜백 함수이다.


첫 번째 매개변수(ESP+4)에 EXCEPTION_RECORD 구조체 포인터가 저장되어 있다.


두 번째 매개변수(ESP+8)에 EXCEPTION_REGISTRATION_RECORD 구조체 포인터가 저장되어 있다. 이 구조체 포인터는 Next SEH를 가리키는 주소로 ESP+14(EBP-C)에 위치해있는 Next SEH를 가리키는 주소와 동일하다.


세 번째 매개변수(ESP+C)에 CONTEXT 구조체 포인터로 예외 발생 당시 레지스터 값이 저장한다.


네 번째 매개변수(ESP+10)에 _DISPATCHER_CONTEXT 구조체 포인터가 저장되어 있다.


예외 처리기


0x40190B 주소의 RETN을 실행하면 다음과 같이 SEH를 제거하는 코드를 볼 수 있다.


MOV ESP, DWORD PTR FS:[0]은 SEH 체인의 시작 주소(덤프창에서 확인)를 ESP 레지스터에 복사한다. POP DWORD PTR FS:[0]이 실행되면 SEH 체인에서 0x0012FF38(첫 번째 SEH)을 제거한다.

SEH 제거


RETN까지 실행한 상태에서 덤프창을 보면 FS:[0] 세그먼트 레지스터는 0x0012FF78을 가리키고 있다. 이런식으로 하나씩 디버깅을 해보면 다른 SEH를 해제하는 것도 확인할 수 있다.


SEH 체인 시작 주소


▶ Tomabo MP4 Player 3.11.6 취약점 분석 (SEH Based Stack Overflow)

▶ Stack Adjustment - Stack BOF

▶ SafeSEH 우회 - Stack BOF

▶ SEH Overwrite - Stack BOF

▶ Stack Cookie(GS Cookie) - Stack BOF

▶ Easy RM to MP3 Converter(Stack Buffer Overflow) 취약점 분석

▶ UAC(User Account Control) 우회 - PowerShell

  • 카카오톡-공유
  • 네이버-블로그-공유
  • 네이버-밴드-공유
  • 페이스북-공유
  • 트위터-공유
  • 카카오스토리-공유