Software BP, Hardware BP를 탐지하는 안티 디버깅

멀웨어를 디버깅하다보면 간혹 소프트웨어 브레이크포인트(Software BreakPoint)나 하드웨어 브레이크포인트(Hardware BreakPoint)를 탐지하여 디버깅을 방해하는 경우가 있다. 각각에 대한 원리와 탐지를 우회하는 방법에 대해 알아보자.
Software BP
Software BP 는 의도적으로 인터럽션을 발생시키는 명령어를 뜻한다. 예를 들어 Software BP 중 하나인 INT3 명령어를 실행하면 EXCEPTION_BREAKPOINT (0x80000003) 이 발생하며, 디버거가 없는 경우에는 Exception Handler 가 이를 처리하고, 디버거가 있으면 디버거가 처리한다.
따라서 x64dbg 와 같은 디버거에서 Software BP 를 걸어 놓으면, 디버거에서는 표시되지 않지만 해당 위치의 명령어를 Software BP 로 변경한다는 뜻이다. 이는 디버깅 시 상당히 주의해야 할 부분인데, 만약 암호화된 데이터에 Software BP 를 걸어놓으면, 해당 위치의 값이 변하면서 복호화한 이후의 데이터가 변경되어 정상적으로 실행되지 않을 수 있다.
아래 이미지가 위에서 말한 상황을 보여준다. f_Decrypt_XOR
로 라벨링된 함수를 실행하면 리턴 직후의 코드들을 XOR 연산한다. 만약 해당 함수를 건너뛰기 위해 step-over 를 실행하거나, 리턴되는 주소에 Software BP를 걸고 Run 하면 복호화된 코드 결과가 잘못되어 Exception 이 발생할 수 있다.

이와 같은 상황에서는 Hardware BP 를 걸고 Run 하면 문제 없이 복호화된 코드를 실행할 수 있다.

Software BP 는 INT3 명령어 뿐만 있는 것은 아니다. x64dbg 는 기본적으로 Software BP 로 INT3(0xCC)를 사용하고 있으나, 설정을 통해 Long INT3(0xCD 0x03), UD2(0x0F 0x0B)로 변경할 수 있다.

멀웨어가 Software BP 를 탐지하는 방법은 간단하다. 특정 메모리의 값을 읽어 값이 Software BP 에 해당하는 명령어인지 검사하는 것이다. 하지만 멀웨어 입장에서는 분석가가 어느 위치에 Software BP 를 걸을 지 예상하기 어렵기 때문에, 분석가가 관심을 가질만한 WinAPI 의 코드 일부를 검사하는 등의 방법을 사용한다.
아래 멀웨어는 CreateProcessInternalW()
API를 호출하기 전, 시작 1~2바이트를 읽어 Software BP 가 걸려있는지 검사한다.

CreateProcessInternalW()
내 Software BP 를 탐지하는 코드Hardware BP
Software BP 가 실행 코드를 이용하여 인터럽트를 발생시키는 방식이라면, Hardware BP 는 CPU 레벨에서 지원하는 기능을 이용하여 인터럽트를 발생시키는 방식이다.
CPU 는 일반적으로 디버그 레지스터(DR0 ~ DR7)을 가지며, 이중 4개(DR0 ~ DR3)이 Hardware BP 의 주소를 저장하는 용도로 사용된다. 이에 따라 Hardware BP 는 최대 4개까지 설정할 수 있다. 또한 DR6 은 활성화된 BP 에 대한 정보를, DR7 은 BP 의 활성화 모드와 관련된 정보를 가지므로, 만약 Hardware BP 가 하나라도 걸려 있으면 해당 레지스터들에는 0이 아닌 값이 저장된다.

아래 코드는 winnt.h
에 정의된 x86 아키텍처의 PCONTEXT
구조체를 보여준다.
typedef struct DECLSPEC_NOINITALL _CONTEXT {
//
// The flags values within this flag control the contents of
// a CONTEXT record.
//
// If the context record is used as an input parameter, then
// for each portion of the context record controlled by a flag
// whose value is set, it is assumed that that portion of the
// context record contains valid context. If the context record
// is being used to modify a threads context, then only that
// portion of the threads context will be modified.
//
// If the context record is used as an IN OUT parameter to capture
// the context of a thread, then only those portions of the thread's
// context corresponding to set flags will be returned.
//
// The context record is never used as an OUT only parameter.
//
DWORD ContextFlags;
//
// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
// set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT
// included in CONTEXT_FULL.
//
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
//
FLOATING_SAVE_AREA FloatSave;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_SEGMENTS.
//
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_INTEGER.
//
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_CONTROL.
//
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;
//
// This section is specified/returned if the ContextFlags word
// contains the flag CONTEXT_EXTENDED_REGISTERS.
// The format and contexts are processor specific
//
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
typedef CONTEXT *PCONTEXT;
이러한 특징을 가지기 때문에 Hardware BP 는 앞서 Software BP 에서 발생하는 암호화 데이터 문제를 피할 수 있으나, 멀웨어의 탐지는 피할 수 없다. GetThreadContext()
API 는 현재 쓰레드와 관련된 데이터를 읽을 수 있고, 이 중에는 Hardware BP 와 연관된 DR0 ~ DR3 레지스터가 포함되어 있다. 따라서 멀웨어는 주기적으로 GetThreadContext()
를 호출, Hardware BP 와 연관된 데이터 값이 0인지 검사하여 브레이크 포인트의 존재 여부를 알 수 있다. DR6, DR7 도 Hardware BP 의 상태와 관련된 값을 가지기 때문에 탐지에 이용할 수 있다.

NtGetContextThread()
를 통해 Hardware BP 설정 여부를 검사하는 코드