본문 바로가기

리버싱/x64dbg 디버거를 활용한 리버싱과 시스템 해킹의 원리

ch08 - 안티 리버싱 / WindDgb

728x90

01 디버거 탐지

02 난독화

 

안티 리버싱(Anti-Reversing) : 디버거를 통한 분석을 어렵게 하는 기술이다. 일반적으로 복제 방지나 디지털 저장권 보호를 위해 소프트웨어적으로 불법적인 접근을 막는 기술을 말한다.(정적 방식 / 동적 방식)

  • 정적 안티 리버싱 기술 - 디버거를 탐지하여 프로그램이 정상적으로 실행하지 못하도록 방해한다. 대표적인 것이 디버거를 탐지하여, 디버거를 수행 중일 때는 다른 동작을 수행하는 방법
  • 동적 안티 리버싱 기술 - 디버거로 코드 수행 과정을 관찰하는 것을 방해하여 원 프로그램의 동작 원리를 이해하기 어렵게 방해한다. 패킹, 암호화, 중단점 설정 무시나 오류 처리, 코드 난독화(code obfuscation), 가상 머신 탐지, API 리다이렉트 등 여러 가지 기법이 존재한다.

 

01 - 디버거 탐지

 - 디버거는 상용 프로그램의 보호 기능을 우회하거나 무력화시키는데 사용된다. 

 

 1.1 프로세스 정보 확인

 - 윈도우 프로세스가 수행되면 프로세스마다 PEB(Process Environment Block) 공간이 할당되고, 쓰레드 마다 TEB(Thread Environment Block) 공간이 부여된다. 프로세스 관련 정보는 연결 리스트로 되어 있고, 검색이 가능하다.

 

TLS(Thread Local Storage) : 콜백 함수는 프로세스의 쓰레드가 생성/종료될 때마다 자동으로 호출되는 함수이다. 메인 쓰레드가 생성될 때, 이 콜백 함수가 호출되어 메인 쓰레드보다 먼저 실행된다.

 

TEB(Thread Environment Block) : 프로세스에서 실행되는 쓰레드에 대한 정보를 담고 있는 구조체이다. 쓰레드별로 TEB 구조체가 하나씩 할당된다. TEB 구조체는 운영체제 종류별로 그 모양이 조금씩 달라진다.

TLS -> TEB -> PEB

 -> NtTib는 NT_TIB 구조체의 시작 주소 정보가 있다. NT_TIB 구조체의 Self 값이 TEB 구조체의 시작 위치다.

 

 

 - PEB(Process Environment Block) : 프로세스마다 고유한 PEB를 가지며, 프로세스의 수행 정보를 담고 있다. PEB 주소는 TEB 구조체의 FS:[30]에 저장되어 있다. PEB에 대한 구조체 정보는 아래와 같다.

-> Ldr 멤버는 프로세스에 로딩된 모듈(DLL)의 로딩 베이스 주소를 구할 수 있다. 프로세스에 로딩된 DLL 모듈마다 _LDR_DATA_TABLE_ENRTY 구조체가 하나씩 생성되고, 이 구조체들은 코드_LIST_ENTRY 양방향 연결 리스트로 연결된다.

 

-> IsDebuggerPresent() 함수는 PEB.BeingDebugged 멤버의 값을 보고 디버깅 여부를 판단한다. GetModulHandle(ModulName)은 인자에 NULL 입력하고 호출하면, 프로세스에 로딩된 ImageBase 리턴한다.

 

 

 1.2 디버깅 관련 API 활용

# 디버거의 존재 판단 방법

- PEB의 BeingDebugged 플래그 확인

  • PEB(Process Environment Block)에서 BeingDebugged 플래그가 포함되는지 확인한다.

mov eax, dword prt fs: [30h] //PEB (TEB0x30번째 값)

mov eax, dword prt fs: [eax+2] 

test eax, eax

jne <BeingDebugged routine>

 

  • IsDebuggerPresent() API는 위 플래그 값을 확인할 수 있다. 디버거가 동작될 경우 IsDebuggerPresent() 함수가 1을 반환한다.

call <Kernel32.IsDebuggerPresent>

cmp eax, 1

je <BeingDebugged routine>

 

  • 우회 방법은 플래그 값을 직접 고치기, IsDebuggerPresent()를 후킹 하거나 반환 값을 0으로 바꾸면 된다.

 

- 코드

디버그 모드면 1, 아니면 0

{TEB주소의 30만큼 떨어진 곳에 PEB의 주소가 있다.}

 

- PEB의 NtGlobalFalgs 확인

  • PEB(Process Environment Block)의 0x68번째 비트에 있는 NtGlobalFlags를 이용한다. 디버깅 중이면 이 값이 0x70이고 정상이라면 0x00이다.

