목차

0. 개요

1. SEH

2. PEB

3. DLL 및 API 함수 주소 얻기

4. 안티디버깅

5. 기타

7. 참고





0. 개요

  32비트 윈도우의 유저 모드 기준으로 FS 레지스터는 TEB(Thread Environment Block) 또는 TIB(Thread Information Block)이라고 불리는 구조체를 가리킨다. 이 구조체는 현재 실행 중인 스레드에 대한 정보를 저장하고 있다. 주로 API 함수를 호출하지 않고도 정보를 얻을 수 있기 때문에 이러한 목적으로 사용된다. 참고로 FS 레지스터는 TEB의 첫 번째 주소를 가리키므로 여기에 위치만큼 값을 더해서 원하는 필드에 접근할 수 있다.


  그리고 마찬가지로 PEB(Process Environment Block)를 살펴보도록 하겠다. PEB의 경우 몇 개의 필드는 문서화되어 있다. 바로 참조할 수는 없고 FS 레지스터를 이용하여 TEB에서 PEB의 주소를 얻은 후에 사용된다.


  아래에는 자주 사용되는 부분들을 정리할 것이며 차례대로 어떻게 보여지는지와 어떻게 사용되는지에 대하여 예제와 함께 설명하도록 하겠다.


FS:[0x00] : 현재 SEH 프레임

FS:[0x18] : TEB

FS:[0x20] : PID

FS:[0x24] : TID

FS:[0x30] : PEB

FS:[0x34] : Last Error Value


  참고로 위를 보면 오프셋 0x18의 주소가 TEB 자체를 가리키는 것을 볼 수 있다. 즉 만약 현재 스레드에서 TEB의 위치가 0x00379000인 경우 0x00379018에는 0x00379000이 들어있는 것이다.


  x64 환경에서는 FS 레지스터 대신 GS 레지스터가 사용된다.


GS:[0x30] : TEB

GS:[0x40] : PID

GS:[0x48] : TID

GS:[0x60] : PEB


  궁극적으로 이 문서의 목적은 리버스 엔지니어링을 수행하면서 TEB 또는 PEB를 참조하는 루틴을 봤을 때 직접 찾아볼 필요 없이 정리된 내용을 제공하는 것이다. 물론 모든 것을 제공하지는 않지만 자주 사용되는 것들과 어떻게 사용되는지에 대한 내용을 쓰도록 하겠다.





1. SEH

  FS:[0x00] 다시 말해서 fs:0은 현재 SEH 핸들러의 주소를 나타낸다. SEH는 안티 디버깅 기법으로 사용되기도 하므로 아마 가장 많이 보이는 형태 중 하나일 것이다. 예외 처리 관련 내용은 다음의 문서를 [ http://sanseolab.tistory.com/16 ] 통해 더 공부하기로 하고 여기서는 간단하게만 정리하겠다. 예외 발생 시 SEH 체인의 Top 부터 찾아나가는데 이 주소가 FS:[0x0]에 들어있는 것이다. 물론 SEH 프레임의 구조 상 핸들러의 주소 및 다음 핸들러의 주소를 가리키는 포인터로 구성되어 있기 때문에 SEH를 참조할 때는 이렇게 Top의 주소만 알면 충분할 것이다.


PUSH 0040184B

XOR EAX, EAX

PUSH DWORD PTR FS:[EAX]

MOV DWORD PTR FS:[EAX], ESP


  또는 다른 방식으로 사용될 수도 있다.


PUSH 0040184B

PUSH DWORD PTR FS:[0]    ; [FS:00000000] = [00379000] = 0019FFCC

..

MOV DWORD PTR FS:[0], EAX


  위는 Ollydbg로 본 SEH 핸들러 설치 루틴이다. 핸들러의 주소를 PUSH한 후 SEH 체인의 Top을 나타내는 주소를 FS[0x0]을 통해 얻어와서 PUSH한다. 왜냐하면 현재 SEH 핸들러가 체인의 Top이 될 것이기 때문에 이전에 Top이었던 SEH 프레임을 가리켜야 하기 때문이다. 물론 마지막으로 방금 설치한 SEH 프레임의 주소를 다시 FS:[0x0]에 넣어서 Top은 방금 설치한 SEH 프레임을 가리키게 된다. 참고로 FS 레지스터의 주소로는 FS:[0]처럼 직접 0을 넣을 수도 있고 XOR EAX, EAX 명령을 통해 EAX를 0으로 초기화한 후에 FS:[EAX] 형태를 사용할 수도 있다. 참고로 x64에서는 예외 처리 시에 이러한 메커니즘을 사용하지 않는다.





2. PEB

  PEB의 경우 뒤에서 꾸준히 나올 것이기 때문에 미리 정리하도록 한다. PEB는 직접 참조할 수 없고 32비트의 경우 FS 레지스터를 통해 TEB를 얻은 후 여기서 오프셋 0x30 즉 PEB 필드에 들어있는 값을 통하여 PEB의 주소를 얻는 방식이 사용된다.


MOV EAX, DWORD PTR FS:[18]

MOV EAX, DWORD PTR DS:[EAX+30]


  위에서는 먼저 FS:[18]을 통해 TEB의 주소를 직접 얻은 후 이 주소 즉 EAX에 오프셋 0x30만큼의 값을 더하여 PEB를 얻는다. 또는 다음과 같이 직접 사용할 수도 있다.


MOV EAX, DWORD PTR FS:[30]


  이제 PEB의 주소가 EAX에 들어가 있으므로 PEB의 필드들 또한 EAX에 주소를 더해가며 원하는 필드를 찾는 방법이 사용된다. 다음으로 64비트 환경에서 설명하겠다. 64비트에서는 FS 레지스터 대신 GS 레지스터가 사용되며 오프셋 0x30이 TEB 주소를 가진다. 또한 PEB는 TEB 기준 오프셋 0x60에 위치한다.


MOV RAX, QWORD PTR GS:[30]

MOV RAX, QWORD PTR DS:[RAX+60]


  이것도 아래와 같이 직접 사용할 수 있다.


MOV RAX, QWORD PTR GS:[60]


  x86 기준 자주 사용되는 PEB의 멤버들은 다음과 같다.


0x002 BYTE BeingDebugged;

0x008 void* ImageBaseAddress;

0x00C _PEB_LDR_DATA* Ldr;

0x018 void* ProcessHeap

0x064 DWORD NumberOfProcessors;

0x068 DWORD NtGlobalFlag;





3. DLL 및 API 함수 주소 얻기

  일반적으로 프로그램은 개발자는 모든 기능을 구현할 필요 없이 제공되는 함수들을 사용할 것이다. 사용하는 함수가 써드파티에서 제공되는 라이브러리에 포함된 것이라고 하더라도 내부적으로는 결국 운영체제에서 제공해주는 API 함수를 이용하게 된다. 물론 악성코드들 같이 직접 API 함수들을 이용하여 개발된 프로그램도 많다. 어쨌든 사용하는 함수들이 들어있는 라이브러리를 정적으로 링크하지 않는 이상 DLL을 임포트하고 필요한 함수들의 주소를 얻어와 호출한다. 이쪽 내용을 위해서는 PE 그리고 임포트 테이블 같이 기본적인 내용이 당연히 필요하며 여기서는 이러한 내용들을 숙지하고 있다는 가정 하에 적도록 한다.


  우리는 악성코드 분석 시에 악성코드가 임포트하고 있는 함수들을 통해 어떠한 방식으로 동작할지를 예측할 수 있으며 정적 분석에서도 많이 사용된다. 물론 패킹된 경우에는 LoadLibrary()와 GetProcAddress()를 이용하여 사용할 API들을 실행 중에 임포트함으로써 이러한 정적 분석을 방해하기도 한다. 여기서는 위의 두 함수조차도 없이 즉 아무런 API 호출 없이 API 함수의 주소를 알아내는 방식을 설명한다. 이 방식은 셸코드에서 자주 사용되는 것으로 보이며 PEspin이라는 프로텍터에서도 비슷한 메커니즘이 사용되었다.


  다음 링크에 [ http://ezbeat.tistory.com/283 ] 관련 내용이 매우 자세하게 나와있으며 여기서는 간단하게 설명하도록 한다. PEB의 Ldr이라는 필드는 PEB_LDR_DATA 구조체를 가리키는 포인터로서 PEB에서 오프셋 0xC에 위치한다. 이 구조체는 로드된 모듈에 대한 정보를 제공하는데 0x1C만큼 떨어진 필드는 InInitializationOrderModuleList 즉 PEB->Ldr.InInitializationOrderModuleList.Flink로서 초기화된 모듈 순서로 구성된 리스트(_LIST_ENTRY)를 가리킨다. 리스트를 구성하는 각 구조체는 _LDR_MODULE로서 첫 번째 필드는 NextModule이며 다음 리스트를 가리킨다. 오프셋 0x8에 위치 필드는 해당 모듈의 BaseAddress이며 오프셋 0x20은 유니코드 문자열로 저장된 해당 모듈의 이름에 대한 포인터이다.


  이제 위의 내용을 가지고 차례대로 관련 루틴을 분석해 보겠다.


MOV EAX, DWORD PTR FS:[30]

; EAX에는 PEB의 주소가 들어가 있다.

MOV EAX, DWORD PTR DS:[EAX+0C]

; EAX에는 PEB_LDR_DATA 구조체의 주소가 들어가 있다.

MOV ESI, DWORD PTR DS:[EAX+1C]

; ESI는 PEB->Ldr.InInitializationOrderModuleList.Flink의 주소이다.

MOV EAX, PTR DS:[ESI+20]

; EAX에는 해당 유니코드 문자열 형태를 가진 해당 모듈의 이름에 대한 주소가 들어간다. 이 이름을 가지고 비교하여 원하는 DLL을 찾을 수 있을 것이다.


MOV ESI, PTR DS:[ESI]

MOV EAX, PTR DS:[ESI+20]

; 참고로 ESI가 PEB->Ldr.InInitializationOrderModuleList.Flink인 경우에 해당 주소에는 리스트의 다음 _LDR_MODULE를 가리키는 값이 들어가 있다. 그러므로 위와 같이 사용하면 ESI는 새로운 모듈을 가리키게 된다. 이후 0x20만큼을 더하여 새로운 모듈의 이름에 대한 주소를 얻을 수 있다. 즉 해당 주소를 저장하고 반복문을 통해서 리스트의 다음으로 계속 이동하면서 이름을 구할 수 있는 것이다.


  이러한 방식과 반복문을 통하여 우리가 원하는 DLL의 이름을 구할 수 있을 것이고 이름이 동일하다면 BaseAddress를 구해야 한다. 이것은 다음 명령어를 통해 구할 수 있다.


MOV EAX, PTR DS:[ESI+8]

; ESI가 PEB->Ldr.InInitializationOrderModuleList.Flink인 경우 BaseAddress는 오프셋 0x8이다. 참고로 오프셋 0x20은 이름을 나타내는 문자열에 대한 포인터였다.


  이제 해당 모듈의 BaseAddress를 구했으니 마지막으로 Export Table을 찾은 후 AddressOfNames 테이블에서 찾는 API 함수의 이름을 구한 후 AddressOfFunctions 테이블에서 API 함수의 주소를 구한다. 그리고 RVA 형태의 주소를 VA 형태로 변환하면 해당 API 함수의 주소를 구하게 된다.



3.1 구조체들에 대한 정리

  그림으로 만드려고 했지만 검색을 위해서 보기 불편하더라도 다음과 같이 표시하기로 한다. 기본적인 그림은 위의 링크를 참고하여 보면 이해하기 쉬울 것이다. 아래의 것들은 문서화되어 있지는 않지만 윈도우 XP SP3와 윈도우 10에서 확인한 결과 동일해서 찾은 내용을 정리하려고 한다.



TEB

0x30 : PEB


PEB

0x0C : PEB_LDR_DATA


PEB_LDR_DATA

0x0C : ?? ( InLoadOrderModuleList )

0x14 : ?? ( InMemoryOrderModuleList )

0x1C : LDR_MODULE ( InInitializationOrderModuleList )



  먼저 가장 많이 사용되는 InInitializationOrderModuleList를 기반으로 설명하겠다.


LDR_MODULE ( InInitializationOrderModuleList )

( ntdll.dll  ->  kernelbase.dll  ->  kernel32.dll )

0x00 : Next Module

0x04 : Previous Module

0x08 : ImgBase

0x0C : EP

0x10 : Size of Img

0x20 : Name



  TEB에서 오프셋 0x30은 PEB를 의미한다. PEB에서 오프셋 0x0C는 PEB_LDR_DATA 구조체를 의미한다. PEB_LDR_DATA 구조체에서 오프셋 0x1C는 InInitializationOrderModuleList를 가리킨다.

정확히 말하자면 어떠한 구조체들이 특정 목적에 따라 링크드 리스트로 연결되어 있다. 링크드 리스트로 연결된 각 구조체들은 이름과 완벽한 구조는 찾지 못했지만 위의 링크에서는 LDR_MODULE로 나와있어서 그대로 사용하기로 한다.


  어쨌든 각 구조체들은 이름에 따르면 모듈들의 초기화된 순서에 따른 것으로 확인할 수 있고, 

직접 살펴보면 ntdll, kernelbase, kernel32 순이다. 참고로 이것은 윈도우 7 이상이며 윈도우 xp에서는 ntdll 다음이 kernel32이다. 악성코드에서 주로 사용하는 구조체의 멤버는 아마도 Name(0x20)과 ImageBase(0x08)일 것이다.


  앞의 것이 대표적으로 사용되지만 PEB_LDR_DATA의 오프셋 0x0C, 0x14도 사용할 수 있다. 0x0C 즉 InLoadOrderModuleList부터 보겠다. 이 구조체는 이름을 알 수 없어서 물음표로 표시하였다.


?? ( InLoadOrderModuleList )

( 바이너리  ->  ntdll.dll  ->  kernel32.dll  ->  kernelbase.dll )

0x00 : Next Module

0x04 : Previous Module

0x18 : ImgBase

0x1C : EP

0x20 : Size of Img

0x30 : Name


  이것은 이전의 것과는 달리 로드 순서라서 그런지 바이너리가 가장 먼저 있는 것을 확인할 수 있다.


  다음으로  0x14 즉 InMemoryOrderModuleList를 보겠다.


?? ( InMemoryOrderModuleList )

( 바이너리  ->  ntdll.dll  ->  kernel32.dll  ->  kernelbase.dll )

0x00 : Next Module

0x04 : Previous Module

0x10 : ImgBase

0x14 : EP

0x18 : Size of Img

0x20 : Path


  앞과 순서는 같아 보인다. 하지만 특징이 있는데 0x20의 값이 단순한 이름이 아니라 해당 모듈의 경로명이라는 점이다.





4. 안티디버깅

4.1 PEB.BeingDebugged

  IsDebuggerPresent() API 함수는 PEB의 BeingDebugged 필드를 사용한다. 이 필드는 오프셋 0x2에 위치한 바이트 값으로서 현재 디버깅 중이라면 0x1을 갖으며 일반적인 경우에는 0x0이 설정되어 있다. 이 함수가 반환하는 값도 마찬가지이다. 참고로 직접 라이브러리를 확인해보면 알겠지만 단지 3줄의 어셈블리 명령어를 갖는다. 그렇기 때문에 API를 호출하는 대신 직접 구현하는 방식이 사용되는 경우가 있다고도 한다. 32비트에서 해당 루틴은 다음과 같다.


MOV EAX, DWORD PTR FS:[30]

MOVZX EAX, BYTE PTR DS:[EAX+2]

RETN


  아래는 64비트에서의 루틴이다.


MOV RAX, QWORD PTR GS:[60]

MOVZX EAX, BYTE PTR DS:[RAX+2]

RET



4.2 PEB.NtGlobalFlag

  이 필드는 일반적인 경우에 0x0이며 디버깅 시에 0x70으로 설정된다. 즉 FLG_HEAP_ENABLE_TAIL_CHECK (0x10), FLG_HEAP_ENABLE_FREE_CHECK(0x20), FLG_HEAP_VALIDATE_PARAMETERS(0x40)가 설정되는 것이다. 참고로 실행 중인 프로세스를 Attach한 경우에는 변경되지 않는다고 한다.


  32비트 환경의 경우 PEB에서 오프셋 0x68에 있으며 다음을 통해 검사할 수 있다.


MOV EAX, DWORD PTR FS:[30]

MOV AL, DWORD PTR DS:[EAX+68]

AND AL, 70h

CMP AL, 70h

JE being_debugged


  64비트의 환경의 경우 PEB에서 오프셋 0xBC에 위치해 있으며 다음을 통해 검사할 수 있다.


MOV RAX, QWORD PTR GS:[60]

MOV AL, BYTE PTR DS:[RAX+BC]

AND AL, 70h

CMP AL, 70h

JE being_debugged


  64비트 윈도우에서 32비트 애플리케이션 실행 시 즉 Wow64를 통해 실행 중인 경우에는 각각에 대한 PEB가 존재한다. x64 TEB는 x86 TEB보다 0x2000만큼 아래에 위치하므로 이만큼을 뺸 후에 0x60만큼 더하여 x64 PEB를 얻을 수 있다. 그리고 NtGlobalFlag 필드는 위와 마찬가지로 오프셋 0xBC에 위치한다.


MOV EAX, DWORD PTR FS:[18]

SUB EAX, 2000h

MOV EAX, DWORD PTR DS:[EAX+60]

MOV AL, BYTE PTR DS:[EAX+BC]

AND AL, 70h

CMP AL, 70h

JE being_debugged



4.3 PEB.ProcessHeap

  이 필드는 힙 구조체를 가리킨다. 참고로 GetProcessHeap() API도 이런 역할을 하는데 해당 함수의 루틴을 보겠다. 먼저 32비트 환경의 경우 PEB에서 오프셋 0x18에 위치해 있으며 다음을 통해 검사할 수 있다.


MOV EAX, DWORD PTR FS:[30]

MOV EAX, DWORD PTR DS:[EAX+18]


  64비트 환경의 경우 PEB에서 오프셋 0x30에 위치해 있으며 다음을 통해 검사할 수 있다.


MOV RAX, QWORD PTR GS:[60]

MOV RAX, QWORD PTR DS:[RAX+30]


  중요한 것은 이 구조체의 멤버들 중에서 Flags와 ForceFlags이다. 프로세스가 디버깅 중이지 않을 경우 Flags는 0x2를 ForceFlags는 0x0의 값을 갖는다. 참고로 비스타 이후로 조금 달라졌는데 여기서는 비스타 이후의 버전만 보도록 한다. 


  먼저 Flags 필드를 보겠다. 디버거가 존재할 때 Flags 필드는 일반적으로 아래의 플래그들의 조합으로 설정된다.


HEAP_GROWABLE (2)

HEAP_TAIL_CHECKING_ENABLED (0x20)

HEAP_FREE_CHECKING_ENABLED (0x40)

HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)


  32비트 환경의 경우 PEB에서 오프셋 0x18에 위치한 ProcessHeap를 구한 후 여기에서 오프셋 0x40에 위치한 것이 Flags 필드이다.


MOV EAX, DWORD PTR FS:[30]

MOV EAX, DWORD PTR DS:[EAX+18]

MOV EAX, DWORD PTR DS:[EAX+40]


  EAX를 비교하여 0x2보다 큰 경우 디버깅 중으로 판단할 수 있다. 64비트 환경의 경우 PEB에서 오프셋 0x30에 위치한 ProcessHeap를 구한 후 여기에서 오프셋 0x70에 위치한 것이 Flags 필드이다.


MOV RAX, QWORD PTR GS:[60]

MOV RAX, QWORD PTR DS:[RAX+30]

MOV EAX, DWORD PTR DS:[RAX+70]


  다음으로 ForceFlags 필드를 보자. 디버거가 존재할 때 ForceFlags 필드는 일반적으로 이 플래그들의 조합으로 설정된다.


HEAP_TAIL_CHECKING_ENABLED (0x20)

HEAP_FREE_CHECKING_ENABLED (0x40)

HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)


  32비트 환경의 경우 PEB에서 오프셋 0x18에 위치한 ProcessHeap를 구한 후 여기에서 오프셋 0x44에 위치한 것이 ForceFlags 필드이다.


MOV EAX, DWORD PTR FS:[30]

MOV EAX, DWORD PTR DS:[EAX+18]

MOV EAX, DWORD PTR DS:[EAX+44]


  EAX를 비교하여 0x0보다 큰 경우 디버깅 중으로 판단할 수 있다. 64비트 환경의 경우 PEB에서 오프셋 0x30에 위치한 ProcessHeap를 구한 후 여기에서 오프셋 0x74에 위치한 것이 ForceFlags 필드이다.


MOV RAX, QWORD PTR GS:[60]

MOV RAX, QWORD PTR DS:[RAX+30]

MOV EAX, DWORD PTR DS:[RAX+74]



4.4 기타

  TEB를 통해서 프로그램의 PID 등의 정보를 쉽게 구할 수 있으므로 이러한 것들을 인지하고 있다면 분석에 도움이 될 수 있다. 안티 디버깅 루틴을 직접 구현한 경우 특정 함수를 호출할 때 인자로 PID가 필요할 때 TEB를 통해 쉽게 값을 얻어올 수 있어서 이러한 방식으로 자주 사용되기 때문이다. 


  이렇게 간접적으로 사용되는 방식으로서 TEB의 LastErrorValue도 있다. Kernel32.dll의 GetLastError() API 함수 대신 직접 사용할 수 있는 것이다. 비스타 이전에 사용되는 안티 디버깅 방식 중에서 OutputDebugString()을 이용하는 것이 있다. Last Error Value를 설정한 후 OutputDebugString()을 호출하는데 만약 디버거가 붙어있다면 해당 값은 변화가 없을 것이며 디버거가 붙어있지 않아서 실패한다면 거기에 해당하는 에러 코드가 Last Error Value에 설정되어 이전 값과 차이가 발생할 것이다. 안티 디버깅으로서 전과 후의 값을 비교하여 변화가 없을 때 디버깅 중으로 판단한다. Last Error Value를 설정/확인할 때 SetLastError()와 GetLastError()를 이용할 수도 있지만 직접 FS:[0x34]를 사용할 수도 있다. 


  마지막으로 PEB의 ImageBaseAddress를 통해 ImageBase 주소를 얻을 수 있다. 참고로 GetModuleHandle() API 함수는 특정 모듈의 ImageBase 주소를 반환해주는 함수인데 인자로 NULL을 넣은 경우에는 현재 실행 중인 모듈의 ImageBase 주소를 반환해준다. 즉 32비트의 경우 내부를 보면 인자로 NULL 즉 0이 들어올 때 실행되는 부분은 다음과 같으며 PEB에서 오프셋 0x8에 위치한다.


