안티 디버깅(Anti-debugging, 디버거 탐지) – BeingDebugged (시스템 구조)

 

How to debugging detect?

프로그램 역분석, 즉 프로그램 디버깅 방법을 배웠다면, 해당 프로그램의 코드를 그대로 보는 것과 같아서, 숙달되면 프로그램의 모든 코드를 손쉽게 확인할 수 있다. 이를 통해 개발자가 원하지 않았던 방식으로 처리 흐름을 바꾸거나 데이터를 바꿔 치기 하는 등 악용할 수 있는 소지가 많다.

이를 방지하기 위해 개발된 기술이 바로 안티 디버깅으로, 말 그대로 디버깅 방지를 위한 기술들이라고 할 수 있다. 디버깅 자체를 막는 기술은 많이 개발되었으나, 이에 맞춰 안티-안티 디버깅 역시 함께 발전하면서 서로 뚫고 막고를 반복하고 있는 상황이다.

그리고 안티 디버깅 방법들은 대부분 이미 여러 포럼에서 공개해 놓은 상태이다. 아래는 시만텍에서 공개한 안티 디버깅 코드 참조용 내용이니 학습 전에 먼저 들러보기 바란다.

http://www.symantec.com/connect/articles/windows-anti-debug-reference

이외에도 많은 사이트에서 안티 디버깅과 관련된 참조 문서들을 많이 확인할 수 있는 만큼 안티 디버깅은 프로그램 보안 기술 중 기본 중 기본이라 할 수 있다(실제 대부분의 안티 디버깅 역시 방어 방법이 이미 나와 있다).

여기서는 안티 디버깅 중 유용한 몇 가지 방법에 대해 소개하여, 여러분이 안티 디버깅이라는 기술의 시각을 넓힐 수 있는 기본 지식을 다질 수 있도록 원리와 기법을 여러분들에게 소개하는 데 목적을 두고 진행하도록 하겠다.

디버거 탐지는 사용자가 프로그램을 분석하기 위해서 디버깅을 진행하는지에 대해 탐지하여 분석을 할 수 없도록 프로그램을 오동작하도록 하거나 중지, 함정에 빠뜨리는 방법이다.

이렇게 컴파일한 기계어인 프로그램의 보호가 필요한 이유는 3부에서 배웠듯이 역분석을 진행하면, 해당 프로그램의 동작 상황을 소설책을 읽어 내려가듯 분석하게 되면, 원하는 문장을 고쳐, 처리 흐름을 바꿔놓거나, 해당 프로그램의 목적과 반대로 동작하도록 수정할 수도 있다.

따라서 디버깅을 진행하는 프로그램에 대한 제재를 진행해야 하는데, 이를 실행하기 위해 프로그램 자체에 디버깅을 방지하는 보호 코드를 삽입하여야 한다. 이러한 안티 디버깅 코드들은 유명 개발 커뮤니티에도 다수 공개되어 있으므로, 전체적인 내용을 진행하는 것에는 큰 의미 없으므로, 여기서 에서는 몇 가지 개념 이해를 돕기에 유용한 분석을 진행해보도록 하겠다.

BeingDebugged (시스템 구조)

IsDebugged 플래그를 조사하여 디버깅 여부를 검사하는 부분으로 앞서 2부에서 EPROCESS의 구조체에서 유저 모드에서 접근이 가능한 PEB 구조체가 있음을 언급한 바 있다. 그 정보 중 BeingDebugged를 어셈블리를 이용해 직접 확인하여 디버깅 중인지를 확인한다.

그럼 PEB 구조체의 해당 부분을 Windbg를 이용한 커널 디버깅을 통해 다시 확인해 보자.

kd> dt -b _PEB

ntdll!_PEB

+0x000 InheritedAddressSpace : UChar

+0x001 ReadImageFileExecOptions : UChar

+0x002 BeingDebugged : UChar 이값을 이용하여 디버깅 유무를 판단한다.

+0x003 SpareBool : UChar

+0x004 Mutant : Ptr32

+0x008 ImageBaseAddress : Ptr32

[실습] BeingDebugged를 이용해 디버깅 유무를 판단할 수 있다

운영체제에서 현재 디버깅중인 프로세스인 경우 BeingDebugged 구조체에 표시하도록 되어 있다. 그럼 코드를 살펴보자.

isdebugged.cpp

#include
<stdio.h>#include
<windows.h>char BeingDebugged = 1;

int Check()

{

// PEB 구조체에 접근하여 0x2값을 확인하고, 해당 값을 결과를 BeingDebuggedBit에 반환한다.

__asm {

MOV EAX,DWORD PTR FS:[0x30]

// PEB 주소에 0x2를 더한 것과 같다.

XOR EAX, 0x2

// EBX 레지스터 값을 초기화 한다.

SUB EBX, EBX

// EAX 레지스터를 XOR하여 BL에 저장한다.

XOR BL, [EAX]

MOV BeingDebuggedBit, BL

};

return(BeingDebuggedBit);

}