mov eax, dword ptr fs: [30h]

mov eax, dword ptr fs: [eax+0.68]

cmp eax, 0x70

je <NtGlobal routine>

 

  • 또한 디버깅 중이면 ntdll의 힙 조작 루틴을 제어하는 몇몇 플래그가 설정된다.

FLG_HEAP_ENABLE_TAIL_CHECK (Heap Tail Checking) : 0x10

FLG_HEAP_ENABLE_TREE_CHECK (Heap Free Checking) : 0x20

FLG_HEAP_ENABLE_PARAMETERS (Heap parameter Checking) : 0x40

 

  • 우회 방법은 위 플래그 값을 0으로 바꿔주면 된다.

 - 코드

NtGlobalFlag가 0x70이면 디버깅 중이고 0일 때 정상

 

 

 

- EPROCESS 구조체의 DebugPort 확인

  • ntdll!NtQueryInformationProcess 함수를 사용하여 프로세스 정보를 검색할 수 있다. 두 번째 매개변수 ProcessDebugPort를 0x7로 설정하고 ProcessInformationClass와 함께 호출하면 디버깅 여부를 판단할 수 있다. 이 함수의 ProcessInformation 값이 -1이면 디버깅 중이고, 0이면 정상 수행 중이다.

call dword ptr ds: []

test eax, eax

jnz <ProcessInformation routine>

 

  • EPROCESS 구조체에서 DebugPort플래그가 디버깅 여부를 판단할 수 있는 값이다. 이것은  Kernel32!CheckRemoteDebuggerPresent 함수를 통해서 확인이 가능하다. 이 함수는 내부적으로 ntdll!NtQueryInformationProcess 함수를 호출한다.

 

  • 우회 방법은 NtQueryInformationProcess 함수의 반환 값을 0으로 변경하면 된다.

 - 코드

PEB의 0x18는 ProcessHeap이 있다.

flag가 정상 실행 시에는 0x2, 디버깅 중일때는 다른 값이 나온다.

force Flags는 정상 실행시에 0x0이고 디버깅 중에는 다른 값이 나온다.

 

- 디버깅 정보 확인

  • Kernel32!OutputDebugStringA 함수는 디버거에 출력할 문자열을 전달하는 함수이다. 정상적인 상황에서는 1을 반환하고 디버깅 중이라면 매개변수로 전달된 문자열의 주소를 반환한다.

push [문자열 주소]

call <&kernel32.OutpitDebugStringA>

cmp eax, 1

je <OutputDebugString routine>

 

  • 우회 방법은 해당 구간을 우회하거나 OutputDebugStringA 함수의 반환 값을 수정하면 된다.

 

- 수행 시간 검사

  • 정상적인 프로세스 수행 시간보다 디버거가 동작될 때 수행 시간이 더 길어진다는 특징을 이용한다.

 

  • RDTSC(Read Time Stamp Counter) 명령을 사용하여 두 구간 사이의 타임스탬프를 구하고 그 값의 차이를 구하여 수행 시간을 검사할 수 있다.

rdtsc ; 시간 측정 (1)

xor ecx, ecx

add ecx, eax

rdtsc ; 시간 측정 (2)

sub eax, ecx ; 두 지점의 차

cmp eax, 0FFF

jnb <TimeCheck routine>

 

  • 우회 방법은 해당 구간을 우회하거나 GetTickCount() 함수의 반환 값을 수정하면 된다.

-LDR

PEB에서 0xC는 LDR의 위치

사용하지 않는 메모리 주소에 0 xabababab나 0 xFEEEFEEE가 있으면 디버깅 중이다.

 

 

#디버깅 관련 API

 

themida프로그램을 사용하면 디버깅을 못하게 할 수 있다. 카카오톡이 사용

 

-다음과 같이 input에 파일을 넣고 protect를 클릭하면 된다.

파일이 새로 만들어진다. x32dbg로 들어가 보면 난독화되어 읽을 수도 없게 되어있다.

 

 1.3 중단점 탐지

 - 중단점은 디버거를 통해 실행 중인 프로그램을 특정 위치나 조건에 멈추게 하고 싶을 때 사용하는 기법이다.

 

#중단점 설정

  • 하드웨어 중단점 : DR0~DR3 디버그 레지스터를 이용한다. DR7 디버그 레지스터를 이용하여 중단점을 제어할 수 있다.

 - 다음과 설정할 수 있다.

 

 - 윈도우의 경우 SEH 핸들러를 이용하여 하드웨어 디버그 레지스터 값을 변경할 수 있다. 

먼저 SeH 핸들러에 하드웨어 중단점 탐지 루틴을 등록한다.