MOV EAX, DWORD PTR FS:[30]

MOV EAX, DWORD PTR DS:[EAX+8]


  64비트의 경우 PEB에서 오프셋 0x10에 위치한다.


MOV RAX, QWORD PTR GS:[60]

MOV RAX, QWORD PTR DS:[RAX+10]





5. 기타

  먼저 올리디버거에서 관련 내용을 보겠다. 올리디버거의 메뉴 중에서 "Memory map"을 선택하고 확인해보면 Contains에 설명이 "Data block of main thread"라고 적혀있는 영역이 보일 것이다. 이곳이 TEB의 위치이며 해당 라인을 더블클릭해 보면 자세한 내용을 볼 수 있다. 찾지 못한 것인지 모르겠지만 PEB에서는 특별한 설명이 제공되지는 않는 것 같다. 참고로 올리디버거로 분석할 때 처음 화면에서 EBX를 보면 PEB 주소가 들어있는 것을 확인할 수 있다.


  Windbg의 경우 !teb와 !peb라는 명령어가 제공된다. 이름과 같이 TEB 및 PEB에 대한 정보를 보여주는 명령어이다. 참고로 의사 레지스터로도 제공되는데 $proc은 PEB를 의미하며, $thread는 TEB를 의미한다.





7. 참고

* The "Ultimate" Anti-Debugging Reference - Peter Ferrie

* 리버싱 핵심원리 - 이승원

* http://ezbeat.tistory.com/283