void main()

{

while(true)

{

Sleep(1000);

// IsDebugged 플래그 체크

Check();

if(BeingDebugged == 1)

{

printf(“Debugging Detected\n”);

// 임의의 주소에서 종료

exit(0x1EDF0105);

}

else

printf(“Safe\n”);

}

return;

}

[예제] BeingDebugged 체크 C++코드

그럼 주요 분기를 Ollydbg를 이용하여 분석을 진행해 보자(Windbg로 진행해도 되고, Visual C++ 자체로 진행하여도 된다). 만약 main() 처리 어셈블리 코드 확인이 어려우면 아래와 같이 프로그램명.main에 브레이크 포인트(F2키)를 설정하고 진행하면 된다(아래 그림과 같이 main을 쉽게 찾을 수 있는 이유는 Pdb(심볼) 파일이 있어, Ollydbg가 자동으로 관련 정보를 매칭하여 보여주기 때문이다. Pdb 파일이 있으면 이처럼 역분석이 수월해진다).


[그림] int main() 호출 코드

그럼 아래와 같은 코드를 만날 수 있다. 우리가 확인하고자 하는 Check() 호출하는 코드까지 확인하자.

012B13F0 I> 55 PUSH EBP

012B13F1 8BEC MOV EBP,ESP

012B13F3 81EC C0000000 SUB ESP,0C0

012B13F9 53 PUSH EBX

012B13FA 56 PUSH ESI

012B13FB 57 PUSH EDI

012B13FC 8DBD 40FFFFFF LEA EDI,DWORD PTR SS:[EBP-C0]

012B1402 B9 30000000 MOV ECX,30

012B1407 B8 CCCCCCCC MOV EAX,CCCCCCCC

012B140C F3:AB REP STOS DWORD PTR ES:[EDI]

012B140E B8 01000000 MOV EAX,1

012B1413 85C0 TEST EAX,EAX

012B1415 74 6B JE SHORT IsDebugg.012B1482

012B1417 8BF4 MOV ESI,ESP

012B1419 68 E8030000 PUSH 3E8

012B141E FF15 98812B01 CALL DWORD PTR DS:[<&KERNEL32.Sleep>] ; kernel32.Sleep Sleep(1000)

012B1424 3BF4 CMP ESI,ESP

012B1426 E8 06FDFFFF CALL IsDebugg.012B1131

012B142B E8 ACFCFFFF CALL IsDebugg.012B10DC Check()를 호출

012B1430 0FBE05 00702B01 MOVSX EAX,BYTE PTR DS:[BeingDebuggedBit]

012B1437 83F8 01 CMP EAX,1

012B143A 75 2D JNZ SHORT IsDebugg.012B1469 탐지 분기점

012B143C 8BF4 MOV ESI,ESP

012B143E 68 44572B01 PUSH IsDebugg.012B5744 ; ASCII “Debugging Detected\n”

012B1443 FF15 A8822B01 CALL DWORD PTR DS:[<&MSVCR100D.printf>] ; MSVCR100.printf

012B1449 83C4 04 ADD ESP,4

…중략

[실습] Check() 호출

CALL IsDebugg.012B10DC에 브레이크 포인트를 설정하고 프로그램을 실행하면 잠시 후 해당 위치에 프로그램이 일시 정지하게 된다. 이제 Step into(F7키)를 눌려 프로그래밍한 어셈블리 코드의 동작 상황을 살펴보자.

012B1390 I> 55 PUSH EBP

012B1391 8BEC MOV EBP,ESP

012B1393 81EC C0000000 SUB ESP,0C0

012B1399 53 PUSH EBX

012B139A 56 PUSH ESI

012B139B 57 PUSH EDI

012B139C 8DBD 40FFFFFF LEA EDI,DWORD PTR SS:[EBP-C0]

012B13A2 B9 30000000 MOV ECX,30

012B13A7 B8 CCCCCCCC MOV EAX,CCCCCCCC

012B13AC F3:AB REP STOS DWORD PTR ES:[EDI]

012B13AE 64:A1 30000000 MOV EAX,DWORD PTR FS:[30]


[그림] PEB 구조체 주소

012B13B4 83F0 02 XOR EAX,2


[그림] XOR를 통해 EAX주소에 2를 더한다

012B13B7 2BDB SUB EBX,EBX

012B13B9 3218 XOR BL,BYTE PTR DS:[EAX]

012B13BB 881D 00702B01 MOV BYTE PTR DS:[BeingDebuggedBit],BL

012B13C1 0FBE05 00702B01 MOVSX EAX,BYTE PTR DS:[BeingDebuggedBit]

