Anti Debugging

Kail-KM
|2015. 9. 3. 03:32

Windows Debugger Detection


악성코드는 자신이 디버거에 의해 동작되고 있다는 것을 알아내기 위한 방법으로 윈도우 API를 사용하는 방법과 디버깅하는 동안에 발견되는 특징을 찾기 위해 메모리 구조를 수동으로 검사하거나 디버거가 남기는 잔여물을 찾기 위해 시스템을 검색하는 등 다양한 기법을 사용한다. 디버거 탐지는 악성코드가 수행하는 안티디버깅 방법중 가장 일반적이다.


Using the Windows API

윈도우 API 함수 사용은 가장 명백한 안티 디버깅 기법이다. 윈도우 API는 프로그램 자신이 디버깅 중인지 확인할 수 있는 몇개의 함수를 제공한다. 이러한 함수 중 일부는 디버거 탐지를 위한 목적이지만, 다른 함수는 이와 다른 목적임에도 디버거를 탐지하는데 사용할 수 있다.

전형적으로 안티디버깅 API 함수 호출을 막기 위한 가장 쉬운방법은 실행중에 안티디버깅 API 함수들을 호출하지 않게 악성코드를 수동으로 조작하거나 올바른 경로를 취했는지 보장하기 위해 호출되는 플래그를 사전에 수정하는 방식이다. 다음과 같은 윈도우 API 함수들은 안티디버깅으로 사용할 수 있다.


· IsDebuggerPresent

디버거를 탐지하는 가장 간다한 API 함수는 IsDebuggerPresent다. 이 함수는 PEB 구조에서 IsDebugged 필드를 찾아 디버거 환경에서 동작 중이 아니라면 0을 반환하고, 디버거 환경에서 동작하고 있다면 0이 아닌 값을 반환한다.

*PEB는 FS 레지스터를 통해 접근이 가능하며 TEB의 0x30 이 PEB를 기리킨다. 따라서 FS:[0x30] 이라 되어있으면 PEB를 가리키고있다는 것이다.

· CheckRemoteDebuggerPresent

이 함수는 위의 함수와 거의 동일하며 이 함수 또한 PEB 구조에 있는 IsDebugged 필드를 확인하지만, 자기 자신이나 로컬 컴퓨터의 다른 프로세스에 대해서만 검사할 수 있다. 이 함수는 파라미터로 프로세스 핸들을 받아 프로세스가 디버거 환경에서 구동 여부를 확인한다. 단순히 CheckRemoteDebuggerPresent는 프로세스에 핸들을 저날해 프로세스를 확인하는데 사용한다.

· tQueryInformationProcess

이 함수는 주어진 프로세스에 관한 정보를 검색할 수 있는 Ntdll.dll 내의 네이티브 API 함수이다. 첫번째 파라미터는 프로세스 핸들이고, 두번째는 검색하고자 하는 프로세스 정보의 타입을 함수에 알려주는데 사용한다. 두번째 파라미터가 ProcessDebugPort(0x7)의 값이라면 첫번째 파라미터에 해당하는 프로세스가 지금 디버깅 중인지를 알려준다. 프로세스가 디버깅 중이 아니라면 0을 반환할 것이고, 그렇지 않다면 디버거 포트 번호를 반환할 것이다.

· OutputDebugString

이 함수는 디버거에 출력할 문자열을 전달하는데 사용되는데 이를 디버거의 존재를 탐지하기 위해 사용할 수 있다. 아래의 코드를 보면 SetLastError를 이용해 현재 에러코드를 임의의 값을 설정한다. OutputDebugString을 호출할때 디버거에 붙어있지 않아 실패할 경우 이 함수는 에러코드를 설정하므로 GetLastError 함수는 더 이상 임의의 값을 갖지 않는다. 하지만 디버거환경에서 OutputDebugString 함수를 호출하면 OuputDebugString 함수는 성공하고 GetLastError 함수 값은 변하지 않는다.


Manually Checking Structures

