Anti-Debugging

» CyKor-Seminar

Index

Linux - ptrace

Windows - IsDebuggerPresent

Timing Trick

Process 중 상용 디버거 감지

Process Status 확인

중단점 명령어 탐지

무결성 검사

Self-Debugging

Anti-Debugging은 대체로 디버거를 감지하는 방법입니다. 디버거가 부착되어 있다고 감지하게 되면 프로세스가 원래와 다른 동작을 수행하도록 하거나, 아예 프로세스를 종료해 버리는 방식으로 anti-debugging이 구현됩니다.

ptrace와 IsDebuggerPresent는 각각 Self DebuggingProcess Status 확인에 속하는 기법입니다. 다만, 그 빈도수가 타 기법에 비해 비약적으로 높기에 앞서서 따로 설명하겠습니다.


Linux - ptrace

원리

Linux에서 가장 흔하게 볼 수 있는 안티 디버깅 트릭입니다.

본래 용도는 디버깅 용도로 사용되는 함수입니다.

디버깅 원리는 Self-Debugging 방식입니다 자세한 원리에 대해서는 Self-Debugging 설명할 때 같이 설명하겠습니다.

ptrace의 첫 번째 인자가 0인 경우(PTRACE_ME) 자기 자신을 Debugging하겠다는 의미이며, 성공하면 0을 반환하고 실패하면 -1을 반환합니다.

디버거가 이미 부착되어 있다면 ptrace가 실패하여 -1을 반환하고, 성공한다면 0을 반환하는 방식으로 디버거를 탐지합니다. 또한 ptrace가 성공적으로 실행된 뒤에는 디버거가 부착될 수 없습니다.

정상 환경에서 ptrace가 여러 번 수행될 경우 최초 1회만 0을 반환하며 그 외에는 -1을 반환합니다. 일반적인 바이너리에서 동일한 프로세스 대상으로 ptrace를 여러 번 사용하지 않으며, 그렇기에 일반적으로 1번만 우회하면 더 이상 우회할 일이 없는 편입니다.

우회 방법

  1. 디버거(gdb 등)을 이용

    ptrace가 실행되기 전에 디버거가 프로세스에 먼저 부착되어야 하며, 애초에 gdb에서 실행하는 방법 등으로 이를 수행할 수 있습니다.

    ptrace가 수행된 이후에 rax 값을 0으로 설정하면 쉽게 우회할 수 있습니다.

  2. binary patch

    그냥 call ptrace를 nop으로 바꾸어 주면 쉽게 우회가 가능합니다.


Windows - IsDebuggerPresent

원리

windows에서 가장 쉽게 볼 수 있는 안티 디버깅 트릭입니다.

세부적인 원리는 프로세스의 상태를 저장하고 있는 영역인 PEB 영역의 값을 확인하여 디버거가 부착되어 있는지 여부를 판단합니다. 아래에서 설명할 Process Status 확인의 일종입니다.

True를 반환하게 되면 디버거가 부착되어 있는 것으로 판단하게 됩니다.

ptrace와 다르게 바이너리가 IsDebuggerPresent로 도배 되어 있는 경우도 존재합니다.

우회 방법

  1. 디버거로 PEB section 패치

    PEB 영역에서 디버거 정보를 나타내는 메모리 영역의 값을 0으로 수정하면 IsDebuggerPresent의 반환값이 False가 됩니다. 일반적으로 디버거가 부착된 뒤 한 번만 수행하면 됩니다.

  2. x64dbg의 디버거 숨기기 기능

    x64dbg는 1번 방법을 자동으로 수행하는 기능을 제공합니다.

    Untitled

    프로세스에 x64dbg를 부착한 이후에 해당 옵션을 이용하여 우회 가능합니다.

  3. 디버거로 반환 값 patch

    ptrace 때와 마찬가지로 IsDebuggerPresent 호출 지점에 중단점을 걸은 뒤 반환 값인 rax 레지스터 값을 False로 패치해 주면 우회가 가능합니다. 다만, 여러 번 호출될 경우 호출 될 때마다 우회 작업을 수행해야 합니다.

  4. binary patch

    ptrace 때와 마찬가지로 IsDebuggerPresent 함수를 호출하는 어셈블리를 nop으로 패치하면 쉽게 우회가 가능합니다. 다만, IsDebuggerPresent로 도배된 경우에는 일일이 모두 패치해 주어야 합니다.

  5. Hooking

    3번, 4번보다 훨씬 세련된 방법입니다만, 조금 복잡한 방법입니다. Hooking 방법은 2학기 때 소개드릴 기회가 있을 것 같은데, IsDebuggerPresent 함수가 실행될 때 실행 흐름을 가로채서 내가 원하는 코드가 실행되도록 하는 기법입니다. 이를 이용하여 IsDebuggerPresent가 호출될 때마다 무조건적으로 False를 return하도록 하면 IsDebuggerPresent가 얼마나 호출되는지 상관없이 모두 우회가 가능합니다. ptrace도 Hooking으로 우회가 가능하지만, 일반적으로 한 번만 우회하면 되기 때문에 Hooking으로 우회하는 경우는 거의 없습니다.


