-
[리버싱 핵심원리 study] 48장 SEHReverse Engineering 2021. 2. 16. 23:19
1. SEH
SEH(Structured Exception Handler)는 Windows 운영체제에서 제공하는 예외 처리 메커니즘이다. 리버싱에서는 이를 활용하여 안티 디버깅 기법으로 적용되기도 한다.
- SEH 예제 실습#1
예제 파일 seh.exe는 의도적으로 Memory Access Violation을 발생시킨 후, SEH에 새로 Handler를 추가하여 간단한 안티 디버깅 기능을 만든 파일이다. 일반적으로 실행했을 때와 디버거를 통해 실행했을 때 다음과 같은 차이를 보인다.
seh.exe 파일을 디버깅함으로써 SEH를 통해 프로세스의 예외처리를 하는 방법에 대해 알아보자.
2. OS의 예외 처리 방법
- 일반 실행의 경우
프로세스가 실행 중에 예외가 발생하면 프로세스에게 예외처리를 맡긴다. 만약 해당 부분에 대한 프로세스 예외처리가 없다면 OS는 기본 예외 처리기를 동작시켜 프로세스를 종료시킨다.
- 디버깅 실행의 경우
디버깅 중 디버기에서 예외가 발생할 경우 디버거에게 예외를 넘겨 처리하게끔 한다. 이때 별도의 처리를 해줘야 디버깅을 계속할 수 있는데 다음과 같은 조치 방법이 존재한다.
(1) 예외 직접 수정 : 코드, 레지스터, 메모리
예외가 발생했을 경우 해당 주소에 멈춰 있으므로 문제가 있는 부분을 직접 Assemble 혹은 Edit 기능을 이용하여 수정한다.
(2) 예외를 디버기에게 넘기기
디버기 내부에 SEH가 존재해서 예외를 처리할 수 있다면 그대로 디버기에게 넘겨서 해결하도록 할 수 있다. OllyDbg의 경우 Shift+F7/F8/F9를 이용해 디버기에게 예외 처리 권한을 넘길 수 있다.
(3) OS 기본 예외 처리기
디버거와 디버기에서 예외를 처리하지 못하는 경우 OS의 기본 예외 처리기에서 처리한다.
3. 예외
Windows OS의 예외 중 대표적인 다섯 가지로 다음과 같은 것들이 있다.
- EXCEPTION_ACCESS_VIOLATION (0xC0000005)
존재하지 않거나 접근 권한이 없는 메모리 영역에 대한 접근을 시도할 때 발생한다.
- EXCEPTION_BREAKPOINT (0x80000003)
실행 코드에 BP가 설치되어 있고 CPU가 그 주소를 실행하려 할 때 발생한다.
- EXCEPTION_ILLEGAL_INSTRUCTION (0xC000001D)
CPU가 해석할 수 없는 Instruction을 만날 때 발생한다.
- EXCEPTION_INT_DIVIDE_BY_ZERO (0xC0000094)
정수의 나눗셈 연산에서 분모가 0인 경우 발생한다.
- EXCEPTION_SINGLE_STEP (0x80000004)
명령어 하나를 실행하고 멈추는 것으로, CPU가 Single Step 모드로 전환되었을 때 예외가 발생한다. EFLAGS 레지스터의 Trap Flag를 1로 세팅하여서 Single Step 모드로 전환시킬 수 있다.
4. SEH 상세 설명
(1) SEH 체인
SEH는 체인 형태로 구성되어 있다. 첫 번째 예외 처리기에서 예외를 처리하지 못하면 다음 예외 처리기로 넘어가는 과정을 반복한다. SEH는 다음과 같은 구조체의 연결 리스트로 구성된다.
typedef struct _EXCEPTION_REGISTRATION_RECORD { PEXCEPTION_REGISTRATION_RECORD Next; PEXCEPTION_DISPOSITION Handler; } EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;
Next 멤버는 다음 예외 처리기를 가리키는 포인터이며, Handler 멤버는 처리 함수이다. Next 값이 0xFFFFFFFF인 경우 연결 리스트의 마지막을 나타낸다. 프로세스의 SEH 체인 구조를 도식화하면 아래 그림과 같다.
예외가 발생하면 (A)에서 먼저 처리가 가능한지 확인하고, 그렇지 않은 경우 (B)로 넘어가는 방식으로 체인을 순회하며 예외를 처리한다.
(2) 함수 정의
SEH 함수(Handler) 정의는 다음과 같다.
EXCEPTION_DISPOSITION _except_handler ( EXCEPTION_RECORD *pRecord, EXCEPTION_REGISTRATION_RECORD *pFrame, CONTEXT *pContext, PVOID pValue );
위와 가티 4개의 파라미터를 가지며, EXCEPTION_DISPOSITION이라는 enum type을 반환한다.
- EXCEPTION_RECORD
첫 번째 파라미터인 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;
이 중 주요 멤버로 ExceptionCode와 ExceptionAddress가 있는데 ExceptionCode는 발생한 예외의 종류를, ExceptionAddress는 예외가 발생한 코드 주소를 나타낸다.
- CONTEXT
세 번째 파라미터인 CONTEXT 구조체는 CPU 레지스터 값을 백업할 때 이용되는 구조체로, 멀티 스레드 환경에서 Context switching이 일어날 때 백업/복원을 하기 위해 사용된다.
- EXCEPTION_DISPOSITION
반환 값인 EXCEPTION_DISPOSITION은 enum type으로 다음과 같은 값을 가질 수 있다.
typedef enum _EXCEPTION_DISPOSITION { ExceptionContinueExecution = 0, //예외 코드 재실행 ExceptionContinueSearch = 1, //다음 예외 처리기 실행 ExceptionNestedException = 2, //OS 내부에서 사용 ExceptionCollidedUnwind = 3 //OS 내부에서 사용 } EXCEPTION_DISPOSITION;
만약 SEH의 첫 번째 부분에서 예외를 처리할 경우 0을 반환하고, 그렇지 못한 경우 1을 반환하여 다음 리스트에게 넘겨준다.
(3) TEB.NtTib.ExceptionList
프로세스의 SEH 체인에 접근하기 위해서는 TEB의 NtTib 멤버를 따라가면 된다. 그런데 NtTib의 첫 번째 멤버가 ExceptionList로 SEH 체인을 가리키기 때문에 바로 FS:[0]을 통해 간단히 구해낼 수 있다.
(4) SEH 설치 방법
C 언어에서는 __try, __except, __finally 등의 키워드를 이용하여 구현할 수 있고, 어셈블리 언어에서는 다음과 같은 명령어로 SEH를 추가할 수 있다.
PUSH @MyHandler ; 예외 처리기 PUSH DWORD PTR FS:[0] ; SEH 연결 리스트의 head MOV DWORD PTR FS:[0], ESP ; 연결 리스트 추가
위와 같은 코드로 기존의 EXCEPTION_REGISTRATION_RECORD 구조체 연결 리스트에 새로운 구조체를 연결할 수 있다.
5. SEH 예제 실습 #2 (seh.exe)
(1) SEH 체인 확인
OllyDbg로 예제 파일인 seh.exe를 열고 401000 주소까지 실행해보자.
401000, 401005, 40100C는 앞에서 설명한 SEH 설치 방법을 그대로 이용한 것이다. 401000 주소의 명령어를 실행한 후, FS:[0] (SEH 체인 시작 주소)의 값을 확인해보자.
위 코드로부터 SEH 체인의 시작 주소는 19FF60이며, 연결 리스트의 다음 element(EXCEPTION_REGISTRATION_RECORD)는 19FFCC주소에 있으며, 현재 element의 handler 함수는 402730에 있다는 것을 알 수 있다. next SEH record 주소를 계속 따라가다 보면, 다음과 같이 FFFFFFFF가 나타나는 것을 확인할 수 있다. (연결 리스트의 마지막)
(2) SEH 추가
이제 401005의 PUSH DWORD PTR FS:[0] 명령어를 실행한 후 스택 창을 확인해보자.
40100C의 MOV DWORD PTR FS:[0], ESP를 실행하면 스택에 다음과 같이 주석이 나타난다.
SEH 체인은 OllyDbg의 View - SEH 항목을 통해 확인할 수도 있다.
(3) 예외 발생
다음 나타나는 명령어를 살펴보자.
XOR EAX, EAX를 통해 EAX 레지스터를 0으로 만들고 메모리 0번지 주소에 1을 쓰는 명령어가 실행된다. 이때 EXCEPTION_ACCESS_VIOLATION 예외가 발생하는데 현재 디버거를 통해 프로세스를 실행 중이므로 디버거에게 제어권이 넘어간다. 이때 Shift+F9를 눌러 디버기에게 제어를 넘기자. 그렇게 되면 SEH 체인의 첫 번째 element에는 방금 추가한 부분에서 예외를 처리할 수 있는지 먼저 확인하기 때문에 Handler로 스택에 쌓은 40105A가 실행된다. 40105A에 BP를 설정하고 계속 디버깅을 진행하자.
(4) 예외 처리기 파라미터 확인
스택에 저장된 파라미터들을 확인해보자.
첫 번째 파라미터인 EXCEPTION_RECORD pRecord의 주소를 Dump 창에서 확인해보면 다음과 같다.
첫 번째 멤버인 ExceptionCode는 C0000005로 EXCEPTION_ACCESS_VIOLATION이 발생했고, 네 번째 멤버인 ExceptionAddress는 401019 값으로, 401019에서 예외가 발생했음을 알 수 있다.
(5) 예외 처리기 디버깅
40105A의 handler에는 간단한 디버거 탐지 코드가 존재하는데 하나씩 살펴보자.
0040105A MOV ESI, DWORD PTR SS:[ESP+C] ; ESI = pContext
ESP+C가 가리키는 것은 Handler 함수의 세 번째 파라미터로 Context 구조체 포인터를 가리킨다. ESI 레지스터에는 pContext 주소가 입력된다.
0040105E MOV EAX, DWORD PTR FS:[30]
EAX 레지스터에는 PEB 구조체의 시작 주소가 담긴다.
00401064 CMP BYTE PTR DS:[EAX+2], 1
PEB 구조체에서 2 offset 만큼 떨어진 곳에는 BeingDebugged 멤버가 위치한다. 해당 값을 1과 비교하여 프로세스가 디버깅 중인지 아닌지를 판단한다.
이 결과에 따라 [ESI+B8]에 401023 혹은 401039 주소가 담기게 되는데 ESI에는 Context 정보가 담겨 있고, Context 구조체에서 B8 offset에는 EIP 레지스터 정보가 백업되어 있다. 401023에는 디버거 탐지 메시지 박스, 401039에는 단순 Hello 문자열을 출력하는 메시지 박스를 출력하게끔 구성되어있다.
마지막 부분에는 XOR EAX, EAX를 통해 반환 값을 0(EXCEPTION_CONTINUE_EXECUTION)으로 세팅한 후 함수가 종료된다.
(6) SEH 제거
프로그램이 종료되기 전에 SEH는 다음과 같은 코드를 통해 제거된다.
0040104D 직전까지 명령어를 실행하게 되면 스택에는 앞에서 추가한 EXCEPTION_REGISTRATION_RECORD 구조체가 들어있다. POP DWORD PTR FS:[0]을 통해 스택의 상단에 있는 내용을 다시 SEH 체인의 head에 입력시키고 ADD ESP,4로 등록했던 handler를 제거하면 된다.
6. Comment
어려운 실습이었다. SEH 추가 과정이 직관적으로 이해가 가지 않아서 스택의 변화에 주목하면서 실습을 진행했더니 어느 정도 느낌은 잡을 수 있었다.
반응형'Reverse Engineering' 카테고리의 다른 글
[리버싱 핵심원리 study] 46장 TEB & 47장 PEB (0) 2021.02.15 [리버싱 핵심원리 study] 45장 TLS 콜백 함수 (0) 2021.02.13 [리버싱 핵심원리 study] 41장 ASLR (0) 2021.02.08 [리버싱 핵심원리 study] 40장 64비트 디버깅 (0) 2021.02.08 [리버싱 핵심원리 study] 38장 PE32+ (0) 2021.02.02