Anti debugging

shrew·2025년 3월 30일

Debugging

안티 디버깅에 대해 이야기 하기 전에 우리는 디버깅이 정확히 무엇인 지 먼저 알고 있어야 한다. 아마 리버스 엔지니어링이나 취약점 분석 등에 관심이 조금이라도 있는 사람들은 한 번씩은 디버깅을 해보았을 것이다. IDA나 Ghidra에서 제공하는 디버깅 기능, pwndbg나 GEF 같은 디버거 프로그램 등에서도 디버깅을 진행할 수 있다.

Concept

그렇다면 디버깅이 정확히 무엇일까? 디버깅은 'de + bug', 즉, 오류(버그)를 찾고, 분석해서 해당 오류를 수정하는 과정을 디버깅이라고 하며, 더 크게는 오류 수정 뿐 아니라 코드가 개발자의 의도대로 잘 작동하고 있는 지 분석하고, 확인하는 작업 전체를 뜻한다. 디버깅은 모든 프로그램에 있어서 필요한 과정이지만 안 좋은 의도를 가진 공격자가 사용하게 되면 프로그램의 동작 방식을 공격자가 분석할 수 있게 되어 프로그램의 전체적인 보안이 허술해 진다.

Anti degguging

Concept

안티 디버깅은 프로그램을 쉽게 분석하지 못하도록 디버깅 행위를 방지하고자 하는 기법들을 의미한다. 안티 디벅깅은 디버거를 어떻게 탐지하는 지에 따라 크게 정적 안티 디버깅 기법과 동적 안티 디버깅 기법으로 나눌 수 있다.

Static anti debbuging

Concept

정적 기법은 디버거가 어떠한 동작을 하지 않아도 디버거가 attach된 것 만으로 운영체제나 프로세스 내부 상태를 통해 디버거 사용 여부를 확인하게 된다. 정적 기법으로는 API를 사용한 검사와 PEB 같은 구조체 정보 등이 있다.

TEB(Thread environment block)
윈도우 운영체제에서 각 스레드마다 생성되는 정보 구조체이다. 32bit 아키텍처에서는 'FS:[0x00]' 이라는 세그먼트 레지스터에서 시작되고, 64bit 아키텍처에서는 'GS:[0x00]' 이라는 세그먼트 레지스터에서 시작된다.

PEB (Process Environment Block)
윈도우 운영체제에서 각 프로세스마다 생성되는 정보 구조체이다. 32bit 아키텍처에서는 'FS:[0x30]'에서 시작되고, 64bit 아키텍처에서는 'GS:[0x60]'에서 시작된다. TEB 안에 PEB를 가리키는 포인터가 포함되어 있다.

Windows API

안티 디버깅 기능을 하는 윈도우 API 함수는 굉장히 다양하지만 그 중 인지도가 높은 세 가지 함수만 예시로 살펴보자.

IsDebuggerPresent

BOOL IsDebuggerPresent();

IsDebuggerPresent 함수는 가장 간단하고, 기본적인 안티 디버깅 함수이다. PEB 안에는 'BeingDebugged' 라는 필드(fs:[0x32])가 존재하는데 IsDebuggerPresent 함수에서는 해당 필드를 통해 디버깅 여부를 판단한다. 'BeingDebugged' 플래그가 0이면 디버거가 attach되면 true를 반환하고, 아니라면 false를 반환하는 식이다.

CheckRemoteDebuggerPresent

BOOL CheckRemoteDebuggerPresent(
  [in]      HANDLE hProcess,
  [in, out] PBOOL  pbDebuggerPresent
);

CheckRemoteDebuggerPresent 함수는 자기 자신이나 다른 로컬 프로세스의 디버깅 여부를 확인하는 함수이다. CheckRemoteDebuggerPresent 함수는 내부적으로 NtQueryInformationProcess라는 커널에 가까운 수준에서 동작하는 'ntdll.dll'의 네이티브 API를 호출하여 디버그 포트를 확인하는 것으로 디버깅 여부를 판단한다.

__kernel_entry NTSTATUS NtQueryInformationProcess(
  [in]            HANDLE           ProcessHandle,
  [in]            PROCESSINFOCLASS ProcessInformationClass,
  [out]           PVOID            ProcessInformation,
  [in]            ULONG            ProcessInformationLength,
  [out, optional] PULONG           ReturnLength
);

사실 상 실재로 디버깅 유무를 확인해 주는 건 NtQueryInformationProcess 함수이다. 다만, CheckRemoteDebuggerPresent 함수가 유저 모드 API이기 때문에 좀 더 편하게 쓸 수 있다.

OutputDebugString

void OutputDebugStringW(
  [in, optional] LPCWSTR lpOutputString
);

OutputDebugString 함수는 디버거가 attach되어 있을 때만 동작하는 함수로, 호출 시 디버거의 출력창(Debugger output window)으로 문자열을 전송한다. 디버거가 attach되어 있지 않다면 전송할 곳이 없기 때문에 운영체제가 무시하고, 디버거가 attach되어 있다면 문자열을 수신해서 콘솔에 띄운다.

