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할 것이다.



'악성코드 분석' 카테고리의 다른 글

다형성 바이러스  (4) 2017.05.16
API Sets  (0) 2017.05.12
링크 및 책 정리  (0) 2017.04.23
윈도우의 드라이버 개발과 루트킷 그리고 AV  (0) 2017.04.23
간단한 패커 개발  (0) 2017.04.23
Posted by SanseoLab

링크



I. Major

1. 레딧

https://www.reddit.com/r/ReverseEngineering/ ]

리버스 엔지니어링 : 매 주 질문을 할 수 있는 스레드가 올라온다.

https://www.reddit.com/r/Malware/ ]

악성코드

https://www.reddit.com/r/lowlevel/ ]

Low Level 프로그래밍 및 보안 (윈도우 리눅스)


2. StackExchange

http://reverseengineering.stackexchange.com/ ]

스택 오버플로우처럼 리버스 엔지니어링과 관련된 질문 및 답변


3. MSDN


4. 깃허브


5. 위키백과


6. VirusBulletin

https://www.virusbulletin.com/virusbulletin/ ]

https://www.virusbulletin.com/blog/ ]



II. Minor

1. Yobi Wiki

http://wiki.yobi.be/wiki/Reverse-Engineering ]

리버스 엔지니어링 관련 자료가 많다.


2. 0x00SEC

https://0x00sec.org/ ]

만들어진지 얼마 안된 곳이지만 괜찮은 내용들도 많이 올라오고 있다.



III. Twitter

보안뉴스  @boannews

SecurityWeek  @SecurityWeek

PHR34K  @unpacker

Evilcry_  @Blackmond

Ivan  @aszy

red plait  @real_redp

Giuseppe 'N3mes1s'  @gN3mes1s











I. 리버스 엔지니어링

* 리버싱 핵심원리 : 악성코드 분석가의 리버싱 이야기 - 이승원

* 리버스 엔지니어링 바이블 : 코드 재창조의 미학 - 강병탁

* 리버싱 : 리버스 엔지니어링 비밀을 파헤치다 - Eilam, Eldad

* Practical Reverse Engineering (역공학) : X86, X64, ARM, 윈도우 커널, 역공학 도구, 그리고 난독화 - Dang, Bruce

* 해킹의 꽃 디스어셈블링 : 보안 분석에 유용한 리버스 엔지니어링 - Kaspersky, Kris

* 리눅스 바이너리 분석 - 라이언 오닐

* Reverse Engineering for Beginners (실전 연습으로 완성하는 리버싱) : x86/x64 윈도우, 리눅스부터 모바일 ARM iOS까지 - 데니스 유리체프

* 리버스 엔지니어링 : 역분석 구조와 원리 - 박병익

* 즐거운 리버싱 : 리버스 엔지니어링 입문 - 아이코우 켄지

* 그레이햇 해킹 - 다니엘 레갈라도 등



2. 자료

* (Windows 시스템 실행파일의) 구조와 원리 - 이호동

* 리버스 엔지니어링 1,2 - 이호동

* The IDA pro book - Eagle, Chris

* Windows Internals 6th

* 리버싱 윈도우 : 장애, 해킹, 성능, 운영에 관한 실전 교과서 - 한주성



3. 악성코드

* 악성코드와 멀웨어 포렌식 - Aquilina, James M

* 실전 악성코드와 멀웨어 분석 - Sikorski, Michael

* 악성코드 분석가의 비법서 - Ligh, Michael Hale

* 악성코드, 그리고 분석가들 - 이상철

* 표적형 공격 보안 가이드 - 이와이 히로키

* DBD 공격과 자바스크립트 난독화로 배우는 해킹의 기술 - 최우석

* Cuckoo 샌드박스를 활용한 악성코드 분석 - 디지트 오크타비안토, 이크발 무하르디안토



4. 기타

* The Antivirus Hacker's Handbook - Joxean Koret

* 윈도우 시스템 해킹 가이드 : 버그헌팅과 익스플로 - 김현민


