본문 바로가기

My Study/Programming&Theory

Windows DEP Exception 모니터링

특정 프로그램이 작동하고 있을 때 Windows DEP에 의해서 데이터 영역 같은 곳에서 실행이 막힌 경우

Access violation 예외가 발생하게 됩니다. 이 예외 정보를 가져와 처리를 해야하는데 어떠한 함수를 후킹해야 할까 디버깅 해보니 알 수 있었습니다.


윈도우에서는 예외를 발생시킬 때 ZwRaiseException 함수를 사용하는데요. 이 함수만 후킹해서 핸들러에서 적절히 인자 값 보고 판단만 해주면 끝납니다.( 과연? )


NTSTATUS ZwRaiseException(

  IN PEXCEPTION_RECORD ExceptionRecord,

  IN PCONTEXT ContextRecord,

  IN BOOLEAN FirstChance

  );


함수 원형입니다.


ContextRecord 에는 예외가 발생하기 직전의 레지스터 상태들이 담겨있습니다. 당연히 여기 구조체 내부에 있는 Eip를 보면 예외가 발생한 주소를 알 수 있지만 ExceptionRecord 에 더욱 자세한 정보가 담겨 있으므로 굳이 컨텍스트 정보는 볼 필요 없습니다. 그러면 EXCEPTION_RECORD 구조체를 보도록 하겠습니다.


