본문 바로가기

Reverse Engineering

안티 디버깅(Anti-Debugging)

안티 디버깅(Anti-Debugging) : 디버깅을 방지하고 분석을 하지 못하도록 하는 기술.
디버깅을 당한다면 해당 디버거 프로그램을 종료시키거나 에러를 발생시키는 방법등 다양한 방법을 사용하여 분석을 방해한다.

(참고 : 디버깅(Debugging) : 프로그램의 특정 부분에 Break Point를 설정한 후 실행을 하면 그 위치에 프로그램이 멈추게 되며 메모리에 값이 제대로 들어가 있는지, 코딩한 흐름대로 프로그램이 진행되는지 단계적으로 실행할 수 있다)

안티 디버깅 기술에는 수많은 방법들이 존재하며 계속 발전하고 있으며 이에 따라 이를 우회하는 기술도 계속적으로 발전하고 있다.

 CheckRemoteDebuggerPresent() Windows API
 Detecting Breakpoints by CRC
 Detecting SoftlCE by Opening Its Drivers
 UnhandledExceptionFilter
 Hardware Breakpoint Detection
 INT 2D Debugger Detecton
 IsDebuggerPresent() Direct PEB Access
 IsDebuggerPresent() Windows API
 LordPE Anti Dumping
 NtGlobalFlag Edbugger Detection
 Obfuscated RDTSC
 OllyDbg Filename Format String
 FindWindow
 OllyDbg Instruction Prefix Detection
 OllyDbg INT3 Exception Detection
 NtSetInformationThread
 Memory Breakpoint Detection
 NtQueryInformationProcess()
 OllyDbg OllyInvisible Detection
 OllyDbg OpenProcess() HideDebugger Detection

 OllyDbg OpenProcess() String Detection
 OllyDbg OutputDebugString() Format String Vulnerability
 OllyDbg PE Header Parsing DoS Vulnerabilities
 OllyDbg Registry Key Detection
 OutputDebugString on Win2K and WinXP
 PEB ProcessHeap Flag Debugger Detection
 PeID GenOEP Spoofing
 PeID OEP Signature Spoofing
 ProcDump PE Header Corruption
 RDG OEP Signature Spoofing
 RDTSC Instruction Debugger Latency Detection
 Ring3 Debugger Detection via LDR_MODULE
 Single Step Detection
 SoftIce Driver Detection
 SoftIce Registry Detection
 SoftIce WinICE.dat Detection
 TLS-CallBack +IsDebuggerPresent() Debugger Detection
 Using the CMPXCHG8B with the LOCK Prefix
 kernel32!CloseHandle and NtClose
 kernel-mode timers

 User-mode timers
 Timestamp counters
 Popf and the trap flag
 Stack Segment register
 Debug registers manipulation
 Context manipulation
 CC scanning
 EntryPoint RVA set to 0

