2017. 5. 9. 01:03 악성코드 분석
윈도우의 예외 처리
0. 서론
1. 어셈블리 SEH
2. SEH
3. C++ EH
4. SetUnhandledExceptionFilter()
5. Unwind
6. VEH
7. 64비트
8. 기타
0. 서론
윈도우의 예외 처리에 대한 기본적인 내용은 인터넷 자료들만 봐도 충분히 정리할 수 있다. 여기서는 종류별로 정리와 함께 조금 더 깊게 정리하고자 한다. 먼저 간단한 어셈블리 루틴을 이용한 SEH를 시작으로 기본적인 SEH를 사용할 때 그리고 C++ 방식의 예외 처리, 마지막으로 SetUnhandledExceptionFilter()에 대해 알아볼 것이다. 그리고 VEH 및 64비트와 간단하지만 Unwind 부분을 거쳐서 예외와 관련된 여러 사항들을 살펴보겠다.
1. 어셈블리 SEH
1.1 소스 코드
여기서는 어셈블리를 이용해서만 만들 수 있는 아주 간단한 형태의 SEH를 통해 기본적인 분석을 수행할 것이다. 기본적인 내용은 알고 있을 것이므로 간단한 설명과 함께 분류하고 정리하는 식으로 설명할 것이다.
간단한 형태의 SEH는 스택에 핸들러 주소와 다음 SEH 체인을 가리키는 포인터만을 PUSH한다. SEH를 설치하는 코드는 다음과 같다.
push [ 핸들러 주소 ] ; 핸들러 주소를 push
mov eax, large fs:0
push eax ; 다음 SEH 체인을 가리키는 포인터를 push
mov large fs:0, esp ; fs:0에 현재 SEH 체인을 넣는다. 즉 toplevel을 이것으로 변경.
기본적으로 알다시피 핸들러의 주소를 PUSH하고 fs:[0]에 위치한 다음 SEH 체인의 주소를 PUSH하여 포인터로서 다음 SEH 체인을 가리키게 한다. 이후에 fs:[0]의 값을 즉 SEH 체인의 toplevel을 방금 설치한 SEH로 설정한다.
1.2 분석
이렇게 설치를 한 후에 INT3 즉 0xCC로 예외를 발생시켜 보자. 예외 발생 시 커널에서의 과정을 거쳐 유저 모드로 복귀할텐데 가장 먼저 복귀되는 부분은 ntdll.KiUserExceptionDispatcher() 함수이다. 즉 일반적인 사용자 모드 디버거를 사용해서 분석하는 경우 예외 발생 이후 처음으로 분석할 수 있는 부분이 이 함수의 시작 루틴이다.
이 함수는 크게 3가지 부분으로 나뉘어져 있다. 가장 처음에는 (Ollydbg에서는 이름이 보이지 않지만) RtlDispatchException() 함수이며 이 함수의 결과에 따라 NtContinue() 또는 NtRaiseException()이 호출된다. 조금 더 자세히 말하자면 RtlDispatchException()를 수행하다가 복귀한 경우(정상적인 경우 대부분 복귀하지 않는다) 이 함수의 반환값은 DISPOSITION_DISMISS 즉 0이 될 수 있는데 이 경우에는 NtRaiseException()이 호출되며 DISPOSITION_CONTINUE_SEARCH 즉 1이 반환되면 NtContinue()가 호출된다.
그리고 추가적으로 말하자면 커널에서 호출될 때 인자를 두 개 받는데 하나는 예외와 관련된 정보 즉 ExceptionRecord이며 다른 하나는 스레드 문맥 정보인 ContextRecord이다. ContextRecord는 이후에 NtContinue()에서 예외를 실행한 코드로 복귀할 때 사용되며 ExceptionRecord는 예외를 처리 시에 필요한 정보를 담고 있다.
ContextRecord 구조체에 대해서 조금 더 설명해 보겠다. 다른 부분보다는 실질적으로 NtContinue()를 통해 실행되는데 사용되는 eip 멤버가 중요할 것이다. 예를들어서 KiUserExceptionDispatcher()가 호출될 때 인자로 받은 포인터 중 하나이자 NtContinue()을 호출할 때의 인자인 ContextRecord 구조체에 대한 포인터 주소가 0x0019E41C라고 하자. 여기에서 0xB8만큼 더한 값 즉 0x0019E4D4 주소에 들어 있는 값인 0x0041A7C1이 eip 즉, 예외 핸들러에서 복귀한 후에 갈 주소이다.
다시 본론으로 와서 실질적으로 모든 예외 처리를 하는 함수가 RtlDispatchException()이기 때문에 뒤에서 분석하도록 하고 먼저 결과값에 따른 뒷 부분을 분석해 보겠다. 앞에서 말했듯이 NtContinue()가 호출되는 True인 경우는 예외 발생 시 받았던 스레드 문맥인 ContextRecord를 통해 예외를 일으킨 명령어 즉 INT3의 다음 명령어부터 다시 실행하게 된다. 참고로 뒤에서 이야기하겠지만 __except 구문의 필터 값으로 EXCEPTION_CONTINUE_EXECUTION을 넣어도 지금처럼 NtContinue()가 호출된다. 물론 eip는 발생한 그 명령어 주소이다. 그렇기 때문에 예외를 발생시키는 조건을 수정하는 특별한 처리를 수행하지 않는 이상 예외는 계속 일어날 것이다. NtRaiseException()은 예외 핸들러를 찾지 못해 디버거 또는 윈도우의 예외 핸들러를 위해 예외를 다시 일으키는 것이다.
RtlDispatchException()는 내부에서 여러 과정을 거치며 RtlpExecuteHandlerForException()를 즉 앞에서 등록했던 핸들러를 호출하고 이 함수의 결과 값에 따라 DISPOSITION_DISMISS나 DISPOSITION_CONTINUE_SEARCH 같은 결과를 반환한다.
조금 정리해서 설명하자면 먼저 예외가 발생할 것이고 현재 Context는 저장된다. 제어는 커널로 넘어가며 커널에서 유저 모드로 제어가 넘어올 때 즉 유저 모드 디버거로 처음 잡을 수 있는 부분은 예외 디스패처 즉 KiUserExceptionDispatcher()이다. 이곳에서 결과적으로 RtlpExecuteHandlerForException()을 호출하여 핸들러를 호출해 주는데 중요한 것은 인자를 넣어주고 호출해 준다는 것이다. 첫 번째 인자는 ExceptionRecord 구조체로서 ExceptionCode, ExceptionAddress 등 예외 발생과 관련된 내용이 있으며 두 번째 인자는 현재 예외 프레임을 가리키는 포인터이다. 따라가 보면 현재 핸들러 주소가 등록되어 있는 그 예외 프레임의 주소라는 것을 확인할 수 있다. 마지막으로 세 번째 인자는 CONTEXT 구조체이다. 앞에서 설명한 0xB8이 EIP라던 그 구조체이다. 이를 통해 현재 핸들러에서도 이 구조체를 확인하여 어디로 복귀할 것인지를 확인할 수 있을 뿐더러 세 번째 인자 즉 ESP + 0x0C를 통해 이 구조체의 주소를 얻어서 EIP 뿐만 아니라 EAX 등 여러 Context를 핸들러에서 수정할 수도 있다.
정리해 보자면 지금까지 설명한 것처럼 간단하게 어셈블리 루틴으로 SEH를 설치한 경우 예외 발생 시에 커널을 통해 KiUserExceptionDispatcher()로 이동하며 여기에서 RtlDispatchException()를 호출하여 등록한 핸들러를 호출하며 이후 ContextRecord를 인자로 받아 다시 실행을 복귀시켜 주는 NtContinue()를 통해 예외를 일으켰던 명령어의 바로 다음 명령어로 다시 복귀한다.
2. SEH
2.1 소스 코드
윈도우에서는 SEH라는 구조적 예외 처리 방식을 제공한다. SEH는 Termination Handler와 Exception Handler 이 두개로 나뉜다. Termination Handler는 __try와 __finally를 사용하는 것으로서 __try 블록을 한 줄이라도 실행하면 반드시 __finally 블록을 실행해야 한다는 것이다. Exception Handler는 __try와 __except(예외 필터)로 나뉜다. 즉 예외 필터를 인자로 받는 것이다.
예외 필터는 __except (EXCEPTION_EXECUTE_HANDLER) 같이 사용할 수도 있지만 조건문을 지정해서 상응하는 예외를 고를 수 있고 필터 함수를 지정할 수도 있다. 참고로 그냥 방금과 같이 사용하면 예외 "필터"라는 의미가 없다.
* EXCEPTION_EXECUTE_HANDLER(1)
예외 발생 시 예외 핸들러를 실행한다. 핸들러는 콜스택에서 가장 위쪽의 핸들러이다. 위에서 설명한 내용과 같이 기본적인 형태이다. 참고로 핸들러를 실행하게 해주는 필터는 이것 뿐이다.
* EXCEPTION_CONTINUE_EXECUTION(-1)
예외 발생 시 예외를 발생 시킨 그 코드로 이동해서 계속 실행한다.
* EXCEPTION_CONTINUE_SEARCH(0)
예외를 여러개 설정한 경우 다음 예외를 찾는다.
즉, 어떻게 사용하든지 앞에서 설명한 3개의 예외 필터 중 하나가 __except() 내에 반환되어야 하며 그것을 받아서 __except()의 내부를 실행하던지 다시 넘기던지 하게 된다.
추가로 기본적인 API들을 설명하겠다. RaiseException(), GetExceptionInformation(), GetExceptionCode()가 존재하는데 RaiseException()은 예외를 발생시키며 GetExceptionInformation()은 예외가 발생했을 때의 정보를 LPEXCEPTION_POINTERS 라는 포인터로 넘겨준다. GetExceptionCode()는 간단한 예외 코드를 반환한다.
// MSDN BOOL SafeDiv(INT32 dividend, INT32 divisor, INT32 *pResult) { __try { *pResult = dividend / divisor; } __except (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { return FALSE; } return TRUE; } // 다음은 조건문을 사용한 예외 필터이다. __except (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) // 다음은 필터 함수를 사용한 예외 필터이다. int seh_filter(unsigned int code, struct _EXCEPTION_POINTERS* ep) { return EXCEPTION_EXECUTE_HANDLER; } __except (seh_filter(GetExceptionCode(), GetExceptionInformation())) {}
2.2 분석
기본적으로는 어셈블리를 통해 만든 SEH와 비슷하지만 추가된 내용들이 많이 존재한다. 먼저 SEH를 설치하는 어셈블리 루틴이 어떤 형식인지와 그에 따른 스택의 형태를 알아보겠다.
PUSH -1 ; Try Level (FFFFFFFF)
PUSH OFFSET xxxx ; Scope table
PUSH _except_handler3 ; handler
MOV EAX, DWORD PTR FS:[0]
PUSH EAX ; Pointer to next chain
MOV DWORD PTF FS:[0], ESP ; installs SE handler _except_handler3
Pointer to Next Chain
__except_handler3
Scope table
Try Level
다음 SEH 체인을 가리키는 포인터는 같지만 핸들러는 직접 설정한 핸들러 함수의 주소가 아니라 __except_handler3라는 함수이다. 이것은 윈도우에서 제공되는 함수이며 뒤에서 자세히 설명하겠다. 이 외에도 Scope Table 및 TryLevel이 존재한다.
사실 소스 코드를 설명한 부분을 보면 알겠지만 앞에서 설명했던 어셈블리 루틴으로 만든 SEH와의 가장 큰 차이점이란 필터 함수의 존재라고 할 수 있다. 즉 앞에서는 예외 발생 시에 핸들러를 호출한다는 것이 중요하였지만 C나 C언어로 만들어 사용하는 SEH는 필터 함수의 도움을 통해 예외 발생 시에 어떤 행위를 할지 설정할 수 있는 것이다. 그래서 SEH도 그냥 핸들러의 주소가 아니라 필터와 관련된 여러 행위가 필요하므로 더 확장된 것으로 보인다.
앞 부분에서 설명한 것처럼 KiUserExceptionDispatcher() 및 RtlDispatchException()를 거쳐서 핸들러 함수를 호출하는 것 까지는 같다. 차이점은 이 핸들러 함수가 윈도우에서 제공되는 _except_handler3() 함수라는 것이다.
사실 VC++에서 /GS 옵션을 사용하면 _except_handler3() 대신 _except_handler4()가 사용되는데 TryLevel의 초깃값이 0xFFFFFFFE라는 것과 쿠키 검사를 한다는 것 등을 제외하면 차이가 없기 때문에 더 간단한 _except_handler3() 함수를 대상으로 분석하겠다.
_except_handler3()는 위에서 언급했던 Scope Table 구조체를 통해 실제 핸들러 주소 및 필터링을 위한 정보를 얻어서 거기에 맞는 일을 수행한다. 예를들면 필터링의 결과가 EXCEPTION_EXECUTE_HANDLER라면 실제 핸들러를 수행하는 것이다.
필터 부분도 필터링을 위한 함수를 호출하는데 예를들어 간단하게 __except (EXCEPTION_EXECUTE_HANDLER) 같은 방식으로 되어 있다면 그 함수는 EAX에 1을 넣고 반환하는 함수가 된다. 그리고 앞에서 언급하였듯이 필터 값으로 EXCEPTION_CONTINUE_EXECUTION을 넣는 경우에는 NtContinue()가 호출되서 예외가 발생한 그 코드로 돌아가서 다시 실행한다.
3. C++ EH
3.1 소스 코드
try와 catch 구문을 사용하는 방식이다. 예외는 try 구문에서 throw를 사용해서 던져진다. 이것을 받아서 catch 구문 즉, 예외 핸들러에서 예외 처리가 수행된다. 간단하게 EH라고도 불린다.
// MSDN #include "stdafx.h" #include#include #include using namespace std; int main() { try { throw invalid_argument("MyFunc argument too large."); } catch (invalid_argument& e) { cerr << e.what() << endl; return -1; } return 0; }
3.2 분석
이것도 기본적인 내용은 같다. 차이점을 설명하자면 throw 구문을 사용한 경우 CxxThrowException() 함수가 사용된다는 점이다. 물론 이것도 내부적으로는 RaiseException()를 사용해 구현된다. 또한 EH에서는 C++의 예외 처리 규약과 맞게 EXCEPTION_CONTINUE_SEARCH나 EXCEPTION_EXECUTE_HANDLER 필터만 사용한다. 그리고 가장 중요한 차이점으로 핸들러가 CxxFramehandler() 류의 함수이다.
4. SetUnhandledExceptionFilter()
4.1 소스 코드
이것은 Callback 함수를 등록하는 방식으로서 따로 예외 영역을 지정하지 않는다. 그러므로 Unhandled Exception이 발생했을 때 호출할 Handler를 등록할 수 있게 된다. 즉 핸들러가 존재하지 않거나 존재하는 모든 핸들러가 EXCEPTION_CONTINUE_SEARCH를 반환한 경우에 사용된다.
// https://www.codeproject.com/Articles/30815/An-Anti-Reverse-Engineering-Guide #include "stdafx.h" #include#include #include LONG WINAPI UnhandledExceptFilter(PEXCEPTION_POINTERS pExcepPointers) { SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER) pExcepPointers->ContextRecord->Eax); // EAX was 0 and because of this, exception occurs. So we set this function's parameter to 0, it means default handling within UnhandledExceptionFilter. pExcepPointers->ContextRecord->Eip += 2; return EXCEPTION_CONTINUE_EXECUTION; } int main(int argc, char **argv) { SetUnhandledExceptionFilter(UnhandledExceptFilter); __asm{xor eax, eax} __asm{div eax} printf_s("No Debugger! \n"); return 0; }
EAX를 0으로 설정하고 div 함으로써 예외를 발생시킨다. main에는 미리 등록된 SEH가 없기 때문에 예외가 발생하면 Unhandled되며 이에 따라 등록된 UnhandledExceptFilter()가 호출된다. 이 콜백 함수를 보면 파라미터로 0을 넣고 SetUnhandledExceptionFilter()를 호출함으로써 앞에서 설정한 UnhandledExceptFilter() 대신 디폴트 Unhandled 예외 처리기를 설정하고 예외가 발생했던 명령어의 주소를 2만큼 증가시켜 다시 실행을 재개할 때 예외가 발생하지 않게 해준다.
참고로 위의 예제는 이렇게 단지 Unhandled 예외 처리기를 삽입함으로써 안티 디버깅을 수행한다. 왜냐하면 일반적으로 디버깅 중이라면 우리가 등록한 UnhandledExceptFilter() 함수가 실행되지 않기 때문이다. 이것은 예외 발생 후 KiUserExceptionDispatcher()에서 UnhandledExceptionFilter()를 거쳐서 실행되는 도중에 NtQueryInformationProcess()의 인자에 ProcessDebugPort를 파라미터로 넣고 실행하는 부분이 있는데 디버깅 중인 경우 이 함수 호출 시에 이 부분이 -1 즉 FFFFFFFF 값이 나오게 되며 이 값을 받은 경우에는 프로세스를 종료해 버린다. 그렇기 때문에 이 부분을 0으로 직접 수정해주지 않으면 우리가 등록한 UnhandledExceptFilter() 함수가 실행될 수 없다.
참고로 Ollydbg 2.01 버전에서는 Debugging - Exceptions 옵션에서 Pass unprocessed exceptions to Unhandled Exception Filter를 체크해 주면 위처럼 귀찮게 NtQueryInformationProcess()에 BP를 걸고 ProcessDebugPort를 인자로 넣을 때까지 실행해보는 귀찮음을 덜 수 있다. 즉 이 옵션을 체크하면 아무 설정 없이도 위의 예제가 디버깅 중에도 정상적으로 동작하는 것을 볼 수 있다.
4.2 분석
일반적인 예외처럼 KiUserExceptionDispatcher()를 지나 RtlDispatchException()를 호출한다. 이후 등록된 SEH가 있다면 호출하고 해당하지 않거나 없는 경우에는 kernel32.UnhandledExceptionFilter()를 호출하고 궁극적으로 우리가 SetUnhandledExceptionFilter()로 등록했던 UnhandledExceptFilter() 함수가 호출된다.
5. Unwind
Unwind 즉 해제는 __try 블록 내부에 return, continue, break 등의 처리를 하는 경우에 필요하다. 예를들면 __try, __finally 구문이 있으며 __try 내부에 return이 존재한다면 return 호출 이전에 __finally를 실행하고 마지막에 return이 호출되어야 한다. 위와 같은 __finally 구문의 특징 때문에 Unwind가 필요한 것이다. 참고로 __try와 __finally 모두에 return 구문이 있는 경우 __try에서는 return 1, __finally에서는 retrun 2라면 2가 반환된다. 왜냐면 처음에 1이 반환 후보였지만 __finally에 의해서 2가 결정되고 반환되기 때문이다.
사실 지금까지 Unwind나 __finally 부분을 다루지 않았다. 앞에서 RtlDispatchException() 함수를 설명하며 각 예외 체인에 대한 RtlpExecuteHandlerForException() 함수를 호출한다고 하였다. 하지만 __finally 부분에서는 Unwind를 다루는 RtlpExecutehandlerForUnwind() 함수가 호출된다.
6. VEH
6.1 소스 코드
VEH는 SEH보다 더 높은 우선 순위를 갖는다. 즉 어떤 SEH 보다도 먼저 실행된다. 그렇기 때문에 다음의 코드를 보면 VEH 핸들러가 더 먼저 실행되고 이후에 SEH 핸들러가 실행되는 것을 볼 수 있다.
#include "stdafx.h" #include#include LONG WINAPI VEHandler( struct _EXCEPTION_POINTERS *ExceptionInfo ) { printf_s("VEH Handler \n"); return 0; } void main() { PVOID forVEH; forVEH = AddVectoredExceptionHandler(1, VEHandler); __try { RaiseException(1, 0, 0, NULL); } __except (EXCEPTION_EXECUTE_HANDLER) { printf_s("SEH handler \n"); } RemoveVectoredExceptionHandler(forVEH); }
6.2 분석
일반적인 예외처럼 KiUserExceptionDispatcher()를 지나 RtlDispatchException()를 호출한다. RtlDispatchException()에서 일반적인 SEH를 검사하고 호출하는 루틴 이전의 초반부에 RtlpCallVectoredHandler()를 호출한다. 여기에서 등록한 VEH 핸들러가 호출된다. 즉 SEH 핸들러 이전에 VEH 핸들러가 호출되는 것이다.
6.3 기타
다음 내용에 대해서 더 찾아봐야 할 것 같다. [ To decode addresses of VEH handlers, OllyDbg hacks NTDLL.RtlAddVectoredExceptionHandler(), therefore process must be started from the OllyDbg ]
7. 64비트
7.1 개요
64비트에서는 예외와 관련된 정보가 PE 헤더의 .pdata 섹션에 존재한다. 이 섹션은 IMAGE_RUNTIME_FUNCTION_ENTRY 구조체들로 이루어져 있으며 각 구조체의 멤버는 각각 BeginAddress, EndAddress, UnwindInfoAddress이다. 64비트에서는 이런 방식으로 모든 함수들의 시작 주소와 끝나는 주소가 PE 헤더에 삽입되어 있다. 예외와 관련된 정보는 UnwindInfoAddress를 살펴본다.
예를들어 간단한 SEH 예제를 64비트로 컴파일해서 분석해 보겠다. main 함수는 0x1000에 존재하며 .pdata를 보니 첫 번째 구조체가 0x1000으로 시작하는 함수이므로 이것이 main 함수에 상응하는 IMAGE_RUNTIME_FUNCTION_ENTRY 구조체이다. 즉 시작 주소는 0x1000이며 끝나는 주소는 1049이다. UnwindInfoAddress 값에는 0x26A0이 들어있다. 이 말은 main 함수의 예외와 관련된 정보 즉 UNWIND_INFO 구조체가 .rdata 섹션에 위치한 0x26A0 주소에 들어있다는 것을 말한다.
.rdata 섹션에 위치한 UNWIND_INFO 구조체에는 SCOPE_TABLE 구조체가 존재한다. 이 구조체는 BeginAddress(__try 구문의 시작), EndAddress(__try 구문의 끝), HandlerAdress(필터), JumpTarget(__except 구문의 시작)로 이루어져 있다. BeginAddress 즉 __try 구문의 시작 주소를 보니 0x00001012이다. EndAddress 즉 __try 구문의 끝은 주소가 0x00001029이며 JumpTarget 또한 0x00001029이다. 사실 EndAddress 주소는 __try 구문의 마지막 부분 이후의 첫 명령어의 주소이며 이에 따라 __try 구문인 어셈블리 루틴 바로 다음에 __except 구문이 시작되는 것을 알 수 있다. HandlerAddress 멤버는 필터 함수가 존재할 경우에는 그 RVA 값이 들어가지만 만약 필터가 EXCEPTION_EXECUTE_HANDLER 즉 1을 갖는다면 값은 그냥 1이 된다. 즉 내부에서 이 값이 1인 경우 바로 __except 구문(JumpTarget에 주소가 들어있다)을 실행하며 아닌 경우에는 등록된 필터 함수를 호출하고 결정하는 것이다.
7.2 분석
64비트에서도 예외가 커널을 지나 KiUserExceptionDispatcher() 및 RtlDispatchException()로 진행되는 것은 같다. 이 내부에서 가장 중요한 함수는 _C_specific_handler() 함수이다. 위에서 설명한 필터 함수나 __except 구문의 실행을 이 함수가 담당한다.
8. 기타
8.1 올리디버거와 예외
* Debugging
Set permanent breakpoints on system calls
; KERNEL32.UnhandledExceptionFilter(), NTDLL.KiUserExceptionDispatcher(), NTDLL.ZwContinue(), NTDLL.NtQueryInformationProcess() 같은 시스템 호출에 INT3, 즉 브레이크포인트를 설정한다.
* Exceptions
Ignore memory access violations in KERNEL32
; kernel32.dll 모듈에서 발생하는 access violation 예외를 무시한다.
Step over INT3 breaks in MSCORWKS
; .NET 엔진이 디버거에게 이벤트를 알린다.
Ignore (pass to debugged program) following exceptions : [INT3 breaks, Single-step breaks, Memory access violations, Integer division by 0, Invalid or privileged instructions, All FPU exceptions, All service exceptions]
; 해당하는 예외를 무시한다. 이것은 디버거로 넘어온 예외를 Shift+Run/Step을 하지 않고 자동으로 다시 디버기에게 넘기겠다는 것을 의미한다.
Ignore also the following custom exceptions or ranges
; 나머지 예외들을 직접 설정할 수 있다.
Pass unprocessed exceptions to Unhandled Exception Filter
; 처리하지 않은 예외들을 Unhandled Exception Filter에 넘긴다. 일반적으로 SetUnhandledExceptionFilter() 함수를 사용해 Unhandled Exception을 처리하는 경우에는 디버깅 시에 등록된 Unhandled Exception Handler가 실행되지 않고 중간에 프로세스가 종료되어 버린다. 이것은 예외 발생 시 KiUserExceptionDispatcher() 및 RtlDispatchException()를 지나 등록한 Unhandled Exception Handler를 실행하기 이전에 ProcessDebugPort를 인자로 넣는 NtQueryInformationProcess()가 호출되는데 이 때 결과가 FFFFFFFF이기 때문이다. 그래서 이 값을 0으로 설정해 주어야 디버거에서도 정상적으로 실행될 수 있다. 이 옵션을 설정하면 NtQueryInformationProcess()에 BP를 걸고 확인하는 수고로움 없이 정상적으로 디버깅을 수행할 수 있게 된다.
Report ignored exceptions to log
; 무시한 예외들을 로그에 기록한다.
* SFX
Pass exceptions to SFX extractor
; extract 하는 동안 모든 예외를 extractor에 보냄으로써 무시한다.
참고] 올리디버거에서 예외 처리
명령어를 실행시키는 중 예외가 발생할 경우 맨 아래를 보면 "Shift+Run/Step to pass exception to the program"이라는 내용이 보인다. 이 때 Shift+F7/F8/F9를 누름으로써 예외 처리 루틴으로 넘어갈 수 있다. 이것은 원래 디버거라는 것이 디버기의 제어를 갖음으로 인해 당연히 예외 발생 시에도 디버거에게 제어가 넘어오기 때문에 일어나는 일인데 이것을 누름으로써 예외를 다시 프로그램으로 넘겨주는 것이다.
이것을 리버싱하는 방법은 SEH의 경우에는 등록된 핸들러의 주소에 BP를 거는 방법이 있겠고 내부까지 들어가서 직접 분석하기 위해서는 ntdll.KiUserExceptionDispatcher()의 진입점에 BP를 걸면 예외 발생 후 커널로 넘겨진 제어가 커널로부터 복귀한 그 시점부터 바로 분석할 수 있다. 또는 Debugging 옵션에서 Set permanent breakpoints on system calls를 선택하면 이후 예외 처리 루틴으로 넘길 때 자동으로 ntdll.KiUserExceptionDispatcher()의 진입점에서 멈추게 된다.
8.2 VC++ 예외 관련 옵션
* 일반의 코드생성 항목
- C++ 예외 처리 가능
기본적으로 SEH는 운영체제 차원에서 지원된다. 즉 __try, __except 구문을 사용하면 된다. 아래의 옵션들은 C++ 문법의 catch문에서 SEH나 extern "C"로 선언된 함수의 예외를 catch할 수 있는지의 여부를 판단한다.
=> /EHa
C++ EH 뿐만 아니라 SEH도 catch 구문에서 catch할 수 있다.
=> /EHs
catch 구문에서 C++ EH를 catch하지만 SEH는 받을 수 없다. 또한 컴파일러에 extern "C"로 선언된 함수가 예외를 throw할 수 있다.
=> /EHsc
catch 구문에서 C++ EH를 catch하지만 SEH는 받을 수 없다. 또한 컴파일러에 extern "C"로 선언된 함수가 예외를 throw할 수 없다.
* 링커의 고급 항목.
- 이미지에 안전한 예외 처리기 포함
=> /SAFESEH : 예
SEH 오버플로우 공격을 방어하는 기법으로 /GS 옵션도 있지만(SEH 프레임에 GS 쿠키 사용) /SAFESEH 옵션도 존재한다. 이것은 간략하게 말해서 핸들러가 유효한 핸들러인지를 검사해 준다.
/SafeSEH 설정 시에 PE 헤더에 IMAGE_LOAD_CONFIG_DIRECTORY 구조체가 생성된 것을 볼 수 있다. 이 구조체의 여러 멤버 중에서 여기에 사용되는 것은 SEHandlerTable 필드와 SEHandlerCount 필드이다. SEHandlerCount 필드는 프로그램에서 설치되는 SEH 핸들러의 개수이며 SEHandlerTable 필드는 핸들러들의 주소들을 담는 __safe_se_handler_table의 주소이다.
로더는 PE가 로드될 때 IMAGE_LOAD_CONFIG_DIRECTORY 구조체의 SEHandlerTable 필드의 값인 __safe_se_handler_table에 이 프로그램에서 사용되는 핸들러들의 주소를 설정하고 그 개수는 IMAGE_LOAD_CONFIG_DIRECTORY 구조체의 SEHandlerCount 필드에 설정한다. 이후 예외가 발생하면 KiUserExceptionDispatcher()를 지나 RtlDispatchException()에서 호출할 특정 핸들러와 __safe_se_handler_table의 엔트리들을 비교해서 상응하는지를 검사한다.
8.3 First Chance Exception / Second Chance Exception
Windbg로 예외를 처리하는 상황에서 설명해 보겠다. 디버거의 원리 상 프로그램에서 발생한 이벤트를 받게 된다. 만약 예외가 발생한다면 당연히 디버거가 예외를 받게 된다. 이 때 first chance 예외가 발생한다. 이후 프로그램에 예외 핸들러가 존재한다면 예외를 프로그램에게 넘길 수 있고(gn 명령어) 그렇지 않다면 디버거로 예외가 발생한 부분을 수정해서 예외가 발생하지 않게 한 후에 예외가 발생한 부분을 다시 실행할(gh 명령어) 수 있다. 어쨌든 프로그램에서 적절한 핸들러가 없어서 예외를 제대로 처리하지 못하면 다시 예외가 발생하게 되고 디버거는 이러한 unhandled 예외를 다시 잡게 되는데 이것을 second chance 예외라고 한다. 만약 디버거가 존재하지 않는다면 프로그램은 crash할 것이다.