본문 바로가기
reversing

안티 디버깅 Dynamic

by 코끼리_땃쥐 2022. 10. 5.
반응형

안티 디버깅 분류

    Static, Dynamic

        - Static 기법은 디버깅 시작할 때 한번만 해체를 해주는 기법

        - Dynamic 기법은 디버깅을 진행하면서 (해당 Anti기법을) 만날 때마다 해결하는 기법

 

Dynamic 안티 디버깅의 목적

목적은 내부 코드와 데이터를 리버싱으로부터 감추고 보호하는 것.

보통PE 프로텍터들에서 많이 사용되며 원본 프로그램의 핵심 알고리즘을 보호하기 위하여 사용됩니다. 디버거로 해당 프로그램이 실행될 수는 있을지언정 원본 프로그램의 핵심코드(OEP)로 트레이싱 하여 찾아갈 수 없도록 방해합니다.

 

예외

예외(Exception)를 이용하는 방법은 안티 디버깅의 단골 메뉴입니다. 정상적으로 실행된 프로세스에서 예외가 발생하면 SEH(Structured Exception Handling) 메커니즘에 의해 OS에서 예외를 받아서 프로세스에 등록된 SEH를 호출해줍니다.

그러나 디버기(Debuggee)에서 예외가 발생하면 디버거(Debugger)에서 예외 처리를 담당합니다. 바로 이러한 특징을 이용해서 정상 실행되는 경우와 디버깅 당하는 경우를 판별해서 서로 다른 동작을 수행할 수 있는 안티 디버깅 기법입니다.

 

SEH
Windows OS의 대표적인 예외
EXCEPTION_DATATYPE_MISALIGNMENT             (0x80000002)
EXCEPTION_BREAKPOINT                                     (0x80000003)
EXCEPTION_SINGLE_STEP                                    (0x80000004)
EXCEPTION_ACCESS_VIOLATION                          (0xC0000005)
EXCEPTION_IN_PAGE_ERROR                               (0xC0000006)
EXCEPTION_ILLGAL_INSTRUCTION                       (0xC000001D)
EXCEPTION_NONCONTINUABLE_EXCEPTION     (0xC0000025)
EXCEPTION_INVALID_DISPOSITION                       (0xC0000026)
EXCEPTION_ARRAY_BOUNDS_EXCEEDED           (0xC000008C)
EXCEPTION_FLT_DENORMAL_OPERAND              (0xC000008D)
EXCEPTION_FLT_DIVIDE_BY_ZERO                        (0xC000008E)
EXCEPTION_FLT_INEXACT_RESULT                       (0xC000008F)
EXCEPTION_FLT_INVALID_OPERATION                  (0xC0000090)
EXCEPTION_FLT_OVERFLOW                                  (0xC0000091)
EXCEPTION_FLT_STACK_CHECK                            (0xC0000092)
EXCEPTION_FLT_UNDERFLOW                                (0xC0000093)
EXCEPTION_INT_DIVIDE_BY_ZERO                        (0xC0000094)
EXCEPTION_INT_OVERFLOW                                   (0xC0000095)
EXCEPTION_PRIV_INSTRUCTION                            (0xC0000096)
EXCEPTION_STACK_OVERFLOW                             (0xC00000FD)

 

EXCEPTION_BREAKPOINT

가장 대표적인 예외는 바로 BP(Break Point) 예외 입니다. Break Point 명령어를 이용하여 예외를 발생시키면 일반 실행인 경우 등록된 SEH가 자동 실행되겠지만, 디버깅 실행의 경우 즉시 실행이멈춰지고 제어권은 디버거로 넘어옵니다. 예외 처리기에는 보통 EIP값을 변경시키는 코드가 들어있습니다. 디버거 옵션을 조정해서 디버깅 중이라도 관련 예외를 OS에게 넘겨서 SEH가 자동 실행되도록 만들 수 있습니다. 하지만 그렇게 한다고 해도 예외 처리기 내부에서 Static 안티 디버깅 기법을 적절히 사용한다면 디버깅 여부를 쉽게 판별해낼 수 있습니다.

또한 예외 처리기 내부에서 EIP 값이 어떻게 변경될지 모릅니다. 즉 디버깅을 계속 진행하기 위해서는 결국 예외처리기를 따라 들어가야 합니다.

 

SetUnhandledExceptionFilter()

프로세스에 예외가 발생했을 때 만약 SEH에서 예외 처리가 되지 않았거나 등록된 SEH가 없다면 어떻게 될까요 ? 끄때는 시스템의 kernel32!UnhandledExceptionFilter() API가 호출됩니다. 이 함수 내부에서 Top Level Exception Filter(혹은 Last Exception Filter)라고 불리는 시스템의 마지막 예외 처리기를 실행시킵니다.

재미있는 점은 kerenl32!UnhandledExceptionFilter()의 내부에서 Static안티 디버깅 기법인 ntdll!NtQueryInformationProcess(ProcessDebugPort) API를 호출하여 프로세스의 디버깅 여부를 판별한다는 것입니다. 일반 실행이라면 시스템의 마지막 예외 처리기를 실행 시키고, 디버깅 중이라면 예외를 디버거에게 넘겨줍니다. Kernel32!SetUnhandledExceptionFilter() API를 이용하면 시스템의 마지막 예외 처리기(Top Level Exception Filter)를 변경시킬 수 있습니다.

 

LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(
	__in	LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);

lpTopLevelExceptionFilter 파라미터에 새로운 Top Level Exception Filter 함수 주소를 넘겨주면 됩니다(리턴 값은 이전 Last Exception Filter함수 주소 입니다). Top Level Exception Filter 함수의 정의는 아래와 같습니다.