(참조 : http://www.openrce.org/reference_library/anti_reversing)



1. IsDebuggerPresent() :

PEB 구조체의 디버깅 상태값을 확인하여 디버깅을 당하고 있다면 1을, 아닐경우 0을 리턴.
커널 모드 디버거는 탐지하지 못하며 kernel32.dll에서 export되는 함수이며 보통 프로그램의 보호 차원에서 쓰인다.

IsDebuggerPresent() 샘플 코드다. (Microsoft Visual Studio 6.0에서 작성)

#define _WIN32_WINNT 0x0500
#include <stdio.h>
#include <windows.h>

int main()
{
         while(1)
        {
                      Sleep(1000);
                      if(IsDebuggerPresent())
                                printf("디버깅 당함\n");
                     else
                               printf("정상\n");
        }

        return 0;
}


(tip. #define_WIN32_WINNT 0x0500은 어떤 함수를 사용할 때 해당 프로그램이 실행될 최소 OS를 지정해 주는 것이다. 넣지 않으면 해당 함수를 인식하지 못하게 된다 0x0500은
Windows 2000이다)

파일을 실행시켜보면 디버깅을 당하지 않았기 때문에 '정상'이라는 메시지를 출력한다.
이제 올리디버거로 Attach 하면은


위와 같이 '디버깅 당함' 메시지를 출력한다. 이제 IsDebuggerPresent()를 우회하는 방법을 생각해보자. (물론 OllyDbg Advenced 플러그인을 이용하면 자동 우회된다)

IsDebuggerPresent()함수는 디버깅 당하면 1을 리턴한다.
즉 Call IsDebuggerPresent() 이 구문이 실행된 후에 EAX 값은 1일 것이다. ( 함수의 리턴값은 EAX에 저장됨)
mov eax, 0 을 추가한다면 항상 정상으로 인식할 것이다.

IsDebuggerPresent()함수를 콜하는 부분을 찾아보자.
Search for -> All intermodular calls를 선택한다.
(그상태에서 IsDebuggerPresent()를 입력해보면 제목표시줄에 Find 되는것이 보일것이다.)

00401047 CALL DWORD PTR DS:[<&KERNEL32.IsDebuggerPresent>]
kernel32.IsDebuggerPresent

더블클릭하여 이동후 IsDebuggerPresent()에서 마우스 클릭 후 'Enter'를 누르면 함수의 내용을 볼 수 있다. (되돌아가는 것은 '-')

Call IsDebuggerPresent() 대신에 mov eax, 0 으로 변경한 후 실행을 하면


'정상' 메시지만 출력되는 것을 볼 수 있다.



2. IsDebugged :

kernel32!IsDebuggerPresent 함수는 PEB 구조체의 BeingDebugged 멤버의 값을 확인하여 디버깅을 당하고 있는지의 여부를 알려준다. 따라서 실제 정보를 주는 IsDebugged에 대해서 알아보자.

PEB(Process Environment Block)는 프로세스에 대한 정보를 담고 있는 구조체

PEB의 구조체

typedef struct _PEB {
       BOOLEAN InheritedAddressSpace;
       BOOLEAN ReadImageFileExecOptions;
       BOOLEAN BeingDebugged;
       BOOLEAN Spare;
       ...
       ULONG PostProcessInitRoutine;
       ULONG TlsExpansionBitmap;
       BYTE TlsExpansionBitmapBits[0x80];
       ULONG SessionId;
} PEB *PPEB;

IsDebugged 샘플 코드다. (Microsoft Visual Studio 6.0에서 작성)

#define _WIN32_WINNT 0x0500
#include <stdio.h>
#include <windows.h>

Int Check();

int main()
{
       while(1)
      {
               Sleep(1000);
               if(Check()==0xFFFF0001)
                           printf("디버깅 당함\n");
               else
                           printf("정상\n");
       }
       return 0;
}

int Check()
{
        _asm
       {
              mov eax, fs:[30h]
              mov eax, [eax+2]
              test eax, eax
       }
}

(tip. 인라인 어셈블러를 쓰는 목적은 프로그램의 최적화를 위해 쓰이고 프로그래밍 언어에서 지원하지 않거나 미흡한 부분을 어셈블리어를 이용해서 직접 데이터를 가져오거나 계산할 때 유용하게 쓰일 수 있다)

실행을 하면 1초(1000ms) 단위로 Check()함수가 실행이 되고 리턴 값이 0 이어서 '정상'메시지가 출력된다.

올리디버거(OllyDbg)로 열어서 실행시켜보면 '디버깅 당함'이라는 메시지가 출력된다.
IsDebugged 를 우회할 방법을 생각해보자. IsDebugged 는 BeingDebugged 값을 가져오게 된다. 이값이 1이냐 0이냐에 따라 가져올 값이 1이냐 0이냐가 되어 디버깅 여부를 판단하게 된다. 따라서 무조건 0을 가져온다면 우회를 할수 있을것이다.

자세히 분석하기 위해 Search for -> All intermodular calls 눌러 Sleep를 찾아 이동한다.
(IsDebugged는 검색되지 않습니다)

004010A0  |.  E8 5B000000   |CALL Sample.__chkesp
004010A5  |.  E8 5BFFFFFF   |CALL IsDebugged.00401005

함수의 내용들 'Enter'로 들여다 보니 Check()함수는 두번째 Call 부분이다.

내용을 보니

mov eax, fs:[30h]
mov eax, [eax+2]
test eax, eax

인라인 어셈으로 입력했던 부분이다.
함수의 시작 부분에 BreakPoint 를 걸어 Run을 하여 여기까지 실행시킨 후 덤프창에서 fs:[30]로 이동해보면 00 00 01 00 FF FF FF FF 가 보일것이다. 3번째 값 01 이므로 디버깅 당했다는걸 알 수 있다.(안당했을시 00) 한단계식 실행해보니 mov eax, [eax+2]부분에서 값을 가져온다. 따라서 이부분을 0을 가져오게 한다면 우회가 될것이다.
즉 mov eax, [eax+2] -> mov eax, 0


다시 실행해보면 '정상'메시지가 뜨는 것을 볼수가 있다.(이상태에서 restart 하면 수정한 부분이 지워진다는것을 간과하지 마세요 restart 하고 수정하고 실행하세요)



3. NtGlobalFlags :

NtGlobalFlag 값을 이용해서 디버깅 여부를 판단하는 방법이다.
해당 프로세스가 디버깅될 때 설정되는 Flag 중의 하나이다

PEB(Process Environment Block)의 0x68 위치에 있는 NtGlobalFlag 값이 0이면 정상 0이 아니라면 디버깅을 당한것이라고 볼 수 있다.

PEB의 구조체에서 0x68 위치에 있는 NtGlobalFlag

000 byte InheritedAddressSpace
001 byte ReadImageFileExecOptions
002 byte BeingDebugged
003 byte SpareBool
004 void *Mutant
008 void *ImageBaseAddress
...
05c void *OemCodePageData
060 void *UnicodeCaseTableData
064 uint32 NumberOfProcessors
068 uint32 NtGlobalFlag
070 union _LARGE_INTEGER CriticalSectionTimeout
...
1dc uint16 Length
1de uint16 MaximumLength
1e0 uint16 *Buffer

디버깅을 당하게 되면 0x70이 설정되는데 다음 FLAG 값들이 더해진 결과이다.

FLG_HEAP_ENABLE_TAIL_CHECK(Heap Tail Checking)                    0x10
FLG_HEAP_ENABLE_FREE_CHECK(Heap Free Checking)                  0x20
FLG_HEAP_VALIDATE_PARAMETERS(Heap Parameter Checking)      0x40

NtGlobalFlags 샘플 코드다. (Microsoft Visual Studio 6.0에서 작성)

#define _WIN32_WINT 0x0500
#include <stdio.h>
#include <windows.h>

int Check();

int main()
{
       while(1)
      {
               Sleep(1000);
               if(Check()!=0)
                           printf("디버깅 당함\n");
               else
                           printf("정상\n");
       }
       return 0;
}

int Check()
{
        _asm
       {
              mov eax, fs:[30h]
              mov eax, [eax+68h]
              and eax, 0x70
              test eax, eax
       }
}

우회할 방법을 생각해보자. 디버깅을 당하면 0x68 위치에 0x70 값이 들어간다. 따라서 이 값을
0으로 바꿔주면 디버깅 여부에 상관없이 '정상' 메시지를 출력할 것이다.

Search for -> All intermodular calls 클릭 후 Sleep를 더블클릭하여 이동한다.

004010A0  |.  E8 6B000000   |CALL NtGlobal.__chkesp
004010A5  |.  E8 5BFFFFFF   |CALL NtGlobal.00401005

아까 보았듯이 Check() 함수는 두번째 부분이다
'Enter'키로 내용을 들여다본다.

00401038  |.  64:A1 3000000>MOV EAX,DWORD PTR FS:[30]
0040103E  |.  8B40 68       MOV EAX,DWORD PTR DS:[EAX+68]
00401041  |.  83E0 70       AND EAX,70
00401044  |.  85C0          TEST EAX,EAX

덤프창에서 fs:[30]로 이동해보면 7FFD4000으로 가는데 여기에 0x68을 더한 7FFD4068을 입력하면 0x70이 들어있는것을 확인할 수 있다.


따라서 AND EAX, 70 -> mov eax, 0 으로 바꿔준다.

Run을 해보면 '정상' 메시지가 출력된다.



4. CheckRemoteDebuggerPresent() :

CheckRemoteDebuggerPresent ()는 Windows XP 이상부터 사용할 수 있고
ZwQueryInformationProcess()를 사용하여 프로세스의 DebugProt 정보를 얻게 되는데 리턴값이 0이면 정상이고 0이 아니면 디버깅 중이다.

BOOL CheckRemoteDebuggerPresent(
        HANDLE hProcess,
        PBOOL pbDebuggerPresent
);

CheckRemoteDebuggerPresent() 샘플 코드다. (Microsoft Visual Studio 2005에서 작성)
(Microsoft Visual Studio 6.0에서 작성해봤었는데 에러 뜨네요)

#define _WIN32_WINNT 0x0501   ( 0x0501 = XP)
#include <stdio.h>
#include <windows.h>

int main()
{
         BOOL bDebugged = FALSE;
         while(1)
         {
                   Sleep(1000);
                   CheckRemoteDebuggerPresent( GetCurrentProcess(), &bDebugged );
                   if(bDebugged)
                               printf("디버깅 당함\n");
                   else
                               printf("정상\n");
          }
         return 0 ;
}

역시 올리디버거로 열어보면 '디버깅 당함' 이라는 메시지가 출력된다.

우회할 방법을 생각해보면 bDebugged 값을 0으로 수정해주면 디버깅 여부에 상관없이
'정상'메시지가 출력 될것이다. 또 분기문에서 점프구문을 무조건 정상쪽으로 이동하게 흐름을 정해주면 될것이다. 이번엔 점프구문을 수정하여 우회를 해보겠다.

Search for -> All intermodular calls 을 눌러 이번엔 CheckRemoteDebuggerPresent()
를 더블클릭하여 이동한다.

004113F8    FF15 A0814100   CALL DWORD PTR DS:[<&KERNEL32.CheckRemot>; kernel32.CheckRemoteDebuggerPresent
004113FE    3BF4            CMP ESI,ESP
00411400    E8 31FDFFFF     CALL CheckRem.00411136
00411405    837D F8 00      CMP DWORD PTR SS:[EBP-8],0
00411409   /74 19           JE SHORT CheckRem.00411424

위의 소스코드와 비교해보면 [EBP-8] = bDebugged 이다. 또 이 값은
한단계씩 진행을 하며 확인해보면 CheckRemoteDebuggerPresent()의 리턴값
(즉, EAX값 = 1)이라는 것을 알 수 있다. 따라서 CMP DWORD PTR SS:[EBP-8],0 에서 같지 않으므로 JE 구문에서 점프를 하지 않아 '디버깅 당함' 메시지가 출력된다. JE 문을 JMP 문으로 바꾼다면 디버깅 여부와 상관없이 ( 리턴값에 상관없이) 무조건 '정상' 메시지가 출력될 것이다.

(tip. Hex 값으로 수정하려면 74(=JE)를 EB(=JMP)로 하면 된다)



5. FindWindow :

FindWindow() API는 특정 윈도우 이름이나 클래스 이름을 찾아주는 함수이다.
(tip. 특정 프로그램의 윈도우 이름이 Null로 되어 있는 경우가 있기 때문에 클래스 이름으로 찾는것이 좋다)

FindWinodw()
HWND FindWindow(
        LPCTSTR lpClassName,
        LPCTSTR lpWindowName
);

파일의 정보들을 나타내주는 프로그램을 이용하여 클래스 이름이나 윈도우 이름을 찾는다.
(참조 : Property Edit(http://www.mh-nexus.de))



'OllyDbg'가 있다면 '디버깅 당함' 메시지를 출력해주는 FindWindow 샘플 코드다
(Microsoft Visual Studio 6.0에서 작성)

#define _WIN32_WINNT 0x500
#include <stdio.h>
#include <windows.h>

int Check();

int main()
{
         while(1)
         {
                 Sleep(1000);
                 if(Check()!=0)
                          printf("디버깅 당함\n");
                 else
                          printf("정상\n");
          }
          return 0;
}

int Check()
{
         char debugger[] = "OLLYDBG";
         int ret;
         _asm
         {
                 push 0        // WindowName
                 lea eax, debugger
                 push eax    // ClassName
                 call dword ptr FindWindowA
                 mov ret, eax
        }
        return ret;
}

올리디버거를 실행하는 즉시 '디버깅 당함' 이라는 메시지가 출력된다.

우회할 방법을 생각해보자 FindWindow 함수는 윈도우 이름과 클래스 이름을 인자로 가지는데 여기서는 윈도우 이름을 0으로 넣었다.(즉 파일명 변경으로는 해결할 수 없다) 그렇다면 두번재 클래스 이름에 넣은 'OLLYDBG' 문자열을 다른 값으로 변경한다면 FindWindow가 올리디버거를 찾지 못 할것이다.

해당 구문을 찾기위해 Ultra String Reference 플러그인을 이용한다.

(Release 폴더에 ustrref.dll을 ollydbg폴더에 Plugin폴더로 옮긴다)

올리디버거로 FindWindow.exe를 Attach 한 후
메뉴에 Plugin -> Ultra String Reference -> Find ASCII 를 누르면
해당 프로그램에서 쓰이는 문자열들을 볼 수 있으며 여기서 '디버깅 당함' 문자열을 클릭하면 해당 문자열을 stack에 push하는 곳으로 이동한다. ('OLLYDBG'라는 문자열은 보이지 않는다)

00401048  |.  FF15 80514200 |CALL DWORD PTR DS:[<&KERNEL32.Sleep>]   ; \Sleep
0040104E  |.  3BF4          |CMP ESI,ESP
00401050  |.  E8 4B010000   |CALL FindWind.004011A0
00401055  |.  E8 ABFFFFFF   |CALL FindWind.00401005
0040105A  |.  85C0          |TEST EAX,EAX
0040105C  |.  74 0F         |JE SHORT FindWind.0040106D
0040105E  |.  68 24004200   |PUSH FindWind.00420024                  ; /디버깅 당함\n

JE 구문에 의해서 흐름이 갈라지며 그 위에 두번의 CALL 이 존재하는데 'Enter'내용을 분석해보면 두번째 CALL 구문이 우리가 찾는 FindWindow 함수 이다. 그 내용을 보면

004010D9  |.  6A 00         PUSH 0                                   ; /Title = NULL
004010DB  |.  8D85 F8FFFFFF LEA EAX,DWORD PTR SS:[EBP-8]             ; |
004010E1  |.  50            PUSH EAX                                 ; |Class
004010E2  |.  FF15 94524200 CALL DWORD PTR DS:[<&USER32.FindWindowA>>; \FindWindowA

PUSH 0은 윈도우 이름이며 PUSH EAX는 클래스 이름이다. 즉 EAX 에 문자열 'OLLYDBG'가 있을 것이다. 이 부분에 BreakPoint 를 설정하여 확인해보면 추측이 맞다는 걸 알 수 있다.

004010D9  |.  6A 00         PUSH 0                                   ; /Title = NULL
004010DB      6A 01         PUSH 1
004010DD      90            NOP
004010DE      90            NOP
004010DF      90            NOP
004010E0      90            NOP
004010E1      90            NOP
004010E2      FF15 94524200 CALL DWORD PTR DS:[<&USER32.FindWindowA>>;  USER32.FindWindowA

PUSH EAX 대신에 PUSH 1 으로 바꿔주면 클래스 이름이 1인 것을 찾게 되므로 FindWindow함수를 우회할 수 있다. 실행을 해보면 '정상' 메시지가 출력된다.
(주의! : 처음에 PUSH EAX 를 PUSH 1로 바꾸니 밑에 4줄이 NOP로 바뀌더군요. 그래서 윗줄에 LEA EAX,DWORD PTR SS:[EBP-8] 을 PUSH 1로 바꾸었는데 원래 밑에 줄에는 영향을 안주더군요. 그러고 밑에 PUSH EAX는 NOP 처리 해주었습니다)

(위의 5가지의 안티 디버깅 기술은 Olly_Advanced에 있는 옵션의 안티 디버깅 우회에서 체크만 해주면 자동으로 해결된다 - 연습을 위해서)