Timing Trick

원리

굉장히 고전적인 방법입니다. 디버깅이라고 하는 것이 중단점을 걸고 여러가지 프로세스의 상황을 확인하는 작업인데, 이렇게 되는 경우 정상적으로 실행되는 프로세스에 비해 실행 시간이 비약적으로 길어지게 됩니다. 이를 이용하여 시간이 정상 실행 시간에 비해 비약적으로 길어지는 경우 디버깅으로 판별하는 방법입니다.

우회 방법

  1. 패치

    시간 검증 루틴을 수행하지 않도록 패치하여 우회합니다.

  2. 디버깅 자동화

    현대의 디버거들은 대부분 script 기능을 제공합니다(window - windbg & x64dbg, linux - gdb에서 script 사용 가능). 이를 이용하여 디버깅 작업을 자동화할 수 있으며 자동화된 디버깅 작업은 시간 검증을 무사히 통과할 가능성이 높아집니다.


Process 중 상용 디버거 감지

원리

이것 또한 고전적인 방법입니다. 현재 실행 중인 모든 프로세스 목록을 가져온 뒤 유명한 디버거의 이름을 가진 프로세스가 실행되고 있으면 디버깅 중이라고 판단하는 방식입니다.

이러한 원리로 인하여 오탐의 가능성이 상당히 높은 방식이며, 막아야 할 경우보다 더 과하게 막는 경향이 있습니다. 하지만 여전히 많은 상용 프로그램에서 채택하는 방식입니다.

요즘에는 안하지만 제가 옛날에 메이플을 즐겨 했는데, IDA가 켜져 있으면 IDA로 무슨 작업을 하던지 상관 없이 바로 메이플 서버에서 강퇴 당했습니다….

우회 방법

간단하게 디버거 이름을 바꾸면 됩니다. x64dbg에서는 이름 바꾸는 기능도 존재하는 것으로 기억하는데, 이게 플러그인 기능인지 본래 기능인지는 생각이 잘 안 나네요.

수동으로 디버거 실행 파일의 이름을 바꿔도 가능합니다.


Process Status 확인

원리

IsDebuggerPresent의 원리에 해당합니다. 디버거가 부착된 경우 프로세스 상태에 디버거가 부착되어 있고 디버거에 해당하는 프로세스의 식별자가 같이 기록됩니다. window와 linux의 경우 디버거의 pid가 기록됩니다. 디버거가 없으면 0이 적히고, 있으면 해당 디버거의 pid가 기록되는 방식입니다.

window는 PEB 영역에 기록되며, linux의 경우에는 /proc/self/status 파일에 기록됩니다.

우회 방법

IsDebuggerPresent를 우회하는 방법과 사실상 동일합니다.


중단점 명령어 탐지

원리

디버거에서 중단점을 거는 원리는 디버거가 타깃 프로세스의 중단점에 해당하는 주소의 명령어를 중단점 명령어로 패치하는 원리입니다.

이렇게 패치를 하게 되면 프로세스가 실행되다가 중단점 명령어를 실행하게 되면서 강제로 프로세스 실행을 멈춥니다. 중단점이 걸리고 나면 다시 원래대로 명령어를 복구하게 됩니다.

이를 이용하여 중단점이 걸린 상태에서는 로드된 코드 섹션 내부에 중단점 명령어가 존재하게 됩니다.

Intel x86 architecture의 경우에는 0xCC라는 1byte 기계어가 중단점 명령어입니다.

해당 기계어가 존재하는지 검색하여 현재 중단점이 걸려 있는지 판단하는 원리로 안티 디버깅이 작동합니다.

우회 방법

주로 중단점 검증을 우회하는 방향으로 패치하는 편입니다.