TopLevelExceptionFilter() API
typedef struct _EXCEPTION_POINTERS {
	PEXCEPTION_RECORD	ExceptionRecord;
    PCONTEXT			ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

LONG TopLevelExceptionFilter(
	PEXCEPTION_POINTERS	pExcept
);

안티 디버깅 기법에서는 고의로 예외를 발생시킨후 새로 등록한 Last Exception Filter 내에서 일반 실행과 디버깅 실행을 각각 판별하여 그에 맞도록 EIP 값을 변경시켜 버립니다. 이 과정에서 디버깅 판별은 시스템에서 자동으로 해줍니다. 이러한 안티 디버깅 기법은 Static과  Dynamic 방법이 혼합된 형태입니다.

 

Timing Check

디버거를 이용해 코드를 한 줄씩 트레이싱하면서 진행하면 그냥 정상적으로 실행 될때보다 엄청나게 오랜 시간이 소요됩니다.

Timing Check 기법은 그러한 실행 시간의 차이를 측정하여 디버깅 여부를 판별하는 기법입니다.

동작원리는 상당히 단순합니다.

 

시간 간격 측정방법

아래와 같이 시간 간격을 측정하는 다양한 방법이 존재합니다.

1. Counter based method
    RDTSC
    kernel32!QueryPerformanceCounter() / ntdll!NtQueryPerformanceCounter()
    kernel32!GetTickCounter()

2. Time base method
    timeGetTime()
    _ftime()

크게 두가지 방법이 존재합니다. CPU의 카운터(Counter)를 이용하는 방법과 시스템의 실제시간(Time)을 이용하는 방법입니다.

 

Trap Flag

Trap Flag(TF)란 EFLAGS 레지스터의 9번째(Index) 비트입니다.

 

Single Step

TF 값을 1로 세팅하면 CPU는 Single Step 모드로 변경됩니다.

CPU는 Single Step 모드에서 하나의 명령어를 실행시킨 후 EXCEPTION_SINGLE_STEP 예외를 발생시킵니다. 이후 Trap Flag는 자동으로 초기화(0)됩니다.

이 EXCEPTION_SINGLE_STEP 예외는 SEH 기법과 결합하여 디버거를 탐지하기 위한 안티 디버깅 기법에 사용됩니다.

 

INT 2D

ITN 2D 명령어는 본래 커널 모드에서 작동하는 Break Point 예외를 발생시키는 명령어입니다만, 유저 모드에서도 예외를 발생시킵니다. 그러나 디버깅 실행의 경우에는 예외를 발생시키지 않고(그냥 무시하고) 넘어갑니다. 이렇게 일반 실행과 디버깅 실행의 동작에서 차이가 발생하면 안티 디버깅 기법의 좋은 재료가 됩니다.

INT 2D 명령어를 디버깅 해보면 몇 가지 흥미로운 특징들이 있습니다.

1. 1바이트 무시
    디버깅 모드에서 INT 2D를 실행(혹은 StepInto/ StepOver)하면
    다음 명령어의 첫 바이틑 무시되고, 그다음 바이트부터 새로운 명령어로 인식하여 실행됩니다.

2. BP까지 그대로 실행
    또 다른 특징은 INT 2D 명령을 Trace (StepInto[F7], StepOver[F8])하면,
    그 다음 명령어 시작에서 멈추지 않습니다. 마치 RUN[F9] 명령을 수행한 것과 마찬가지로
    도중에 BP를 만날 때 까지 계속 실행됩니다.

 

 

0xCC Detection

일반적으로 프로그램을 디버깅할 때 Software BP(Break Point)를 많이 설치합니다. BP의 x86 Instruction은 '0xCC' 입니다. 이 값을 정확히 발견할 수만 있다면 디버깅 여부를 판별할 수 있습니다. 이러한 아이더를 적용한 안티 디버깅 기법이 '0xCC 탐지' 입니다.

 

API Break Point

어떤 프로그램에서 원하는 기능만 빠르게 디버깅 하는 방법 중의 하나는 해당 프로그램이 사용할 만한 API에 BP를 설치 후 실행 하는 것입니다. 만약 해당 BP에서 실행이 멈춘다면 스택에 저장된 복귀 주소(Return Address)를 확인 합니다. 복귀 주소로 따라가서 그부분을 디버깅 하는 식으로 작업을 하면 넓은 코드 영억의 디버깅 범위를 크게 줄일수 있습니다. 안티 디버깅 기법 중에는 이와 같은 API에 설치된 BP를 확인해서 현재 디버깅을 당하지는 판단하는 방법이 있습니다. 일반적으로 API코드 시작 부분에 BP를 설치 하므로 첫 바이트가 CC인지 검사해서 디버깅 중이라고 판단할 수 있습니다.

 

Checksum 비교

코드에 설치된 Software Break Point를 확인하는 또다른 방법은 특정 코드 영역의 Checksum 값을 비교하는 것 입니다. 예를 들어 프로그램 401000 ~ 401070 주소 영역의 Checksum 값이 0x12345678 이라고 하겠습니다 만약 이코드 영역에 디버깅을 위해서 BP('0xCC')를 하나라도 설치 하였다면 새로운 Checksum 값은 원본 값과 달라집니다. 이렇게 Checksum 값의 비교를 통해서 디버깅 여부를 판단 할 수 있습니다.

반응형

'reversing' 카테고리의 다른 글

바이트 오더링  (0) 2022.11.03
고급 안티 디버깅  (0) 2022.10.20
안티 디버깅 Static  (2) 2022.10.04
NtQueryInformationProcess  (0) 2022.09.29
IA-32 Instruction 포맷  (0) 2022.09.27

댓글