윈도우 API 사용이 디버거의 존재를 탐지하는 가장 명확한 방법이지만, 악성코드 제작자는 가장 일반적으로 구조체를 수동으로 검사하는 방식을 사용한다. 수동 검사를 수행함에 있어 PEB 구조체 안에 있는 일부 플래그에서 디버거의 존재에 관한 정보를 제공한다. 이에 대하여 알아보자.

· BeingDebugged 플래그 검사

아래의 그림과 같이 운영체제는 실행 중인 각 프로세스에 대해 윈도우 PEB 구조체를 관리한다. 프로세스와 관련한 모든 사용자 모드 파라미터를 갖고 있다. 이 파라미터는 환경 변수 값, 로드된 모듈 목록, 메모리 주소, 그리고 디버거 상태같은 프로세스의 환경 데이터를 포함한다.

프로세스 실행 중에 PEB 위치는 FS:[0x30]에서 참조할 수 있다. 악성코드는 안티디버깅을 수행하기 위해 특정 프로세스의 디버깅 여부를 알려주는 BeingDebugged 플래그 값을 확인하는 위치를 사용한다. 아래의 그림은 해당 검사를 수행하는 두가지 예이다.

좌측의 코드는 PEB 위치를 EAX 로 이동시키고 이 오프셋에 2를 더하여 EBX로 이동시키는데 바로 이 위치가 BeingDebugged 플래그 위치의 PEB 오프셋에 해당한다. 그 후 EBX가 0인지를 확인하는 것으로 EBX가 0이면, 즉 디버거가 동작하고 있지 않다면 디버거가 발견되지 않았음을 알고 NoDebuggerDetected로 이동한다. 우측의 코드는 push와 pop 명령어를 조합해 PEB 위치를 EDX로 이동한 후 EDX에서 오프셋 2에 있는 BeingDebugged 플래그를 1과 직접 비교한다. 

이 확인 과정은 다양한 형태를 취할 수 있지만, 궁극적으로 조건부 점프가 코드 경로를 결정한다. 이 문제를 해결하려면 다음과 같은 처리 방법 중 하나를 선택할 수 있다.

- 점프 명령어가 실행하기 직전에 ZF를 직접 수정해 점프를 취하거나 취하지 않게 강제한다.

- BeingDebugged 플래그를 직접 0으로 변경한다.


· ProcessHeap 플래그 검사

로더는 ProcessHeap으로 알려진 Reverse4 배열 내에 문서화하지 않은 위치를 할당된 프로세스의 첫번째 힙의 위치로 설정한다. ProcessHeap은 PEB 구조체의 0x18에 위치한다. 첫번째 힙은 디버거 내에서 힙의 생성 여부를 커널에게 알려주는 필드가 있는 헤더를 포함한다.

* 윈도우 버전에 따라 다른 오프셋에 위치할 수 있으며 값 또한 다르게 설정되어 있다.

이 기법을 극복하는 가장 좋은 방법은 ProcessHeap 플래그를 수동으로 변경하거나 사용하는 디버거의 플러그인을 사용하는 것이다.


· NTGlobalFlag 검사

프로세스를 디버거에서 시작할 때 약간 다르게 동작하므로 메모리 힙을 다르게 메모리 힙을 다르게 생성한다. 힙 구조체 생성을 결정할 때 사용하는 시스템 정보는 PEB의 0x68 오프셋에 있는 문서화하지 않은 위치에 저장된다. 이 위치의 값이 0x70 이라면 프로세스가 디버거에서 동작중이라는 사실을 알 수 있다.

0x70의 값은 디버거가 힙을 생성할 때 다음과 같은 플래그를 조합한다. 프로세스를 디버거 내에서 실행하면 프로세스는 이 플래그를 설정한다. 아래의 그림은 이 값을 확인하는 어셈블리 코드이다.

이 기법을 우회하는 가장 쉬운 방법은 수동으로 해당 플래그를 변경하거나 사용중인 디버거의 플러그인을 사용하는 방식이다.


Checking for System Residue