무결성 검사

원리

위에서 설명한 디버거의 중단점 설정 원리를 생각해 보면, 바이너리를 패치한다고 되어 있습니다.

중단점을 걸면서 바이너리를 패치 → 바이너리가 변조됨 → 무결성 검사에서 잡힘 이러한 논리로 무결성 검사가 anti-debugging 트릭으로 작동할 수 있습니다.

우회 방법

주로 검증 루틴을 패치합니다.


Self-Debugging

원리

가장 현대적인 방식의 anti-debugging 루틴 중 하나입니다.

대부분의 OS는 프로세스 하나 당 하나의 디버거만 부착할 수 있도록 되어 있습니다. 이는 실제로 디버거를 위한 프로세스 간 통신 파이프가 하나 밖에 존재하지 않기 때문에 특정 디버거가 이 파이프를 점유해 버리면 다른 디버거가 프로세스와 통신할 수단이 없어지게 되면서 디버깅이 불가능해지게 됩니다.

이를 이용하여 본인이 본인을 디버깅하는 Self-Debugging이나 믿을 수 있는 프로그램을 하나 더 개발하여 보호하고자 하는 프로세스를 디버깅하는 형태로 anti-debugging을 수행합니다.

이렇게 해서 타 디버거보다 먼저 부착에 성공하면 더 이상 다른 프로세스가 디버깅 할 수 없는 상태가 되어 버리게 되고, 다른 디버거가 먼저 부착에 성공하더라도 debugging 성공 여부를 판별할 수 있으므로 디버깅 시도가 실패하게 되면 다른 디버거가 부착되어 있다고 판단할 수 있습니다.

이제 ptrace를 여러 번 호출하더라도 최초 1회만 0을 반환하고 이후에는 -1을 반환하는 이유를 이해하실 수 있을 겁니다.

우회 방법

  1. ptrace를 우회한 것처럼 디버거로 return 값을 변조합니다.
  2. 바이너리를 패치하여 Self-Debugging 루틴을 아예 지워버리는 것이 유효합니다.
  3. 후킹도 좋은 방법 중 하나입니다.

발전된 Self-Debugging

위 우회 방법으로 우회하는 것을 방지하기 위하여 디버깅의 작업이 실제 동작에 영향을 미치게끔 구현할 수 있습니다.

위 우회 방법으로 우회할 시에 본래의 디버깅 루틴은 아예 수행할 수 없습니다. 이 점을 이용하여 디버깅 루틴에서 실제로 유효한 작업을 수행하도록 하면 anti-debugging 우회 시에 실제 바이너리의 동작도 수행하지 않게끔 되어 버려서 분석을 방해할 수 있습니다.

일반적으로 완전한 Self-Debugging 말고 디버거-보호할 프로세스를 같이 개발한 경우나 fork 등으로 프로세스를 여러 개 만든 경우에서 실제 프로세스 간 통신을 디버깅 pipe로 수행하는 경우에 이와 같은 안티 디버깅을 수행합니다.

우회 방법 2

  1. 디버거 역할을 대신할 spyware를 작성하고 이 spyware에서 원하는 디버깅 작업을 수행하도록 개발합니다. 디버거 대신에 붙일 프로그램을 개발하여 개발한 프로그램을 대신 붙이는 방법으로, 이는 디버거를 그대로 구현하고 거기다가 원하는 디버깅 작업까지 같이 구현해야 하므로 효율 이 잘 안 나오고 난이도가 굉장히 높은 우회 방법입니다
  2. HyperVisor 기반 디버깅

    VM 등의 Hypervisor를 이용하면 디버깅이 가능합니다. 이론상 Hypervisor 내부의 상태는 Hypervisor 외부에서 볼 수 있습니다(커널 디버깅을 Hypervisor로 합니다). 일반적인 디버거와 동일한 원리로 이를 수행하기도 하지만(커널 디버깅이 주로 이 원리입니다), Hypervisor 자체가 내부 상황을 알고 있으므로 Hypervisor에서 내부 상황을 볼 수 있는 기능을 제공해 주기도 합니다. 이는 debugging pipe를 이용하여 프로세스 상황을 전달하는 것이 아니므로 디버깅 파이프가 이미 점유 되어 있다고 하더라도 프로세스를 유사 디버깅하는 것이 가능합니다.

    Window의 경우 Cheat Engine - DBVM이 있고, Linux의 경우 qemu가 가능한 것으로 알고 있습니다.