본문 바로가기

REVERSING/SEH

SEH에 대하여..

SEH : Structured Exception Handler의 약자로 OS는 프로세스 실행 중에 예외가 발생하면 프로세스에게 처리를 맡긴다.

프로세스 코드에 SEH와 같은 예외 처리가 구현되어 있다면 해당 예외를 잘 처리한 후 계속 실행 될 것이다. 이렇게 Windows 운영체제에서 제공하는 예외처리 시스템을 말한다.



<단순한 메세지 박스를 띄우는 프로그램에 사용자정의 SEH함수를 정의 한 코드>

-이 코드를 중심으로 설명할 예정





push nHandler

push fs:[0]

mov fs:[0] , esp


이 명령문은 새로운 예외처리 함수를 추가하기 위한 것이다.


nHandler  :  추가할 예외처리함수의 포인터


fs:[0] : fs가 가르키는 곳에서 0만큼 떨어진 곳의 4바이트를 뜻한다.


fs레지스터가 가르키는 곳은 TEB구조체인데 이 TEB구조체의 0번째 필드는 TIB구조체가 되고 TIB구조체의 0번째 필드는 EXCEPTION_REGISTRATION_RECORD 구조체의 포인터이다. EXCEPTION_REGISTRATION_RECORD 구조체는 SEH체인에 대한 정보가 담겨있다.



 

 

기존의 FS:[0] (현 ESP 위치)

예외처리  핸들러

 

<위 명령문의 의한 스택 프레임 >


*


<SEH체인의 EXCEPTION_REGISTRATION_RECORD 구조체 형태>


위의 사진을 통해 실제로 스택에 저장 되는 SEH체인 형태를 확인 할 수 있다.



EXCEPTION_REGISTRATION_RECORD 구조체



 typedef struct EXCEPTIONREGISTRATIONRECORD{


     DWORD NEXT;   //SEH체인에서 다음 ExceptionHandler의 포인터

     DWROD ExceptionHandler; // SEH 함수의 포인터


} EXCEPTION_REGISTRATION_RECORD;


SEH체인에 대한 정보는 스택에 저장을 하게 된다.

처음 에러 이벤트가 발생할 시 현재 ESP에 위치한 SEH체인의 형태를 이동 하며 해당 예외에 대한 이벤트를 찾게 되고 실행을 하게 된다,


임의로 삽입한 코드를 살펴볼 때 ,

MOV DWORD PTR FS:[0], ESP <=왜 스택을 사용해서 FS:[0]의 위치를 ESP로 하는지 의문이 생길 수 있다.

이유:이것을 알려면 FS:[0]이 의미하는 부분을 이해해야 한다.


FS 세그먼트 레지스터

FS 세그먼트 레지스터는 현재 스레드의 TEB를 지시하는데 사용된다.

사실 FS 레지스터는 직접 TEB 주소를 가리키는 것이 아니다. 실제 TEB 주소를 가지고 있는 Segment Descriptor Table의 Index값을 가지고 있는 것이다.

(Segment Descriptor Table은 커널 메모리 영역에 존재한다.)

세그먼트 레지스터는 실제로 Segment Descriptor Table의 Index를 가지고 있기 때문에 'Segment Selector'라는 별명을 가지고 있다. FS세그먼트 셀렉터가 가리키는 세그먼트 메모리의 시작주소에 TEB구조체가 위치하는 것이다. 

아래의 그림은 위의 설명을 전체적으로 나타낸 것이다.



흐름을 살펴보면서 이해를 해보자.

예외가 발생하면 FS:[0]의 값을 참조하게 되는데 해당 값이 스택의 ESP를 가르키게 되고 커널에 위치한 TEB로 가는 것이 아닌 스택에 형성된 SEH체인을 참조하게 되는 것이다. 즉, TEB가 세그먼트 메모리(커널)이 아닌 스택에 형성된 것이다.(임의의 추가한 SEH를 참조하기 위해)



모든 프로세스의 모든 스레드는 스레드의 초기화 과정에서 적어도 한 개의 예외 핸들러를 가지는데 이 핸들러에 관한 정보는 EXCEPTION_REGISTRATION_RECORD 구조 형태로 Stack에 저장되고 첫 번째 EXCEPTION_REGISTRATION_RECORD구조를 가리키는 포인터는 스레드 환경 블록 TEB or TIB에 저장된다.


정리 : 예외가 발생하면 운영체제는 해당 예외가 발생한 스레드의 TEB를 찾아 EXCEPTION_REGISTRATION_RECORD 구조체의 포인터를 가져온 뒤 가져온 구조체의 첫 번째 필드(TIB)를 통해 _except_handler 함수를 호출하는 것이다.(EXCEPTION_REGISTRATION_RECORD 구조의 head는 FS:[0] 레지스터를 통해 접근 할 수 있다.) 


에러 핸들러 : 에러가 발생한 현 시점에서의 CPU정보와 예외코드 등의 정보를 담고 있는 함수이다.

우선 리턴값은 EXCEPTION_DISPOSITION으로 enum 형태이다.


 EXCEPTION_DISPOSITION 열거형

