리버스 엔지니어링은 바이너리의 내부 로직을 들여다보는 가장 강력한 도구다. 그러나 소프트웨어 보호 관점에서는 이를 차단해야 할 필요가 있다. 그래서 등장한 기술이 Anti-Debugging이다. 하지만 Anti-Debugging은 만능이 아니다. 리버서는 API 후킹이나 PEB 패치를 통해 이를 우회할 수 있다. 따라서 필자는 이번 글에서 Anti-Debugging의 내부 동작 원리와 주요 우회 기법, 그리고 강화된 방어 전략까지 설명하도록 하겠다.
1. Anti-Debugging의 내부 동작 원리
프로세스가 디버거에 attach되면 운영체제는 해당 사실을 프로세스 자신이 인지할 수 있도록 여러 흔적을 남긴다. Anti-Debugging은 이 흔적을 능동적으로 탐지하여 디버깅 환경에서의 실행을 거부하는 방어 메커니즘이다. Windows의 프로세스 구조를 간단히 표현하면 다음과 같다.
| PEB (Process Environment Block) |
|---|
| BeingDebugged (1 byte) <- 디버거 부착 시 0x01 |
| NtGlobalFlag (4 bytes) <- 힙 디버깅 플래그 |
| ProcessHeap.Flags <- 디버그 모드 힙 플래그 |
리버서는 디버거를 사용해 이 구조 위에서 분석을 수행하며 Anti-Debugging은 바로 이 흔적을 검사하여 디버깅 여부를 판별한다.
1.1 디버거 탐지의 기본 메커니즘
가장 널리 쓰이는 Anti-Debugging은 Windows API와 PEB 검사에 기반한다.
-
IsDebuggerPresent()는 현재 프로세스에 user-mode 디버거가 부착되어 있는지를 반환한다. 내부적으로 PEB의BeingDebugged필드를 그대로 읽어온다. -
CheckRemoteDebuggerPresent()는 커널 객체를 통해 디버그 포트(DebugPort)의 존재 여부를 확인하므로 PEB 변조만으로는 우회할 수 없다. -
NtQueryInformationProcess()는ProcessDebugPort,ProcessDebugFlags,ProcessDebugObjectHandle등을 조회하여 디버거 부착 여부를 정밀하게 판단한다.
1.2 탐지의 특성
-
PEB 기반 탐지는 사용자 공간에서 직접 메모리를 읽기 때문에 속도가 빠르지만 PEB 자체를 패치당하면 무력화된다.
-
커널 기반 탐지(
NtQueryInformationProcess)는 시스템 콜을 통해 커널 영역의 정보를 가져오므로 단순 PEB 패치로는 우회할 수 없다. -
x86_64 아키텍처에서는
gs:[0x60]을 통해 현재 프로세스의 PEB에 접근할 수 있다.
1.3 시간 기반 탐지
디버거에서 단일 스텝(single-step) 실행을 수행하면 명령어 사이 간격이 비정상적으로 길어진다. 이를 활용한 시간 기반 탐지는 다음과 같이 동작한다.
unsigned long long t1 = __rdtsc();// 보호 대상 코드 블록unsigned long long t2 = __rdtsc();if (t2 - t1 > THRESHOLD) { ExitProcess(0); // 디버깅 의심, 즉시 종료}__rdtsc()는 CPU의 Time Stamp Counter를 읽는 명령어로 클럭 사이클 단위의 매우 정밀한 시간 측정이 가능하다. 디버거 환경에서는 코드 실행 사이에 디버거의 개입이 발생하므로 사이클 차이가 임계값을 크게 초과한다.
2. Anti-Debugging 우회 기법
2.1 PEB 직접 패치
가장 기본적인 우회 기법이다. PEB의 BeingDebugged 필드를 0으로 덮어쓰면 IsDebuggerPresent()가 항상 false를 반환하게 된다.
// x86_64 기준mov rax, gs:[0x60] ; PEB 주소 로드mov byte ptr [rax+2], 0 ; BeingDebugged = 0해당 코드는 PEB 구조체의 오프셋 +0x02에 위치한 BeingDebugged 필드를 직접 0으로 설정한다. 이후 호출되는 IsDebuggerPresent()는 내부적으로 이 필드를 그대로 읽기 때문에 디버거가 부착되어 있어도 false를 반환하게 된다. 다만 NtGlobalFlag(+0xBC)와 ProcessHeap.Flags 역시 별도로 패치해야 우회가 완전해진다.
2.1.1 PEB 오프셋 구조
x86_64 환경에서 주요 안티 디버깅 필드의 오프셋은 다음과 같다.
| Offset | Field |
|---|---|
| +0x02 | BeingDebugged |
| +0xBC | NtGlobalFlag |
| +0x30 | ProcessHeap |
| ProcessHeap+0x70 | Flags |
리버서가 디버거를 부착한 채로 정상 흐름을 분석하려면 이 네 필드를 모두 정리해야 단순 PEB 기반 탐지를 무력화할 수 있다.
2.2 API 후킹과 반환값 조작
타깃 프로세스가 IsDebuggerPresent()나 NtQueryInformationProcess()를 호출할 때 함수 시작부에 jmp를 삽입하여 직접 작성한 핸들러로 흐름을 우회시키는 기법이다. 핸들러는 항상 false 또는 디버거 미부착을 의미하는 값을 반환하도록 만든다. Detours, MinHook 같은 후킹 라이브러리가 이 작업을 자동화한다. 다만 보호 대상 코드가 API를 거치지 않고 PEB나 시스템 콜을 직접 호출하는 경우 후킹만으로는 우회되지 않는다.
2.3 시스템 콜 직접 호출 회피
최신 보호 솔루션은 user-mode API를 거치지 않고 syscall 명령으로 커널 함수를 직접 호출하여 후킹을 무력화한다. 우회 측에서는 SSDT 후킹이나 커널 드라이버를 이용해야 하지만 PatchGuard와 DSE(Driver Signature Enforcement)로 인해 일반 환경에서는 사실상 차단된다. 따라서 리버서는 ScyllaHide, TitanHide 같은 통합 도구로 user-mode와 kernel-mode 양쪽의 흔적을 동시에 숨기는 방식을 사용한다.
2.4 시간 기반 탐지 우회
__rdtsc() 기반 탐지는 디버거 플러그인을 통해 우회할 수 있다. 플러그인은 rdtsc 명령을 패치하거나 가상화하여 디버거가 일시정지된 시간만큼 카운터를 보정한다. 또는 코드 패치를 통해 비교 분기 자체(jg, ja 등)를 무력화하는 방법도 자주 사용된다.
3. Anti-Debugging 강화 방어
3.1 다층 탐지 결합
단일 탐지에 의존하면 한 지점만 패치당해도 무력화된다. PEB 검사, 커널 기반 NtQueryInformationProcess 호출, 시간 기반 탐지, 하드웨어 브레이크포인트 레지스터(DR0~DR3) 검사 등을 무작위 시점에 분산하여 호출하면 우회 비용을 크게 높일 수 있다.
3.2 코드 난독화 및 무결성 검증
탐지 로직 자체가 노출되면 곧바로 패치 대상이 된다. Control Flow Flattening, Opaque Predicate 같은 난독화로 탐지 코드를 숨기고 함수 진입 시 자신의 코드 영역 해시를 계산하여 변조 여부를 확인하는 self-checksum 기법을 함께 사용한다.
3.3 가상화 기반 보호
VMProtect, Themida 같은 솔루션은 보호 대상 코드를 자체 가상 머신의 바이트코드로 변환하여 원본 x86_64 명령어 흐름 자체를 사라지게 만든다. 리버서는 가상 머신의 디스패처와 핸들러를 먼저 분석해야 하므로 정적·동적 분석 모두 난이도가 비약적으로 상승한다.
오늘은 Anti-Debugging의 동작 원리와 우회 기법, 그리고 강화된 방어 전략에 대해 알아보았다.