TracerPid 검사

리눅스에서 어떤 프로세스가 ptrace를 사용해서 다른 프로세스를 디버깅하고 있으면, 디버깅 당하는 쪽 프로세스의 '/proc/[pid]/status'에 디버거 프로세스의 PID가 TracerPid 항목에 기록된다. 따라서 해당 파일의 내용을 확인했을 때, '0'이 아니라면 디버거가 attach되어 있다고 보면 된다.

$ cat /proc/self/status | grep TracerPid
TracerPid:	0

파일 내용은 cat 명령어로 간단하게 확인 가능하다.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int is_debugger_attached() {
    FILE *f = fopen("/proc/self/status", "r");
    if (!f) return 0;

    char line[256];
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, "TracerPid:", 10) == 0) {
            int tracer_pid = atoi(&line[10]);
            fclose(f);
            return tracer_pid != 0;
        }
    }
    fclose(f);
    return 0;
}

int main() {
    if (is_debugger_attached()) {
        printf("디버거 있음\n");
    } else {
        printf("디버거 없음\n");
    }
    return 0;
}
$ ./test
디버거 없음
gef➤  r
Starting program: /home/pdh/test 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
디버거 있음
[Inferior 1 (process 17963) exited normally]

위는 디버깅 여부를 좀더 간단하게 확인할 수 있는 예제 코드이다. 보면 평범하게 프로그램을 실행하면 '디버깅 있음' 문자열이 출력되고, 디버거애서 run 하면 '디버깅 있음' 문자열이 출력된다.

Dynamic anti debbuging

Concept

동적 안티 디버깅 기법은 디버거가 attach된 것 뿐 아니라 디버거가 trace, step, 예외 처리 등 실제 실행 흐름에 관여했을 때 발생하는 반응을 이용하여 디버깅 사용 여부를 판단한다.

ptrace(PTRACE_TRACEME) 호출 실패

리눅스에서 디버깅 중인 프로세스는 'PTRACE_TRACEME'를 호출할 수 없다. (이미 디버거가 attach되어 있기 때문이다.) 따라서 '-1'이 반환되면 디버거가 attach되어 있다는 뜻이다.
ptrace 함수 포스트

#include <sys/ptrace.h>
#include <stdio.h>

int main() {
    if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
        printf("ptrace 실패, 디버거 있음\n");
    } else {
        printf("ptrace 성공, 디버거 없음\n");
    }
    return 0;
}
$ ./test
ptrace 성공, 디버거 없음
gef➤  r
Starting program: /home/pdh/test 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
ptrace 실패,  디버거 있음
[Inferior 1 (process 18069) exited normally]

위는 디버깅 여부를 좀더 간단하게 확인할 수 있는 예제 코드이다. 보면 평범하게 프로그램을 실행하면 'ptrace 성공, 디버거 없음' 문자열이 출력되고, 디버거애서 run 하면 'ptrace 실패, 디버거 있음' 문자열이 출력된다.

실행 시간 비교

break point나 step 기능 등을 사용하면 그냥 프로그램을 실행하는 것 보다 실행 속도가 굉장히 느려진다. 따라서 실행 속도가 느리다면 디버깅이 여부를 유추할 수 있다.

#include <stdio.h>
#include <sys/time.h>

int main() {
    struct timeval start, end;
    gettimeofday(&start, NULL);

    for (volatile int i = 0; i < 100000000; i++);

    gettimeofday(&end, NULL);

    long us = (end.tv_sec - start.tv_sec) * 1000000 + (end.tv_usec - start.tv_usec);
    printf("실행 시간: %ld us\n", us);

    if (us > 300000)
        printf("느리게 실행됨, 디버깅 중일 가능성 있음\n");
    else
        printf("빠르게 실행됨\n");
    return 0;
}
$ ./test
실행 시간: 240109 us
빠르게 실행됨
gef➤  c
Continuing.
실행 시간: 10611646 us
느리게 실행됨, 디버깅 중일 가능성 있음
[Inferior 1 (process 18118) exited normally]

위는 디버깅 여부를 좀더 간단하게 확인할 수 있는 예제 코드이다. 보면 평범하게 프로그램을 실행하면 실행 시간과 '빠르게 실행됨' 문자열이 출력되고, 디버거애서 break point 설정 후 continue하면 '느리게 실행됨, 디버깅 중일 가능성 있음' 문자열이 출력된다. 이 방법의 경우, 느리게 실행된다고 해서 무조건 디버깅 중이라고 확신할 수 없고, 빠르게 실행된다고 디버깅 시도가 없었다고 확신할 수 없다는 단점이 있다. 다만, 윈도우나 리눅스 등 대부분 시스템 환경에서 사용 가능한 방법이라는 장점이 있다. (위 예시는 리눅스 기준이다.)

profile
보안 공부 로그

0개의 댓글