* TEB x86 : [ https://www.aldeid.com/wiki/TEB-Thread-Environment-Block ]

* PEB x86 : [ https://www.aldeid.com/wiki/PEB-Process-Environment-Block ]

'악성코드 분석' 카테고리의 다른 글

COM, OLE, .NET Frame work 등의 개념 및 사용  (0) 2017.12.18
exeinfo PE 사용법  (1) 2017.12.17
EFLAGS 상태 레지스터  (0) 2017.11.22
USB 악성코드 분석  (0) 2017.11.09
윈도우에서 스크립트 악성코드  (0) 2017.11.02
Posted by SanseoLab



0. 개요

  여기서는 EFLAGS에 대해서 알아본다. 먼저 기본적인 구조를 알아본 후 Ollydbg, Windbg, GDB 같은 디버거들에서 어떤 식으로 보여지는지를 설명한다. 그리고 이것이 사용되는 방식과 기타 사항들을 설명하기로 한다.





1. 구조

  eflags는 상태 레지스터이며 x86에서는 32비트, x64에서는 64비트의 크기이다. 여기서는 기본적으로 사용되며 중요한 부분만 설명하기로 하고 하위 12비트만 보겠다. 참고로 x64에서 확장된 32비트는 모두 Reserved이며 기본 32비트에서도 일반적으로 사용되는 부분은 하위 16비트이기 때문에 이것만 살펴보기로 한다.


  여러 디버거에서 디버깅을 하다보면 eflags는 0x00000246, 0x00000212 등의 값을 갖는 것을 볼 수 있다. 하위 12비트를 제외한 나머지 상위 비트들은 보여줄 때 무시되는 것인지 아니면 0의 값을 갖는 것인지는 알아보지 않았다.


OF DF IF TF    SF ZF 0 AF    0 PF 1 CF


  0과 1로 적은 것은 Reserved된 것으로서 기본적으로 위와 같이 0 또는 1의 값을 갖는다. 각각 Overflow Flag, Direction Flag, Interrupt Flag, Trap Flag, Sign Flag, Zero Flag, Auxiliary Carry Flag, Parity Flag, Carry Flag이다.


  TF는 뒤에서 설명할 것이며 어쨌든 디버거들 입장에서는 기본적으로 0으로 보여주는것 같은데 이것까지 고려해서 불필요한 부분을 x로 표시하면 다음과 같다.


000x    00x0    x0x0


  이제 이것을 기본으로 예제를 통해 살펴보겠다. 먼저 0x00000246의 12비트를 2진수로 보겠다. 활성화된 것은 IF, ZF, PF이다.


OF DF IF TF    SF ZF 0 AF    0 PF 1 CF

0010    0100    0110

001x    01x0    x1x0


  다음으로 0x00000212를 보겠다. IF, AF가 Set되었다.


OF DF IF TF    SF ZF 0 AF    0 PF 1 CF

0010    0001    0010

001x    00x1    x0x0





2. 디버거에서

  먼저 Windbg부터 살펴보겠다. Windbg에서는 Single Step이나 r 명령어로 레지스터를 볼 때 eflags 관련 부분은 다음과 같이 보여준다.


nv up ei pl zr na pe nc

efl=00000244


  살펴보면 먼저 TF를 제외하고 8가지만 보여준다는 것을 알 수 있다. 위의 약자들을 정리하면 다음과 같다. 아래에 정리된 것을 숙지하면 efl 값을 통해가 아닌 이렇게 일일이 직접 확인할 수 

있다.


--------------------------------------------------------------------

FlagName        Set                        Clear

Overflow         ov (Overflow)             nv (No Overflow)

Direction         dn (Direction Down)     up (Direction Up)

Interrupt          ei (Interrupts Enabled)   di (Interrupts Disabled)

Sign              ng (Sign Flag negative)  pl (Sign Flag positive)

Zero              zr (Zero)                  nz (Not Zero)

Auxiliary Carry   ac (Auxiliary Carry)       na (No Auxiliary Carry)

Parity             pe (Even Parity)          po (Odd Parity)

Carry              cy (Carry)                 nc (No Carry)

--------------------------------------------------------------------


  다음으로 GDB에서 어떻게 보여지는지 알아보겠다. GDB에서는 i r 명령어가 레지스터를 보여주는 명령어이다. 어떻게 보면 더 직접적으로 보여준다.

efl = 0x246    [ PF ZF IF ]


  마지막으로는 OllyDbg에서 어떻게 보여지는지 알아보겠다. 지금까지 살펴본 것과는 많이 다른 것을 볼 수 있다. 사실 어떤 면에서는 디버거를 사용하는 입장에서 이것이 가장 좋은 방식이 아닐까 생각한다.


EFL  00000246  ( NO, NB, E, BE, NS, PE, GE, LE )


  OllyDbg도 어떤 플래그가 Set 되었는지 여부를 보여주며 이 플래그들을 이용하는 조건 점프문들 중에서 어떤 것들이 활성화되는지 또한 보여준다. 예를들어서 위의 사항들 중에서 "E"가 보인다. 이 말은 현재 eflags 상태에서 만약 JE 명령어를 만나게 된다면 이것이 활성화될 것이라는 것을 보여준다. 또는 JLE 명령어의 경우에도 마찬가지이다. 이러한 사항들은 다음 항목에서 따로 정리한다.





3. 플래그의 사용

  앞에서 우리는 각각의 플래그들이 어떻게 보여지는지에 대해서 배웠다. 여기서는 OllyDbg를 설명하면서 다 설명하지 못한 부분인 어떻게 사용되는지에 대해서 정리하겠다. 만약 어떤 수를 비교하거나 해서 같은 경우에 ZF 즉 Zero Flag가 1이 되는데 이 상태에서 JE (Jump if equal) 명령어가 존재한다면 그것이 활성화된다. JNE 명령어는 ZF가 0인 상태에 활성화되는 조건 점프문이다.


  이러한 조건 점프문들의 활성화 여부는 각각의 플래그 값을 알고 있어야 예상할 수 있다. 물론 OllyDbg는 활성화된 플래그가 아닌 활성화된 조건을 직접 보여주므로 매우 좋은 인터페이스를 가지고 있는것 같다. 다음은 각각의 플래그에 맞는 조건들을 정리하였다. 참고로 각 조건에 따라 어셈블리 명령어가 2개씩 존재하는데 처음 조건은 OllyDbg에서 보여주는 조건이며 두 번째 ''로 표시한 부분은 OllyDbg에서 보여주는 것은 아니지만 동일한 명령어이므로 다른 디버거에서 볼 때 같은 것으로 여기면 된다.


--------------------------------------------------------------------

Mnemonic    Condition Tested For             Status Flags Setting


O             Overflow                            OF == 1

NO            No overflow                        OF == 0


B             Below                               CF == 1

NAE          Neither above nor equal            ''

NB            Not below                          CF == 0

AE            Above or equal                     ''


E             Equal                            ZF == 1

Z             Zero                                  ''

NE            Not equal                           ZF == 0

NZ            Not zero                             ''


BE   Below or qual                      CF == 1 || ZF == 1

NA          Not above                          ''

A             Above                               CF == 0 && ZF == 0

NBE          Neither below nor equal           ''


S             Sign                                  SF == 1

NS            No sigh                              SF == 0


PE            Parity even                          PF == 1

P             Parity                                 ''

PO            Parity odd                           PF == 0

NP            No parity                             ''


GE            Greater or equal                    SF == OF

NL            Not less                              ''

L              Less                                 SF != OF

NGE          Neigher greater nor equal          ''


LE            Less or equal                         ZF == 1 || SF != OF

NG           Not greater                           ''

G             Greater                               ZF == 0 && SF == OF

NLE          Neither less nor equal                ''

--------------------------------------------------------------------





4. 기타

  참고로 디버거는 Trap Flag를 이용한다. 즉 Single Step을 위해 이것을 활성화시켜서 각 명령어 실행 시 디버거에게 제어가 가게 되는 원리이다. 이 말은 이것이 안티 디버깅에도 사용될 수 있다는 것이다. 일반적인 실행에서 트랩 플래그는 사용되지 않는데 이것을 설정하고 이 예외를 받는 예외 핸들러를 설치한 경우 일반적인 실행에서는 예외 핸들러가 호출된다. 하지만 디버거의 입장에서는 이것을 구분할 능력이 없다. 왜냐하면 자체적으로 이미 사용하고 있기 때문인데 디버깅 시에 이 트랩 플래그는 당연히 무시되고 예외 핸들러가 호출되지 않을 것이다. 이것을 이용해 예외 핸들러에 현재 디버깅 당하는 중인지를 검사하는 루틴을 삽입할 경우 성공적인 안티 디버깅 기법이 될 수 있다.


  트랩 플래그를 설정하는 어셈블리 명령어는 없지만 트릭을 이용해 설정하는 루틴은 다음과 같다.


--------------------------------------------------------------------

PUSHFD

OR DWORD PTR SS:[ESP], 100

POPFD

--------------------------------------------------------------------


Posted by SanseoLab



0. 개요

1. 분석

.... 1.1 기본적인 내용

.... 1.2 바로가기 파일

.... 1.3 악성 스크립트

........ 1.3.1 기본

........ 1.3.2 디코딩

........ 1.3.3 간단한 분석

........ 1.3.4 자세한 분석

2. 치료

.... 2.1 원본 파일 복구

.... 2.2 악성코드 확인 및 종료

.... 2.3 완전히 삭제

3. 결론





0. 개요

  프린트를 목적으로 학교의 공용 컴퓨터를 사용하다가 일명 "USB 바이러스"라고 불리는 악성코드에 여러 번 감염되었다. 어느날 시간이 남기도 하고 스크립트 악성코드 분석도 공부할 필요가 있다는 생각에 VBS부터 공부한 후 이 악성코드를 분석해 보기로 했다.


  조사해서 찾아보았는데 2개를 확인하였고 하나는 감염된 컴퓨터가 있어서 직접 조사할 수 있었으며 다른 하나는 백신 프로그램의 로그를 통해 확인하였다. 인터넷을 찾아보니 굉장히 오래된 악성코드로 보이며 변경된 사항이 있는지는 모르겠지만 기본적인 행위는 비슷해 보인다. 


  당연히 어지간한 안티바이러스 프로그램에서도 탐지가 되긴 하지만 겸사겸사 공부도 할 겸 그리고 불안한 것도 있고 해서 분석해 보기로 했다. 또한 이렇게 오래된 것임에도 불구하고 라이선스 문제 때문인지 안티바이러스 프로그램이 설치되지 않은 여러 공용 컴퓨터들에서 아직까지 살아남아 있는 것을 보면 보안에 무관심한 사람들이 정말 많구나라는 생각도 든다.





1. 분석

1.1 기본적인 내용

  먼저 기본적인 내용부터 살펴보겠다. 이 악성코드는 이미 감염된 컴퓨터에 USB를 꽂을 시 악의적인 행위를 수행한다. 루프문을 돌다가 USB가 꽂아지면 먼저 원본 파일들에 "시스템 파일" 및 "숨김" 속성을 추가하여 일반적인 경우에 보이지 않게 한다. 대신 해당하는 모든 파일들에 상응하는 바로가기 파일들을 각각 생성한다. 그리고 악성 스크립트 파일을 USB에 복사한 후 똑같이 "시스템 파일" 및 "숨김" 속성을 추가한다.


  이후 다른 컴퓨터에서 바로가기 파일이 실행 되면 원본 파일과 동시에 악성 스크립트 파일을 실행시킨다. 악성 스크립트는 이 컴퓨터를 감염시키는데 주된 행위는 백도어와 다른 USB 감염이다. 또한 오토런에도 등록되어 컴퓨터 부팅 시마다 실행된다.


  참고로 안티바이러스 프로그램을 설치하지 않은 경우에는 위와 같이 위험한 상황이 되지만 상당히 오래된 악성코드이기 때문에 기본적으로 백신은 잘 탐지한다. 그래서 USB를 꽂자마자 바로가기 파일들과 VBE 파일이 삭제되는 것을 볼 수 있다. 하지만 문제는 원본 파일들은 계속 숨김 속성을 가지고 있기 때문에 USB를 보면 파일은 하나도 없는데 용량만 차지하는 것을 볼 수 있다. 이 경우에는 해당 폴더에서 이 속성들을 제거해 주어야 한다.



1.2 바로가기 파일

  바로가기 파일을 사용하는 이유는 피해자들이 이것을 USB가 감염되기 전과 같이 원본 파일인로 여기게 하는 것이다. 그래서 아무런 생각 없이 이 바로가기 파일을 실행하게 된다. 앞에서도 설명했듯이 바로가기 파일들은 두 가지 목적이 있다. 하나는 악성 스크립트를 실행시킴으로써 악성코드 트리거의 역할을 하는 것이고 다른 하나는 피해자들이 눈치채지 못하게 원본 파일도 실행시킴으로써 이것이 악성코드에 감염된 것인지를 눈치채지 못하게 하는 것이다.

 

  바로가기 파일 즉 .lnk 파일은 마우스 오른쪽 클릭을 통해 속성을 누른 후 "대상" 탭을 보면 알겠지만 사실 명령어로 이루어졌다고 보면 된다. 일반적인 경우 원본 파일의 경로만 적어져 있지만 이곳에 cmd.exe 즉 명령 프롬프트를 통해 실행 가능한 명령어를 넣은 경우 그 명령어가 실행된다. 이 명령어는 다음과 같은 형태로 되어있다.


C:\windows\system32\cmd.exe /c start MerciJacquieMichel.vbe&start [원본 파일] &exit


  참고로 MerciJacquieMichel.vbe는 여기서 분석하는 분석 대상인 악성 스크립트 파일이다. 이 것을 보면 알겠지만 악성 스크립트와 원본 파일이 동시에 실행되는 것을 볼 수 있다.



1.3 악성 스크립트

1.3.1 기본

  이제 악성 스크립트 파일에 대해서 알아보자. 개인적으로 감염된 컴퓨터들을 조사하다가 ymrimwqtym.vbe라는 것도 발견했다. 이것은 V3의 로그를 통해서 확인했기 때문에 원본 파일을 확보하지 못했고 그래서 분석을 못했다. 하지만 하는 행위를 봤을때 바로가기 파일들을 생성하는 것과 숨기는 것 등 일반적인 행위는 같아 보이므로 이름만 바뀐 것인지 아니면 둘 중 하나가 업그레이드 버전인지 모르겠지만 어쨌든 큰 차이는 없을 것 같다.


  인터넷에서 이름들을 검색해보니 ymrimwqtym.vbe라는 이름을 가진 바이러스는 2014년 말에도 활동한 걸로 보이며 MerciJacquieMichel.vbe는 더 최근인 것으로 보이므로 전자의 바이러스를 업그레이드한 것인지 이름만 바뀐 것인지는 잘 모르겠지만 비슷한 행위를 하는 것은 확실한 것 같다.


  이제 실질적인 분석을 하기로 한다. 먼저 vbe 확장자는 Encoding된 즉 암호화된 VBS(Visual Basic Script)다. VBS는 과거 Visual Basic을 기반으로 만든 스크립트 언어인데 당연히 문법적으로 많이 닮았고 이 외에도 VBA도 비슷한 개념이다. 만약 엑셀을 공부하면서 마이크로소프트 오피스의 VBA(Visual Basic for Application)를 배웠다면 관련 언어에 대해서 문법적으로는 어느 정도 익숙해 졌다고 보면 된다. VB를 이용해 엑셀 등에서 사용할 수 있게 만든 것이 VBA라면 VBS는 원래 자바스크립트와 비슷한 목적으로 만들어졌다가 이후 윈도우에서 컴퓨터를 관리하는 용도로 사용되고 있다. 물론 악성코드에서도 많이 사용되었고 요즘은 악성코드에서든 관리 용도에서든 많이 사용되는 편은 아닌것 같다.


  과거 MS에서는 screnc.exe라는 툴로 vbs를 vbe로 인코딩하는 기능을 제공해 왔다. 하지만 비스타부터는 이 프로그램이 기본 제공이 아니다. 참고로 이 파일을 직접 다운로드해서 사용할 수 있으며 윈도우 7까지는 사용 가능하다고 한다. 물론 인코딩과 디코딩 스크립트를 이용해서 직접 사용할 수도 있다. 다음 링크에서 [ https://gallery.technet.microsoft.com/Encode-and-Decode-a-VB-a480d74c ] 인코딩 및 디코딩 기능이 담긴 vbs 스크립트 파일들을 다운로드 받을 수 있다. 사실 인코딩 관련 내용보다는 난독화와 관련된 내용이 더 중요할 것으로 보인다.



1.3.2 디코딩

  악성코드를 분석하다 보니 이것은 확장자만 VBE지 실제 내용은 VBS였다. 대신 여러 난독화 기법을 이용해 암호화된 것으로 보이게 만든 것이다. 물론 스크립트의 특성 상 일일이 보다 보면 그리고 진행하다 보면 쉽게 원본 코드를 획득할 수 있다. 


  조금 더 자세히 설명하자면 처음 형태는 주석과 수 많은 개행 문자들, 그리고 암호화된 데이터가 존재하지만 동시에 복호화 코드 및 실행 코드도 존재하므로 wscript.echo를 통해 쉽게 받아낼 수 있다. 이 말은 Execute나 ExecuteGlobal 대신 wscript.echo 명령어를 사용하면 실행 되신 결과를 보여주는 메커니즘을 이용한 것이다. 그리고 각 명령어들에서 사용되는 난독화 부분도 마찬가지로 wscript.echo로 출력시키면 결론적으로는 완성된 내용이 나오기 때문에 쉽게 분석할 수 있다. 


  참고로 간단한 난독화 부분이야 한 문장 짜리일 것이고 간단하게 옮겨적으면 되지만 코드 전체는 매우 큰 편이고 명령 프롬프트에 다 안들어올 수 있다. 이럴 경우에는 간단하게 명령 프롬프트에서 다음과 같이 사용하면 된다. cscript.exe는 현재 가상환경이 윈도우 XP여서인지 wscript.exe를 이용한 실행이 되지 않아서 사용했다.


> cscript.exe aa.vbs > bb.txt


  문제는 이렇게 복호화한 내용도 또 다시 암호화되어 있다는 것이다. 앞의 예제와 같이 인코딩된 데이터와 디코딩 함수가 존재하며 난독화된 부분도 존재한다. 난독화된 부분을 조금 보면 이 스크립트 파일을 실행하는 프로세스가 wscript.exe인지를 확인하는 부분이 나온다. 어쨌든 이것을 복호화시키면 또 다른 형태가 나온다. 이것도 마찬가지의 형태이다. 마지막으로 이것을 복호화시키면 드디어 원하는 코드를 얻을 수 있다. 문제는 이 코드도 대부분은 아니지만 조금씩 난독화된 부분이 있는데 이것도 wscript.echo를 이용해 출력하다 보면 쉽게 처리할 수 있다. 어쨌든 스크립트이므로 우리는 쉽게 소스 코드를 획득하게 되었다.



1.3.3 간단한 분석

  이제 디코딩 부분은 건너왔고 실제 수행하는 행위를 알아보겠다. 이것은 전형적인 Backdoor로서 설치된 이후 특정 URL 및 포트를 가지고 C&C 서버와 통신한다. URL을 검사해보니 약 1년 전에 확인된 것으로 보아 적어도 이 버전(MerciJacquieMichel.vbe)은 그 때 즈음에 전파된 것으로 보인다. ymrimwqtym.vbe의 경우에는 검색해보니 적어도 2014년 11월 경에는 탐지된 것으로 보인다.


  어쨌든 서버에서 보내는 명령들에 따라 수행되는 루틴들을 분석해보면 Uninstall부터 시작해서 생각도 못했던 악성코드의 업데이트 그리고 감염된 컴퓨터에 대한 정보(예를들면 운영체제의 버전, 설치된 백신 프로그램 이름 등)를 보내는 등의 기능이 존재한다. 이 외에도 특정 디렉토리 내에 존재하는 파일들 목록이라던가 실행 중인 프로세스들 목록 등도 있고 아예 명령 프롬프트를 이용한 명령어도 실행시킬 수 있다. 가장 눈에 띄는 것은 특정 파일을 다운로드하고 실행시키는 기능이다.  



1.3.4 자세한 분석

  먼저 변수들을 초기화하고 초기화 함수를 실행한다. 이 함수는 다음 레지스트리에 값을 쓴다. 

HKEY_LOCAL_MACHINE\software\[악성코드 이름]\


  값은 "true - [날짜]" 또는 "false - [날짜]"인데 해당 레지스트리 값이 이미 존재하면 true, 존재하지 않는다면 false이다. 이것을 담당하는 변수 이름이 usbspreading인 것으로 보아 USB를 통해 감염된 초기 컴퓨터의 경우 이 값이 존재하지 않기 때문에 true가 될 것이고 다시 실행되었다면 이 값이 이미 존재하므로 이후부터는 false가 될 것이다.


  이후 오토런 레지스트리들에 해당 스크립트를 등록한다.


HKEY_CURRENT_USER\software\microsoft\windows\currentversion\run\[악성코드 이름]

HKEY_LOCAL_MACHINE\software\microsoft\windows\currentversion\run\[악성코드 이름]


  값은 다음과 같다.


wscript.exe //B "\[악성코드 이름]"


  그리고 시작 프로그램 폴더와 Temp 폴더에 현재 스크립트를 복사한다. 막 감염된 경우라면 현재 스크립트는 USB에서 실행된 그 스크립트가 될 것이다.


C:\Users\[사용자 이름]\AppData\Local\Temp\[악성코드 이름]

C:\Users\[사용자 이름]\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\[악성코드 이름]


  이제 초기화의 마지막 단계로서 현재 Temp 폴더에 존재하는 악성코드가 실행 중인지 판단하여 아니라면 그것을 실행하고 종료한다. 초창기에는 USB에서 실행 중일 것이고 이제 컴퓨터 내의 악성 스크립트를 실행시키는 것이다.


  이후 부터는 루프문이다. 이 루프문에서는 설치 함수가 실행되며 POST로 서버의 URL에 현재 컴퓨터의 여러 정보들을 보내며 동시에 명령을 받는다. 참고로 포트 번호는 3030 - 3040의 값을 갖는다. 먼저 설치 함수부터 살펴보겠다.


  설치 함수에서는 앞의 오토런 레지스트리를 등록하고 Temp 및 시작 프로그램 디렉터리에 스크립트 파일을 복사하는 행위를 수행한다. 이후 우리에게 익숙한 루틴이 등장한다. 이동식 드라이브가 현재 삽입되어 있고 거기에 여분의 공간이 있는지를 확인한 후 현재 스크립트 즉 Temp 폴더에 존재하는 스크립트를 USB에 복사하고 속성을 준다. 이 속성은 앞에서도 설명한 "시스템 파일" 및 "숨김" 속성이다. 이후 원본 파일들도 속성을 추가하며 각각 해당하는 바로가기 파일을 만든다. 이 외에도 각 확장자에 맞는 아이콘도 설정해 준다.


  이제 POST를 통해 서버에서 받은 명령어를 실행할 수 있는데 각각 간단한 설명을 넣겠다. "execute"는 인자로 받은 VBS 명령어를 실행하며 "cmd-shell"은 CMD 명령을 받아 실행한다. 원래 이 루프문은 주기가 디폴트로 5초인데 "sleep" 명령을 통해 이 주기를 변경할 수 있다. 그리고 "enum-driver", "enum-faf"와 "enum-process"로 각각 드라이버 목록, 인자로 받은 폴더의 파일들 또는 실행 중인 프로세스들의 목록을 보여준다. 또는 "delete" 및 "exit-process"로 파일 및 프로세스를 삭제 및 종료시킬 수 있다. 이 외에도 "uninstall"로 악성코드를 삭제할 수도 있으며 "update"로 새로운 악성 스크립트를 다운로드 받아 실행시킬 수도 있다. "recv"는 피해자 컴퓨터의 파일을 서버로 업로드하며 "send", "site-send"는 파일을 다운로드 받아 피해자의 컴퓨터에서 실행시킨다.


  아마도 이 악성 스크립트는 공격자가 원하는 실질적인 내용 보다는 악성코드 전파 및 백도어 그리고 실제 악성코드에 대한 다운로더로서의 기능을 가진 것으로 보인다. 당연하게도 이 스크립트를 보면 키로깅이라던지 랜섬웨어, DDoS 등 일반적인 악성코드가 하는 기능은 포함되지 않기 때문이다. 이 이상은 단지 이 악성 스크립트 파일만을 통해서는 알 수 없다.





2. 치료

2.1 원본 파일 복구

  간단하게 설명하자면 원본 파일들은 "숨김" 및 "시스템 파일" 속성이 추가된다. 그래서 볼 수가 없는데 만약 보고싶다면 윈도우 10 기준으로 "폴더 옵션"에서 "보호된 운영 체제 파일 숨기기(권장)"을 해제하고, "숨길 파일, 폴더 및 드라이브 표시"을 체크하면 볼 수 있다. 하지만 이 파일들은 계속 이 속성을 가지고 있기 때문에 이 속성을 해제할 필요가 있다. 그래야 원래의 원본 파일로 돌아올 것이기 때문이다.


  먼저 명령 프롬프트를 실행한다. 그리고 USB 드라이브로 이동하는데, 드라이브의 이름에 따라서 다음 명령어를 수행한다. 본인의 경우 F 드라이브였기 때문에 다음 명령어를 실행한다. 참고로 >는 명령어가 아니라 프롬프트이므로 > 이후의 명령어를 입력하면 된다.


> F:


  왼쪽 프롬프트를 보면 F 드라이브로 이동된 것을 볼 수 있다. 이제 다음 명령어를 통해 현재 폴더와 모든 하위 폴더에 존재하는 파일 및 폴더의 "읽기 전용 속성", "시스템 파일 속성", "숨김 속성"을 제거한다.


> attrib -r -s -h /d /s


  이로써 우리는 간단하게 USB를 원상복구할 수 있다. 즉 악성코드 "MerciJacquieMichel.vbe"와 여러 바로가기 파일들을 삭제하고, 원본 파일들의 속성을 복구시키면 완전히 원래 상태로 복귀된다.



2.2 악성코드 확인 및 종료

  위의 복구 과정을 매번 하기도 귀찮으므로 현재 악성코드가 실행 중인지 검사하며 실행 중인 경우 종료시키는 방법을 설명하겠다. 간단히 말해서 윈도우의 스크립트 파일들은 wscript.exe라는 프로그램을 통해서 실행되며 이 악성 스크립트도 마찬가지이다. 그래서 우리는 Ctrl + Alt + Del 키의 조합을 통해서 프로세스들을 확인하다가 "wscript.exe"라는 이름을 가진 프로세스가 실행 중이라면 종료시키면 된다.


  물론 컴퓨터 관리자가 vbs 스크립트로 컴퓨터를 관리하는 경우라면 함부로 종료시키면 안되겠지만 본인이 사용하는 공용 컴퓨터에서는 이 악성코드가 감염될 때만 저 프로그램이 사용되는 것으로 봐서 그냥 종료시키면 될 것으로 보인다.



2.3 완전히 삭제

  문제는 이 바이러스는 컴퓨터를 부팅할 때마다 자동으로 실행이되게 되어있다. 하지만 우리는 레지스트리를 건들기 귀찮기 때문에 파일이 존재하는 곳으로 가서 삭제만 해주면 된다. 


  시작 프로그램 디렉터리와 Temp 디렉터리로 이동합니다. 위치는 대략 다음과 같다.


C:\Users\[사용자 이름]\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\[악성코드 이름]

C:\Users\[사용자 이름]\AppData\Local\Temp\[악성코드 이름]


  위의 두 경로로 가보면 MerciJacquieMichel.vbe나 ymrimwqtym.vbe라는 이름을 발견할 수 있다. 이렇게 두 경로에 위치한 악성코드를 삭제하면 레지스트리에 오토런으로 등록되어 있다고 하더라도 파일이 존재하지 않으므로 실행되지 않는다.


  물론 레지스트리도 삭제하는 것이 좋기 때문에 아래에 정리하도록 한다. 실행 창 또는 윈도우 10의 경우에는 시작 메뉴를 클릭하고 regedit을 입력하면 해당 프로그램을 볼 수 있다. 이것을 실행하면 이름과 같이 레지스트리(Registry)를 수정(Edit)할 수 있다. 감염된 경우 차례대로 순서를 따라가 보면 다음의 항목들을 찾을 수 있을 것이다.


HKEY_CURRENT_USER\software\microsoft\windows\currentversion\run\[악성코드 이름]

HKEY_LOCAL_MACHINE\software\microsoft\windows\currentversion\run\[악성코드 이름]

HKEY_LOCAL_MACHINE\software\[악성코드 이름]\





3. 결론

  결론은 항상 안티바이러스 즉 백신 프로그램을 설치하는 것이 중요하다는 것이다. 모든 공격 특히 마음먹고 하는 공격은 쉽게 막기 힘들 수도 있지만 일반적인 그리고 오래된 악성코드는 쉽게 막아준다. 항상 보안에 신경쓰는 마인드와 기본적인 보안 프로그램을 설치하는 것만으로도 사이버 환경에서의 피해를 대부분 막을 수 있다.



Posted by SanseoLab



0. 개요

  cmd.exe는 유닉스 계열에서 셸과 비슷한 특징을 갖는다. 셸 스크립트로 리눅스를 관리하듯이 이것을 이용해 윈도우를 관리하기 위해서는 batch 파일 즉 .bat 파일을 이용해 스크립팅을 할 것이다. 하지만 문법을 보면 알겠지만 이것은 초창기 때 부터 한계를 가지고 있었다. 물론 최근에는 파워셸의 등장으로 무엇보다도 강력한 스크립팅이 가능해졌다.


  저 둘 사이에서는 무엇이 사용되었을까를 생각해 보자. WSH (Windows Script Host)는 윈도우에서 스크립팅 기능을 제공해 준다. 이것은 COM (Component Object Model) 인터페이스들의 집합을 expose함으로써 Object Model을 구현한다. 관련 내용을 보기 전에 기본적인 개념을 위하여 다음 링크를 먼저 참고하자. [ http://sanseolab.tistory.com/49 ] WSH는 GUI 모드에서 사용될 때는 wscript.exe를 통해서, CLI 모드에서는 cscript.exe를 통해서 실행된다.


  실제 사용되는 것들을 통해 설명해 보겠다. 앞에서도 설명했듯이 batch의 한계로 인해 윈도우 관리자들은 VBS(Visual Basic Script)를 사용해 왔다. vbs는 VB (Visual Basic) 문법을 이용한 스크립트 언어이다. 우리는 vb 문법과 COM 오브젝트를 사용하는 vbs를 통해서 스크립팅이 가능해 진다. WSH에서 vbs 말고도 무엇이 사용 가능할까.


  윈도우는 JavaScript와 비슷한 JScript를 지원한다. 웹 개발에 대해서는 거의 모르지만 JavaScript는 웹 브라우저에서 사용되며 일반적으로 HTML 내부에 구현되어 있다. 물론 스크립트 파일로서 존재할 수도 있으며 그럴 때에는 .js 확장자를 가진다. 마이크로소프트가 제공하는 JScript는 JavaScript와 거의 같은 것으로 생각된다. 악성코드 분석을 공부하는 입장에서 중요한 부분을 보자면 이 파일도 .js 확장자를 갖는다는 점이다. 확장자도 같고 기능도 거의 같으며 일반적으로 웹에서 사용되는데 굳이 여기에 정리를 하는 이유를 설명하겠다. WSH는 vbs 말고도 js 즉 JScript도 실행할 수 있다.


  이 외에도 Windows Script File 즉 .wsf 파일도 있다. 이것은 약간 특이한데 스크립트 언어는 아니고 확장자 즉 파일 포맷이다. 이 파일 내부에서 vbs와 js를 같이 포함할 수 있다. 즉 각각의 스크립트 언어를 하나로 통합하는데 사용된다. 이 외에도 per이나 python도 포함할 수 있다고 한다.


  지금까지는 WSH에서 사용 가능한 스크립트 및 파일을 알아보았다. 이제 악성코드 분석가의 입장에서 정리해 보려고 한다. 윈도우에는 디폴트로 스크립트 파일을 실행할 수 있는 프로그램 즉 wscript.exe와 cscript.exe가 존재한다. 그렇기 때문에 악성코드 개발자의 입장에서는 바이너리 형태의 악성코드 외에도 스크립트 파일로도 악의적인 행위를 수행할 수 있다.


  vbs는 문법이 vb라는 것이 중요한 점이 아니라 WSH에서 제공해주는 오브젝트를 사용할 수 있다는 것이 중요하다. JScript 또한 마찬가지이다. 이것도 문법은 JavaScript와 비슷하다. 하지만 악성코드에서는 웹에서 사용되는 내용은 의미가 없다. 대신 이 문법을 사용해서 WSH가 제공해주는 오브젝트를 사용할 수 있다는 점이 중요하다. .wsf 파일은 이러한 스크립트 언어를 포함하며 WSH에서 실행시킬 수 있는 파일 포맷이기 때문에 악성코드에서 자주 사용된다. 정확한 정보인지는 모르겠지만 batch 파일과 파워셸을 제외한 스크립트 형태로 존재하는 악성코드의 경우 .vba 또는 .wsf 파일 포맷이 사용되며 .js로는 잘 존재하지 않는다고 한다.


  물론 이러한 스크립트 언어들은 바이너리가 아니기 때문에 난독화가 필요하다. 마이크로소프트에서는 vbs 파일의 경우 난독화를 지원하며 난독화된 파일은 .vbe라는 확장자를 갖는다. JScript 또한 마찬가지인데 .jse라는 확장자를 갖는다. 참고로 JavaScript는 .js 확장자 외에 인코딩된 형태의 파일 포맷은 지원되지 않는다.


  정리해 보자면 윈도우에서 제공하는 WSH로 인해서 스크립트가 실행 가능하다. 이에 따라 악성 스크립트는 VBS를 이용해 만들어진 .vbs(인코딩된 경우에는 .vbe), JScript를 이용한 .js(인코딩된 경우에는 .jse), 그리고 이 두 가지 언어가 사용될 수 있는 .wsf 파일 포맷으로 존재한다.


  다음으로는 .hta 파일을 알아보자. 이것은 HTML Application 파일이다. 기본적으로 hta파일은 html과 같은데 이것은 html 파일을 만든 후 확장자를 .hta로 변경하여도 상관 없다는 의미이다. 차이점은 html은 기본적으로 웹 브라우저를 이용해 실행되며 hta 파일은 mshta.exe라는 개별 프로그램이 이것을 실행시켜 준다는 점이다. 악의적인 스크립트를 포함하는 html은 웹 브라우저로 실행시켜도 브라우저의 자체적인 보안 기능 하에 실행되어 수행하는 능력에 한계가 있다. 하지만 mshta.exe는 아무런 보안 기능을 제공하지 않기 때문에 악의적인 내용의 스크립트가 hta 파일에 포함되어 있는 경우 자유롭게 실행될 수 있다. 일반적으로 이 hta 파일 내부에 스크립트로 악성코드를 다운로드하고 실행시키는 루틴을 작성하는 방식으로 사용된다. html 형태이기 때문에 <script></script>에 js 뿐만 아니라 당연히 vbs도 포함할 수 있다.


  참고로 mshta는 보안상으로 문제가 있는데, 악성 hta(내부에 악성 vbs나 js를 포함하는)을 만들고 일반 파일에 붙인 후 해당 파일의 확장자를 hta로 바꾸면 붙여넣은 악성 스크립트가 실행된다. 즉 다음 명령처럼 jpg 파일에 악성 hta 파일을 붙여넣으면 생성된 파일의 끝 부분을 확인하지 않는 이상 정상 jpg 파일로 생각될 수 있다. (아예 jpg 파일로 유포하고 이후 hta로 바꾸어 실행시키는 악성 행위도 존재한다)


> copy /b normal.jpg+mal.hta malware.hta


  그리고 chm 파일도 있다. 이것은 Compiled HTML이다. 여러 개의 html 파일과 그림을 압축하여 하나의 파일로 합친 압축된 html 파일로서 일반적으로 소프트웨어 설치 시 도움말 파일을 생각하면 익숙할 것이다. 내부에 당연히 html이 존재할 것이고 파일 실행 시에 html이 읽어진다. html의 OBJECT 태그를 이용해 ActiveX 컨트롤을 생성한 후 HHClick() 메소드를 이용한다. 참고로 .chm은 더블 클릭으로 파일 실행 시 hh.exe라는 개별 프로그램이 실행시켜 준다.


  마지막으로 batch 파일은 건너뛰기로 하고 파워셸이 있는데 이것은 다른 문서에서 [ http://sanseolab.tistory.com/29 ] 자세히 다룬다.



1. Visual Basic

  여기서는 Visual Basic과 관련된 전체 개념을 정리하며 악성코드에서 사용되는 형태를 간단한 예제와 함께 정리한다. Visual Basic은 과거에 사용되어 왔던 언어이다. VB 6.0까지 존재하다가 이제는 더 이상 지원이 되지 않으며 대신 VB .net의 형태로 새롭게 만들어졌다. 언어로서의 VB .net 및 VB 6.0은 여기서 다루지 않는다. 대신 아직까지도 존재하는 여러 형태들에 대해서 알아보려고 한다.


  먼저 가장 익숙한 것은 VBA일 것이다. Visual Basic for Application으로서 엑셀 등의 마이크로소프트 오피스에서 사용되는 간단한 형태의 매크로 언어이다. 이것은 매크로 악성코드라는 이름으로 알려져 있는데 문서 파일을 통해 전파되며 문서를 열 때 실행되어 감염시킨다.


  다른 형태로는 VBS가 있다. 이것은 자바스크립트와 같은 개념으로 여기면 될 것이다. 즉 웹 브라우저에서 사용 가능한 언어이다. 이것은 원래 HTML에서 사용되기 위한 것이었지만 WSH를 통해 자체적으로도 실행될 수 있다.


  어쨌든 VB 및 VBA, VBS는 문법적으로는 많은 부분이 비슷한 것을 알 수 있다. 여기서는 다른 용도보다는 악성코드에서의 모습을 다루기로 하겠다. 난독화나 vbe 같이 암호화된 형태는 나중에 다루도록 한다.



1.1 VBS 기본 문법

  사실 이 부분은 그냥 간단히 정리한 것이므로 굳이 여기에 쓸 필요는 없지만 쓸 곳이 여기밖에 없어서 우선 이곳에 적어놓기로 한다.


- 주석 : 첫 부분에 '가 사용된다. 끝은 없고 라인으로 구분하는 듯 하다.


- function과 sub의 차이 : 참고로 시작은 function AAA () 같이 시작하고 끝은 end function 이렇게 사용한다. 이것은 sub BBB () 그리고 end sub 같이 sub도 같다. sub은 그냥 실행되는 것이지만 function은 반환 값이 존재한다. sub도 인자를 받는다는 것이 특징이다. 그리고 function은 반환하는 방법이 특이한데 정말 간단하게 "함수명 = 반환값" 식으로 한다. 아예 함수 이름에 반환값을 넣는 것이다.


- 지역 변수 및 전역 변수 : sub과 function이 존재하므로 지역 변수와 전역 변수의 차이가 있을 것이다. sub이나 function 밖에서 선언하면 전역 변수이며 내부에서 선언하면 지역 변수이다.


- 제어문 : 조건문은 "if .. then / else / end if"이다. "if .. then / elseif .. then / end if"도 있다. then은 if나 elseif인 경우에 뒤에 붙는다. 반복문은 "for .. to .. / next"이다. "for each .. in .. / next"도 있다. 그리고 "while .. / wend"도 있다. end if와 next는 끝맺음을 의미한다.



1.2 자주 사용되는 Object 및 기본 예제

  Object의 경우 WScript.Shell (WScript Object), Shell.Application (Shell Object), Scripting.FileSystemObject, msxml2.xmlhttp 등이 사용되는 것 같다. WScript.Shell의 경우 specialfolders, expandenvironmentstrings, run, createshortcut, regread, regdelete, regwrite, exec, scriptname, sleep 등이 있으며 Scripting.FileSystemObject의 경우 folderexists, opentextfile, drives, fileexists, copyfile, getfile, getfolder, deletefile, deletefolder 그리고 msxml2.xmlhttp의 경우에는 open, setrequestheader, send, responsetext 등이 자주 사용된다.

  참고로 이 외에도 Object는 아니지만 주로 난독화에 사용되는 기본 함수들을 보자면 Replace, Chr, ChrW 등과 숫자들에서는 Int, Rnd도 사용되는 것으로 보인다. 


  다음은 각각 WScript Object 및 Shell Object를 이용한 예제이다.

---- wscript.vbs -------------------------------------------------------------

dim shellObj

set shellObj = CreateObject("WScript.Shell")

shellObj.run "notepad.exe"

--------------------------------------------------------------------------


---- shell.vbs ---------------------------------------------------------------

dim shellObj

set shellObj = CreateObject("Shell.Application")

shellObj.ShellExecute "notepad.exe"

--------------------------------------------------------------------------



2. JScript

  JScript의 경우에는 따로 정리할 것이 없다. 이것 또한 Object를 이용하여 시스템을 제어하는 방식이 중요할 뿐이고 문법은 크게 의미가 없기 때문이다. 간단히 정리해서 기본 JS 인터프리터가 지원해주지 않는 기능은 ActiveX 오브젝트 즉 ActiveXObject() 또는 COM 오브젝트 즉 Wscriipt.CreateObject() 같은 외부 객체를 이용한다.


  JScript의 경우도 난독화와 암호화된 형태인 .jse는 다음에 정리하도록 한다. 다음의 예제도 위와 같이 Object를 이용한 내용만 존재하기 때문에 크게 의미는 없지만 테스트 용으로 간단히 적어본다.


---- wscript.js --------------------------------------------------------------

var shellObj = new ActiveXObject("WScript.Shell");

var runProgram = "notepad.exe";

var retVal = shellObj.run(runProgram);

--------------------------------------------------------------------------


---- wscript.js --------------------------------------------------------------

var shellObj = new ActiveXObject("Shell.Application");

var runProgram = "notepad.exe";

shellObj.ShellExecute(runProgram);

--------------------------------------------------------------------------



3. WSF

---- aaa.wsf ----------------------------------------------------------------

<package>

<job id="Test">

<script>

var shellObj = new ActiveXObject("Shell.Application");

var runProgram = "notepad.exe";

shellObj.ShellExecute(runProgram);

</script>

</job>

</package>

--------------------------------------------------------------------------



4. wsh

---- bbb.wsh ---------------------------------------------------------------

[ScriptFile]

Path=C:\Users\aaa\Desktop\aaa.wsf

[Options]

Timeout=0

DisplayLogo=1

BatchMode=0

--------------------------------------------------------------------------

  직접 실행시킬 수는 없지만 다른 스크립트를 실행시켜줄 수는 있다.



5. etc

  정확한 이유는 파악하지 못했지만 IE(Internet Explorer)에서는 JavaScript와 VBScript 모두 Shell.Application 객체를 이용한 ShellExecute() 사용이 불가능하다. 디버깅 해보면 권한이 없다고 나온다. 대신 JavaScript와 VBScript 모두 Wscript.Shell을 이용한 Run은 가능하다. 물론 활성화를 직접 해주어야 하지만 ShellExecute() 함수는 아예 불가능하다는 것이 특징이다. 그러므로 직접 JavaScript나 VBScript 형태로 두던지 아니면 hta 확장자로 만들어서 mshta.exe로 실행시키면 된다.



Posted by SanseoLab


0. 개요

  여기서는 가상 머신이나 에뮬레이터 같이 바이너리를 실행할 수 있는 메커니즘을 이용한 악성코드 분석 자동화에 대해서 정리하려고 한다. 이 개념들이 비슷한 면이 있으면서도 차이점 또한 크므로 어떻게 정리해야 할지 모르겠다. 그래서 가장 큰 분류를 자동화로 잡고 정리하기로 한다. 어떤 종류의 자료들은 그 특징에 따라 오픈 소스 및 문서화가 잘 되어 있지만 다른 어떤 종류의 자료들은 취업하지 않는 이상 아무런 자료를 찾을 수 없어보인다. 가장 먼저 개념을 설명할 것이고 이후 큰 분류부터 시작하겠다.





1. 개념

  샌드박스, 에뮬레이터, 가상 머신 이것들은 서로 대응하는 개념이 아니어서인지 굉장히 다양한 의미로 사용되는 것 같다. 사실 이렇게 정리하는 입장에서도 확신을 가질 수가 없으며 다른 사람들도 마찬가지인 것 같다. 하지만 어떻게 해서든 정리를 해보려고 한다.


  먼저 가장 넓은 개념이라고 할 수 있는 샌드박스부터 보겠다. 요즘 악성코드라던지 취약점을 이용한 APT 공격 등은 갈수록 복잡해지고 있으며 과거처럼 정적인 분석만으로는 충분치 않다. 하지만 동적 분석이라는게 사람이 하는 일인데 수 많은 바이너리들을 직접 분석하는데에는 한계가 있을 것이다. 샌드박스라는 것은 간단히 말해서 제한된 환경을 제공해 주는데 보안에서는 이 샌드박스에서 악성코드를 직접 실행시키는 방식으로 분석하는데 사용된다. 물론 실행되는 악성코드를 모니터링하는 도구들이 내부에서든 외부에서든 담겨 있어야 할 것이다.


  앞에서는 간단하게 악성코드 실행 시 처럼 실제 환경에 영향을 받지 않는 독립적인 환경을 제공한다고만 말했다. 실제로는 다양한 의미로 더 많이 사용되는 것 같지만 먼저 자세한 설명을 한 후에 언급하도록 하겠다. 


  샌드박스를 구현하는 방식을 알아보기 전에 에뮬레이터와 가상화에 대해서 설명하겠다. 에뮬레이터란 CPU 등의 하드웨어를 순수하게 소프트웨어로 구현한 방식이다. 예를들어 고전 게임들의 경우 x86 CPU에서 실행되던 것들이 아니다. 그래서 이러한 게임을 실행하기 위한 에뮬레이터가 존재하는데 이를 실행하기 위해서는 그 게임기에 맞는 가상의 CPU를 구현한 후 여기에 해당 게임을 실행시키는 방식이 사용된다. 이렇게 전혀 다른 CPU라고 하더라도 소프트웨어로 구현할 수 있기 때문에 별다른 제약이 없다. 하지만 모든 것을 소프트웨어로 구현한다면 성능적으로 많이 떨어진다. 만약 CPU 등 하드웨어를 직접 사용할 수 있다면 성능 상으로 많은 이점이 있을 것이다. 물론 CPU가 이러한 기능을 제공해야 하며 하드웨어를 직접 사용하기 때문에 같은 아키텍쳐를 가진 CPU에만 해당하는 방식일 것이다. 어쨌든 이러한 방식을 전가상화, 반가상화 등으로도 나뉘는 가상화 방식이다.


  사실 에뮬레이터든 가상화 방식이든 실제 하드웨어가 아니라 하드웨어를 흉내내어 지원하는 방식은 큰 의미로 가상 머신이라고 불린다. 물론 좁은 의미에서는 VMware, VirtualBox 같은 것들을 가상 머신이라고 부른다.


  물론 이러한 분류 외에 다른 분류도 존재한다. 말 그대로 가상 머신을 통해 운영 체제를 설치할 수 있는 환경을 제공하는 것 뿐만 아니라 단순히 하나의 프로세스만을 실행시키는 것도 있다. 즉 와인 처럼 리눅스에서 윈도우의 바이너리를 실행시킬 수 있게 해주는 도구가 그러한 예이다.


  이제 여기서 설명할 보안적인 의미에서 이것을 알아보겠다. 여기서도 앞 처럼 샌드박스를 두 종류로 나누어서 설명한다. 하나는 가상화이고 다른 하나는 에뮬레이션이다.


  에뮬레이션은 두 가지로 나뉠 수 있는데 하나는 시스템 호출 에뮬레이션(OS 에뮬레이션)이고 다른 하나는 시스템 에뮬레이션이다. 시스템 호출 에뮬레이션은 운영 체제 환경을 에뮬레이션하고 시스템 호출을 후킹해서 운영 체제에 넘기지 않고 직접 값을 반환해주는 방식이다. 시스템 에뮬레이션은 CPU나 메모리 같은 하드웨어를 가상으로 구현하고 운영체제를 설치한 후에 이것을 통해 실행시킨다. 에뮬레이터 우회 관련 문서들을 보면 안티바이러스에서 사용되는 x86 및 스크립트 용도의 에뮬레이터는 시스템 호출 에뮬레이터로 보인다.


  가상화 방식은 일반적으로 VMware 같은 가상 머신을 이용하는 방식일 것이다. 참고로 에뮬레이터와 달리 이 방식은 실제 OS에서 실행시키기 때문에 악성코드 즉 애플리케이션의 정보만을 모으는 방법은 쉽지 않을 것이다. 주로 사용되는 방식은 시스템 호출 관련 정보를 모으는 것인데 시스템 호출을 기록하는 방식은 두 가지가 있다. 하나는 현재 운영 체제에서 도구를 이용하는 방식으로서 후킹 엔진 인젝션이 그 예일 것이다. 하지만 이것은 많은 정보를 얻을 수 있더라도 악성코드가 탐지할 수 있다고 한다. 다른 하나는 하이퍼바이저를 감시하는 것이다. 가상 머신이므로 시스템 호출 시 권한 때문에 하이퍼바이저에게 통지가 가기 때문이다. 사실 이 부분은 처음 들은 데다가 실제로 사용되는 방식인지는 잘 모르겠다.





2. 실제 예시

  안티바이러스 벤더들을 비롯한 보안 업체들은 수 많은 악성코드들을 수집하고 분석해야 한다. 하지만 요즘 날마다 쏟아지는 바이너리가 한 두개가 아닐 것이므로 모든 샘플들을 사람이 일일이 분석하기는 힘들 것이다. 그렇기 때문에 어느 정도의 자동화가 필요한데 이를 위해서 회사 내에서 수집한 악성코드들을 자동으로 분석하기 위한 시스템이 존재할 것이다. 이러한 시스템은 각각의 회사 자체적으로 구축할 것이기 때문에 공개된 자료는 거의 없는 것으로 보인다. 대신 Cuckoo 샌드박스 등이 오픈 소스로 존재하여 이것을 통해 개인도 어느 정도의 자동 악성코드 분석 시스템을 구축할 수 있는 환경을 제공한다. 아마 보안 업체들도 이와 크게 다르지는 않을 것이다.


  분석에는 기본적으로 정적 분석 및 동적 분석이 필요할텐데 정적 분석이야 쉽게 처리할 수 있겠지만 분석할 바이너리들이 한 두가지가 아닐 것이므로 정적 분석을 위한 솔루션이 구축되어야 할 것이며 동적 분석은 더더욱 필요할 것이다. 다시 말해서 가상 머신 등을 기본으로 하여 앞에서도 언급했듯이 API 호출 등의 로그를 기록하기 위한 후킹 엔진 인젝션 외에도 패킷 로그를 기록하기 위한 프로그램 등 여러 보안 프로그램들이 필요할 것으로 보인다. 어쨌든 Cuckoo 샌드박스, Joe 샌드박스 등 많은 샌드박스가 있으며 이러한 샌드박스들은 보통 가상 머신을 이용하며 여러 가상 머신들을 지원한다. 꼭 이렇게 가상 머신 등 다른 제품들을 사용하는 형태가 아니더라도 회사에서 자체적으로 필요한 부분들을 구축할 것이다. 이러한 시스템은 악성코드를 분석하는데 큰 제약 조건이 없을 것으로 보인다.


  참고로 어떤 샌드박스에서는 굳이 가상 머신 환경이 아니라 실제 머신 환경을 이용하여 분석할 수 있는 기능도 존재한다고 한다. 이것은 많은 악성코드들이 가상 머신만의 특징을 이용하여 바이너리가 실행되는 환경이 가상 머신인지 탐지하고 우회하는 루틴을 가지고 있기 때문인 것으로 보인다.


  최근에는 APT로 인해서 안티 APT 솔루션들도 많이 존재한다. 이러한 제품들의 경우 APT에 대해 일반 안티바이러스 보다는 더 나은 성능을 보여주어야 한다. 그렇기 때문에 수집한 바이너리를 솔루션의 가상 머신을 이용하여 동적 행위 기반 분석을 수행할 수 있는 기능이 제공된다. 물론 APT이기 때문에 문서 파일이나 이메일 등 여러 많은 기능들이 추가되어 있겠지만 여기서는 가상 머신을 이용한 행위 기반 분석이 사용된다는 것에 중점을 두려고 한다. 참고로 FireEye의 경우에는 샌드박스를 자체적으로 제작하였다고 하는데 여러 가상 머신들을 이용하는 다른 샌드박스 제품들과 비교하는 것을 보면 에뮬레이터 보다는 가상 머신에 더 가까운 바이너리 실행 메커니즘을 구현한 것으로 보인다.


  마지막으로 안티바이러스가 있다. 성능 외에도 자원을 적게 점유하는 것 또한 중요한 안티바이러스에서 실시간 검사를 수행하는데 가상 머신까지 설치해서 동적 행위 기반 분석을 수행하기는 힘들 것이다. 앞의 자동 악성코드 분석 시스템이나 안티 APT 솔루션의 경우 개별 솔루션으로 존재하지만 안티바이러스는 사용자의 컴퓨터에서 실행되기 때문이다. 다른 문서[ http://sanseolab.tistory.com/35 ]에서도 정리했지만 HIPS 방식을 사용하지 않는 안티바이러스에서 동적 행위 기반 분석 기능을 제공할 수 있는 것은 에뮬레이터가 최선으로 보인다. 물론 에뮬레이터도 아주 간단한 형태부터 시작해서 종류가 많겠지만 아무리 성능이 좋다고 하더라도 가성 머신에서 직접 바이너리를 실행시키는 것 보다는 한계가 많을 수 밖에 없을 것이다.





4. 결론

  사실 가상 머신 우회를 정리하려던 중 제대로된 정리를 원해서 계속 찾아가다 지금까지 오게 되었다. 앞에서도 말했듯이 제대로된 자료가 거의 존재하지 않기 때문에 지금까지 정리한 내용이 맞는지도 잘 모르겠다. 원래 보안 제품들이 그런 면이 있지만 이쪽은 정말 부족한 것 같다. 물론 가상 머신 우회나 안티 에뮬레이션 같은 구체적인 자료들은 많이 존재하기 때문에 우선 이렇게만 정리한 후에 구체적인 내용들을 정리하고자 한다.





5. 참고 자료

i. Full System Emulation: Achieving Successful Automated Dynamic Analysis of Evasive Malware - Christopher Kruegel. Lastline, Inc.

Posted by SanseoLab

0. 개요

[ http://sanseolab.tistory.com/13 ]

[ http://sanseolab.tistory.com/33 ]

[ https://github.com/101196/simpleAVdriver ]


  앞의 글들과 참조하면서 손대본 코드들을 통해서 보안 관련 드라이버 개발에 대해서 조금씩 배웠고 이제 간단한 프로젝트를 통해서 구현해보려고 한다. 비슷한 부분도 많고 더 추가된 부분도 많다. 드라이버 개발 환경 구축 및 기본 설정들과 관련해서는 꼭 위의 문서들을 읽어볼 필요가 있으며 여기서는 간단하게 소스 코드 설명만 하려고 한다.


  이 프로젝트의 이름은 ProcLogger이며 간단하게 프로세스의 생성 및 종료를 기록하는 역할 및 기타 기능을 수행한다. 각종 프로세스의 시작 및 종료 및 기타 정보들에 대한 기록을 남기며 보호할 프로세스를 설정할 수 있다. 동시에 드라이버의 자체 레지스트리 보호도 존재한다. 소스 코드는 다음 주소이다.


[ https://github.com/101196/ProcLogger ]





1. Registry Monitor

  Registry Monitor에서는 이 드라이버와 관련된 설정이 들어있는 레지스트리를 보호한다. 정확히 말하자면 아예 이 레지스트리에 대한 오픈을 차단하는 방식이다. 다른 프로세스는 물론이고 regedit.exe에서도 해당 키를 클릭한 경우 접근이 거부되는 것을 알 수 있다. [ ref : https://github.com/markjandrews/CodeMachineCourse/tree/master/source/kerrkt.labs/labs/HideReg ]


  ProcLogger.c를 보면 DriverEntry()에서 InstallRegMonitor()를 통해 등록하며 DriverUnload() 루틴에서 UnInstallRegMonitor()를 통해 제거한다. RegMonitor.h에는 함수 선언만 들어가 있으므로 실질적인 구현은 RegMonitor.c를 보면 된다. 


  InstallRegMonitor() 함수부터 살펴보면 윈도우에서 제공되는 CmRegisterCallbackEx()를 통해 레지스트리 콜백 루틴 즉 RegistryFilterCallback()을 등록하는 것을 볼 수 있다. CmRegisterCallbackEx()는 콜백 루틴을 등록하여 레지스트리에 대한 작업이 이루어지기 전에 이 루틴이 호출될 수 있게 한다. 그러므로 레지스트리에 대한 작업이 이루어질 때 마다 호출되는 이 콜백 루틴이 중요하다. 먼저 CheckProcess() 함수를 호출한다. 이 함수는 레지스트리 작업을 수행하는 프로세스가 services.exe 또는 svchost.exe라면 TRUE를 반환하여 더 이상의 작업을 하지 않는다. 만약 다른 프로세스의 경우에는 다음 조건문이 실행된다. 


  여기서는 Argument1 즉 레지스트리에 대한 작업이 RegNtPreCreateKeyEx 또는 RegNtPreOpenKeyEx인 경우에만 작업을 수행한다. 그리고 Argument2를 통해 RootObject와 CompleteName을 넣고 RegPreOpenKey() 함수를 호출한다. 이것들은 각각 루트 레지스트리 키를 나타내는 레지스트리 키 오브젝트에 대한 포인터와 레지스트리 키의 경로 및 이름을 나타낸다. 참고로 경로는 상대적일 수도 있고 절대 경로일 수도 있다.


  RegPreOpenKey() 함수에서는 앞에서도 말했듯이 경로가 상대 경로일 수도 있고 절대 경로일 수도 있기 때문에 추가적인 작업을 수행한다. 만약 RootObject가 유효하다면 이것은 CompleteName이 전체 경로를 가지고 있다는 의미이므로 간단하게 CheckPolicy() 함수에 이 CompleteName을 넣는다. 아닌 경우에는 복잡한데 CmCallbackGetKeyObjectID()를 이용한다. 이 함수에 RootObject를 넣고 RootObjectName으로 경로를 받아온다. 이후 CompleteName이 유요한지 검사한 후, 유효하다면 받아온 경로 즉 RootObjectName과 CompleteName을 연결하고 CheckPolicy()에 넣어 호출하며 유효하지 않다면 RootObjectName만 넣어서 호출한다.


  지금까지는 CheckPolicy()에 넣을 제대로된 경로를 만드는 작업이었다.  참고로 우리는 "\\REGISTRY\\MACHINE\\System\\CurrentControlSet\\Services\\ProcLogger" 등의 문자열을 미리 정의해 놓았다. 이 레지스트리 값들은 일반적으로 드라이버가 등록될 때 설정 정보가 저장되는 위치이다. CheckPolicy()는 앞에서 정의한 이 문자열들과 인자로 받은 경로를 비교한다. 전체적으로 정리해 보자면 services.exe 또는 svchost.exe가 아닌 프로세스에서 RegNtPreCreateKeyEx 또는 RegNtPreOpenKeyEx의 작업을 수행할 때 이 대상이 되는 레지스트리 값이 ProcLogger 드라이버라면 작업을 거부하는 것이다. 즉 CheckPolicy()에서 Matched가 TRUE가 되며 이 경우 결국 콜백 함수에서는 STATUS_ACCESS_DENIED를 반환하여 접근 거부가 되게 하는 것이다.





2. ProcLogger

  Logger에서는 프로세스의 생성과 종료를 감시하여 텍스트 파일에 저장한다. ProcLogger.c를 보면 DriverEntry()에서 InstallProcLogger()를 통해 등록하며 DriverUnload() 루틴에서 UnInstallProcLogger()를 통해 제거한다. loggers.h에는 함수 선언만 들어가 있으므로 실질적인 구현은 loggers.c를 보면 된다. 


  InstallProcLogger() 함수부터 살펴보면 윈도우에서 제공되는 PsSetCreateProcessNotifyRoutineEx()를 통해 콜백 루틴을 등록한다. 간단하지만 그래도 눈여겨 볼만한 것은 콜백 루틴에서 받는 인자들이다. PEPROCESS Process, HANDLE ProcessId, PPS_CREATE_NOTIFY_INFO인데 각각 생성 또는 종료되는 프로세스에 대한 포인터 그리고 PID 마지막으로 생성되는 경우에만 존재하는 CreateInfo이다. 이것들을 각각 이용할 것이며 CreateInfo의 경우에는 생성될 때만 존재하기 때문에 종료시에는 생성 시 보다 로그에 기록되는 정보가 적을 것이다. 


  소스 코드와 간단한 주석만으로도 쉽게 이해할 수 있는 내용이다. 시간의 경우 KeQuerySystemTime(), ExSystemTimeToLocalTime(), RtlTimeToTimeFields()를 통해 구했다. Integrity Level은 PEPROCESS Process을 인자로 넣고 PsReferencePrimaryToken() 및 SeQueryInformationToken()을 통해 구한 후 값을 비교하여 가각의 레벨에 맞게 정의하였다. 참고로 생성인지 종료인지는 CreateInfo가 NULL 값을 갖는지에 대한 여부로 판단하였다.


  우리는 인자를 통해 프로세스의 PID를 알 수 있으며 CreateInfo->ImageFileName로 생성 시에는 이미지의 경로도 알 수 있다. 그리고 부모 프로세스와 생성자 프로세스의 경우에는 각각 CreateInfo->ParentProcessId, CreateInfo->CreatingThreadId.UniqueProcess를 통해 알 수 있고 이것을 가지고 GetProcessNameFromPid()를 호출하여 프로세스 이름도 알 수 있다.


  로그 파일은 ZwCreateFile() 및 ZwWriteFile()을 통해 기록된다. 결국 로그에는 다음과 같이 기록될 것이며 각각 생성 또는 종료 시에 해당한다.


[ 날짜 및 시간 ] [ 생성 또는 종료 ] [ 이미지 경로 ]  [ 프로세스 이름 (PID) ] [ 부모 프로세스 이름 (부모 PID) ] [ 생성자 프로세스 이름 (생성자 PID) ] [ Integrity Level]

- 날짜 및 시간 :  년:월:일:시:분:초

- 생성 또는 종료 : CREATE / EXIT

- Integrity Level :  LOW / MEDIUM / HIGH / SYSTEM


[ 날짜 및 시간 ] [ 생성 또는 종료 ] [ N/A ]  [ 프로세스 이름 (PID) ] [ N/A ] [ N/A ] [ Integrity Level]

- 날짜 및 시간 :  년:월:일:시:분:초

- 생성 또는 종료 : CREATE / EXIT

- Integrity Level :  LOW / MEDIUM / HIGH / SYSTEM


  실제 결과는 다음과 같다.






3. SelfProtect

  Self Protect에서는 보호하고자 하는 프로세스의 이름을 적는다. 참고로 이전 문서에서도 언급했듯이 드라이버가 로드된 상태에서 해당 프로세스를 실행시키면 제대로 실행이 되지 않기 때문에 드라이버 로드 이전에 프로세스가 미리 실행 중이어야 한다. [ ref : https://github.com/KKamaa/Driver-Loader/tree/master/ProtectDriver/ProtectDriver ]


 ProcLogger.c를 보면 DriverEntry()에서 InstallSelfProtect()를 통해 등록하며 DriverUnload() 루틴에서 UnInstallSelfProtect()를 통해 제거한다. SelfProtect.h에는 함수 선언만 들어가 있으므로 실질적인 구현은 SelfProtect.c를 보면 된다. InstallSelfProtect() 함수부터 살펴보면 윈도우에서 제공되는 ObRegisterCallbacks()를 통해 프로세스 핸들 작업 이전과 이후에 호출되는 루틴을 등록한다. 제거는 ObUnRegisterCallbacks()를 통해 수행한다.


  ObRegisterCallbacks() 함수는 스레드, 프로세스 그리고 데스크탑 핸들 오퍼레이션 시에 호출되는 콜백 루틴들을 등록해 준다. 구체적으로 예를들어 보자면 어떤 프로세스가 보호하려는 프로세스를 종료시키고 싶다고 하자. 이 경우에는 특정한 행위를 수행하기 위해 먼저 핸들을 얻을 것이다. 등록된 콜백 함수는 이렇게 핸들을 얻을 때 호출된다. 핸들을 얻을 때는 대상에 대한 접근 권한을 설정하는데 이 콜백 함수는 얻으려는 접근 권한 중에서 특별한 것들을 제거할 수 있다. 이로써 다른 프로세스가 보호받는 프로세스를 종료하기 위해 핸들을 얻고 종료시키려고 하지만 종료할 수 있는 권한이 제거되어 있기 때문에 종료가 불가능하게 된다. 물론 Handle Operation 이전 뿐만 아니라 이후에 호출되는 콜백 루틴도 등록할 수 있다.


  이 예제에서는 먼저 이름이 "notepad.exe"인 프로세스에 대한 핸들을 얻을 때 PROCESS_TERMINATE, PROCESS_VM_READ, PROCESS_VM_WRITE, PROCESS_VM_WRITE 권한을 제거한다. PROCESS_TERMINATE 권한 제거로 인해 다른 프로세스에서 이것을 종료시킬 수 없게 된다. 나머지 3개는 DLL 인젝션 시에 많이 본 권한일 것이다. DLL 인젝션 시에는 DLL을 삽입할 프로세스의 메모리를 조작할 필요가 있기 때문에 해당 권한이 필요했다. 하지만 이 권한을 제거함으로써 보호받는 프로세스는 다른 프로세스에 의해 종료될 수도, DLL 인젝션 공격을 받을 수도 없게 된다.



Posted by SanseoLab



  안티바이러스(백신)에서 사용하는 악성코드 탐지 메커니즘은 종류가 너무 많이 있기 때문에 한 가지 방식으로 분류할 수는 없는 것 같다. 참고로 아래에서 말하는 것들 보다 훨씬 많은 종류가 있지만 여기에서는 개인적인 공부를 위해 필요한 부분만 정리하였고 계속 추가해 나갈 생각이다. 또한 여기에서는 악성코드만 다루었지만 이 외에도 패킷 즉 네트워크, 그리고 같은 악성코드 안에서도 랜섬웨어 등 형태에 따라 탐지 방식이 달라질 것이다.


  첫 번째 분류 방식에서는 On-Demand 및 On-Access 방식으로 나눈다. On-Demand 방식은 검사를 위해 안티바이러스에서 버튼을 누른다던지 하는 방식을 통해 진행된다. 물론 주기적으로 특정 시간에 검사를 진행하게 하는 것도 여기에 속할 것이다. On-Access 방식은 안티바이러스의 실시간 검사의 개념이다. 말 그대로 실시간으로 검사하여 컴퓨터 사용 중 파일이 생성되거나 또는 실행될 때 같이 관련 이벤트가 있을 때 마다 감시하는 방식이다. 일반적으로 대부분의 안티바이러스는 항상 실시간 검사를 이용해 감시하며 On-Demand 방식은 정밀 검사 같은 방식을 통해 제공되는 것으로 보인다.


  두 번째 분류 방식으로는 시그니처 기반과 행위 기반 즉 휴리스틱이 있다. 시그니처 기반 방식은 말 그대로 악성코드가 판정나면 이것의 시그니처를 저장해 둔 후 검사 시에 파일과 악성코드 시그니처들을 비교하는 방식이다. 이것도 구현 방식 뿐만 아니라 종류가 매우 많지만 여기서는 이렇게 간단하게만 정리한다. 


  행위 기반은 크게 두 가지로 나뉜다. 하나는 정적 휴리스틱이며 다른 하나는 동적 휴리스틱이다. 정적 휴리스틱의 경우에는 파일을 분석하여 의심스러운 특징을 찾아내어 판단하는 방식이다. 동적 휴리스틱의 경우에는 두 가지 방식으로 나뉘는 것 같다. 하나는 HIPS(Host based Intrusion Prevention System)에서 사용되는 방식이다. 이 기능을 순수하게 휴리스틱 방식으로 사용하는 안티바이러스가 있는가 하면 HIPS로 분리하여 보여주는 안티바이러스도 있는 것으로 보인다. 이것은 API 함수들을 후킹하여 실시간으로 어떤 함수들이 사용되는지를 검사하여 의심스러운 행위로 보일 때 탐지하는 방식이다. 다른 하나는 에뮬레이터를 이용한 방식이다. 안티바이러스에서는 에뮬레이터 외에도 샌드박스라고도 불리는데 그냥 같은 의미로 사용되는 개념인 것 같다. 에뮬레이터는 굳이 동적 휴리스틱 방식 외에도 다양하게 사용된다. 즉 실행 파일 에뮬레이팅 외에도 셸 코드 분석에 그리고 실행 파일 언패킹에도 사용된다. 어쨌든 에뮬레이터 내에서 실행 파일을 실행시키고 그 행위를 분석하는 방식이다.



Posted by SanseoLab



1. 개요

2. 패킹된 바이너리

3. 언패킹

.... 3.0 No option

.... 3.1 API Redirection

.... 3.2 Code Redirection

.... 3.3 Remove OEP

.... 3.4 Debug Blocker

.... 3.5 etc

4. OEP 찾기





1. 개요

  PEspin(http://pespin.w.interiowo.pl/)은 전형적인 프로텍터로 보인다. Themida 같은 코드 가상화 기술은 사용하지 않지만 상당히 많은 안티 디버깅 기술이 사용된다. 물론 압축 알고리즘을 통해 보호 뿐만 아니라 패킹의 기능도 갖는다. 참고로 실행 압축 알고리즘은 aPLib을 사용한다.


  다음은 옵션들이다. 여기에서는 옵션 없이 처리한 바이너리부터 분석한 후에 각각의 옵션을 사용한 바이너리를 분석하여 어떤 방식으로 구현되었는지를 확인하고 정리할 것이다.


- Anti-debugger protection : [ Add Debugger detection ( Exit without any message, Display message and exit ) ]

- Protection : [ API Redirection ], [ Antidump protection ], [ Remove OEP ], [ Code redirection ], [ Debug Blocker ]

- Advanced : [ Compress resources ], [ Strip Overlays ], [ Strip .reloc section if possible ], [ Optimize MS-DOS header size ]

- Other : [ Password protection ], [ Close program after : "" 'minute ], [ Create backup file ]

- Section names : [ User name : ], [ Random known protector, packer ], [ Don't rename ]





2. 패킹된 바이너리

  이 부분은 "Section names" 옵션과 같이 설명하겠다. "Don't rename"을 사용한 경우에는 초기 섹션들 외에 .taz 섹션이 추가된 것을 볼 수 있다. 옵션 중 "Compress resources" 옵션을 사용하지 않았기 때문에 리소스 섹션(.rsrc)의 내용은 그대로이지만 나머지 섹션들은 모두 압축되어 있다. 만약 "User name" 옵션을 사용하여 이름을 짓는 경우 .rsrc 섹션명을 제외하고 .taz 섹션 포함 모든 섹션 이름이 그 이름으로 설정된다. 또한 "Random known protector, packer"로 설정한 경우 앞과 동일한 섹션들의 이름이 모두 .PELOCKnt, .ficken, .peco 등으로 변경된다.


  .taz 섹션은 일반적인 패커들처럼 언패킹 루틴과 압축된 데이터가 존재하며 동시에 Import Table이 존재한다. PEspin은 MessageBoxA(), InitCommonControls(), LoadLibraryA(), GetProcAddress()를 기본으로 임포트한다.





3. 언패킹

  PEspin은 암호화 및 복호화에 다형성 루틴을 사용한다. 이것은 하나의 바이너리를 패킹하더라도 생성될 때 마다 출력된 바이너리는 모두 다르다는 것을 의미한다. 압축된 데이터가 들어간 섹션들은 완전히 다르며 복호화 루틴 및 압축된 데이터가 저장된 .taz 섹션도 차이점이 존재한다. 복호화 루틴이 기본적으로 Self-modifying code인 면도 있다.


  여기서는 32비트 기준 작은 바이너리 두 개(하나는 calc.exe)를 대상으로 하여 분석했다. 그렇기 때문에 큰 바이너리의 경우 인지하지 못한 부분에서 차이가 있을 수 있다. 이 섹션에서는 아무런 옵션을 주지 않은 채로 생성된 바이너리를 언패킹하며 하위 섹션들에서 하나씩 옵션을 설정하여 비교하면서 분석하겠다. 참고로 프로텍터의 특징처럼 난독화 코드가 매우 많다. 기본적으로 garbage 코드를 비롯하여 "Jumping into the middle of an instruction" 또는 "Breaking Code Alignment"로 불리는 안티 디스어셈블러 기법이 사용되어 Ollydbg를 이용하여 분석하는데 많은 방해가 되었다. garbage 코드는 그렇다 치더라도 jmp 및 call을 이용하여 코드를 깨는 저 방식은 한 두개라면 감당하겠지만 저렇게 대량으로 존재하는 경우 Ollydbg를 이용하여 분석하는 것은 굉장히 힘든 일로 보인다. "Analyse Code" 즉 "Ctrl + A"도 이것들을 제대로 분석하지 못하기 때문에 어지간한 경우에는 그냥 분석을 해제하는 편이 낫다. 이것을 극복할 수 있는 방식을 최대한 찾을 것이며 없으면 만들어 보도록 시도라도 할 생각이다. 


  하나씩 옵션을 설정하면서 깨달은 것은 언패킹 루틴이 기본적으로 동일하며 옵션에 따라서 조금씩 기능이 추가되는 경우가 많았다는 점이다. 개인적으로 능력이 부족하여 대충 넘긴 부분이 특정 옵션 사용 시에 사용되어 이 때서야 깨닫는 것을 보며 확신하건데, 아마 이렇게 최종적으로 정리한 글에서도 놓친 부분이 많이 남아있을 것이다. 그렇기 때문에 이 글은 참고 용으로만 보고 정확한 분석은 직접 수행하는 것을 추천한다.



3.0 No option

  앞에서도 언급하였듯이 "Jumping into the middle of an instruction" 기법 때문에 Step Over (F8) 대신 Step Into (F7)를 이용해야 한다. 참고로 분석 대상 바이너리가 다를 수 밖에 없지만 이것을 설명하면서 주소를 적는 것이 참고하는데 많은 도움이 될 것 같아서 주소도 같이 적으려고 한다. 본인의 바이너리는 00406000이 .taz 섹션의 시작이며 EP도 이 섹션의 거의 처음 부분이다. 사족으로 EP의 앞 부분에는 Import Table이 존재한다.


  비록 Ollydbg에서는 우리의 섹션들이 모두 통합되어 보이지만 어쨌든 영역을 구분하는 것은 필요한 일이다. 참고로 왜 Ollydbg에서 통합되어 보이는지에 대한 확실한 이유는 알 수 없지만 추측은 다음과 같다. 원래 일반적인 경우 .text 섹션은 읽고 실행하기 즉 rx, .rdata 섹션은 r, .data 섹션은 rw 등과 같이 섹션 별로 접근 권한이 부여된다. 하지만 PEspin으로 패킹한 경우 전체 섹션들이 모두 rwx 권한을 갖게된다. 아마 Ollydbg는 이 섹션들이 일반적인 이름을 갖고 있지만 일반적인 권한을 갖고 있는 것이 아닌데다가 권한도 모두 같으니 한 뭉텅이로 통합하여 인식시킨 것으로 보인다.


  시작하자 마자 앞에서 언급한 "Jumping into the middle of an instruction" 기법과 garbage 코드들이 나온다. 가장 대표적인 garbage 코드는 분기문으로서 retn을 이용하는 방식이다. jmp와 call을 통한 분기 뿐만 아니라 주소를 push한 후에 retn함으로써 분기하거나 " ADD DWORD PTR SS:[ESP], 0C " 이후 " RETN " 을 이용해 분기하는 방식이 자주 사용된다.



a.

  EP는 004060D4이며 대부분 Step Into (F7)로 진행하였다. 앞에서 00406000 즉 .taz 섹션의 처음 부분은 Import Table이라고 말했다. 정확히 말해서 로드 이후 00406050에 MessageBoxA()의 주소가 들어가 있다. 이 값 즉 주소를 004084C1에 복사한다. 이 외에도 LoadLibraryA()는 00408566에, 0040856B에는 GetProcAddress()를 넣는다. 참고로 이 세 함수들은 모두 Import 하는 API들이다. 일종의 Import Table의 역할을 수행하는데 복잡하기 때문에 뒤에서 따로 정리하도록 하겠다.


  복호화 루틴이 자가 수정 코드 즉 Self-modifying code라고 했듯이 실행 중에 코드를 계속 수정해 가면서 진행한된. 이 수정 즉 복호화는 바로 다음 명령어가 해당되기도 하지만 뒤에서 나올 반복문 처럼 13A8 정도 되는 크기를 복호화하기도 한다. ECX가 13A8을 갖는 반복문인데 004085xx부터 004099xx 부근까지 복호화를 담당한다. 이후 ECX가 BA인 반복문도 있다. 이제 복호화된 부분으로 이동한다.


  참고로 ECX를 이용한 루프의 경우 간단하게 조건부 BP 즉 "Shift + F2"를 누른 후 조건으로 "ecx == 1"을 입력하고 F9 즉 실행을 누르면 해당 명령어 실행 중 ECX가 1이될 때 정지하게 된다. ECX를 이용한 루프는 이렇게 처리하기로 한다.


  다음 부분은 조금 설명이 필요하다. 패커들의 경우 대부분 LoadLibrary()와 GetProcAddress()를 임포트하며 언패킹 후 마지막으로 IAT를 복구하는데 두 함수를 사용한다. 패커는 위의 두 함수를 제외하면 API 함수들이 그다지 필요한 경우가 많이 없지만 프로텍터의 경우에는 원본 바이너리의 IAT 복구 외에도 언패킹 루틴에서 보호에 사용되는 여러 함수들이 필요할 수 있다. 하지만 대상이 어떤 함수를 사용하는지 알 수 있다는 것은 대상을 분석하는데 많은 힌트가 될 수 있으므로 직접 import 하지는 않는다. 대신 언패킹 루틴에서 사용되는 함수들도 이 두 함수를 이용해서 주소를 얻어와 사용하는 경우가 많다.


  뒷 부분에서 나올 때 마다 언급하겠지만 여타 프로텍터들처럼 PEspin도 보호를 위해 여러 함수들이 필요하다. 하지만 PEspin은 이 두 함수를 임포트하고 있어도 이 함수들을 사용하지 않는다. 즉 언패킹 루틴에 필요한 함수들 뿐만 아니라 원본 바이너리의 IAT 복구를 위한 함수들 주소도 이 두 함수를 이용하지 않는 것이다. 필요한 함수들은 Export Table을 이용하여 함수 주소를 얻어온다. 오직 LoadLibrary()만 마지막 즈음에 원본 바이너리의 임포트 테이블을 복구할 때 사용되기도 한다. 이 두 함수를 사용하여 필요한 함수들의 주소를 가져오지 않는 것은 아마 보호의 방식 중 하나로 보인다.


  Export Table을 이용하여 api 함수의 주소를 얻는 방식은 한국어로 되어있는 좋은 자료들이 존재하므로 (예를들면 [ http://ezbeat.tistory.com/283 ]) 자세히 설명하지 않겠다. 참고로 PEspin에서는 이미 LoadLibraryA() 등 kernel32.dll의 함수를 사용하기 때문에 kernel32.dll의 BaseAddress를 구하는데 PEB를 사용하지는 않는다. 그냥 LoadLibraryA()를 이용해서 BaseAddress를 구하며 이후 EAT를 이용한 방식은 비슷하다. 물론 문자열을 비교하는 부분 같이 세세한 부분은 다르지만 기본적으로 Export Table을 이용하는 메커니즘이 사용된다. LoadLibrary()와 GetProcAddress()를 이용하면 매우 간단할 것이지만 이 방식을 사용함으로 인해 익숙하지 않다면 막힐 수도 있다. 이 방식의 특징 상 차례대로 모든 API 함수 이름을 비교하면서 찾아내야 하기 때문이다. 


00408653 CMP BYTE PTR DS:[EDI+2], 69


  EDI에는 API 함수 이름 문자열의 주소가 들어가 있다. 저 명령어는 함수 이름의 세 번째 문자와 특정 알파벳을 비교한다. PEspin 개발자만의 방식으로 보인다. 이후 3번째 글자가 맞다면 나머지 부분도 비교한다.


00408738 MOV DWORD PTR DS:[EDI+1], EAX


  이 명령어는 api 함수 이름이 원하는 것과 같다면 그 주소를 아까 위에서 설명한 004084C1 이후 부분 즉 새로운 임포트 테이블에 쓰는 역할을 한다. 어쨌든 이 루틴을 벗어나는 것을 더 원할 것이다. 아래의 JE가 활성화되면 이 루틴이 종료된다. 그냥 00408740에 BP를 걸면 된다.


00408624 CMP DOWRD PTR DS:[EDI], 0

00408627 JE 00408740


  마지막으로 PEspin이 사용하는 api 함수들을 정리하겠다. 사실 여기서는 아무 옵션도 사용하지 않았기 때문에 아래의 함수들 중 몇 개만 사용된다. 옵션이 추가될 수록 사용되는 함수들이 많아질 것이다.


[ ExitProcess, VirtualProtect, Closehandle, VirtualAlloc, VirtualFree, CreateFileA, ReadFile, GetTickCount, GetModuleHandleA, CreateThread, Sleep, GetCurrentProcessId, OpenProcess, TerminateProcess, GetFileSize, GetModuleFilenameA, CreateMutexA, CreateProcessA, GetCommandLineA, GetLasterror, GetThreadContext, SetThreadContext, VirtualProtectEx, WaitForDebugEvent, ContinueDebugEvent, ReadProcessMemory, WriteProcessMemory, VirtualQueryEx ]


  계속 진행하다 00408467로 가고 또 진행하다 보면 ecx를 이용한 루프가 존재한다. 이것도 조건부 BP를 이용해 넘긴다. 



b.

  이후 다음 명령어들이 존재한다.


0040663A XCHG DWORD PTR FS:[EBX], EAX

; EBX는 0이다. 그래서 EAX에는 SEH를 가리키는 포인터가 들어간다.

0040663D LEA EBX, [EBP+403AFF]

; EBX에는 SEH 핸들러의 주소가 들어간다.

00406643 PUSH EBX

00406644 PUSH EAX

; 둘을 스택에 push함으로써 새로운 SEH 핸들러가 설치된다.

00406645 OR EDI, FFFFFFFF

00406648 OR ECX, EDI

0040664A REPE SCAS BYTE PTR ES:[EDI]

; EDI가 0xFFFFFFFF이므로 예외가 발생한다.


  위의 루틴은 뒤에서도 나오겠지만 전형적인 SEH를 이용한 안티 디버깅이다. 예외 핸들러를 설치하고 예외를 발생시키는 것이다. 우리는 간단하게 예외를 발생시키는 코드 실행 이전에 또는 이미 발생하여 Ollydbg가 아래의 패널에서 "Access violation when reading [FFFFFFFF] - Shift+Run/Step to pass exception to the program" 메시지를 내놓고 중지된 상황에서 스택 패널에 보이는 SEH 핸들러의 주소를 참조하여 핸들러에 BP를 걸고 "Shift + F9"하여 SEH 핸들러로 넘어올 수 있다. 이런 방식은 뒤에도 자주 나오는데 예외를 일으키는 방식만 다르지 메커니즘은 같다.


00406660 REP MOVS BYTE PTR ES:[EDI], BYTE DS:[ESI]


  핸들러에서는 위의 명령을 실행하여 ESI 즉 004085C0에서 EDI 즉 0040664D로 데이터를 옮긴다. 그리고 다시 위 즉 0040664D로 가서 명령어를 실행한다. 그냥 방금 사용한 SEH 핸들러를 제거하는 부분이다.



c.

  VirtualAlloc()으로 메모리를 할당한다. 명령어는 아래와 같은데 앞에서도 설명했듯이 참조하는 주소는 새로운 Import Table이다. 참고로 정확히 004084E4이다.


0040668B CALL DWORD PTR SS:[EBP+405996] ; VirtualAlloc()


  간략하게 정리해서 GetModuleFileNameA()로 이 바이너리의 파일 경로 명을 얻어와서 앞에서 할당한 메모리에 저장한다. 이후 이 경로명을 인자로 CreateFileA()를 이용해 파일의 핸들을 얻고 GetFileSize()로 크기를 얻는다. 그리고 이 크기만큼을 인자로하여 VirtualAlloc()으로 메모리를 할당한 후 ReadFile()로 파일을 읽어서 이 영역에 저장한다. 마지막으로 CloseHandle()로 핸들을 닫는다.


  읽어온 파일 자체의 데이터를 이용한 루틴이 수행된다. 뭔가 무결성 검사처럼 파일 전체 데이터를 이용하여 연산한 후 그 결과를 0040AE62에 저장한다. 이후 VirtualFree()로 할당한 두 메모리를 해제한다. 그리고 SEH 핸들러를 설치한 후 다음 명령어로 예외를 발생시킨다. 참고로 BL은 0이다.


0040684A DIV BL


  이번에 사용되는 핸들러에서는 retn을 이용해 반환하기 때문에 결국 ntdll로 들어가서 NtContinue()까지 가게된다. 예외와 관련하여 정리한 글이 이미 이 블로그에 존재하기 때문에 결과만 말하겠다. 인자로 넣은 pContext의 주소는 0019FBFC이다. 여기에 B8을 더하면 되는데 결과는 0019FCB4가 된다. 스택 패널에서 이 영역을 찾아가면 값이 0040859D인 것을 볼 수 있으며 이곳에 BP를 걸면 된다. 


  이후 계속 복호화는 수행된다. 이 부분은 조금 더 구체적인게 각 섹션 별로 주소를 EDI에 넣고 크기만큼 ECX에 넣고 복호화를 수행하는게 보인다. 참고로 아까 0040AE62에 저장한 값을 이용한다. 그리고 SEH 핸들러를 이용한 안티 디버깅이 존재한다. 조금 특이하기 때문에 설명을 더 하겠다. 인터럽트는 STI 명령어를 이용하는데 핸들러에서 핸들러의 주소를 변경한다. 이후 반환하는데 다시 이 주소로 복귀하여 STI를 실행하고 또 예외가 발생한다. 대신 이번 핸들러의 주소는 이전의 00408F8A가 아닌 변경된 00408F8F가 된다.


0040900B STI


  그리고 또 예외가 발생한다. 물론 다음 명령어 실행 이전에 핸들러 주소를 00408FDA로 변경하였으므로 여기에 BP를 건다.


00408FCF XLAT BYTE PTR DS:[EBX+AL]


  이후 00406C23부터 ECX 11D4 만큼의 자가 수정 부분을 지나 각 섹션에 대해 크기만큼 복호화를 수행한다.



d.

  예외가 발생한다.


00406C00 MOV DWORD PTR DS:[EAX], EBX


  이후 00406C25 부터 11D4만큼 자가 수정을 수행한다. 이것도 ECX를 이용한 루프문이므로 똑같이 처리한다. 다시 이곳으로 이동하여 진행한다. 조금 지나서 VirtualAlloc()을 호출한다.


004097B0 VirtualAlloc()


  여기서는 이제 마지막으로 복호화가 수행된다. 즉 이 과정을 거치고 난 후 코드 섹션 근처로 가보면 원본 바이너리와 거의 같은 형태로 복호화가 완료되었다는 것을 볼 수 있다. 조금 더 자세히 살펴보면 PUSHAD로 시작해 POPAD로 끝나는 루틴이다. 이 부분은 다른 부분과 다르게 "Analyse Code"를 이용하면 깔끔하게 분석된 것을 볼 수 있다.


004071E1 PUSHAD

...

00407288 POPAD

00407289 RETN


  이후 retn을 하면 004097FF로 이동하며 다음 명령어를 통해 할당된 메모리에서 코드 섹션으로 E00의 크기 만큼을 이동시키는 것을 볼 수 있다. 참고로 이 부분도 분석된 코드를 보면 깔끔하게 이해할 수 있다.


00409809 REP MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI]


  참고로 이 예제 바이너리에서 해당 루틴은 .text 섹션과 .rdata 섹션만 해당하는 것으로 보인다. 어쨌든 위에서 언급하였듯이 위의 명령어 수행 후 해당 섹션으로 가보면 원본 바이너리처럼 복호화되어 있는 것을 확인할 수 있다. 물론 다른 프로텍터나 패커처럼 JMP 및 CALL의 주소와 관련해서는 아직 남아있다. 이후 VirtualFree()로 할당한 메모리를 해제한다.


00409866 VirtualFree()



e.

004089E5 VirtualProtect()


  004001F0부터 528의 크기 만큼 PAGE_EXECUTE_READWRITE 권한을 부여한다. 이후 이 부근에 값들을 쓰고 이곳으로 이동하여 진행하다가 예외를 발생시킨다.


004001F7 JMP FAR FWORD PTR DS:[EDX+9090]


  핸들러의 주소인 00408A4D로 이동하여 핸들러를 지우고 GetTickCount()를 호출한다.


00408A8B GetTickCount()


  결과에 대하여 반복적으로 특정 연산을 수행하며 00406000부터 D4의 크기만큼 차례대로 결과값을 집어넣는다. 이후 자가 수정 코드를 진행하다 0040956F로 이동한다.



f.

  이제 Import Table 복구를 시작한다. 일반적인 패커들의 경우 로더가 하는 역할과 같이 LoadLibrary()로 해당 DLL에 대한 핸들을 얻어온 후 GetProcAddress()로 해당 API 함수의 주소를 얻어온 후 IAT에 주소를 복사해 넣는다. 이후 바이너리에서는 보통과 같이 IAT의 주소를 참고해 API 함수를 호출한다. 즉 로더의 역할을 대신해줄 뿐 차이점은 없는 것이다.


  하지만 PEspin의 경우에는 더 복잡하다. 프로텍터로서 언패킹 루틴에서 사용할 함수들이 많이 존재하기 때문이다. 게다가 보호의 목적인지 LoadLibrary(), GetProcAddress() 처럼 기본적인 API를 제외하고는 직접적으로 임포트 하지 않는다. 사실 개인적인 생각은 이 두 함수도 거의 사용되지 않는 것과 섹션 이름을 rename할 수 있는 기능이 있는 것으로 보아 다른 패커 및 프로텍터와 혼동시키게 하려는 목적도 있는 것 같다.


  어쨌든 초반부에 설명했 듯이 원래 임포트하던 MessageBoxA()는 주소 004084C1에 저장한다. 여기서는 사용되지 않지만 이것을 호출할 일이 있다면 이 주소를 IAT로 삼아서 함수를 호출할 것이다. 그 다음으로는 차례대로 00408566에 LoadLibraryA(), 0040856B에 GetProcAddress()를 저장한다. 다음으로는 Export Table을 이용해 ExitProcess()부터 VirtualQueryEx() 까지의 주소를 얻어와서 각각 MessageBoxA() 뒤의 위치에 저장한다. 이 주소들은 MessageBoxA()와 LoadLibraryA() 및 GetProcAddress() 사이에 저장되게 된다.


  지금까지 여러 API 함수들을 호출하였으며 모두 저 주소에 위치한 값을 주소로 사용해 API 함수들을 호출했었던 것을 알 수 있다. 이제부터는 언패킹 이후 사용되는 즉 바이너리 자체에서 실제로 사용되는 함수들을 위한 IAT를 복구할 것이다. 하지만 이것도 설명이 필요하다.


  참고로 현재 예제 바이너리는 Visual Studio 2015로 개발하여 PEspin 1.33에서는 그다지 호환되지 않는 것으로 보인다. 자세한 설명은 이 블로그의 "API Sets" 문서를 보면 될 것이다. 어쨌든 아까처럼 Export Table을 이용하여 kernel32.dll에서 임포트 하는 함수들부터 차례대로 문자열을 비교하며 주소를 얻어오는데 해당하는 경우에는 0040AE67을 시작으로 이곳에 주소를 저장한다. 즉 언패킹 이후 바이너리에서는 GetCurrentProcessId() 같은 kernel32.dll의 함수를 호출할 때 이 영역을 IAT로 생각하고 호출하게 될 것이다. 간단하게 말해서 새로운 IAT의 위치라고 할 수 있다.


  하지만 이 예제 바이너리는 PEspin 1.33에서 지원하지 않는 것으로 보이는 VCRUNTIME140.dll 등의 DLL에 위치한 함수들도 임포트한다. 이 경우 PEspin은 일반적인 바이너리처럼 00402000 영역에 해당 주소를 복구한다. 다시 말해서 패킹한 바이너리가 PEspin에서 지원되는 kernel32.dll 같은 DLL의 함수를 사용할 경우에 0040AE67에 새로운 IAT가 만들어지고 이곳이 사용된다. 지원되지 않는 DLL의 함수들을 사용할 때는 일반적인 경우처럼 원래 IAT에 함수를 복구하여 원래 IAT가 사용되는 것이다. 참고로 GetProcAddress()를 이용하지 않는 것은 앞 부분과 같지만 VCRUNTIME140.dll 같은 DLL의 핸들을 얻을 경우에는 LoadLibrary()가 사용된다. 이것을 보면 지원되는 경우에 PEspin은 LoadLibrary()와 GetProcAddress()를 임포트함에도 불구하고 전혀 사용하지 않는다는 것을 알 수 있다.


  참고할 코드 부분을 설명하겠다. 


00406F4E CMP BYTE PTR DS:[EDI], 5F


  이 부분은 구해온 API 함수 이름 문자열과 특정 알파벳을 비교하는 부분이다. 참고로 이 부분도 수도 없이 진행되기 때문에 더 참고할만한 부분을 설명한다.


00406FBB JBE SHORT 00406FF2


  이곳에 BP를 걸고 실행하다 보면 바이너리에서 사용되는 함수들이 EAX에 차례대로 보이는 것을 볼 수 있다. 어쨌든 넘기기 위해서는 0040708D의 JE 00407125가 활성화되어야 한다. 진행하다 보면 00407A84로 가고 계속 진행하다보면 다음을 볼 수 있다.


00407DA2 JMP 004012EA


  OEP로 가는 점프문이다.



3.1 API Redirection

  위의 [ f ] 과정 즉 IAT를 복구하기 이전에 먼저 VirtualAlloc()으로 메모리를 할당한다. 그리고 IAT 복구를 수행하는데 앞에서 일반적인 경우 0040AE67에 새로운 IAT가 들어가 있고 여기에 함수의 주소가 들어가 있다고 했다. API Redirection 옵션을 사용한 경우 이 주소는 할당한 메모리의 주소가 된다. 그리고 이 메모리의 그 주소에서는 API 함수의 처음 부분이 복사된다. 마지막 부분은 API의 다음 부분으로 분기하는 점프가 추가된다.


  즉 API 호출 시 할당된 메모리로 이동하며 거기서 함수의 처음 부분을 실행하고 이후 실제 함수의 다음 부분으로 분기하여 거기서 다시 실행하는 것이다. 이 예제를 이용해 설명하도록 하겠다. 참고로 "API Sets" 문서에 관련 내용이 정리되어 있다. 이 바이너리에서는 Kernel32.GetVersionExA()를 호출한다. 하지만 실제로 이 함수는 kernelbase.dll로 이동하였고 kernel32.dll에는 kernelbase.dll로 분기시켜 주는 스텁 코드가 들어있다. 즉 다음과 같다.


76FB56D0 MOV EDI, EDI

76FB56D2 PUSH EBP

76FB56D3 MOV EBP, ESP

76FB56D5 POP EBP

76FB56D6 jmp dword ptr ds:[<&api-ms-win-core-sysinfo-l1-2-1.GetVersionExA>]


  이 API를 바이너리에서 사용한 경우 새로운 IAT에는 이 함수에 대한 주소가 76FB56D0가 아닌 할당된 메모리인 001E00xx로 되어 있다. 즉 호출 시 이 주소로 가는데


001E00xx MOV EDI, EDI

001E00xx PUSH EBP

001E00xx MOV EBP, ESP

001E00xx POP EBP

001E00xx jmp 76FB56D6


  위와 같이 실제 kernel32.GetVersionExA()의 처음 부분이 (참고로 이 스텁 함수의 경우 매우 짧기 때문에 대부분이 실행되는 것처럼 보인다) 실행되고 이후 그 다음 주소인 76FB56D6으로 jmp하게 된다.



3.2 Code Redirection

  [ e ] 단계에서 004001F0 영역은 0xFF로 채워졌었다. 이 옵션을 사용하면 IAT가 완성되고 OEP로 이동하기 바로 전에 특정 데이터가 004001F0을 시작으로 해당 영역에 삽입되는 것을 볼 수 있다. 이것은 E9로 시작하는 jmp 문들이다. 다음은 0040AEA3에서 004001F0으로 166 크기의 데이터를 이동하는 명령어이다. 0040AEA3에 해당 내용을 삽입하는 부분은 분석하지 않았다.


00407C9E REP MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI]


  이후 언패킹 루틴을 종료하고 OEP로 간 후 실행해 보겠다. 실행 중 일반적인 분기문이 평소와 다르게 004001F0을 시작으로 하는 저 영역으로 가는 jmp 문으로 수정된 것을 볼 수 있다. 명령어를 실행하여 해당 영역으로 이동하면 E9를 시작으로 하는 jmp 문이 존재하는데 이 명령어들은 결국 원래 가려고 했던 곳의 주소를 가지고 있다. 이렇게 Code Redirection 옵션은 API 함수가 아닌 일반적인 분기문을 대상으로 Redirect를 수행하여 준다.



3.3 Remove OEP

  OEP는 004012EA이며 언패킹 루틴에서 OEP로 가는 마지막 분기는 다음과 같은 형태이다.


00407DA2 JMP 004012EA


  그리고 실제 바이너리의 OEP 부분은 다음과 같다.


004012EA CALL 0040155F [ 참고 : __security_init_cookie() ]

004012EF JMP 0040116E


  Remove OEP 옵션을 사용하는 경우에는 마지막 분기문이 다음과 같다.


00407D2F JMP 0040155F

00407D34 JMP 004012EF


  설명해 보자면 바로 OEP로 가지 않고 실제 OEP의 첫 코드 부분을 직접 호출한 후에 복귀하여 다시 그 다음 부분으로 가는 것이다. 실질적인 흐름은 차이가 없지만 OEP인 004012EA로 직접 가지는 않는다는 점이 Remove OEP 옵션의 의미인 것으로 보인다. 참고로 이쪽을 구현한 언패킹 루틴은 분석하지 않았다.



3.4 Debug Blocker

  이 방식은 전형적인 Debug Blocker (또는 Self-Debugging) 방식이다. 기본적인 형태와 관련해서는 많은 자료들이 있고 아직 분석하지 않아서 PEspin만의 특징을 알지 못하지만 추후에 분석한 경우 올리도록 할 것이며 여기서는 기본 구현만 알아보도록 한다. 


  앞에서 설명했듯이 [ a ] 단계에서는 이후에 수행할 영역에 대한 자가 수정 코드가 있으며 언패킹 루틴에서 사용될 함수들을 위한 새로운 Import Table을 생성한다. [ a ] 단계를 지나면 00409D86으로 간다.


  이제부터는 상당히 직관적이다. CreateMutexA()로 뮤텍스를 생성한 후 GetLastError()로 검사한다. 부모 프로세스의 경우에는 성공할 것이고 자식 프로세스의 경우에는 이미 있기 때문에 GetLastError()로 0x000000B7 즉 ERROR_ALREADY_EXISTS를 받을 것이다. 참고로 이 예제의 경우 004099C0에 저장된 URXYTLAH 문자열을 사용하며 이것은 [ a ] 단계의 마지막 루프에서 생성된다. 이후부터는 부모와 자식 프로세스 사이에서 차이가 발생한다. 


  부모 프로세스의 경우 이후 두 번의 VirtualAlloc()을 통해 메모리를 할당한다. GetModuleFileNameA()로 파일 이름을 얻어와 첫 번째 할당한 메모리에 저장하고 GetCommandLineA()으로 커맨드 라인을 받아온 후 이것들을 이용해 CreateProcessA()를 호출한다. 이 때 플래그는 DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS를 사용한다. 이후에는 WaitForDebugEvent(), ContinueDebugEvnet()를 반복적으로 호출하여 자식 프로세스와 통신한다. 부모 프로세스를 더 이상 분석하지 않았고 자식 프로세스도 아예 분석하지 않았다. 



3.5 etc

  옵션 중 눈여결 볼만한 [ Add Debugger detection ( Exit without any message, Display message and exit ) ], [ Antidump protection ], [ Close program after : "" 'minute ]는 다루지 않았다. "Antidump protection"의 경우에는 무 옵션과의 차이를 찾지 못했으며 몇 개는 굳이 시도하지 않았다.





4. OEP 찾기

  OEP를 찾기 위해서 예제에서 사용한 바이너리와 calc.exe를 비교하면서 분석해 보았다. 두 개의 바이너리를 사용하고 API Redirection 옵션을 사용해 4개의 샘플을 이용했으며 각각의 크기는 9KB, 113KB이다. 아래의 주소는 .taz 섹션의 시작 주소를 기반으로 한다.


a. 예제 바이너리 + No option

+ 00001CF3 ADD DWORD PTR SS:[ESP], 0C

+ 00001CF7 RETN

...

+ 00001DA2 JMP OEP


b. 예제 바이너리 + API Redirection 옵션

+ 00001CF3 ADD DWORD PTR SS:[ESP], 0C

+ 00001CF7 RETN

...

+ 00001DAE JMP OEP


c. calc.exe + No option

+ 00001C7D ADD DWORD PTR SS:[ESP], 0C

+ 00001C81 RETN

...

+ 00001D8E JMP OEP


d. calc.exe + API Redirection 옵션

+ 00001C7D ADD DWORD PTR SS:[ESP], 0C

+ 00001C81 RETN

...

+ 00001D8A JMP OEP


  비교해보면 알겠지만 주소가 조금씩 다르다. 하지만 "ADD DWORD PTR SS:[ESP], 0C", "RETN" 명령어 및 "JMP" 명령어는 항상 동일한 것으로 보이므로 이것을 이용해 OEP를 쉽게 구할 수 있을 것으로 보인다. 즉 특정 API에 BP를 설치하거나 예외를 단계로 보고 뒷 부분까지 넘기다가 저 부근의 주소에서 해당 명령어를 찾은 후 OEP로 실행시키면 될 것으로 보인다.



Posted by SanseoLab



차례

1. 개요

.... 1.1 배경지식

.... 1.2 문서 정리

2. 드라이버 기본

.... 2.1 컴퓨터 설정

.... 2.2 드라이버 개발 환경

.... 2.3 도구

.... 2.4 DriverEntry / DriverUnload

3. 자가 보호

4. 스캐닝

.... 4.1 파일 시그니처 기반 스캐닝

.... 4.2 행위 기반 스캐닝

5. 프로세스 / 스레드 생성 보호

6. 레지스트리 보호

7. ELAM 및 Protected Process

8. 유저 모드 애플리케이션과의 통신

9. 기타

.... 9.1 네트워크 보호



[ 소스 코드 : https://github.com/101196/simpleAVdriver ]





1. 개요

1.1 개요

  악성코드 분석을 위주로 공부하고 있지만 개인적으로 안티바이러스가 어떻게 구현되는지에 대해서도 항상 궁금해 하여 왔다. 어떻게 보면 크게 관련이 없는 분야라고 할 수도 있겠지만 적어도 내 분야 중에서 주요한 부분을 차지한다고 생각하기 때문에 메커니즘에 대해서 공부하고 정리하려고 한다. 사실 더 관심이 갔던 이유는 찾기 정말 힘들었기 때문이다. 만약 제대로 정리된 자료들이 많았다면 그것만 정리하면서 공부하고 그 정도로만 알려고 했을 것이다.


  여기서는 배경 지식과 함께 전체적인 방식을 설명한다. 이 문서 외에도 깃허브를 통해 간단한 형태로 구현한 소스 코드도 같이 제공한다. 이미 있는 자료들을 기반으로 최소한 간략하게 만든 것으로서 원본 링크도 같이 제공한다. 실질적인 구현에 관한 것은 소스 코드의 주석을 통해 정리하기로 할 것이고 이 문서에서는 전체적인 방식 및 흐름을 정리하기로 한다. 


  각 내용들은 안티바이러스를 구현하는데 필요한 기능들을 차례대로 설명한다. 참고로 드라이버 개발과 관련해서도 정리할 수 밖에 없는데 순수하게 직접 구현한 안티바이러스와 관련된 내용에 한정해서 정리한다. 디바이스 드라이버와 관련된 자료들은 종종 보임에도 불구하고 순수하게 안티바이러스 관련 드라이버의 내용은 많이 없는 편이다. 물론 아주 상세하게 정리하는 것은 의미도 없고 능력도 되지 않기 때문에 아주 기초적인 것은 안다는 가정 하에 그리고 충분히 MSDN 등의 자료들을 보고도 알 수 있는 내용들은 제외하고 나머지의 내용을 적기로 한다. 이것들은 어차피 공부하면서 보아야 되기도 하고 정리도 잘 되어 있으며 시간을 가장 많이 낭비하는 것은 의도치 않은 내용들일 것이기 때문이다.


  안티바이러스는 물론이고 드라이버 개발 경력도, 주 분야가 개발인 것도 아닌 학생으로서 공부한 자료를 올린다는 것이 맞는 것인지는 모르겠다. 하지만 공부하는 입장에서 그리고 찾아보며 알아가고 구현해 보는데 너무 많은 시간을 보낸 입장에서 혹시나 공부하는 사람들이 적어도 검색에 필요한 키워드라도 하나 찾아갈 수 있지 않을까 하는 생각으로 올려본다. 다시한번 말하지만 전문가가 아닌 공부한 내용을 올린 것이다. 전체적인 수준은 당연하고 심지어 틀린 부분이 있을 수도 있다. 모르는 부분도 많이 있고 그렇게 적어 놓았으므로 읽는데 참고하길 바란다.



1.2 배경 지식

  안티바이러스에 조금의 관심이라도 있다면 인터넷에 널려있는 자료인 파일 시그니처 기반 안티바이러스 소스 코드들을 보면서 우리가 사용하는 안티바이러스가 단순하게 이러한 형태로 구현되어 있을 것이라고는 생각하지 않을 것이다. 아마도 20여 년 전에는 비슷한 형태를 가지고 있지 않았을까 싶다. 윈도우 XP 즈음에는 안티바이러스도 루트킷과 비슷한 형태를 갖었다고 한다. 인터넷에는 커널 모드에서의 후킹을 이용한 루트킷 관련 자료들이 많이 존재한다. 간략하게 구현적인 내용만 보자면 안티바이러스도 이것과 비슷하다고 한다. 그리고 이 정도까지도 인터넷에는 수많은 자료들이 존재한다.


  이제 최신 형태를 알아보겠다. 윈도우 비스타의 x64 버전부터는 많은 부분이 달라지게 되었고 내가 찾던 부분도 이쪽이었지만 아직도 인터넷에서는 관련 자료들을 찾기가 매우 힘든 편이다. 아마 이유가 있을 것이다. 보안 관련 정보는 대부분 공격과 관련된 정보이고 과거에도 보안 프로그램과 관련된 직접적인 내용이 많았다기 보다는 안티바이러스가 루트킷의 형태와 비슷하므로 굳이 찾아보면 어느 정도는 비슷하게라도 찾아볼 수 있었을 것 같다. 하지만 최신 윈도우 버전부터는 많은 부분이 달라져서 공격 입장에서 즉 루트킷이 그렇게 많이 활동하는것 같지는 않아 보인다. 그래서 공격자의 입장과는 다른 순수 보안 프로그램 구현과 관련된 내용이 인터넷에 많이 돌아다닐 일이 없어진 것이 아닌가 싶다.


  어쨌든 이제 비스타 x64 이후부터의 변화에 대해서 알아보려고 한다. 첫 번째는 KPP(Kernel Patch Protection)이다. 우리는 앞에서 루트킷이 커널 영역에서 후킹을 사용한다고 했다. 이것과 관련된 내용은 많이 있을 것이므로 간략하게 말하자면 KPP 다시 말해서 패치가드는 후킹의 대상이 되는 SSDT 같은 부분을 주기적으로 모니터링하여 변경이 일어나지 않게 하는 방식이다. 변경이 인식되는 즉시 BSOD가 뜨기 때문에 루트킷은 물론이고 이 영역을 후킹해서 보안 목적으로 사용하던 안티바이러스도 커널 코드 후킹을 사용할 수 없게 되었다.


  다른 하나는 KMCS(Kernel Mode Code Signing)이다. 이름과 같이 이제부터는 서명된 드라이버만 커널에 로드할 수 있게 되었다. 루트킷도 드라이버이기 때문에 이것을 우회하지 않는 이상 루트킷이 활동하기 어려워졌다. 물론 KPP와 달리 안티바이러스의 입장에서는 큰 상관은 없는 부분이다.


  마지막으로 필터 관리자가 커널에 추가되었는데 안티바이러스의 경우 이것을 사용하는 파일시스템 미니필터 드라이버를 개발함으로써 과거 후킹을 통해 파일들을 검사하던 것보다 더 개선된 방식을 사용할 수 있게되었다.


  지금까지 언급한 것들을 정리하자면 과거에는 커널 모드에서 후킹을 사용해서 악성코드 자체 프로세스 및 레지스트리를 보호하였을 것이고 파일 및 프로세스 모니터링도 수행하였다. 하지만 KPP로 인해서 커널 후킹이 불가능해졌고 마이크로소프트는 대신 파일시스템 미니필터 드라이버 및 여러 콜백 함수들을 제공하여 원래 사용하던 기능들을 제공해 준다. 





2. 드라이버 기본

2.1 컴퓨터 설정

  처음 공부하는 입장에서는 드라이버 개발과 관련해서 시작하는것 부터 까다롭다. 개발 환경에 대해서 설명하기 이전에 실행 대상이 되는 현재 컴퓨터부터 말해보겠다. 이것은 윈도우 10 x64이며 현재 테스트 모드로 실행 중이다. 앞에서도 언급하였듯이 KMCS로 인해서 본인이 개발한 드라이버를 본인 컴퓨터에 로드하는 것도 서명이 되어있어야만 가능하기 때문에 서명 정책을 사용하지 않게 설정한 것이다.


  기본적으로는 관리자로 명령 프롬프트를 열고 이곳에 다음 명령들을 입력하면 된다.

> bcdedit.exe -set loadoptions DDISABLE_INTEGRITY_CHECKS

> bcdedit.exe -set TESTSIGNING ON


  조금 된 컴퓨터의 경우 이 두 명령을 통해 "작업을 완료했습니다."라는 결과를 얻을 수 있다. 이후 컴퓨터를 재부팅(종료하고 다시 켜는 것이 아니다)하면 바탕화면의 오른쪽 아래 부분에 테스트 모드라는 글씨가 삽입되어 있는 것을 볼 수 있다.


  하지만 두 번째 입력에서 "요소 데이터를 설정하는 동안 오류가 발생했습니다. 값은 보안 부팅 정책에 의해 보호되며 수정 또는 삭제할 수 없습니다."라는 오류가 뜰 때가 있다. 일반적으로 최신 컴퓨터의 경우 강화된 보안을 위해 바이오스(BIOS) 펌웨어가 아닌 UEFI 펌웨어를 사용하는데 이것을 통해 Secure Boot(안전 부팅)가 지원된다. 어쨌든 이 설정을 없애주어야 한다. 컴퓨터 부팅 시 "F2" 또는 "DEL" 키를 계속 누르다 보면 UEFI 부팅 메뉴를 볼 수 있다. 이것은 설치된 것마다 다르겠지만 나의 컴퓨터의 경우 "부팅 옵션" 메뉴에서 "Safe Booting"을 OFF시켰다. 이제 아까처럼 명령어를 입력하면 오류가 뜨지 않을 것이고 재부팅 이후 테스트 모드를 확인할 수 있게 된다.


  반대의 경우 원상복구하는 명령들은 다음과 같다. 재부팅을 하고 다음에는 UEFI를 on시키면 된다.

> bcdedit.exe -set loadoptions ENABLE_INTEGRITY_CHECKS

> bcdedit.exe -set TESTSIGNING OFF



2.2 드라이버 개발 환경

  나의 경우 지원 문제로 인해 Visual Studio 2015와 WDK 10을 설치했다. WDK 10의 경우 아직 Visual Studio 2017을 지원하지 않았기 때문이다. 프로젝트의 경우 "템플릿 -> Visual C++ -> Windows Driver"까지 고른다. 여기서는 두 가지를 사용할 것이다. 일반적인 대부분의 것들은 "Legacy -> Empty WDM Driver"를 고른 후에 개발하면 된다. 하지만 미니필터 드라이버의 경우 "Devices -> Filter Driver: Filesystem Mini-filter"를 골라야 한다. 여기서 다루는 예제 중 미니필터 드라이버를 제외하고 다른 것들만 간단하게 테스트해보고 싶다면 WDM으로 간단하게 개발하면 될 것이고 미니필터 드라이버를 포함하여 전체적으로 개발하고자 한다면 미니필터 드라이버를 고른다. 이것은 미니필터 드라이버 뿐만 아니라 다른 추가적인 내용을 넣을 수도 있기 때문이다.


  현재 컴퓨터가 x64이기 때문에 Release 모드 및 x64를 선택했다. 소스 코드의 경우 생성할 때 확장자를 c로 설정한다. 이제부터 여러 설정을 추가하도록 하겠다. 사실 개발하고 로드하고 하면서 여러 개의 에러가 뜨는데 물론 쉽게 검색하여 설정할 수 있지만 여기서는 정리하면서 미리 설정하도록 한다. 


  먼저 "프로젝트 -> 속성" 항목을 연다. "Driver Settings -> Target OS Versiion"은 드라이버가 로드될 컴퓨터의 운영체제를 선택할 수 있다. 내 컴퓨터는 윈도우 10이기 때문에 윈도우 10으로 설정한다. 다른 컴퓨터에서 드라이버를 설치할 경우 윈도우 7 같이 다른 버전을 설정할 수도 있다. 


  다음으로 "inf2Cat -> UseLocalTime"를 "예" 즉 "/uselocaltime"으로 설정한다. 이것을 설정하지 않으면 "Inf2Cat, signability test failed." 에러를 볼 수 있다. 마지막으로 "링커 -> 명령줄"의 추가 옵션 부분에 "/integritycheck"를 입력한다. 뒤에서 사용할 확장 함수들의 경우(Ex가 붙은) 무결성 검사 옵션을 사용하지 않으면 STATUS_ACCESS_DENIED 에러가 발생하여 해당 함수를 사용할 수 없게 된다.


  현재 개발하는 것이 파일시스템 미니필터 드라이버라면 .inf 파일을 수정할 필요가 있다. 대부분 실제 회사에서 개발하는 입장에서야 손댈 것이 많아보이지만 테스트하는 입장에서는 에러가 뜨지 않게만 하면 될 것으로 보인다. 에러 코드는 다른 것과 다르게 직접적인데 Class, ClassGuid 부분을 위의 주석에 보이는 형태로 수정하면 된다. 그대로 복사해서 붙여넣기 해도 테스트하는데에는 지장이 없다.


  마지막으로 "C/C++ -> 일반 -> 경고를 오류로 처리"를 아니요(/WX-)로 설정한다. 능력의 부족으로 최대한 좋게 만드려 해도 코드가 조잡하므로 간단한 경고들은 무시하려고 했다.



2.3 도구

  드라이버 개발 및 테스트를 위해서는 Visual Studio 2015와 WDK 10 외에도 여러 도구들을 알아야 한다. 먼저 드라이버 로더의 경우 개인적으로 다음 링크의 오픈 소스 프로그램을 사용한다. [ https://github.com/tandasat/DrvLoader ] 물론 로드 기능밖에 없긴 하지만 Legacy, WDM 뿐만 아니라 파일 시스템 미니필터 드라이버도 간단하게 로드할 수 있다. 해당 소스 코드를 컴파일하고 사용할 때는 관리자 모드로 명령 프롬프트를 열고 인자로 드라이버 바이너리의 경로를 주면 된다. 파일 시스템 미니필터 드라이버를 설치할 때는 --filter 등의 옵션을 주면 성공적으로 설치해 준다. 참고로 미니필터 드라이버의 경우 소스 코드를 보면 Altitude(고도)가 370000으로 설정되어 있으므로 컴파일 시에 수정하면 된다. 나의 경우에는 굳이 수정하지 않고 테스트했다. 참고로 명령 프롬프트에서 fltmc를 입력하면 현재 설치되어 있는 필터 드라이버들과 Altitude를 볼 수 있다. 


  원래는 sc를 이용해서 옵션으로 create를 주는 방법도 있지만 DvrLoader를 사용하는 것이 훨씬 간단하기 때문에 이것을 사용한다. 대신 다른 명령어는 sc를 사용하기로 한다. DvrLoader를 통해 드라이버를 로드하면 자동으로 sc create 뿐만 아니라 sc start도 사용된다. 즉 드라이버 로드 이후 바로 시작하는 것이다. 우리는 등록한 드라이버를 삭제하거나 잠시 언로드 시킬 필요가 있을 것이다. 다음은 myDriver를 설치한 이후 설치된 드라이버의 이름이 맞는지 확인해 보고 stop, 다시 start 그리고 delete까지 수행해 보겠다.


> sc query myDriver

  myDriver로 설치된 것이 맞다면 기본 정보들이 보일 것이다.

> sc stop myDriver

  드라이버를 중지시킨다.

> sc start myDriver

> sc stop myDriver

  드라이버 재실행 이후 다시 중지시킨다.

> sc delete myDriver

  드라이버를 삭제한다. 참고로 위처럼 드라이버를 stop시킨 후에 삭제해야 한다. 참고로 create 옵션은 다음과 같이 사용한다. 하지만 미니필터 드라이버의 경우 복잡해지기 때문에 그냥 DvrLoader를 사용하기로 한다.

> sc create [drivername] binPath= [path] type= kernel


  지금까지 기본 개발 뿐 아니라 로드와 언로드를 알아보았다. 하지만 우리는 드라이버가 제대로 동작하는지 확인하기 위해서 DbgPrint(), DbgPrintEx() 등의 함수를 이용해 메시지를 받아 볼 것이다. 이 메시지들은 DebugView를 이용해서 확인할 수 있다. 참고로 "Capture" 항목에서 [ Capture Kernel, Enable Verbose Kernel Output, Capture Events ]를 설정해야 메시지를 받아 볼 수 있다. 뒤에서는 DLL을 로드하게 될 것인데 우리는 이것이 제대로 로드되었는지 확인하기 위해 dllmain()에서 OutputDebugStringA()를 이용해 메시지를 출력하게 할 것이다. 이 메시지는 "Capture" 항목에서 [ Capture Win32, Capture Events ]를 설정하면 볼 수 있다.


  마지막으로 Process Explorer를 알아보겠다. 이것은 프로세스 종료 시도 시에나 DLL 인젝션을 수행했을 때 DLL이 제대로 로드되었는지를 확인할 때 사용한다. "View -> Lower Pane View -> DLLs"를 클릭하면 아래에 DLL 창이 뜬다. 이제 특정 프로세스를 클릭하면 아래에 해당 프로세스에서 로드한 DLL들의 목록을 볼 수 있다. 프로세스 종료는 해당 프로세스를 마우스 오른쪽 키로 클릭한 후 "Kill Process"를 클릭하면 된다.



2.4 DriverEntry / DriverUnload

  앞에서도 설명했듯이 아주 기초적인 내용은 적지 않기로 한다. 물론 개인적인 정리를 포함하기도 하기 때문에 뜬금없는 내용은 있을 것이다. 프로그램 작성 시 인자를 선언만 하고 참조를 하지 않는 경우에는 C4100 에러가 발생한다. 물론 앞에서 "경고를 오류로 처리"를 아니오로 설정하였기 때문에 컴파일하는데는 지장이 없겠지만 UNREFERENCED_PARAMETER 매크로를 이용해서 참조하지 않을 경우에도 warning을 없앨 수 있다.


  이것 외에도 여기에는 보이지 않지만 다른 곳에서 자주 사용되는 내용을 정리하겠다. 먼저 NTSTATUS는 시스템에서 내부적으로 사용하는 오류 코드이다. 대표적인 값으로 STATUS_SUCCESS가 있는데 이것은 0x00000000 즉 성공을 의미한다. 일반적으로 특정 함수를 실행시키고 반환 값을 NTSTATUS 변수로 받는다. 이후 NT_SUCCESS 매크로에 해당 변수를 인자로 넣음으로써 반환 값이 성공했는지 여부를 확인한다. 이 매크로는 NTSTATUS 값이 STATUS_SUCCESS 같이 성공에 해당하는 값인 경우에는 TRUE를, 실패의 경우에는 FALSE를 반환한다. NTSTATUS 값들 중에 성공을 의미하는 값이 하나가 아니기 때문에 NT_SUCCESS 매크로를 이용해서 간단하게 성공 여부를 TRUE / FALSE로 확인할 수 있다.


  마지막으로 RtlInitUnicodeString() 함수도 자주 사용된다. 유저 모드에서와는 달리 드라이버에서는 이 함수를 이용해서 유니코드 문자열을 초기화해서 사용하는 경우가 많은 것 같다.


  DriverEntry()에서는 먼저 DriverUnload를 등록할 것이다. 그리고 Installxxx 형태의 함수들을 호출하는데 이것은 우리가 다룰 주제들 즉 각 기능들을 설치하는 함수들이다. 반대로 DriverUnload()에서는 이렇게 설치한 기능들을 제거한다. 드라이버 로드 시에 DriverEntry()가 호출되면서 이 기능들이 설치되고, 드라이버 언로드 시에 DriverUnload()가 호출되면서 설치한 기능들을 제거할 것이다. 


  소스 코드는 크게 3개로 나누었다. 하나는 파일시스템 미니필터 드라이버(miniFilter), 두 번째는 행위기반 분석을 위해 후킹 DLL을 삽입하는 방식(BehaviorBased), 마지막으로 프로세스 보호 및 자가 보호 그리고 레지스트리 보호(threeProtection)이다. 3개를 통합하려고 했지만 전혀 안정적으로 보이지 않아서 포기했다. 





3. 자가 보호

  코드 상으로 보면 DriverEntry의 InstallSelfProtect()와 DriverUnload()의 UnInstallSelfProtect()가 관련된 내용이다. 실제 함수는 SelfProtect.h에 구현되어 있다. 여기서 사용하는 함수는 ObRegisterCallbacks()와 ObUnRegisterCallbacks()이다.


  ObRegisterCallbacks() 함수는 스레드, 프로세스 그리고 데스크탑 핸들 오퍼레이션 시에 호출되는 콜백 루틴들을 등록해 준다. 구체적으로 예를들어 보자면 어떤 프로세스가 보호하려는 프로세스를 종료시키고 싶다고 하자. 이 경우에는 특정한 행위를 수행하기 위해 먼저 핸들을 얻을 것이다. 등록된 콜백 함수는 이렇게 핸들을 얻을 때 호출된다. 핸들을 얻을 때는 대상에 대한 접근 권한을 설정하는데 이 콜백 함수는 얻으려는 접근 권한 중에서 특별한 것들을 제거할 수 있다. 이로써 다른 프로세스가 보호받는 프로세스를 종료하기 위해 핸들을 얻고 종료시키려고 하지만 종료할 수 있는 권한이 제거되어 있기 때문에 종료가 불가능하게 된다. 물론 Handle Operation 이전 뿐만 아니라 이후에 호출되는 콜백 루틴도 등록할 수 있다.


  이 예제에서는 구체적으로 이름이 "calc.exe"인 프로세스에 대한 핸들을 얻을 때 PROCESS_TERMINATE, PROCESS_VM_READ, PROCESS_VM_WRITE, PROCESS_VM_WRITE 권한을 제거한다. PROCESS_TERMINATE 권한 제거로 인해 다른 프로세스에서 이것을 종료시킬 수 없게 된다. 나머지 3개는 DLL 인젝션 시에 많이 본 권한일 것이다. DLL 인젝션 시에는 DLL을 삽입할 프로세스의 메모리를 조작할 필요가 있기 때문에 해당 권한이 필요했다. 하지만 이 권한을 제거함으로써 보호받는 프로세스는 다른 프로세스에 의해 종료될 수도, DLL 인젝션 공격을 받을 수도 없게 된다.


  개인적으로 이 함수는 아직까지 완벽하게 이해를 하지 못했다. 먼저 PROCESS_TERMINATE 권한을 제거하더라도 직접 종료가 가능한 것으로 보인다. 그리고 명령 프롬프트에서 taskkill 명령어를 이용한 종료도 가능한 것 같다. Process Explorer를 통해 제거하는 경우에는 종료가 불가능한 것으로 나온다. 그리고 나머지 세 권한도 이해하지 못하는 부분이 있다. "calc.exe" 프로세스를 먼저 실행시킨 후 드라이버를 로드하면 해당 권한을 얻는 것이 불가능한 것은 맞는 것 같다. 하지만 드라이버를 먼저 로드한 이후에 해당 프로세스를 실행하면 실행이 제대로 되지 않는다. Process Explorer를 보면 해당 프로세스가 Suspended 상태로 존재한다. 이것을 Resume 시키면 제대로 실행되며 이후에 종료가 불가능한 것으로 보아 제대로 동작하는 것 같지만 실행 자체가 제대로 되지 않으며 그 이유를 아직도 모르겠다. 뭔가 잘못된 부분이 있든지 아니면 프로세스 실행 시에 위의 세 권한이 필요한 것인지 잘 모르겠다.


  마지막으로 Altitude 즉 고도에 관한 설정도 존재하는데 현재 개발 중인 드라이버가 미니필터 드라이버가 아니라면야 아무 값을 넣어도 상관 없어 보인다. 그리고 미니필터 드라이버라고 하더라도 값이 달라도 큰 차이는 없는 것 같다. 이것과 관련된 강제적인 사항을 아직 찾지는 못했다.





4. 스캐닝

4.1 파일 시그니처 기반 스캐닝

  처음 파일시스템 미니필터 드라이버를 오픈하면 800라인이 넘는 스켈레톤 코드가 보인다. 여기서는 이것 대신 최소화된 내용인 예제 코드를 이용하도록 한다. 기본 방식은 아주 간단하다. minifilter driver registration structure를 설정한 후 FltRegisterFilter()에 인자로 넣고 미니필터 드라이버를 등록한다. 이후 FltStartFiltering()을 이용해 필터링을 시작한다. Registration 구조체에 등록한 것들 중 중요한 부분은 FLT_OPERATION_REGISTRATION 구조체들의 배열이다. 이 예제에서는 IRP_MJ_CREATE, IRP_MJ_SET_INFORMATION의 경우에 PFLT_PRE_OPERATION_CALLBACK 루틴을 등록하였다. 자세히 설명하자면 파일의 쓰기 및 삭제에 해당하는 IRP에 대해 우리가 구현한 콜백 함수를 등록하겠다는 의미이며 PFLT_PRE_OPERATION_CALLBACK이므로 이 행위 이전에 호출되게 된다. 


  일반적으로 백신에서는 필터링을 통해 파일을 검사하고 허용할지 여부를 판단하겠지만 이 예제에서는 단지 DbgPrint()로 어떤 파일에 어떤 행위가 발생하였는지를 출력하고자 한다. 등록한 루틴 즉 PreOperationCallback()을 살펴보면 IRP_MJ_CREATE의 경우 FILE_WRITE_DATA 또는 FILE_APPEND_DATA일 때 process_irp() 함수를 호출하고, IRP_MJ_SET_INFORMATION의 경우 DeleteFile일 때 process_irp()를 호출한다. process_irp()에서는 각 상황에 맞게 어떤 이름의 파일에서 쓰기 또는 삭제 행위가 일어났는지를 DbgPrint()로 보여준다.



4.2 행위 기반 스캐닝

  행위 기반 즉 휴리스틱 스캐닝은 크게 두 가지로 나뉜다. 하나는 정적 휴리스틱이며 다른 하나는 동적 휴리스틱이다. 정적 휴리스틱의 경우에는 파일을 분석하여 의심스러운 특징을 찾아내어 판단하는 방식이다. 동적 휴리스틱의 경우에는 두 가지 방식으로 나뉘는 것 같다. 


  하나는 HIPS(Host based Intrusion Prevention System)에서 사용되는 방식이다. 이 기능을 순수하게 휴리스틱 방식으로 사용하는 안티바이러스가 있는가 하면 HIPS로 분리하여 보여주는 안티바이러스도 있는 것으로 보인다. 이것은 API 함수들을 후킹하여 실시간으로 어떤 함수들이 사용되는지를 검사하여 의심스러운 행위로 보일 때 탐지하는 방식이다. 


  다른 하나는 에뮬레이터를 이용한 방식이다. 안티바이러스에서는 에뮬레이터 외에도 샌드박스라고도 불리는데 그냥 같은 의미로 사용되는 개념인 것 같다. 에뮬레이터는 굳이 동적 휴리스틱 방식 외에도 다양하게 사용된다. 즉 실행 파일 에뮬레이팅 외에도 셸 코드 분석에 그리고 실행 파일 언패킹에도 사용된다. 어쨌든 에뮬레이터 내에서 실행 파일을 실행시키고 그 행위를 분석하는 방식이다.


  여기서는 앞에서 HIPS로 설명한 부분 즉 PsSetLoadImageNotifyRoutine()을 이용한 방식을 설명하도록 한다. PsSetLoadImageNotifyRoutine() 함수는 이미지가 로드될 때 호출되는 콜백 루틴을 등록한다. 이 예제에서는 이미지 로드 시 로드된 이미지가 ntdll.dll인지 먼저 검사한다. ntdll.dll인 경우 Kernel to User land APC injection 방식을 이용한다. 아직 제대로 정리하진 못했지만 유저 모드에서 APC를 이용한 전형적인 DLL 인젝션 방식과 비슷해 보인다. 구체적으로는 ntdll.dll의 LdrLoadDll() 함수를 호출시키는데 인자로 삽입하길 원하는 DLL의 경로를 넣는다. 


  이 예제에서는 x64 DLL의 경우 System32 폴더에, x86 DLL의 경우 SysWOW64 폴더에 각각 이름을 InjectionMitigationDLLx64.dll 그리고 InjectionMitigationDLLx86.dll로 정하고 넣어 놓는다. 개인적으로는 dllmain()에서 OutputDebugStringA()를 이용해 메시지를 출력하게 함으로써 dll이 제대로 로드되었는지를 확인하였다. 또는 Process Explorer로 현재 로드된 DLL들을 확인해 볼 수 있다.


  이것은 예제일 뿐이고 검색하여 얻은 정보에 의하면 안티바이러스에서는 이러한 방식으로 ntdll.dll이 로드될 때 후킹 엔진이 구현된 DLL을 삽입시킨다. 삽입된 DLL은 로드될 때 dllmain()이 실행되는데 악의적인 행위에 사용될 가능성이 있는 API 함수들을 후킹한다. 즉 어떤 API 함수들이 호출되는지에 대하여 보고받기 위해 후킹하는 것이다. 안티바이러스는 각 후킹된 API들이 호출되는 그 행위를 이용해 분석하여 행위 기반 분석을 수행한다. 





5. 프로세스 / 스레드 생성 보호

  코드 상으로 보면 DriverEntry의 InstallPsProtect()와 DriverUnload()의 UnInstallPsProtect()가 관련된 내용이다. 실제 함수는 PsProtect.h에 구현되어 있다. 여기서 사용하는 함수는 PsSetCreateProcessNotifyRoutineEx()이다. 이것은 PsSetCreateprocessNotifyRoutine()의 확장된 버전이다. 이 함수로 등록한 콜백 루틴은 프로세스 생성 직전에 호출된다. 참고로 두 번째 인자의 경우 설치 시에는 FALSE, 제거 시에는 TRUE를 입력한다.


  이 예제에서는 간단하게 프로세스 실행 전에 호출되어 이 프로세스가 특정한 이름을 갖는지 검사한 후에 실행 여부를 판단한다. PsSetCreateThreadNotifyRoutineEx() 및 PsSetCreateThreadNotifyRoutine()도 프로세스가 아니라 스레드라는 것만 빼면 같은 내용이다. 이 예제에서는 이 4가지 함수들 중에서 PsSetCreateProcessNotifyRoutineEx()만 사용하였다.


  일반적으로 안티바이러스에서는 이 함수를 이용해서 (어느 안티바이러스 업체에서는 PsSetCreateThreadNotifyRoutine()만 사용하는 것 같다) 프로세스가 실행될 때 마다 이 콜백 함수를 이용하여 검사한 후에 실행 여부를 판단할 것이다. 물론 여기서 사용하는 예제처럼 검사에 이름을 이용하지는 않을 것이다.





6. 레지스트리 보호

  코드 상으로 보면 DriverEntry의 InstallRegMonitor()와 DriverUnload()의 UnInstallRegMonitor()가 관련된 내용이다. 실제 함수는 RegMonitor.h에 구현되어 있다. 여기서 사용하는 함수는 CmRegisterCallbacksEx()와 CmUnRegisterCallback()이다. 참고로 CmRegisterCallbacksEx()는 CmRegisterCallbacks()의 확장 버전이며 여기서는 이 확장 버전을 사용한다.


  이 함수는 Registry Operation 시에 호출되는 콜백 루틴을 등록시켜 준다. 이 예제에서는 콜백이 호출된 경우 어떤 operation을 수행하려고 하는지 검사한다. 해당 operation이 RegNtPreCreateKeyEx, RegNtPreOpenKeyEx라면 다시 말해서 특정 프로세스에서 해당 키를 생성하거나 읽으려는 operation이 요청되었다면 그 키가 우리가 보호하려는 키인지 검사한 후에 맞다면 STATUS_ACCESS_DENIED를 반환시킨다.


  안티바이러스의 경우 드라이버 뿐만 아니라 서비스도 실행 중일 것인데 악성코드가 이것들의 정보를 담고 있는 레지스트리를 삭제하거나 수정하려는 시도를 할 수 있다. 테스트를 위해서 regedit.exe를 실행한 후에 보호하고 있는 레지스트리 키를 클릭하면 접근이 거부되었다는 것을 확인할 수 있다.





7. ELAM 및 Protected Process

  윈도우 8부터 Secure Boot라는 기능이 도입되었다. 이것은 윈도우 부팅 설정 및 구성 요소들을 보호하고 ELAM(Early Launch Anti-Malware) 드라이버를 로드한다. ELAM 드라이버는 다른 boot-start 드라이버들보다 먼저 실행되어 다른 드라이버들을 검사하는 역할을 한다. 즉 다른 드라이버들의 무결성을 검사하며 부트 드라이버가 수정되었는지 확인하여 부트킷을 방지한다.

  안티바이러스의 유저 모드 서비스는 과거부터 지금까지 악성코드의 공격 대상이 되어 왔다. 윈도우 8.1부터는 Protected Service라는 개념을 통해 유저 모드 서비스를 보호한다. 이것은 System Protected Process로서 실행되는 서비스이다. 안티바이러스 벤더들은 ELAM 드라이버를 이용해 사용자 모드 서비스를 Protected Service로 실행한다. 이를 위해서 ELAM 드라이버의 리소스 섹션에는 해당 바이너리(서비스)를 서명하는데 사용되는 인증서에 대한 정보가 있어야 한다. 부트 타임 시 이 리소스 섹션이 추출되어 인증 정보를 검증하고 사용자 모드 서비스를 등록하면 서비스는 Protected Service로서 실행된다.

  권한 관련 문서에서 설명한 내용이지만 간략하게 언급하자면 관리자 권한인 High Integrity Level을 가진 프로세스의 경우 SeDebugPrivilege를 활성화할 수 있고 이를 통해 System Integrity Level을 가진 프로세스에 코드 인젝션을 수행할 수 있다. 다시 말해서 Protected Process는 관리자 권한으로 실행된 프로세스로부터의 공격을 방어하기 위한 메커니즘이다. Protected Process는 PROCESS_ALL_ACCESS 뿐만 아니라 PROCESS_CREATE_PROCESS, PROCESS_CREATE_THREAD, PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE 등의 접근 권한이 거부된다.





8. 유저 모드 애플리케이션과의 통신

  이 내용은 예제 코드의 communication 부분에 정리되어 있다. 기본적인 사항을 정리한 후 예제와 함께 설명할 것이다. 유저 모드 애플리케이션과 커널 모드 드라이버가 통신하는 방법 중에서 IOCTL 명령을 이용한 방식을 설명한다. IOCTL 명령은 둘 사이의 통신을 위해 프로그래머가 정의한 명령이다. IRP는 일종의 데이터 구조체로서 드라이버에서는 IRP의 종류에 맞게 이것을 처리하는 함수가 필요하다. I/O 관리자는 애플리케이션의 요청이 있을 때 IRP를 생성해 드라이버 오브젝트의 MajorFunction[] 필드를 이용해 해당 디스패치 루틴으로 IRP를 전달(dispatch)해 준다. 그래서 Dispatch 루틴으로 불린다. 즉 드라이버가 처리하는 IRP 명령어들을 위한 디스패치 루틴들이다.


  애플리케이션은 드라이버와 통신하기 위해서는 CreateFile()하는데 필요한 핸들을 구해야 한다. 이것을 위해 드라이버는 디바이스 이름, 이것의 심볼릭 링크 그리고 디바이스 오브젝트에 대한 포인터가 필요하며 이것을 전역으로 설정한다. 이후 IoCreateDevice()로 디바이스를 그리고 IoCreateSymbolicLink()를 통해 심볼릭 링크를 생성한다. IoCreateDevice()로 디바이스 생성 이후 IoCreateSymbolicLink()로 디바이스에 심볼릭 링크를 지정하는 것이다. 마지막으로 처리할 IRP에 대한 Major Function 즉 디스패치 루틴을 등록한다. IRP_MJ_CREATE의 경우에는 애플리케이션이 CreateFile()로 디바이스 드라이버의 핸들을 얻을 때 발생한다. IRP_MJ_CLOSE는 애플리케이션이 CloseHandle()을 사용할 때 발생한다. 여기서 중요하게 다룰 것은 IRP_DEVICE_CONTROL이다. 이것은 애플리케이션에서 deviceIoControl()을 이용해서 드라이버와 통신할 때 발생한다. 먼저 CTL_CODE 매크로를 이용해 커스텀 컨트롤 코드를 정의한다. 이것은 애플리케이션에서도 똑같이 정의할 것이며 애플리케이션에서 이 컨트롤 코드를 보낼 시에 특정 행위를 수행한다. 이 예제에서는 애플리케이션에서 커스텀 컨트롤 코드를 받은 경우에 같이 받은 데이터를 DbgPrint()로 보여주고 동시에 데이터를 애플리케이션으로 보낸다.


  애플리케이션의 경우 CreateFilte()을 통해 통신할 디바이스의 핸들을 얻는다. 이후 DeviceIoControl()을 통해 드라이버와 통신한다. 이 함수는 2개의 버퍼를 제공하며 디바이스 드라이버와 양방향으로 통신이 가능하다. 드라이버에서와 같이 CTL_CODE 매크로를 이용해 커스텀 컨트롤 코드를 정의한다. DeviceIoControl()에는 커널에서 받은 데이터를 저장할 버퍼도 갖는다. 이 예제에서는 애플리케이션에서 커스텀 컨트롤 코드와 함께 드라이버로 데이터를 보냄과 동시에 드라이버로부터 데이터를 읽는다.




9. 기타

9.1 네트워크 보호

  안티바이러스에서도 기본적으로 방화벽 기능이 제공된다. 과거에는 NDIS 및 TDI 필터 같은 드라이버를 개발해야 했지만 최근 비스타 부터는 WFP(Windows Filtering Platform)라는 플랫폼을 제공한다. 이것은 네트워크 필터링 애플리케이션을 만들기 위해 제공되는 플랫폼으로서 API와 시스템 서비스들의 집합이다. 참고로 과거의 NDIS / TDI는 다른 필터 드라이버들보다 개발하기 위한 난이도가 있었지만 WFP라는 간단한 개발 플랫폼이 제공됨으로 인해 최근 버전의 윈도우에서 돌아가는 안티바이러스 프로그램들은 이것을 사용한다고 한다.



Posted by SanseoLab

0. 개요

  여기서는 안드로이드 앱의 다운로드에서부터 설치, 시작까지의 과정을 다루기로 한다. 아직 안드로이드에 익숙하지 않고 인터넷에 자료가 거의 없으며 꾸준히 업데이트되고 있음으로 인해 정확한 자료가 아닐 가능성이 포함되어 있을 것으로 예상한다. 하지만 찾아보며 공부하는 사람들의 입장에서 키워드 제공의 의미라도 있길 바라며 정리한 내용을 올린다.





1. 다운로드

  일반적으로 앱은 구글 플레이 스토어에서 설치하는 방식이 대부분일 것이다. 물론 직접 apk 파일을 다운로드 받아서 설치하는 방식도 존재한다. 구글 플레이 스토어에서 악성코드가 발견되었다는 뉴스가 자주 보이기는 하지만 기본적으로 악성코드는 직접 설치 방식을 악용한다. 예를들어 블랙 마켓에서 무료 버전이라고 올려놓지만 내부에 악의적인 코드가 존재하는 경우가 있을 수 있고 성인물 사이트에서 성인 앱을 위장해서 설치하게 유도하는 방법이 있을 수 있다. 마지막으로 가장 대표적인 방식은 SMS/MMS를 이용한 피싱이다.





2. 설치

  변경 및 생성된 결과물을 기반으로 설명해 보겠다. 먼저 apk 파일 부터 보자면 시스템 앱들 즉 사전에 설치된 apk들은 /system/app/에 저장되어 있다. 사용자가 설치한 앱은 /data/app/<appname>/에 base.apk라는 이름으로 복사되며 Native 라이브러리는 /data/app/<appname>/lib/*.so 형태로 저장된다.


[ /data/app/<appname>/base.apk ]

[ /data/app/<appname>/lib/*.so ]


  apk에서 추출된 dex 파일은 /data/dalvik-cache/data@app@<appname>@classes.dex로 복사된다. 그리고 애플리케이션의 데이터는 /data/data/<appname>/과 서브디렉터리에 저장된다. 마지막으로 /data/data/<appname>/lib은 위의 /data/app-lib/<appname>/libapp.so에 대한 심볼릭 링크이다.


[ /data/dalvik-cache/data@app@<appname>@classes.dex ]

[ /data/data/<appname>/ ]

[ /data/data/<appname>/lib ]


  참고로 설치 과정에서 AndroidManifest.xml의 내용이 파싱되어 패키지 정보가 /data/system/packages.xml과 /data/system/packages.list에 추가된다.


[ /data/system/packages.xml ]

[ /data/system/packages.list ]


  이제 dex 파일에 대해서 조금 더 설명하기 위해 odex라는 확장자를 설명하도록 하겠다. 먼저 dexopt라는 유틸리티가 존재하는데 dex 파일을 pre-optimization한 .odex 파일 즉 optimized DEX 파일로 변환해주는 역할을 한다. 이것은 설치 과정에서 일어나는데 저장된 파일을 보면 .dex 확장자를 갖지만 실제로는 .odex 파일이다. Dalvik 가상 머신에서 실행되는 바이트 코드인 것은 같지만 최적화되어 성능을 향상시켜 준다.


  이 개념을 기반으로 하여 설명하겠다. 최근 안드로이드에서는 Dalvik VM이 아니라 ART 즉 Android RunTime이 사용된다. 이 메커니즘은 바이트 코드를 가상 머신에서 실행시키는 방식이이 아니라 설치 시점에 컴파일하여 ELF 파일 포맷을 만들며 이것을 실행하는 것이다. 앞에서 dexopt를 통해 .odex를 만드는 것이 아닌 dex2oat를 통해 oat 파일 포맷의 바이너리를 만든다. 이렇게 기능적으로도 많은 차이가 나지만 호환성을 위해서 이것도 Dalvik VM과 같은 과거의 이름을 확장자로 갖는다. 예를들어 과거에는 /data/dalvik-cache/<arch>/data@app@<packagename>.<appname>-1@base.apk@classes.dex였으며 안드로이드 6.0부터는 /data/app/oat/<pachagename>.<appname>/<arch>/base.odex이 되었다. 즉 어느 방식이든지 내부의 포맷은 달라도 이름은 같은 것이다.


[ /data/dalvik-cache/<arch>/data@app@<packagename>.<appname>-1@base.apk@classes.dex ]

[ /data/app/oat/<pachagename>.<appname>/<arch>/base.odex ]





2. 실행

  앞에서도 언근했듯이 안드로이드 5.0부터는 Dalvik VM 대신 ART를 사용한다. ART는 애플리케이션들과 시스템 서비스들에서 사용되는 런타임으로서 가상 머신을 통해 실행시키는 메커니즘이 아니라 런타임 환경을 제공하는 방식이다. apk의 내용을 보면 dex 파일이 존재하는데 이것은 Dalvik VM에서 실행할 수 있는 바이트 코드가 존재한다. 이것을 통해 Dalvik VM은 JIT 방식을 사용하였다. 하지만 가상 머신에서 실행시킨다는 것은 속도 등의 면에서 많이 부족할 수 밖에 없다. 그래서 앱 설치 시에 dexopt를 통해 .odex 파일로 최적화하지만 가상 머신에서 실행된다는 것 자체는 차이가 없기 때문에 한계가 존재하였다.


  ART 메커니즘은 AOT 즉 ahead-of-time 컴파일을 방식을 사용한다. 즉 apk의 dex 파일을 컴파일하여 oat 포맷 더 정확히는 elf 바이너리 형태로 만든다. 이것은 네이티브 기계어로서 가상 머신에서가 아닌 바이너리로서 동작할 수 있게 된다. 참고로 과거 방식에서는 설치 시에 dexopt를 통해 .odex 파일을 만들었다면 ART는 dex2oat를 이용해 oat 파일 포맷으로 앱을 컴파일한다. 물론 앞에서도 언급하였듯이 하위 호환을 위해 확장자는 oat가 아니라 dex 또는 odex가 사용된다. 참고로 안드로이드 7.0부터는 AOT와 JIT(just-in-time) 컴파일의 조합을 이용한다.


  이런 차이를 기반으로 비록 이제는 사용되지 않지만 Dalvik VM부터 시작하여 ART 런타임 메커니즘을 비교하면서 알아보도록 하겠다. 안드로이드 부팅 후 가장 먼저 시작되는 프로세스인 Init은 /system/core/rootdir/init.rc를 읽고 여기에 적힌 내용을 수행한다. 여러가지가 있겠지만 여기서는 앱의 실행과 관련된 것을 위주로 보겠다. Init은 /system/bin/app_process를 통해 AndroidRuntime 객체를 생성하며 이것은 DalvikVM을 실행시킨다. 그리고 DalvikVM에 Zygote를 실행하도록 요청한다.


  조금 더 진행하기 전에 Zygote에 대해서 설명하도록 하겠다. 이것은 안드로이드 앱에서 공통적으로 필요로 하는 자원, 라이브러리 등을 미리 로드시킨 후 대기하다가 새로운 애플리케이션 실행 시에 간단하게 자신을 fork 하는 메커니즘이다. 만약 애플리케이션 실행 시 마다 메모리 및 자원을 할당하고 로드하고 등의 작업을 한다면 부하가 많이 걸릴 것이다. 대신 Zygote라는 프로세스를 미리 만들어 놓아서 대기시킨 후 애플리케이션 실행 시에 fork를 통해 복사하여 필요한 부분 즉 실행시킬 애플리케이션과 관련된 부분만 처리해 준다면 중복된 행위를 통한 낭비를 줄일 수 있을 것이다.


  앞에서 DalvikVM을 실행시킨 후 Zygote를 실행하도록 요청한다고 하였다. 이 말은 Zygote 자체도 네이티브 바이너리가 아니라 가상 머신 위에서 돌아가는 바이트 코드 형태로 존재한다는 것을 의미한다. 즉 다른 애플리케이션과 차이가 없는 것이다. 그렇기 때문에 Zygote를 fork()한 후 실행시킬 애플리케이션을 로드하여 이 애플리케이션도 DalvkVM 위에서 실행될 수 있게 한다.


  이제부터는 ART 런타임 메커니즘을 알아보겠다. 이것은 가상 머신 방식이 아닌 런타임 환경을 제공하는 방식이다. 하지만 이 메커니즘이 Dalvk VM과 비교해서 완전히 바뀐 것은 아니다. Zygote를 이용하는 방식의 장점에 따라 이것을 수용하였고 단지 차이가 있다면 가상 머신을 이용하는 방식이 아니라는 점이다.


  app_process는 DalvikVM을 실행시키고 이것에 Zygote를 실행하도록 요청하지 않고 단지 Zygote를 실행시킨다. Zygote가 시작되면 ART 즉 libart.so를 로드하고 system/framework/<arch>/boot.oat, system/framework/<arch>/boot.art를 로드한 후 클래스 링킹 과정을 거친다. boot.art는 pre-initialized 클래스 및 객체들의 힙 등을 가진 이미지로서 기본적으로 프레임워크 함수와 실행 가능한 코드의 실제 주소 사이의 매핑 테이블을 제공한다. boot.oat는 pre-compiled 코드를 담고 있는 elf 파일인데 모든 안드로이드 프레임워크 바이트코드의 컴파일된 바이너리를 갖는다. 앱에서 프레임워크 함수를 호출하기 위해서는 boot.art 매핑 테이블에 쿼리하고 boot.oat의 텍스트 섹션에서 실제 코드를 호출하게 된다. 이것은 미리 zygote의 초기화 과정 즉 클래스 링킹 과정을 통해 프레임워크 라이브러리들의 클래스 멤버들에 접근할 수 있게 된다.


  이후부터는 Dalvik VM 방식과 비슷하다. 먼저 SystemServer 프로세스를 fork하는데 이것으로부터 다시 Framework Services, Package manger, Activicty Manager(Launcher) 등이 fork된다. 마지막으로 ActiveManagerService와 상호작용하기 위한 소켓을 생성하고 기다린다.


  이제 앱을 실행해 보도록 하자. 화면에서 클릭함으로써 Launcher의 onClick() Callback이 호출되고 이것은 Binder를 통해 Activity Manager의 StartActivity()를 호출한다. Activity Manager는 이 요청을 받으면 startViaZygote()를 호출하는데 Zygote가 이것을 받은다면 fork()한 후 Application Binding 과정이 시작된다. 이것은 프로세스를 실행할 애플리케이션에 어태치하는 과정이다. 여러 과정을 거쳐서 makeApplication() 메소드가 실행되는데 이것은 앱 관련 클래스를 메모리에 로드해 준다. 이후 Activity Manager는 realStartActivity()를 시작으로 하여 프로세스를 launch 시킨다.


  지금까지의 설명을 보충하자면 ART 메커니즘으로 바뀜으로서 윈도우나 리눅스에서처럼 oat 즉 elf 바이너리를 직접 실행할 수 있는지 궁금할 수 있다. 하지만 Zygote는 초기에 boot.art 및 boot.oat를 통한 초기화, ART 런타임 로드 등의 작업을 미리 하였고 이렇게 대기 중인 프로세스를 fork()한 후 oat 바이너리를 로드하여 실행하는 메커니즘이었다. 그렇기 때문에 이 많은 과정 없이 순수하게 oat 하나만으로 무엇인가를 할 수는 없다.




Posted by SanseoLab
이전버튼 1 2 3 4 5 6 이전버튼

블로그 이미지
Malware Analyst
SanseoLab

태그목록

공지사항

Yesterday
Today
Total

달력

 « |  » 2024.4
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30

최근에 올라온 글

최근에 달린 댓글

글 보관함