typedef struct _EXCEPTION_RECORD {

 DWORD                    ExceptionCode;        0xC0000005 ( EXCEPTION_ACCESS_VIOLATION )
 DWORD                    ExceptionFlags;       0x00000000
 struct _EXCEPTION_RECORD  *ExceptionRecord;    0x00000000
 PVOID                    ExceptionAddress; 예외가 발생된 주소
 DWORD                    NumberParameters;  예외에 대한 파라미터 개수(2)
 ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;



DEP 에 의해 예외가 발생된 경우는 ExceptionCode가 0xC0000005 입니다.

먼저 이 값이 EXCEPTION_ACCESS_VIOLATION 인지 체크해 주면 되겠지요?


그리고 각 예외 코드에 따른 추가적인 정보들이 ExceptionInformation 에 따라오는데요. 여기 배열의 개수는 NumberParameters 값을 보면 알 수 있습니다. 각 예외코드마다 추가정보 파라미터의 개수는 다릅니다.


MSDN 에 있는 설명을 보도록 하겠습니다.


The first element of the array contains a read-write flag that indicates the type of operation that caused the access violation. If this value is zero, the thread attempted to read the inaccessible data. If this value is 1, the thread attempted to write to an inaccessible address. If this value is 8, the thread causes a user-mode data execution prevention (DEP) violation.

The second array element specifies the virtual address of the inaccessible data.


배열의 첫번째 요소는 접근 위반이 발생된 원인의 타입을 나타낸다고 되어 있습니다.

0 인 경우는 접근할 수 없는 데이터를 읽으려다 발생한거구요.

1 인 경우는 접근할 수 없는 주소에 데이터를 쓰려다 발생한거고

8 인 경우는 유저모드 DEP 에 의해 발생된 경우라고 나와있습니다.

그리고 두 번째 배열 요소는 예외가 발생된 주소를 가지고 있습니다.


그러면 우리는 해당 배열을 조사에 [0] 이 8인지를 확인하고 맞다면 그에대한 주소인 [1] 값을 가져오면 끝나는 것입니다.


그 외에도 윈도우에서 발생되는 예외는 ZwRaiseException 함수를 후킹하면 대부분 볼 수 있겠죠?


하지만 안타깝게도 저러한 조건을 확인하는 것은 DEP에 의해 예외가 발생된 것인지 알 수 있지만 ZwRaiseException 함수를 후킹하는 것은 반토막짜리 방법입니다.


ZwRaiseException 함수를 호출하는 RaiseException 함수의 설명을 MSDN에서 보도록 하겠습니다.

  1. The system first attempts to notify the process's debugger, if any.
  2. If the process is not being debugged, or if the associated debugger does not handle the exception, the system attempts to locate a frame-based exception handler by searching the stack frames of the thread in which the exception occurred. The system searches the current stack frame first, then proceeds backward through preceding stack frames.
  3. If no frame-based handler can be found, or no frame-based handler handles the exception, the system makes a second attempt to notify the process's debugger.
  4. If the process is not being debugged, or if the associated debugger does not handle the exception, the system provides default handling based on the exception type. For most exceptions, the default action is to call the ExitProcess function.

위 순서는 RaiseException 함수를 호출했을 때 예외핸들러를 찾는 순서입니다.
첫번째만 읽어도 멘붕이 왔습니다.

1. 시스템은 첫번째로 프로세스를 어테치하고 있는 디버거가 있다면 해당 디버거에 알림을 시도합니다.

라고 나와있습니다. 그 다음은 디버거에 안 붙어 있다면 스택에서 예외핸들러를 찾고 예외 핸들러가 없다면 또 다시 디버거에게 알림을 시도하고 디버거에 안붙어 있다면 디폴트 예외핸들러를 호출한다고 되어 있습니다.

아무튼 위 ZwRaiseException 함수가 호출된다는 것은 디버깅을 통해서 안 사실이므로 실제로 디버깅을 하지 않고 프로그램을 실행시키면 ZwRaiseException 함수는 호출되지 않습니다. 실제로 해보니 제 후킹 루틴으로 안 넘어오더군요.

그러면 디버깅 시에는 ZwRaiseException 함수를 후킹하면 예외를 잡아낼 수 있다는 것을 알았지만 일반적으로 실행시킬 땐 어떻게 잡을까요? 바로바로바로바로 아래 함수를 사용하면 됩니다.
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(
  _In_  LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);

위 함수는 lpTopLevelExceptionFilter 인자에 루틴을 등록해놓으면 예외 발생시 해당 함수가 호출되게 됩니다.
등록될 루틴의 함수 형태는 다음과 같습니다.

LONG WINAPI UnhandledExceptionFilter(
  _In_  struct _EXCEPTION_POINTERS *ExceptionInfo
);

이 형태로 등록하면 됩니다. 그러면 정상적으로 실행할 때는 어떠한 후킹도 없이 저 함수만 사용해주면 될까요? 인터넷 찾아보니 저 함수로 그냥 루틴 등록만 해놓으면 발생되는 문제점이 있다고 합니다.


다른 라이브러리에서 SetUnhandledExceptionFilter 함수를 사용해버리면 제가 등록한 예외는 덮어 씌워지고

제가 등록한 예외루틴은 실행되지 않을 것이라고 하네요. 그러면 이를 방지하기 위해선?


먼저 제 코드에서 SetUnhandledExceptionFilter 함수를 사용해 예외루틴을 등록 후 SetUnhandledExceptionFilter 함수를 후킹해 무조건 NULL을 리턴하도록 해버리는 것입니다. 실제로 이런식으로 많이 쓰고 있다고 합니다.


아무튼 저 함수로 예외 루틴 등록 후 DEP가 발생하면 제가 등록한 루틴으로 넘어올 것이고 저희는 ExceptionInfo로 넘어온 값을 이용해 DEP로 넘어온 건지 아닌건지를 판단해주면 됩니다. 판단 방법은 위에서 설명한 것과 같습니다.


ExceptionInfo->ExceptionRecord 를 사용하면 됩니다.


정리해보겠습니다

1. 일반적인 상황에서 Crash 예외 정보를 얻어오려면 SetUnhandledExceptionFilter 함수를 사용하면 됩니다. 하지만 다른 라이브러리에 의해서 덮어 씌워질수도 있으므로 후킹해 NULL을 리턴하도록 해줍니다.


2. 디버깅 상황에서 Crash 예외 정보를 얻어오려면 ZwRaiseException 함수를 후킹해 처리하면 됩니다.


끝..........