'악성코드 분석' 카테고리의 다른 글

API Sets  (0) 2017.05.12
윈도우의 예외 처리  (0) 2017.05.09
윈도우의 드라이버 개발과 루트킷 그리고 AV  (0) 2017.04.23
간단한 패커 개발  (0) 2017.04.23
Yoda's Protector 분석  (0) 2017.04.23
Posted by SanseoLab

-1. 현재

http://sanseolab.tistory.com/33 ] 해당 글과 같이 공부 및 실습 중이며 간단한 프로젝트를 해볼 생각이다.





0. 서론

  커널 관련 공부를 시작하면서 드라이버를 직접 개발해보는 것이 공부하는데 많은 도움이 될 것이라고 생각해 왔다. 루트킷의 원리를 이해하고 안티바이러스나 게임 보안 등의 프로그램을 개발해 보는 것도 관련 지식을 쌓는데 큰 도움이 될 것 같아서 드라이버 개발과 관련된 내용을 공부하고 정리하고 있다. 하지만 가장 큰 단점 중 하나는 인터넷에서 떠도는 대부분의 자료들이 너무 오래된 것들이고 심지어는 최근에 올라오는 자료들도 이러한 SSDT 후킹 같은 기술들 위주로 올라오는 것이 개인적으로는 마음에 들지 않았다.

  안티바이러스를 보더라도 국내 뿐만 아니라 외국의 자료들도 마찬가지인 것이 대부분 안티바이러스 개발과 관련된 자료는 스캐너 뿐이며 이것도 사용자 영역에서 시그니처를 기반으로한 것이 대부분이다. 하지만 분명히 내가 모르는 수많은 기술들과 자료들이 있을 것이라는 생각으로 많이 찾아보았지만 능력의 한계로 개략적인 방식과 관련된 자료들만을 겨우 얻을 수 있었다.





1. 드라이버 개발과 관련된 변화

  현재는 32비트 운영체제 보다는 64비트 운영체제가 주류를 이루고 있다. 윈도우 비스타 x64부터 시작해서 64비트 운영체제에는 많은 변화가 추가되었고 더 추가되고 있는 중이다. 눈여겨 볼 것 중 하나는 필터 관리자가 커널에 추가되었다는 점이다. 이것으로 인해 특히 안티바이러스 업체의 경우에 큰 변화가 생기게 되었는데 미니필터 드라이버를 개발함으로써 과거에 했던 방식보다 더 진보된 방식의 개발이 가능해지게 되었다고 한다. 커널의 필터 관리자가 제공해주는 기능을 사용함으로써 더 쉽고 빠르게 더 안전한 안티바이러스를 개발할 수 있게 되었다는 것이다.

  또한 드라이버와 관련된 정책에서도 여러 변경 사항이 추가되었다. 첫 번째는 KPP(Kernel Patch Protection)이다. 이것은 다른 말로 패치 가드라고도 불리는데 x64 버전의 비스타부터는 이 보호 정책으로 인하여 SSDT 후킹 같은 커널 영역의 수정이 불가능해 졌다. 즉 이것을 우회하지 않고서는 과거처럼 SSDT 후킹을 이용한 루트킷이 존재할 수 없으며 이것은 안티바이러스 프로그램에게도 마찬가지이다. 대신 앞에서 말한 미니필터 드라이버 외에도 후술할 MS에서 제공하는 다른 기능들을 사용할 수 있게 되었다.

  두 번째 변화는 KMCS(Kernel Mode Code Signing)이다. 이것도 x64 버전의 비스타부터 시행되는 정책으로서 간단히 말해서 모든 드라이버는 서명이 필요하다는 것이다. 즉 테스트 모드로 설정을 하지 않는 이상 모든 드라이버는 서명이 되어 있지 않다면 커널에 설치할 수 없게되었다. 

  지금까지 언급한 것들 이외에도 많은 변화들이 있을 것이며 추가될 것으로 여겨진다. 확실한 사실은 과거와는 달리 이제는 윈도우의 보안 정책들을 우회하는 취약점을 사용하지 않고서는 과거처럼 간단하게 루트킷을 만들고 감염시킬 수 없다는 점이다. 

  참고로 이런 저런 시도를 해보는 도중에 알게된 것은 윈도우 7의 32비트 버전의 경우에 32비트이기 때문에 KPP와 KMCS가 적용되지 않는다는점 외에도 필터 관리자도 커널에 존재한다는 장점이 있다. 하지만 추후에 살펴볼 PsSetCreateProcessNotifyRoutineEx()나 ObRegisterCallbacks() 같은 확장된 함수나 새로 추가된 함수들을 사용하기 위해서는 32비트 윈도우 7에서도 드라이버에 서명이 필요한 것은 마찬가지라는 것이다.

  개인적으로 KPP 같은 강력한 보안 및 코드 서명 등으로 인한 루트킷 수의 감소와 (물론 더 복잡하고 강력한 방식으로 계속되겠지만) 드라이버 관련 개발과 연구가 아마추어 개발자 입장에서는 쉽게 이루어질 수 없는 현실(코드 서명을 통해)로 인해서 루트킷의 원리를 공부하고자 하는 것 뿐만 아니라 안티 바이러스나 게임 보안 프로그램의 원리를 공부하려고 했던 계획이 잘 진행이 되지 않고 있다. 또한 SSDT 후킹 같은 과거의 기술을 공부해야 하는지에 등의 생각 때문에 아직도 혼란스럽다.