만약 예외 상황(exeception)이 발생하면 미리 등록해 놓은 SEH 핸들러가 실행된다.

이 핸들러에서는 ContextRecord를 이용하여 디버그 레지스터 값을 변경하여 하드웨어 브레이크 포인터를 무력화시킬 수 있다.

 

-코드

 

우회 방법으로는 예외상황을 발생시키는 코드를 NOP처리, 하드웨어 중단점을 탐지하거나 제거하는 코드 다음에 중단점을 설정

 

  • 소프트웨어 중단점 : 디버거가 직접 관리하며, 명령어의 첫 번째 바이트를 0xCC(INT3)로 변경한다.

명령의 처음 1바이트를 0 xCC(INT3) 변경하여 프로그램의 진행을 멈춘다.

탐지 방법은 처음 몇 바이트를 검사하여 0xCC를 확인, 코드 전체의 체크섬 값을 계산하여 비교하는 것

 

INT3 또는 INT1 명령어를 지나갈 때 디버거에서는 보통 예외처리를 하지 않고 무시한다.

확인 방법 - 예외상황 핸들러에 특정 값을 설정하는 루틴을 만들고, INT 명령 이후에 특정 값이 제대로 설정되었는지 확인

int3, 0xCC가 발생하면 디버깅 중이다.

 

  • 명령어 중단점 : 특정 주소에 설정되는 중단점으로 하드웨어나 소프트웨어 방식으로 구현될 수 있다.

 

  • 메모리 중단점 : 특정 메모리 주소의 데이터를 읽거나 변경할 때 멈추고자 사용한다.

 

02 -  난독화

코드 난독화 : 코드 보안을 강화하기 위해 사용[지적 재산권 방어, 디지털 저작권 관리, 악성코드 자체 방어 기능]

 

#데이터 기반 난독화 : 계산 관점에서 같은 결과가 나오게 한다.

 

  • 상수 접기(folding)/펼치기(unfolding)

접기

mov eax, 4;           

mov ebx, 5;               ->          mov eax, 20

mult eax, ebx;

     

펼치기                                    

push 0                     ->          push 0xf8cfe47a

                                           add dword ptr [esp], 0x7301b86

 

  • 안쓰는 코드 삽입

컴파일러 최적화 옵션으로 "안 쓰는 코드 지우기(dead code elimination)"가 있는데 프로그램 연산에 영향을 주지 않는 것을 말한다.

 

int func() {                   int func() {

int a;                           int a,b;

a = 3;           ->            a=1;

return a;                       b=2;

}                                 a=3;

                                  return a}

                                  

  • 패턴 기반 변환

특정 코드는 다른 문장으로 수행하여도 같은 결과가 나타나고 이런 코드들을 모아서 패턴화 한다. 패턴을 구성하고 코드의 변화를 준다.

eax =ebx를 수행하는 코드이며, mov 명령어를 push와 pop으로 바꾸는 패턴을 사용하였고, 그것을 다시 확장

                                             sub esp, 4

                                             mov dword ptr[esp], ebx

mov eax, ebx  -> push ebx    ->   xor eax, eax

                        pop eax          add eax, dword ptr[esp]

                                             add esp, 4

 

# 제어기반 난독화 : 제어 흐름이 달라지도록 변화를 준다.

  • 코드 순서의 파괴

코드의 실행 순서를 바꿔 분석을 느리게 한다. [MessageBoxA(0, DialogText, DialogCaption, MB_DEFBUTTON1) 함수를 호출하는 코드의 순서를 섞어 놓은 것]

 

  • 함수 제어 순서의 우회

함수 호출에 사용되는 call과 ret

call target_address  -  push eip / jmp target_address

ret - pop eip / jmp eip

이 코드를 오른쪽과 같이 변경 가능

 

  • 예외 처리 루틴 활용

윈도우에 예외 처리기(SEH: Structured Exception Handler)벡터 예외 처리기(VEH, Vectored Exception Handler)는 제어 흐름을 난독화할 때 사용할 수 있다.

운영체제에 예외 처리기로 원하는 루틴을 추가하고, 예외가 발생할 수 있는 코드를 삽입하는 것

다음 예는 SEH 수행 변수를 스택에 저장하여 핸들러를 등록한다.

mov [eax]에서 오류가 나면 위에서 등록된 핸들러가 동작된다.

 

  • 쓰레기 코드 삽입

: 데이터 기반 난독화처럼 수행과 관계가 없는 코드를 삽입하는 방법이다.

특정 모듈 앞에 조건부 분기 코드를 두고, 특정 모듈로 진입하지 않도록 한다. 조긴 비교에서 해당 조건이 발생하지 않도록 하면 특정 모듈로 진입하지 않는다.