악성코드를 분석할 때 디버깅 도구를 사용하는데, 일반적으로 시스템에 흔적을 남긴다. 악성코드는 자신이 분석 중인지 여부를 확인하기 위해 디버거를 참조하는 레지스트리를 검색하는 방식으로 그 흔적을 찾을 수 있다. 아래 레지스트리 경로는 디버거가 사용하는 일반적인 위치다.

이 레지스트리 키는 애플리케이션에 에러가 발생할 때 활성화할 디버거를 지정한다. 기본적으로 Dr.Watson을 설정하지만 OllyDbg같은 디버거로 변경했다면 악성코드는 분석 중이라고 판단한다. 아래는 내 PC의 지정되어 있는 디버거가 vsjitdebugger.exe로 지정되어있는 것을 확인할 수가 있다.

악성코드를 분석하는 동안 악성코드는 일반적인 디버거 프로그램 실행 파일과 같은 시스템 파일과 폴더를 검색할 수도 있다. 또는 악성코드는 동작 중인 메모리에서 현재 프로세스 목록을 보거나 더 일반적으로 아래와 같이 FindWindow 함수를 통해 디버거 검색을 수행하는 등 흔적을 탐지한다.





Identifying Debugger Behavior


악성코드 분석가가 리버싱을 하기 위해 디버거는 BP를 설정하거나 프로세스를 단계별로 실행할 수 있다. 하지만 디버거에서 이런 작업을 수행할 때 프로세스 내의 코드를 수정한다. 일부 안티 디버깅 기법은 악성코드가 INT 스캐닝, 체크섬 검사, 시간 검사 같은 형태의 디버거 동작을 탐지하는데 사용한다.


INT Scanning

INT 3은 디버거가 사용하는 소프트웨어 인터럽트로 동작하는 프로그램 명령어를 INT 3로 임시 변경해 디버거 예외 핸들러를 호출하는데, 이는 BP를 설정하는 기본 메커니즘이다. INT 3의 OPCODE는 0xCC이다. 다시 말해 디버거를 이용해 브레이크 포인트를 설정할 때마다 디버거는 0xCC를 삽입함으로써 브레이크 포인트를 삽입해서 코드를 변경한다.

특정 INT 3 명령어뿐만 아니라 INT immediate는 3을 포함해 임의의 인터럽트를 설정할 수 있다. 1바이트인 INT 3과는 다르게 이는 0xCD 0xValue 와 같이 2바이트로 사용한다. 일반적인 안티 디버깅기법은 아래와 같이 0xCC 옵코드를 검색해서 자신의 코드를 INT 3으로 변경했는지 프로세스를 스캔하는 방식이다.

호출 명령어로 시작한 코드 다음 pop 명령어가 EIP를 EDI로 저장한다. 그러면 EDI는 다음 코드의 시작으로 조정된다. 그 코드는 0xCC 바이트를 검색한다. 0xCC 바이트를 발견하면 디버거가 존재한다는 사실을 알게된다. 이 기법은 소프트웨어 BP 대신 HardWare BP를 설정해 우회할 수 있다.


Performing Code Checksums

인터럽트 스캔과 동일한 목적으로 악성코드는 코드 섹션의 Checksum을 계산할 수 있다. 이는 0xCC를 검색하는 대신 간단히 순환 중복 검사(CRC)나 악성코드 OPCode MD5 체크섬을 계산한다. 이 기법 또한 하드웨어 BP를 설정해 우회하거나 디버거에서 실행 경로를 수동으로 변경해 우회할 수 있다. 아래의 사진은 ADD를 통하여 명령어들의 OPcode의 합을 계산하여 CMP를 통해 비교 후 분기 하는 것을 확인할 수가 있다.


Timing Checks

프로세스를 디버깅할 때는 그렇지 않을 때보다 훨씬 느려지기 때문에 timeing checks 방식은 악성코드가 디버거를 탐지할 때 사용하는 가장 대중적인 방법 중 하나다. 디버거 탐지에 사용하는 시간 검사에는 다음과 같은 몇가지 방식이 있다.

- 몇가지 동작 수행에 대해 타임스탬프를 기록한 후 다른 타임스탬프를 가져와 두개의 타임스탬프를 비교한다. 지연차가 있다면 디버거가 존재한다고 가정할 수 있다.