012B13C8 5F POP EDI ; IsDebugg.012B1430

012B13C9 5E POP ESI ; IsDebugg.012B1430

012B13CA 5B POP EBX ; IsDebugg.012B1430

012B13CB 81C4 C0000000 ADD ESP,0C0

012B13D1 3BEC CMP EBP,ESP

012B13D3 E8 59FDFFFF CALL IsDebugg.012B1131

012B13D8 8BE5 MOV ESP,EBP

012B13DA 5D POP EBP ; IsDebugg.012B1430

012B13DB C3 RETN

[실습] Check() 어셈블리 내용

그럼 실제 FS[30]이 가리키는 위치는 바로 PEB 구조체의 포인터로 해당 포인터주소를 습득하고, XOR 명령을 통해 해당 주소에 2를 더하여 BeingDebugged 구조체로 주소값을 변경하게 된다. 그리고 해당 주소의 값을 BL 레지스터 저장한 후 이를 리턴 하게 된다.


[그림] 현재 디버깅 중으로 설정된다

이를 우회하는 방법도 간단히 만들 수 있다. 하나는 BL레지스터에 저장하는 값을 항상 0으로 변경하는 방법도 있고, 탐지 분기점을 JMP 구분으로 수정하여 디버깅을 탐지하여도 무조건 “Safe”가 나오도록 할 수도 있을 것이다.


[그림] 결과로 항상 0을 저장하게 된다

이외에도 여러 가지 무력화 방법이 있을 것이다. 찾아서 적용해 보는 재미도 있으니 한번 시도해보기 바란다.

이와 비슷한 방식으로 GetNtGlobalFlags와 GetHeapFlags를 체크하는 방법도 있으니, 확인해 보기 바란다.

운영체제 버전 확인

윈도우는 운영체제 버전마다 설정된 값이나 프로세스 내부 구조체가 조금씩 달라진다.

따라서 버전을 구분하여 프로그램을 실행할 필요가 있다. 이 때 이를 감지하고 운영체제 버전에 따라 실행 코드를 구분할 수 있도록 PEB의 운영체제 버전정보를 프로그램 개발시 이용할 수 있는 내장 함수를 제공하는데, 이를 알아보도록 하자.

osver.cpp

#include
<stdio.h>
#include
<windows.h>
int main()

{

OSVERSIONINFO ver;

ver.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);

GetVersionEx(&ver);

if (ver.dwMajorVersion == 6) // 윈도우 비스타 이상

{

printf(“윈도우 비스타 이상 버전 실행 코드를 여기에 넣으면 된다.\n”);

return 0;

}

else
// 윈도우2003 이하

{

printf(“윈도우2003 이하 버전 실행 코드를 여기에 넣으면 된다.\n”);

return 0;

}

}

[예제] 운영체제 버전을 판단하여 실행할 수 있다

비주얼 스튜디오를 이용한 OSVERSIONINFO를 디버깅 해보면 해당 구조체를 이용하여 확인할 수 있는 정보가 버전 이외에도 상당히 많다는 것을 알 수 있다. 그리고 이 버전 정보는 윈도우 버전을 구분하는 또 다른 버전 표기 방식으로, 현재 윈도우 비스타와 2008의 경우 6.0 버전으로 표기된다.


↓아래 지역 창을 통해 ver 구조체의 값을 확인해 보면 다양한 정보를 담고 있음을 알 수 있다.


[그림] 제품 버전 정보와 관련된 정보를 확인할 수 있다

dwMajoVersion, dwMinorVersion: 제품 구분 번호

dwBuildNumber: 빌드 번호로서, 버전 번호 다음에 위치하게 된다. 서비스 팩 등의 주요 운영체제 패치를 진행하면 변경된다.

dwPlatformId: 운영체제의 유형으로 현재는 크게 서버, 클라이언트로 구분되는데 추가될 가능성이 있다. 현재는 윈도우 2000 이후 버전인 경우 2로 표시된다.

szCSDVersion: 추가 버전 설명 코드로서, 서비스 팩 설치시 서비스 팩 정보가 표시된다(설치된 서비스 팩이 없으면, 정보는 표시되지 않는다).

그리고 각 운영체제별 제품 구분 번호는 다음 아래와 같다.

운영체제

버전 번호

Windows Server 2012

6.2

Windows 8

6.2

Windows Server 2008 R2

6.1

Windows Server 2008

6.0

Windows Vista

6.0

Windows Server 2003 R2

5.2

Windows Server 2003

5.2

Windows XP

5.1

Windows 2000

5.0

Windows Me

4.9

Windows 98

4.1

Windows NT 4.0

4.0

Windows 95

4.0

[표] 운영체제별 빌드 번호

Facebook Comments

Leave A Reply

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.