2. 안티바이러스 개발에 대한 정리

  비스타에서 여러 API들과 필터 관리자 등의 변화가 생기기 전까지만 해도 대부분의 AV들은 루트킷처럼 만들어져서 감시할 API들을 후킹하였다고 한다. 하지만 MS에서 기능들을 지원함에 따라 후킹 기반 모니터에서 이제는 미니 필터 드라이버와 커널 통지 콜백들을 사용한다.


2.1 자가 보호

  안티바이러스 프로그램은 특성상 자기 자체를 보호할 필요가 있다. 안티바이러스에 대한 공격으로는 프로세스 종료 외에도 메모리 변조 같은 공격이 있을 수 있다. 과거에는 SSDT 후킹을 사용해서 프로세스 관련 함수를 후킹하여 자가 보호를 구현하였다면 KPP에 의한 커널 변경 방지로 인해 최근에는 관련 함수가 제공되고 있다. 

  ObRegisterCallbacks() 함수는 비스타부터 제공되는 함수로서 프로세스 및 스레드 객체에 대한 특정한 사전 / 사후 동작에 관한 통지를 받는 콜백 함수를 등록하는 함수이다. 즉 각 객체에 대한 핸들의 생성 및 복사 등의 동작에 대해 콜백 함수를 등록해 주는 것이다. 예를들면 이로 인해 유저 모드에서 프로세스나 스레드에 접근하려고 하는 경우 이 콜백이 먼저 호출되고 만약 이러한 접근을 차단하거나 권한을 수정하는 내용을 등록했다면 유저 모드에서는 접근할 수 없게되는 것이다.


2.2 파일 스캐닝

  확실하진 않지만 일반적인 스캐너에서 사용하는 방식이라기 보다는 실시간 감시 같은 기능에서 파일이 생성될 때 하는 검사에서 사용되는것으로 보인다. 이것은 파일 시스템 미니필터 드라이버를 사용해서 구현한다. 과거에는 파일 생성과 관련된 API들에 대한 SSDT 후킹을 통해서 구현했던 것으로 보인다.

  조금 더 자세히 설명하자면 커널의 필터 관리자를 이용하는 함수들을 통해 구현하는데 예를들면 FltRegisterFilter()는 필터링 할 IRP에 해당하는 콜백 정보를 구성하고 필터 관리자에게 이 정보를 등록하도록 요청하며 FltStartFiltering()은 등록된 미니필터 드라이버의 필터링을 시작한다. 이 외에도 많은 함수들이 지원된다. 