- 예외가 발생하기 전후의 타임스탬프를 가져온다. 프로세스가 디버그 중이 아니라면 신속하게 예외를 처리하지만, 디버거가 예외를 처리하는 경우 훨씬 느려질 것이다. 기본적으로 대다수 디버거는 예외 처리에 사람이 개입하게 해서 상당한 지연을 발생시킨다. 많은 디버거는 이를 위해 예외를 무시하거나 그냥 지나치게할 수 있는 기능을 제공하지만 이런 경우에도 꽤 긴 지연이 발생한다.


· rdtsc 명령어 사용

가장 일반적인 시간 검사 방법은 rdtsc 명령어(OPCode:0x0F31)를 사용한다. rdtsc 명령어는 가장 최근에 시스템이 리부팅한 이후 흐른 시간 값을 저장한 64 비트를 반환한다. 악성코드는 간단히 이 명령어를 두 번 실행하고 두 값을 비교한다.

위의 그림을 보면 rdtsc를 호출하여 값을 ecx에 저장하고 다시 한번 호출하여 두 값을 비교하는 것을 볼 수가 있다. 만약 그 값의 차가 크지 않을 경우 디버거에게 탐지되지 않았을 때의 명령어를 수행하게 되며 만약 값의 차가 특정한 정도를 넘으면 다시 한번 rdtsc를 호출하여 이 임의의 주소로 ret하게 되는 것을 확인할 수가 있다.

· QueryPerformanceCounter와 GetTickCount 사용

두개의 윈도우 API 함수는 rdtsc와 같이 시간차를 통해 안티디버깅을 수행한다. QueryPerformanceCounter는 비교에 사용할 시간차를 가져올 때 카운터 질의를 두번 호출 한다. 두 호출 사이의 시간차가 너무 크다면 디버거를 사용중이라고 할 수 있다. GetTickCount 함수는 마지막으로 시스템을 재부팅한 이후 경과 시간을 밀리초 단위의 숫자로 반환한다. 아래는 GetTickCounter의 사용 방식이다.

앞서 언급한 모든 시간차 공격은 디버깅 중이나 함수를 두번 연속 호출한 후 비교하는 형태를 식별해서 정적 분석하는 도중에 발견할 수가 있다. 이 방식은 시간차 확인에 사용한 두 호출 중간에 BP를 설정하거나 단계별로 실행했을 때만 디버거에서 찾아낼 수 있다. 그러므로 시간차 탐지를 회피할 수 있는 가장 쉬운 방법은 이런 확인을 그냥 실행한 후 BP를 설정하고 다시 단계별로 디버깅한다.





Interfering with Debugger Functionality


악성코드는 일반 디버거 동작을 방해하는 TLS Callback, 예외 처리, 인터럽트 삽입 같은 여러가지 기법을 사용할 수가 있다. 이 기법은 디버거 통제하에서 프로그램 실행 무력화를 시도하는 방식으로 진행이 된다.


Using TLS Callback

일반적으로 대다수 디버거는 PE 헤더에서 정의한 프로그램 진입점에서 시작한다. 그러나 TLS(Thread Local Storage) CallBack은 진입점에서 실행하기 전에 코드를 실행할 때 사용할 수 있으므로 디버거 몰래 실행할 수 있다. 결국 디버거 사용에만 의존한다면 디버거 내에서 로드하자마자 TLS callback을 실행하므로 악성코드 기능을 놓칠 수 있다.

TLS는 데이터 객체가 자동 스택 변수가 아닌 윈도우 저장 클래스지만, 코드를 실행하는 각 스레드는 지역변수다. 기본적으로 TLS를 통해 각 스레드가 선언한 변수에 다른 값을 유지할 수 있다. 실행 파일에서 TLS를 구현할 때 일반적으로 아래의 그림과 같이 .tls 섹션에 위치해 있는 것을 확인할 수 있다. 윈도우는 정상적인 프로그램 코드 실행 전에 TLS CallBack 함수를 실행한다.