typtdef enum _EXCEPTION_DISPOSITION{


    ExceptionCotinueExecutuion = 0,   // 예외 코드 재 실행

    ExceptionContinueSearch = 1,      // 다음 예외 처리기 실행

    ExceptionNestedException = 2,    // OS 내부에서 사용됨

    ExceptionCollideUnwind = 3        // OS 내부에서 사용됨


} EXCEPTION_DISPOSITION; 



 에러 핸들러의 프로토타입

EXCEPTION_DISPOSITION __cdecl _except_handler(

    struct_EXCEPTION_RECORD *ExceptionRecord,

    void * EstablisherFrame,

    struct_CONTEXT *contextRecord,

    void *DispatcherContext

);


-EXCEPTION_RECORD 구조체는 예외를 발생시킨 원인에 해당하는 예외코드와 예외가 발생한 코드위치 등의 정보를 가지고 있는 구조체이다.

EstablisherFrame은 예외가 발생했을 때의 esp값을 저장하고 있다.

ContextRecord는 예외가 발생했을 때의 CPU각 작업하던 레지스터 값을 백업하는 용도로 사용된다.

-->백업하는 이유 : 윈도우는 멀티 스레드 환경이기 때문이다.

    스레드는 내부적으로 CONTEXT 구조체를 하나씩 가지고 있다. CPU가 다른 스레드를 실행하려 할 때 사용하던 전 스레드 CPU     레지스터 값을 CONTEXT 구조체에 백업한다. 그 후 CPU가 다시 예쩐 스레드를 실행하려 하면 CONTEXT 구조체에 백업된 레지     스터 값들을 사용하게 된다.


MOV DWORD PTR DS : [EAX], 1

EAX의 값을 앞에서 초기화를 해서 0인 상태이고 0이라는 메모리 주소에 접근 하지 못하기 때문에 여기서 ACCESS VIOLATION 에러가 발생한다.

현재 디버깅 중에 발생한 예외이기 때문에 우선 순위에 따라 제어가 디버거에게 넘어온다.예외제어권을 디버기프로세스에게 넘겨주어 SEH체인을 살펴보도록 한다.

디버깅 하는 방법으로는 Shift+F7/F8/F9를 통해 예외를 디버깅에서 처리하는 것을 패스시킨다 즉 이말은 디버기 프로세스가 예외를 처리하고 해결된 후에 다시 디버거로 돌려달라는 것이다.

실제 실습을 통해 확인해보면 예외가 발생한 후 디버기 프로스세스가 SEH함수를 실행하는 단계에서 사용자 정의 SEH 첫 주소에 BP를 걸어주면 다시 디버거에게 제어권이 넘어오면서 디버기가 실행한 명령어가 어딘지를 확인 할 수 있다.


해당 예외 이벤트에 대한 스택 들여다 보기




위 사진은 ACCESS VIOLATION(MOV DWORD PTR DS : [EAX], 1)이 발생 한 후 예외 핸들러를 시작할 때의 스택 모습이다.

에러 핸들러의 프로토타입에서 확인 할 수 있듯이 총 4개의 파라미터가 존재하며 첫번 째 파라미터는 EXCEPTION_RECORD구조체의 포인터이며 


예외 코드 C0000005의 값이 존재함을 확인 할 수 있고, EXCEPTION_RECORD 구조체의 4번 째 멤버인 예외가 발생한 위치의 값인 00401019값이 존재함을 확인 할 수 있다.


두번 째 파라미터는 EstablisherFrame 이며, 세번 째 파라미터는 CONTEXT구조체의 포인터이다.


이제 이벤트가 발생하면 실행이 되는 이벤트에 대해서 할 것이다.

예외 처리기 디버깅





MOV ESI, DWORD PTR SS:[ESP+C]

세번째 파라미터는 CONTEXT구조체의 포인터로 ESI에 담는다.


MOV EAX, DWORD PTR FS : [30]

FS:[30]는 PEB구조체의 포인터를 가르킨다. PEB구조체는 프로세스의 정보가 담겨있는 구조체이다.


CMP BYTE PTR DS : [EAX+2], 1

EAX+2는 PEB.BeingDebugged 멤버를 의미한다.

PEB.BeingDebugged 멤버가 의미하는 값은 현재 프로세스에 대해 디버깅 중인지 아닌지를 판별하는 값을 가지고 있다.

1이면 디버깅 중이며, 0이면 디버깅중이 아님을 애기한다.


MOV DWORD PTR DS:[ESI+B8], seh.00401023

CONTEXT구조체에서 B8만큼 떨어진 곳은 EIP멤버를 말하며 이 멤버는 예외처리가 끝난 후 계속 실행할 다음 명령어의 위치를 가르키고 있다.

즉, 예외가 발생한 위치를 담고 있다.

이 값을 우리가 원하는 행동을 하도록 하는 코드의 위치로 바꾸면 된다.


디버그 탐지 코드이다.


XOR EAX, EAX

위에도 설명해 놨듯이 예외처리기의 리턴값에 따라 다음 행동이 달라진다.

여기서는 정상적으로 예외를 처리했다는 0값을 리턴하기 위해 EAX값을 0으로 초기화 하는 과정을 포함하였다.