[ FltRegisterFilter(), FltBuildDefaultSecurityDescriptor(), FltCreateCommunicationPort(), FltFreeSecurityDescriptor(), FltStartFiltering(), FltReadFile(), FltSendMessage(), FltCancelFileOpen(), FltCloseCommunicationPort(), FltUnregisterFilter() ]

  참고로 MS는 이것과 관련된 드라이버 예제 샘플도 제공하는데 Scanner는 AV에서 사용되는 파일 데이터 스캐너 관련 예제이며 AVScan은 transaction-aware 파일 스캐너와 관련된 에제이다. 


2.3 프로세스 / 스레드 보호

  예전부터 이것과 관련된 함수들이 지원되어 왔다. PsSetCreateProcessNotifyRoutine()은 프로세스의 생성과 소멸을 모니터링하기 위한 콜백 루틴을 등록해주는 함수이다. 비스타 이후에서는 이것의 확장형인 PsSetCreateProcessNotifyRoutineEx() 함수가 제공되며 최신 AV들도 이 함수를 사용하고 있다. 이 확장된 함수는 이전 버전과 달리 프로세스의 객체 등의 많은 정보들 뿐만 아니라 결과적으로 프로세스의 생성을 막을 수 있는 기능도 제공된다.

  이것과 비슷한 함수로 PsSetCreateThreadNotifyRoutine()이 있으며 대상은 스레드이다. 예를들면 외부 코드를 일반 프로세스의 내부에서 스레드로 실행 못하게 하는데 사용될 수도 있다. 윈도우 10부터는 PsSetCreateThreadNotifyRoutineEx() 함수가 추가되었다.

  마지막으로 PsSetLoadImageNotifyRoutine() 함수가 있는데 이것은 이름처럼 이미지가 로드되었을 때 통지해주는 콜백을 등록할 수 있다. 예를들면 이미지가 올라온 경우 아직 프로세스가 시작되지 않았다고 하더라도 어떤 dll을 또는 어떤 드라이버를 로드할지 알 수 있기 때문에 유용하게 사용될 수 있다. 


2.4 레지스트리 보호

  레지스트리 또한 자가 보호에서나 시스템 보호에서나 중요한 보호 대상 중 하나이다. MS는 레지스트리에 대해서도 레지스트리 필터링 드라이버를 제공한다. 옛날부터 RegistryCallback 루틴을 등록하는 CmRegisterCallback() 함수가 제공되어 왔다. 이 콜백 루틴은 프로세스가 레지스트리 관련 동작을 수행하기 전에 각 레지스트리 관련 동작에 관한 통지를 받을 수 있다. 최근 비스타부터는 확장된 버전인 CmRegisterCallbackEx() 함수를 사용할 수 있다.


2.5 네트워크 보호

  안티바이러스에서도 기본적으로 방화벽 기능이 제공된다. 과거에는 NDIS 및 TDI 필터 같은 드라이버를 개발해야 했지만 최근 비스타 부터는 WFP(Windows Filtering Platform)라는 플랫폼을 제공한다. 이것은 네트워크 필터링 애플리케이션을 만들기 위해 제공되는 플랫폼으로서 API와 시스템 서비스들의 집합이다. 참고로 과거의 NDIS / TDI는 다른 필터 드라이버들보다 개발하기 위한 난이도가 있었지만 WFP라는 간단한 개발 플랫폼이 제공됨으로 인해 최근 버전의 윈도우에서 돌아가는 안티바이러스 프로그램들은 이것을 사용한다고 한다.



'악성코드 분석' 카테고리의 다른 글

윈도우의 예외 처리  (0) 2017.05.09
링크 및 책 정리  (0) 2017.04.23
간단한 패커 개발  (0) 2017.04.23
Yoda's Protector 분석  (0) 2017.04.23
패커들 분석  (5) 2017.04.23
Posted by SanseoLab

블로그 이미지
Malware Analyst
SanseoLab

태그목록

공지사항

Yesterday
Today
Total

달력

 « |  » 2024.3
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31

최근에 올라온 글

최근에 달린 댓글

글 보관함