일반적인 프로그램들은 .tls 섹션을 사용하지 않기 때문에 .tls 섹션을 발견할 경우 가장 먼저 안티디버깅을 의심해야 한다. 위회의 방법으로는 OllyDbg의 경우 디버깅 옵션에서 Event 항목을 보면 Make first pause at: 이 존재하는 것을 확인할 수가 있다. 여기서 우린 System breakpoint로 설정을 해주면 TLS Callback이 실행되지 않고 그 전에 멈추므로 분석이 가능해진다.


Using Exceptions

예외 처리는 디버거를 방해하거나 탐지하는데 사용할 수 있다. 대다수 예외 기반 탐지 방식은 디버거가 예외 상황 발생후의 처리를 위해 디버깅중인 프로세스에게 즉각적으로 전달하지 못한다는 사실에 기반을 둔다. 대부분 디버거의 기본설정은 예외를 처리하기 위해 예외를 프로그램에 전달하지 않게 설정한다. 디버거가 즉각적으로 프로세스에게 예외를 전달하지 않는다면 프로세스 예외 처리 메커니즘이 해당 오류를 탐지할 수 있다. 

* SEH는 FS : [0x0]에서 참고한다.


Inserting Interrupts

안티디버깅의 전형적인 형태는 예외를 발생시켜 분석가를 귀찮게 하거나 유효한 명령어 중간에 인터럽트를 삽입해서 정상적인 프로그램 실행을 방해한다. 디버거 설정에 따라 다르겠지만, 인터럽트 삽입은 디버거가 자체적인 소프트웨어 BP 설정과 동일한 메커니즘이기 때문에 이를 통해 디버거를 중단시킬 수 있다.

· INT 3 삽입

디버거는 INT 3을 이용해 소프트웨어 BP를 설정하므로 디버거가 해당 OPcode를 BP라 생각하게 유효한 코드 섹션에 0xCC OPcoe를 삽입하는 방식으로 안티디버깅을 구성한다. 2 바이트 옵코드 0xCD03 시퀀스 또한 INT 3 생성에 사용한다. 이는 WinDBG를 방해할 목적으로 악성코드가 자주 사용하는 유효한 방식이다. 디버거 외부에서 0xCD03은 STATUS_BREAKPOINT 예외를 발생시킨다. 그러나 WinDBG 내부에서 일반적으로 BP는 OPcode가 0xCC이므로 BP가 걸리면 그냥 EIP를 정확히 1바이트 증가시킨다. 이는 WinDG로 프로그램을 디버깅 중일때와 정상적으로 실행할 때 서로 다른 명령어 집합을 실행하게 한다.

· INT 2D 삽입

INT 2D 안티 디버깅 기법은 INT 3과 동일한 기능을 하며, INT 0x2D 명령어는 커널 디버거 접근에 사용한다. INT 0x2D는 커널 디버거가 BP를 설정하는 방식이다.

· ICE 삽입

Intel에서 문서화하지 않은 명령어 중 하나로 ICE(In-Circuit Emulator) BP인 icebp(OPcode:0xF1)가 있다. ICE를 이용해 임의의 BP를 설정하는 것이 어렵기 때문에 ICE에서 디버깅이 용이하게 icebp를 설계했다. icebp 명령어를 실행하면 싱글 스텝 예외가 발생한다. 싱글 스텝을 통해 프로그램을 추적하는 경우 디버거는 싱글 스텝이 생성한 일반적인 예외라고 여기고 이전에 설정한 예외처리를 실행하지 않는다. 악성코드는 정상적인 실행 흐름을 위해 예외 핸들러를 이용해서 이를 악용할 수 있는데, 이 경우 문제가 발생할 수 있다. 이런 기법을 우회하려면 icebp 명령어 실행 시 싱글 스텝을 사용하지 않으면 된다.



Debugger Vulnerabilities


모든 소프트웨어와 같이 디버거 자체에도 취약점을 갖고 있으므로 때때로 악성코드 제작자는 디버깅을 방지할 목적으로 이 취약점을 공격한다. OllyDbg가 PE 포맷을 처리하는 방식에서 잘 알려진 일부 취약점을 소개한다.

PE 헤더 취약점

이 기법은 OllyDBG가 실행 파일을 로드할 때 크래시되게 MS의 실행 가능 바이너리의 PE 헤더를 변조하는 방식이다. 이는 Ollydbg가 PE 헤더와 관련된 사항을 지나치게 엄격하게 따르는데 있다. IMAGE_OPTIONAL_HEADER로 알려진 전형적인 구조가 있다. 아래의 그림을 보자.

이 구조체에서 마지막 일부 요소를 주목해보자. NumberOfRvaAndSizes 필드는 바로 뒤따라오는 DataDirectory 배열의 Entry 개수를 나타낸다. DataDirectory 배열은 파일에서 다른 중요한 실행 요소를 찾기 위한 장소를 가리키고 있다. 하지만 위의 그림에선 이 수를 0x99로 설정하므로 존재하는 16개의 수를 넘는다. OllyDBG는 표준을 따르고 있기 떄문에 반드시 NumberOfRcaAndSizes를 사용한다. 결론적으로 0x99와 같이 0x10 보다 많은 값으로 배열의 크기를 설정했다면 OllyDBG는 실행전에 사용자에게 팝업 윈도우를 생성한다.

이 기법을 가장 쉽게 우회하는 방법은 해당 값을 수정하거나 OllyDBG 2.0 을 사용하는 것이다. 위의 취약점은 ollydbg 1.0에 해당하는 것이다.


섹션 헤더에 관련된 또 다른 PE 헤더 문제점은 OllyDbg가 로드하는 동안 'File contains too much data' 라는 에러와 함께 크래시된다. 각 섹션에는 코드, 데이터, 리소스와 기타 파일 정보가 담겨있다. 각 섹션은 IMAGE_SECTION_HEADER 구조체 형식으로 헤더를 갖고 있다. 위 그림이 이 구조체의 일부이다.

VirtualSize와 SizeOfRawData에 주목하자. 윈도우 로더는 메모리 내의 섹션 데이터를 매핑시키기 위해 더 작은 VirtualSize와 SizeOfRawData를 사용한다. SizeOfRawData가 VirtualSize보다 크다면 VirtualSize 데이터를 메모리에 복사하지만 나머지는 무시한다. 여기서 OllyDBG는 SizeOfRawData만을 사용하기 때문에 SizeOfRawData를 0x77777777처럼 큰 값으로 설정하면 OllyDbg가 크래시된다.

이러한 안티디버깅 기법을 우회하는 가장 손쉬운 방법은 수동으로 PE 헤더를 수정하거나 16진수 데이터를 사용해서 SizeOfRawData를 VirtualSize에 가까운 값으로 변경한다.



Conclusion


대중적인 안티디버깅 기법을 보았으며 이러한 기법을 익히고 인지해서 우회하기까지 인내가 필요하다. 대다수 안티디버깅 기법은 프로세스를 천천히 디버깅하는 과정 중에 상식선에서 발견할 수 있다. 대다수 대중적인 안티디버깅 기법은 FS:[0x30]에 접근해 윈도우 API를 호출하거나 시간을 검사하는 기법을 자주 사용한다.

물론 모든 악성코드 분석과 마찬가지로 안티디버깅 기법을 무산시키는 가장 좋은 방법은 계속해서 악성코드를 공부하고 역공학해보는 것이다.



요약


IsDebuggerPresent FS:[30] + 2    CheckRemoteDebuggerPresent FS:[30]+2    tQueryInformationProcess     OutputDebugString    FindWindow

FS:[30]+2    FS:[30]+18    FS:[30]+68

INT3 : 0xCC    0xCD    Checksum    rdtsc    QueryPerformanceCouter    GetTickCount    TLS    SEH : FS:[0]

INT 3    INT 2D    ICE

'Reversing > Theory' 카테고리의 다른 글

Contents of the TIB (32-bit Windows)  (0) 2015.09.04
Anti Virtual Machine  (0) 2015.09.03
Anti disassembly  (0) 2015.09.01
DLL Injection  (0) 2015.08.29
데이터 인코딩  (0) 2015.08.26