Mafia의 진실
Mafia의 진실 2010.02.10

낚시성 제목에 방문하신 분께는 죄송합니다. 이 글은 오래전부터 twitter 사용자들을 대상으로 퍼져나가고 있는 'Mobster world'라는 온라인 MMORPG 게임과 관련된 글입니다. 트위터 사용자 분들 중에는 위와 유..

iPhoto 슬라이드 쇼

트위터에 질문이 하나 올라와 짧게 작성해봤습니다. iPhoto에서 슬라이드 쇼 생성하는 방법입니다. 1. 먼저 사진 메뉴에서 슬라이드 쇼에 추가할 사진을 선택합니다. 사진 선택은 Command 키와 마우스를 이용합니다. 2...

노키아에서 동작하는 Mac OS X 10.3

핀란드에 사는 Toni Nikkanen이라는 분이 자신의 Nokia N900 모델에 Mac OS 10.3 Panther를 설치하고 실행시키는데 성공했다고 합니다. Toni의 블로그에 가보니 Mac OS X 말고도 Windows..

개요
Junk Code(이하 정크 코드)는 Garbage Code(이하 가비지 코드)와는 약간 다른 의미로 사용됩니다. 가비지 코드는 "진짜 코드"를 감추기 위해서 삽입된 무의미한 코드를 일컫는 말로 리버서 즉 사람을 속여 리버싱을 하는 시간을 더디게 하는 것이 주 목적입니다. 반면 정크 코드는 리버싱을 속도를 더디게 할 목적으로 삽입된 코드라는 점을 같지만 디스어셈블러를 속인다는 점이 가비지 코드와 다릅니다. 이번에 다룰 내용은 바로 이 정크 코드와 가비지 코드를 이용한 안티 리버싱 기법입니다.

가비지 코드를 이용한 안티 리버싱 
정크 코드나 가비지 코드는 다분히 트릭의 성격이 강합니다. 별다른 원리라고 부를 만한게 없다는 것이죠. 예제를 보면 금방 이해갈 것이라고 생각합니다. 먼저 가비지 코드부터 살펴보겠습니다. 앞서 이야기한 대로 가비지 코드는 "진짜 코드"를 보호하기 위해 여기저기 맘대로 삽입된 무의미한 코드입니다. 아래의 예를 봐주세요.

보호할 코드 (PEB.IsBeingDebugged를 이용한 디버거 탐지)

  MOV EAX, DWORD PTR FS:[18h]
  MOV EAX,  DWORD PTR DS:[EAX+30h]
  MOVZX EAX, BYTE PTR DS:[EAX+2h]
  TEST EAX, EAX
  JNZ @DebuggerFound


보호할 코드 + 가비지 코드
  XOR EBX, EBX
  MOV EBX, FFEEFFEEh
  SHR EBX, 4
  ADD ESI, EBX
  MOV EAX, DWORD PTR FS:[18h]
  MOV ECX, 3204F83Eh
  SUB ECX, EBX
  PUSH offset @JumpHere
  ADD ECX, ESI
  LEA EDX, [ESI+ECX]
  MOV EAX,  DWORD PTR DS:[EAX+30h]
  RETN
  SHR EDX, 5
  MOV ESI, EDX
 
@JumpHere:
  MOVZX EAX, BYTE PTR DS:[EAX+2h]
  LEA  EBX, [EAX+ECX]
  XOR  ECX, ECX
  TEST EAX, EAX
  PUSH ESI
  LEA  ESI, [ESP+0Ch]
  POP  EDI
  PUSH EAX
  JNZ @DebuggerFound


^^; 숨은 그림 찾기 같지 않습니까? 진짜 코드를 아무런 의미없는 코드 사이에 숨겨놓았습니다. 가비지 코드가 그럴 듯하면 할수록 리버서는 엉뚱한 것을 분석하느라 시간을 보내게 되겠죠. 이런 코드는 분석하는 제너럴한 테크닉은 없습니다. 예제 코드의 경우라면 FS:[18h]의 값이 EAX에 저장되었다는 사실에 주목하여 EAX 위주로 코드를 찾아가다보면 어느 정도 윤곽을 잡을 수 있습니다. 어쨌든 아는 만큼 밖에 안보이는 형태라 리버서들을 괴롭히는 트릭입니다.

Junk Code를 이용한 안티 리버싱
다음 예제코드를 봐주세요.

main()
{
   __asm
  {
      jmp here+1 ;
     here:
      __emit 0xe9     ;__emit은 뒤에 오는 바이트를 그대로 코드에 포함시킬때 사용
                      ;0xe9은 jmp를 나타내는 opcode

     mov eax, 1 ;
 }
}
위 코드는 실제로 하는 일이 mov eax, 1을 수행하는 것입니다. 일단 컴파일 한 다음 디스어셈블러와 디버거로 열어서 확인해 보도록 하겠습니다. 먼저 dumpbin으로 디스어셈블한 결과입니다.

C:\zesrever\lab\dumpbin /disasm junk.exe
 _main:
  00401000: 55                 push        ebp
  00401001: 8B EC              mov         ebp,esp
  00401003: 53                 push        ebx
  00401004: 56                 push        esi
  00401005: 57                 push        edi
  00401006: E9 01 00 00 00     jmp         0040100C
  0040100B: E9 B8 01 00 00     jmp         004011C8
  00401010: 00 5F 5E           add         byte ptr [edi+5Eh],bl
  00401013: 5B                 pop         ebx
  00401014: 5D                 pop         ebp
  00401015: C3                 ret

   (이하 생략)

어... main 함수안에 mov eax, 1이 안보이네요~ 00401006번지를 잘 살펴보면 0040100C 번지로 점프한다는 것을 확인할 수 있죠. 그런데 그 아래 코드를 보면 0040100B번지에서 시작합니다. 그렇다면 0040100B번지에 있는 E9는 아무런 의미가 없는 바이트가 되겠죠. 따라서 제대로 분석하려면 E9은 버리고 B8을 명령어로 해석해야 합니다. dumpbin과 같이 플로우 분석을 하지 않는 단순한 디스어셈블러들은 이런 사실을 알리가 없습니다. 그래서 위와 같은 결과가 나오는 것이죠... 그럼 ollydbg는 어떨까요?

사용자 삽입 이미지

ollydbg는 좀 똑똑하네요. ^^; 하지만 플로우 분석을 해 주는 이런 툴들에 전적으로 의존하는 것보다는 코드를 주의깊에 살펴보는 것이 필요하겠습니다. 혹시 ollydbg가 아래와 같은 모양을 보인다면 (간혹 그런 케이스도 있더라구요) 바이너리 편집을 하면 합니다.
사용자 삽입 이미지

위와 같이 편집할 부분을 선택하고 Ctrl+E(Edit)를 누릅니다.
사용자 삽입 이미지

그런 다음 정크 코드로 의심되는 부분을 NOP(0x90)으로 변경해 주면 됩니다. 아래는 이렇게 해서 변경한 결과입니다.

사용자 삽입 이미지

 
IDA는 이런 간단한 트릭에는 잘 속지 않습니다. 그나마 다행이죠. 이제 정크 코드를 이용한 안티 리버싱 기법이 이해되시나요?

맺음말
가비지 코드나 정크 코드는 매우 단순하지만 효과적인 안티 리버싱 기법입니다. 이 글을 보시는 분 중 프로그램 잘 하시는 분 계시면 가비지 코드 제거 플러인 같은 것 개발해주실래요? ^^;; 저는 하다가 머리아파서 포기...(점점 살리에르가 되어가는 기분입니다. 원래 모짜르트인 적도 없었기는 하지만) 즐핵~  

Posted by zesrever

개요
윈도우는 프로그램의 디버깅을 돕기 위한 많은 기능들을 가지고 있습니다. 이러한 기능의 사용을 제어할 수 있는 방법 중 하나가 Process Environment Block(이하 PEB)에 저장되어 있는 NtGlobalFlag 값을 조정하는 것입니다. 예를 들자면 Heap 오버플로우가 발생하는지를 체크하기 위해서 NtGlbalFlag의 값을 0x10으로 조정하는 식입니다. 정상적인 환경에서 프로그램이 실행되는 경우 보통은 이러한 디버깅 지원 기능들을 사용하지 않는 것이  일반적입니다. 하지만 디버깅을 하는 경우라면 이야기가 달라지겠죠. 다시 말해 디버거가 붙어 있을 때와 그렇지 않을 때 PEB.NtGlobalFlag의 값이 다르다는 이야기입니다. 이러한 사실은 디버거를 탐지하는데 이용됩니다. 이 글에서 알아볼 내용이 이에 대한 것입니다.

PEB.NtGlobalFlag
앞서 NtGlbalFlag의 역할에 대해서 간단히 알아보았습니다.  이 글에서는 NtGlobalFlag에 포함된 각 플래그의 기능에 대해서 설명하는 대신 디버깅을 할 때와 하지 않을 때 NtGlobalFlag 값은 어떤 차이가 있는지를 먼저 살펴볼 것입니다. NtGlboalFlag에 포함된 각 플래그의 기능에 대해서 궁금하신 분은  MS Technet을 참고하시기 바랍니다.

먼저 테스트에 사용할 간단한 코드를 작성해야 하겠습니다. C/C++로 작성하면 보다 읽기 좋겠지만 그냥 간단하게(?) 어셈블리로 작성하도록 하죠. 일단 PEB의 구조부터 살펴보겠습니다. 어렵게 헤더 파일을 헤집고 다닐 필요없이 WinDbg로 간단히 살펴보죠. (WinDbg로 아무 실행 파일이나 하나 연다음 아래처럼 명령어를 내리면 됩니다. 명령어가 생소하신 분은 RTFM!! Windbg 도움말을 참고하세요. 인터넷 찾아 헤맬필요 없습니다.)

0:000> dt _PEB
ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
     (중간 생략)
   +0x064 NumberOfProcessors : Uint4B
   +0x068 NtGlobalFlag     : Uint4B
   +0x070 CriticalSectionTimeout : _LARGE_INTEGER
     (이하 생략)


위의 결과로부터 우리가 읽어야 하는 NtGlobalFlag는 PEB 시작 위치에서 0x68만큼 떨어진 곳에 위치하고 있는 4bytes 크기의 데이터라는 사실을 알 수 있습니다. 그럼 PEB의 시작 위치를 구하는 방법만 알면 되겠습니다. PEB의 시작 위치는 TEB(Thread Environment Block)에서 찾을 수 있습니다.그럼 TEB의 구조도 살펴볼까요?

0:000> dt _TEB
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : Ptr32 Void
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : Ptr32 Void
   +0x02c ThreadLocalStoragePointer : Ptr32 Void
   +0x030 ProcessEnvironmentBlock : Ptr32 _PEB
   +0x034 LastErrorValue   : Uint4B
    (이하 생략)


음.. PEB의 주소는 TEB+0x30에 저장되어 있군요. ^^; 그럼 TEB의 주소는? TEB는 FS가 가르키는 세그먼트의 시작점에 위치합니다. 다시 말해 FS:0이 바로 TEB의 시작 주소입니다. 따라서 PEB의 시작 주소는 FS:[0x30]이 되는거죠. 그럼 NtGlobalFlag의 주소는? FS:[0x30]+0x68 이 됩니다. 이상의 사전 지식을 바탕으로 작성한 코드가 아래에 준비되어 있습니다.

[예제코드 1] NtGlobalFlag 값을 읽는 코드. ReadGlobalFlag.asm

   .586
      .model flat, stdcall
      option casemap :none   ; case sensitive

      include \masm32\include\windows.inc
      include \masm32\include\user32.inc
      include \masm32\include\kernel32.inc

      includelib \masm32\lib\user32.lib
      includelib \masm32\lib\kernel32.lib
 .data
      MainCaption db "The Value of PEB.NtGlobalFlag", 0h
      fmtstring db "NtGlobalFlag : %x",0h
      GlobalFlag dd 0
 .code
 
start:
  ASSUME FS:NOTHING
  MOV  EAX, FS:[30h] ;EAX에 PEB의 주소를 저장
  MOV  EAX, [EAX+68h] ;EAX+0x68은 PEB.NtGlobalFlag의 주소
             ;따라서 EAX에는 PEB.NtGlobalFlag값이 저장됨
 
 
  ;메시지 박스로 출력할 Text 생성
  PUSH EAX        
  PUSH offset fmtstring
  PUSH offset GlobalFlag
  CALL wsprintfA
 
  PUSH 40h
  PUSH offset MainCaption
  PUSH offset GlobalFlag
  PUSH 0
  CALL MessageBox
  PUSH 0
  CALL ExitProcess
end start


자..이제 실행시켜 볼까요? 테스트는 Ollydbg와 Windbg를 이용하였습니다.  각각의 결과는 아래와 같습니다.

사용자 삽입 이미지
                                        [그림 1] Normal 환경에서의 NtGlobalFlag값

정상적인 경우 즉 디버거를 붙이지 않은 경우 NtGlobalFlag 값은 0임을 확인할 수 있습니다.


사용자 삽입 이미지
        [그림 2] Ollydbg로 디버깅 하였을 때의 NtGlobalFlag 값

Ollydbg로 디버깅하였을 경우 NtGlobalFlag 값은 0x70입니다.


사용자 삽입 이미지
                 [그림 3] Windbg로 디버깅 하였을 때의 NtGlobalFlag 값

Windbg의 경우도 Ollydbg와 마찬가지로 0x70값을 가지는 군요.

위의 실험으로 확인된 것처럼 디버거를 사용하는 경우 일반적으로 NtGlobalFlag는 0이 아닌 다른 값(0x70)으로 설정됩니다. 참고로 0x70은 아래와 같은 플래그가 설정된 결과입니다.

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

위의 각 플래그들은 heap과 관련한 디버깅에 필요한 옵션입니다.

PEB.NtGlobalFlag를 이용한 디버거 탐지
NtGlobalFlag를 이용하여 디버거를 탐지하는 로직은 단순합니다. 그냥 PEB.NtGlobalFlag 값을 읽어 이 값이 0인지를 테스트해 보면 되는 거죠. 사실 [예제코드 1]을 조금만 손 보면 됩니다. 아래 수정된 코드를 참고하세요.

[예제코드 2] PEB.NtGlobalFlag를 이용한 Debugger 탐지

(앞부분 생략)
.data
      MainCaption db "Debugger Detection", 0h
      TextDebuggerFound db "Debugger Found!!",0h
      TextDebuggerNotFound db "Debugger Not Found!",0h
 .code
 
start:
  ASSUME FS:NOTHING
  MOV  EAX, FS:[30h] ;EAX에 PEB의 주소를 저장
  MOV  EAX, [EAX+68h] ;EAX+0x68은 PEB.NtGlobalFlag의 주소
             ;따라서 EAX에는 PEB.NtGlobalFlag값이 저장됨
 
 
  TEST EAX, EAX
  JZ  @DebuggerNotFound
 
 @DebuggerFound :
  PUSH 30h
  PUSH offset MainCaption
  PUSH offset TextDebuggerFound
  PUSH 0
  CALL MessageBox
  PUSH 0
  CALL ExitProcess

 @DebuggerNotFound:
  PUSH 30h
  PUSH offset MainCaption
  PUSH offset TextDebuggerNotFound
  PUSH 0
  CALL MessageBox
  PUSH 0
  CALL ExitProcess
end start


다음은 위의 코드를 ollydbg 안에서 실행한 결과입니다.

사용자 삽입 이미지

       [그림 4] ollydbg에서 실행한 화면

PEB.NtGlobalFlag를 이용한 디버거 탐지 우회
디버거 탐지 코드를 식별할 수 있는 경우라면 NtGlobalFlag의 값을 테스트한 후 조건 분기하는 부분에서 코드를 수정하는 방법을 사용할 수 있습니다. 아래는 예제코드에 사용된 코드를 디버거에서 수정한 예입니다. JZ를 JNZ로 변경하였습니다.

사용자 삽입 이미지
      [그림 5] PEB.NtGlobalFlag를 이용한 디버거 탐지 우회-1

하지만 대부분의 이러한 코드들은 가비지와 섞여 있고 여러가지 트릭들이 사용되어 한눈에 식별되지 않은 경우가 많을 뿐더러, 여러 번 체크할 수 있기 때문에 매번 이런식으로 조작하는 것은 비효율적인 방법입니다. 보다 효율적인 방법은 PEB.NtGlobalFlag의 값을 0으로 변경하는 것입니다. Olly Advanced 플러그인이나 anti-anti 플러그인을 이용하면 디버깅을 시작할 때 이 값을 0으로 설정하여 탐지를 우회할 수 있습니다.  

맺음말
그 간 블로그에 올라온 글을 읽어보면 느끼겠지만 Anti Reversing 테크닉은 그다지 어려운 것들이 아닙니다. 관련된 지식의 난이도도 높지않고 구현하기도 어렵지 않은 것들이 대부분입니다. 하지만 리버서라면 반드시 알고 있어야 할 내용들이죠. 이 글이 조금이라도 도움이 되었으면 하는 바램입니다. 밤샜더니 졸립네요.. -.-;;

Posted by zesrever

개요
오늘은 Timing Detection이라고 불리는 간단한 안티 리버싱 테크닉에 대한 이야기입니다. 매우 상식적인 이야기라 그다지 어려울 것이 없는 내용입니다. ^^; 각설하고~ 바로 시작하죠.

Timing Detection
 타이밍 디텍션 테크닉의 기본 개념은 매우 단순합니다. 먼저 RDTSC등의 (Read Time Stamp Counter) 명령을 이용하여 시스템 부팅 후 실행시점까지 경과된 CPU 사이클을 읽어들인 후  특정지점에서 다시 RDTSC를 이용하여  사이클을  읽어들여 차이값을 계산합니다. 그 차이값이 미리 설정된(또는 일정범위 안에서의 난수) 값보다 크면 디버거가 존재하는 것으로 판단하는 것입니다. 보통 디버깅을 할 때 디버그 인터럽트를 처리하는 과정에서 약간의 CPU 싸이클이 더 소모되는 점과 브레이크 포인트등을 걸면서 분석해가는 점(당연히오랜 시간이 소요되겠죠)을 이용한 것입니다. 간단한 예제 코드를 살펴보도록 하겠습니다.

[예제코드 1] TimingDetection1.asm

   .586
      .model flat, stdcall
      option casemap :none   ; case sensitive

      include \masm32\include\windows.inc
      include \masm32\include\user32.inc
      include \masm32\include\kernel32.inc

      includelib \masm32\lib\user32.lib
      includelib \masm32\lib\kernel32.lib
 .data
      MainCaption db "디버거  탐지", 0h, 0h
       TextDebuggerFound db "디버거가 탐지되었습니다.",0h,0h
       TextDebuggerNotFound db "디버거가 탐지되지 않았습니다.",0h,0h
 .code
 
start:
  RDTSC         ;RDTSC는 경과된 CPU 사이클을 읽어 edx:eax에
                ;저장합니다.
             
  xor ecx, ecx    
  xor ebx, ebx
  mov ecx, edx       ;ecx에 경과된 CPU 사이클의 상위 32bit 저장
  mov ebx, eax       ;ebx에 경과된 CPU 사이클의 하위 32bit 저장
  rdtsc              ;다시 경과된 사이클을 읽어냄
  
  cmp edx, ecx       ;최초 측정된 사이클과 두번째 측정된 사이클의
                     ;상위 32bit값을 비교합니다.
  ja @DebuggerFound  ;상위 32bit 값이 증가되었으면 Deubbger가 존재하는
                     ;것으로 판단합니다.
             
  sub eax, ebx      ;마찬가지로 하위32bit값을 조사합니다.
  cmp eax, 0fffh    ;그 값의 차가 지정한 값보다 크면 Debugger가 존재하는
                    ;것으로 판단합니다.
  jbe @DebuggerNotFound
 
 @DebuggerFound:
  PUSH 30h
  PUSH offset MainCaption
  PUSH offset TextDebuggerFound
  PUSH 0
  CALL MessageBox
  PUSH 0
  CALL ExitProcess
 
 @DebuggerNotFound:
  PUSH 30h
  PUSH offset MainCaption
  PUSH offset TextDebuggerNotFound
  PUSH 0
  CALL MessageBox
  PUSH 0
  CALL ExitProcess
 
end start

위 코드를 디버거 없이 정상적으로 실행한 결과입니다.

사용자 삽입 이미지

아래는 예제 프로그램을 디버거로 오픈한 후 테스트한 결과입니다. 먼저 예제 프로그램을 ollydbg로 오픈한 후 아래와 같이 00401002번지에 브레이크 포인트를 설정하였습니다.
 
사용자 삽입 이미지

그런 다음 F9를 한 번 눌러 브레이크 포인트가 걸리는 것을 확인하고 다시 F9를 눌러 실행을 계속해 보았습니다.
사용자 삽입 이미지

디버거가 탐지되었군요. 어찌보면 당연한 결과라고 할 수 있겠습니다. 하지만 코드를 읽어가면서 다들 눈치챘겠지만 예제 코드에는 몇 가지 문제점이 존재합니다.

먼저 두 RDTSC 사이에 존재하는 코드에 브레이크를 걸지 않으면 너무나 쉽게 우회된다는 것이죠. 이 문제는 두 가지 방법으로 해결할 수 있습니다. 첫번째 방법은 먼저 RDTSC를 이용하여 CPU 사이클 값을 읽어낸 다음 의도적으로 익셉션을 유발시키고 나서 익셉션 핸들러 안에서 다시 RDTSC를 이용하여 사이클 값을 읽어내는 방법입니다. 디버거가 존재한다면 익셉션이 발생했을 때 일차적으로 프로그램이 정지하게 된다는 점을 이용한 것입니다. 코드를 수정하여 다시 테스트해 보도록 하겠습니다.

[예제 프로그램 3]. TimingDetection2.asm

   .586
      .model flat, stdcall
      option casemap :none   ; case sensitive

      include \masm32\include\windows.inc
      include \masm32\include\user32.inc
      include \masm32\include\kernel32.inc

      includelib \masm32\lib\user32.lib
      includelib \masm32\lib\kernel32.lib
 .data
      MainCaption db "디버거  탐지", 0h, 0h
       TextDebuggerFound db "디버거가 탐지되었습니다.",0h,0h
       TextDebuggerNotFound db "디버거가 탐지되지 않았습니다.",0h,0h
 .code
 
start:
 ASSUME FS:NOTHING
  PUSH  offset @ExceptionHandler
  PUSH FS:[0]
  MOV  FS:[0], ESP
 
  RDTSC         ;RDTSC는 경과된 CPU 사이클을 읽어 edx:eax에
                ;저장합니다.
 
  ADD [EAX], EAX ;익셉션 유발, 디버깅을 하는 경우 이 지점에서 디버기가
                 ;정지합니다. 여기에서 시간 차가 발생합니다.
 
 
  PUSH 0
  CALL ExitProcess
 
 @ExceptionHandler:
  PUSH EBP
  MOV  EBP, ESP
  PUSH ESI               ;callee saved register
                         ;ContextRecord를 가르키기 위해 ESI 사용
                        
  MOV  ESI, [EBP + 10h]  ;3번째 아규먼트(ContextRecord)의 값을 ESI에 저장
 
  RDTSC
    
  cmp edx, [ESI+0A8h] ; [ESI+0A8h]는 익셉션 유발 당시의 EDX 값
  ja @DebuggerFound 
            
  sub eax, [ESI+0B0h] ; [ESI+0B0h]는 익셉션 유발 당시의 EAX 값  
  cmp eax, 0ffffh   
 
  jbe @DebuggerNotFound
 
 @DebuggerFound:
  PUSH 30h
  PUSH offset MainCaption
  PUSH offset TextDebuggerFound
  PUSH 0
  CALL MessageBox
  JMP  @ExceptionHandler_end
 
 @DebuggerNotFound:
  PUSH 30h
  PUSH offset MainCaption
  PUSH offset TextDebuggerNotFound
  PUSH 0
  CALL MessageBox
 
 @ExceptionHandler_end :
  MOV  ECX, DWORD PTR DS:[ESI+0B8h] ;EIP값을 변경하기 위해 ECX에 저장
  ADD  ECX, 2                       ;ADD [EAX], EAX는 2bytes
  MOV  DWORD PTR DS:[ESI+0B8h], ECX
  XOR EAX, EAX                     
  POP ESI
  MOV ESP, EBP
  POP EBP
  RET
 
end start

아래는 테스트 결과입니다. Ollydbg로 오프한 다음 F9를 눌러 실행시키면 아래와 같이 익셉션이 발생하는 지점에서 일단 정지합니다. (이미 RDTSC가 실행된 후죠. 시간은 계속 가고 있습니다.)

사용자 삽입 이미지

Shift+F9를 눌러 익셉션을 처리하도록 하겠습니다.
사용자 삽입 이미지

위에서 보는 바와 같이 디버거가 정상적으로 탐지되었습니다.

두번째 방법은 RDTSC를 감추는 방법입니다. 꼼수입니다. ^^; 생각해 볼 수 있는 방법 중 하나는 기존의 DLL등에서 RDTSC를 호출하는 부분을 찾아 해당 주소로 점프하는 방법을 사용하는 것입니다. ollydbg에서 Ctrl+B를 눌러 ntdll.dll, kernel32.dll, user32.dll등 주요 DLL에서 "0f 31"을 찾아보았습니다. "0f 31"은 RDTSC의 opcode입니다. 몇 분정도 삽질한 결과 제가 사용하고 있는 비스타에서는 쓸만한 코드를 찾을 수 없었습니다. 지금 노트북에 있는 마땅한 vmware 이미지로는 Windows XP SP1밖에 없네요. 일단 Windows XP SP1에서는 다음의 주소에서 쓸만한 놈을 찾을 수 있었습니다. 아마 XP SP2에서도 찾을 수 있을 듯합니다. Windows 2000이나 Windows 2003은 찾아보질 않아서... ^^;

사용자 삽입 이미지

 77FA8797로 점프하면 되겠습니다. RDTSC로 읽어들인 클럭 수가 ECX+8번지와 ECX+C번지에 저장되므로 점프하기 전에 ECX값을 적절히 조절할 필요가 있겠습니다. 또한 RDTSC와 RETN 사이에 있는 각 커맨드로 인해 문제가 발생하지 않도록 하기 위해 몇 개의 코드를 삽입할 필요가 있습니다. 아래는 예제코드 1을 수정한 것입니다. 아래 코드는 개념 설명을 목적으로 한 것으로 실제 사용하기에는 좀 부적합 합니다. 유념하시고 살펴보시기 바랍니다.

[예제 코드 3] TimingDetect3.asm
start:
  mov esi, offset Garbage    ;77fa87a5번지에 있는 LOCK XADD [ESI],EAX
                             ;에 의해 익셉션이 발생하지 않도록 하러면
                             ;ESI가 유효한 주소값을 가져야 합니다.
 
  mov ecx, offset SavedEAX1 - 8

  push 01h           ;77fa87b6에있는 retn 4때문에 삽입했습니다.
                     ;즉 return 후 ESP = ESP + 4가 되므로
                     ;에러가 발생하는 것을 방지하기 위해 추가한 코드입니다.

  push offset @ReturnHere1   ;점프 후 복귀할 주소를 스택에 push합니다.
                             ;77fa87b6번지에 있는 retn 4가 실행되면
                             ;@ReturnHere1으로 복귀할 것입니다.
  push 02h                   ;77fa87b5번지에 POP ESI 때문에 삽입했습니다.
  push 77fa8797h
  ret
 @ReturnHere1:
  mov esi, offset Garbage
  mov ecx, offset SavedEAX2 - 8
  push 01h          
  push offset @ReturnHere2  
                 
               
  push 02h          
  push 77fa8797h
  ret
 
 @ReturnHere2:
  mov edx, [SavedEDX2]
  mov eax, [SavedEAX2]
  sub edx, [SavedEDX1]
  cmp edx, 01h          ; 이 값은 시스템마다 다를 수 있습니다.
                        ; 최적화된 값을 찾으려면 RDTSC 사이에 호출되는 각 명령어
                        ; 가 소비하는 싸이클 수를 다 계산해야 하지만
                        ; 단순 삽질로 찾아낸 값입니다. 따라서 부정확할 수 있습니다.
  ja  @DebuggerFound
  sub eax, [SavedEAX1]
  cmp eax, 0f0000000h   ; 역시 부정확한 값입니다.
  jbe @DebuggerNotFound


실제 테스트한 결과입니다.  

사용자 삽입 이미지

성공한 장면을 삽입하였습니다만... vmware(Windows XP SP1 이미지) 상에서 테스트해 본 결과 주석대로 오차가 많이 발생하여 딜레이 시간에 따라 탐지가 되었다가 안되었다가 하는군요.  개념만 파악 용도로 작성되었다는 점 이해해 주세요. ^^;

문제점이 어느 정도 보완되었나요... 사실 큰 문제점이 몇 가지 더 있습니다.
RDTSC를 이용하여 시간을 계산할 때 일반적으로 고려해야 할 점들인데요, 먼저 Counter Overflow 발생에 대한 고려를 해야 합니다. 또한 RDTSC와 RDTSC 사이의 명령어가 순차적으로 실행됨이 보증되지 않는다면 정확한 디버거 탐지가 힘들어진다는 점도 고려해야 합니다. 후자의 경우 CPUID라는 명령어를 이용하면 되는데... 여기에도 복잡한 문제가 존재합니다. 어쨌든 이 부분들은 다루려고 하는 주제에서 사~알짝 벗어나므로 생략하도록 하겠습니다. 생략한 부분에 대한 구체적인 내용은 Using the RDTSC Instruction for Performance Monitoring 을 참고하세요.


Timing Detection 우회 방법
참 재미있게도 간단한 기법임에도 개인적으로는 우회하기가 매우 매우 번거롭게 느껴집니다. 간단하게 구현되어 있는 경우 찾기도 쉽고 우회하는 것도 번거롭지 않겠지만 그런 순진한 코드를 본 적이 별로 없습니다.

가장 근본적인 우회 방법은 역시 커널 드라이버를 작성하는 것입니다. RDTSC가 실행될 때마다 이를 가로채어 리턴값을 조작하도록 하면 되겠죠. 매우 다행스럽게도 Olly Advanced 플러그인에는 유사한 개념의 커널 드라이버가 구현되어 있습니다.

Olly Advanced에서 사용하는 방법은  아래와 같습니다. (아래의 내용은 BlackHat 2007에서 발표된 Art of Unpacking에서 발췌한 내용을 보충하여 정리한 것입니다.)

* 4번 컨트롤 레지스터(CR4)의 TSD(Time Stamp Disable) bit의 값을 1로 설정한다.
   TSD bit가 1로 설정되면 Ring0 이외의 모드에서 RDTSC가 실행될 때마다 GP(Gerneral
   Protection) 익셉션이 발생하게 된다.
* GP 익셉션(보통은 GPF라고 합니다. General Protection Fault)이 발생하면 GPF 핸들러가
   동작한다. GPF의 익셉션 넘버는 0xD이므로 GFP 핸들러의 주소는 IDT 베이스 주소(시작주소)
   로부터 0xD * 4bytes만큼 떨어진 곳에 저장되어 있을 것이다.
 *Olly Advanced 플러그인은 바로 IDT에 등록되어 있는 GPF 핸들러를  hooking 하여
  GPF가 발생한 경우 RDTSC가 원인인지를 조사한다. RDTSC 실행이 원인이 되어
  GPF가 발생한 경우 리턴값을 조작하는 방법으로 timing check를 우회한다.

요지인 즉슨... IDT hooking을 하여 RDTSC가 실행될 때가 실제 경과 사이클이 아닌 조작된 값을 전송하여 디버기를 속인다는 것이죠.  
 
맺음말
지금까지 RDTSC를 이용한 timing detection 기법에 대해서 알아보았습니다. 사실 RDTSC외에도  GetTickCount()를 이용할 수도 있고 SharedUserData에 포함된 TickCountLow를 직접 읽어내는 방법도 존재합니다.(TickCountLow값은 Vista에서 사용하지 않는 것으로 보입니다만...) 공부하시는데 참고하세요. 또한 위에서 언급한 방법 중 RDTSC를 감추는 방법은 다양한 형태로 응용이 가능합니다. 알아두시면 좋을 듯. 오늘도 밤이 늦었습니다. 이 글을 읽으시는 분이 몇 분이나 계실지 모르겠지만... ^^; 즐핵~하세요. (이번글은 내용은 그다지 어렵지 않은데, 쓰기가 좀 어렵네요. 무슨 상황인지...)
Posted by zesrever

개 요
Part 1에 이은 두번째 글입니다. 이번에도 그다지 어려운 내용은 아닙니다. Part 2에서 알아볼 내용은 소프트웨어 인스트럭션 브레이크(메모리 브레이크 포인트 및 하드웨어 브레이크 포인트와 구분하려면 이렇게 부르는 게 맞을 것 같습니다. 하지만 너무 길잖아요? ^^ 편의상 소프트웨어 브레이크 포인트라고 하겠습니다.) 포인트 탐지 기법에 관한 것입니다.

소프트웨어 브레이크 포인트 구현 원리
소프트웨어  브레이크 포인트는 명령어의 처음 1bytes를 0xcc(INT 3)로 변경하는 형태로 구현됩니다. 기계어코드 0xcc는 디버그 인터럽트(INT 3)로 프로그램을 정지시키는 효과를 가지고 있는 명령어입니다. 긴 설명보다는 간단한 예제 코드를 이용하여 이를 확인해 보도록 하겠습니다. 테스트에 사용된 예제코드는 아래와 같습니다.

[예제 코드 1] 소프트웨어 브레이크 포인트의 원리 - bp2.c

 func() { printf("hello zesrever!\n");}

 main()
 {
     unsigned char firstbyte;

      __asm
     {
       mov esi, offset func ;func의 시작 주소를 esi에 저장
       lea edi, [ebp-0x4]   ;firstbyte의 주소를 edi에 저장
       movsb                ;func의 첫번째 바이트를 firstbyte에 복사
     }

     printf("the first bytes of func : %x\n",firstbyte);
}

컴파일된 코드는 아래에 첨부해 두었습니다. 이제 예제 코드를 ollydbg로 오픈한 후 아래와 같이 func의 시작점에 브레이크 포인트를 겁니다. 설명한 대로 처음 1바이트가 0xcc로 변경된다면 콘솔창에는 0x55대신에 0xcc가 출력되어 할 것입니다.  

사용자 삽입 이미지


그런 다음 메인 함수가 리턴하는 부분에는 브레이크 포인트를 설정합니다. 이는 순전히 콘솔창으로 출력되는 메시지를 확인하기 위한 것일 뿐 다른 이유는 없습니다.
사용자 삽입 이미지


이제 F9 키를 눌러 실행시켜 보면 아래와 같은 결과를 확인할 수 있을 겁니다.

사용자 삽입 이미지

물론 func 시작점에 브레이크 포인트를 설정하지 않았다면 55가 출력되었을 것입니다. 이로써 우리는 소프트웨어 브레이크 포인트를 설정하면 처음 1byte가 0xcc로 변경된다는 사실을 확인했습니다.

소프트웨어 브레이크 포인트 탐지
소프트웨어 브레이크 포인트를 탐지하는 방법은 크게 두 가지 정도가 있습니다. 첫번째는 보호하려는 코드의 처음 1byte 또는 처음 몇바이트 정도를 조사하여 0xcc가 발견되는지 확인하는 것입니다.매우 직관적인 방법이죠. 이 방법의 문제점은  보통 보호하려는 코드 전부를 대상으로 0xcc를 스캐닝하는 것이 아니기 때문에 처음 몇 바이트를 건너뛰고 브레이크 포인트를 걸면 쉽게 우회가 가능하다는 것입니다. 예를 들어 처음 3bytes만 검사한다면 3bytes를 건너뛰고 다음 명령에 브레이크 포인트를 걸면 쉽게 우회되겠죠. 그렇다면 보호하려는 전체 코드를 다 스캐닝하는 방법을 생각해 볼 수 있겠습니다. 하지만 보통은 보호하려는 전체 코드를 스캐닝하여 0xcc를 찾는 것보다는 체크섬을 계산하는 방식이 더 효과적입니다. 소프트웨어 브레이크 포인트뿐 만 아니라 디버깅 중 코드가 수정되는 것도 탐지할 수 있기 때문이죠.  정리하면 소프트웨어 브레이크 포인트를 탐지하는 방법은 보호하려는 코드를 스캐닝하여 0xcc를 찾거나 보호하려는 코드 전체의 체크섬을 계산하여 비교하는 방법이 있다는 거죠. 간단한 예제 코드가 아래에 준비되어 있습니다.

[예제코드 2] 0xcc 스캐닝을 통한 소프트웨어 브레이크 포인트 탐지 - bpxdetect1.c

  ProtectedFunc(){ printf("good job\n");}
 
  main()
  {
    __asm
   {
     mov edi, offset ProtectedFunc
     mov ecx, 4      ;조사할 바이트 수
     mov eax, 0x660 
     shr eax, 3      ;0x660을 right shift 3bit하면 0xcc입니다.
                     ;이러한 기법을 code permutation 또는
                     ;runtime calculation 이라고 합니다.
                     ;eax에는 스캐닝할 바이트인 0xcc 값을 넣어줘야 하는데
                     ;mov eax, 0xcc 하면 너무 솔직하잖아요? 눈에 잘 띕니다.
                     ;그래서 코드를 쪼개서 눈에 안보이도록 하는 거죠.
                     ;얍삽!! 그래도 알아두시는 편이..
 
     repne scasb     ;EDI가 가르키는 곳에 EAX에 저장되어 있는 값이 있는지
                     ;스캐닝합니다. repne는 ZF의 값이 1이 아니면 계속하라는
                     ;의미인데 ECX값이 0이되거나 EAX에 저장된 문자를 발견하게
                     ;되면 ZF의 값이 1이 됩니다.
                     ;scasb를 한번 수행할 때 마다 repne 지시자의
                     ;영향으로 ECX의 값은 1씩 감소합니다.  

     test ecx, ecx   ;ECX의 값이 0인지 테스트합니다.
     jz short no_bpx ;ECX의 값이 0이라는 의미는 ECX에 지정된 바이트 범위 안에서
                     ;0xcc를 찾지 못했다는 의미겠죠.0xcc를 찾지 못하면
                     ;ProtectedFunc()을 호출하고 그렇지 않으면 아래의
                     ;코드가 실행됩니다.
     rdtsc          
     push eax  
     retn            ;이것도 봐두세요. retn은 POP EIP와 동일한 의미를 갖는
                     ;커맨드입니다. 물론 POP EIP라는 커맨드는 없습니다만,
                     ;말하자면 그렇다는 이야기죠. ^^;
                     ;따라서 리버싱을 하다가 retn을 보게 되면 항상 스택의 탑에
                     ;어떤 값이 저장되어 있는 지를 살펴보아야 합니다.
                     ;PUSH 주소/RET = JMP 주소 라는 사실도 쉽게 이해되시죠?
                     ;아뭏든 rdtsc/push eax/retn을 이용하여 엉뚱한 곳으로
                     ;점프하도록 하는 코드입니다. 예외가 발생하겠죠. 프로그램
                     ;이 크래쉬 될 겁니다.
     no_bpx:
   }
   ProtectedFunc();
  }

컴파일된 코드는 문서 끝에 첨부하였습니다. (BPXDetect1.exe) 아래는 예제코드를 테스트한 결과입니다.
xxxx

[예제 코드 3] 체크섬 계산을 통한 소프트웨어 브레이크 포인트 탐지 - bpxdetect2.asm
이 예제 코드는 BlackHat 2007에서 Yason님께서 발표한 문서에서 참고하여 완성한 것입니다. CRC 체크섬 계산 예제는 OpenRCE Anti Reverse Engineering Technique Database 를 참고하시기 바랍니다. 컴파일된 코드는 문서의 끝에 첨부하였습니다.별로 설명드릴 부분이 없어 주석은 생략합니다.


        .586
        .model flat, stdcall
        option casemap :none   ; case sensitive

        include \masm32\include\windows.inc
        include \masm32\include\user32.inc
        include \masm32\include\kernel32.inc

        includelib \masm32\lib\user32.lib
        includelib \masm32\lib\kernel32.lib
    .data
        MainCaption db "브레이크포인트 탐지", 0h, 0h
        MainText   db "보호된 코드가 실행되었습니다",0h,0h
        CorrectChecksum dd 0586fB54Fh
    .code
      
  start:
        PUSH EBP    
        MOV  EBP, ESP  
        PUSH ESI
        PUSH  EBX
   
        MOV  ESI, offset @ProtectedCode_Start
        MOV  ECX, offset @ProtectedCode_End - offset @ProtectedCode_Start
        XOR  EAX, EAX
        XOR  EBX, EBX
   
     @Checksum_loop :
        MOV   EBX, [ESI]
        ADD  EAX, EBX
        ROL  EAX, 1
        INC  ESI
        LOOP @Checksum_loop
   
        CMP  EAX, CorrectChecksum
        JE  @ProtectedCode_Start
   
     @ModificationDetected :
        RDTSC
        PUSH EAX
        RETN
 
     @ProtectedCode_Start :
        PUSH 30h
        PUSH offset MainCaption
        PUSH offset MainText
        PUSH 0
        CALL MessageBox
     @ProtectedCode_End :
        POP  EBX
        POP  ESI
        MOV  ESP, EBP
        POP  EBP
        RET
end start

아래는 위의 코드를 테스트한 결과입니다.

메시지박스를 호출하는 부분에 브레이크 포인트를 설정하였습니다. 보호되고 있는 코드이므로 체크섬 값이 달라져 익셉션이 발생할 것입니다.

사용자 삽입 이미지
 
익셉션이 발생했습니다.
사용자 삽입 이미지


소프트웨어 브레이크 포인트 탐지 우회 방법

가장 기본적인 몇 가지 방법만 살펴보겠습니다.

하드웨어 브레이크 포인트 사용
Part 1에서도 했던 다소간 어이없는 이야기. 하지만 하드웨어 브레이크 포인트 탐지와 소프트웨어 브레이크 포인트 탐지를 동시에 하지 않는 경우라면 가장 효율적인 방법입니다. 실제 코드가 변경되는 것이 아니므로 두 가지의 탐지 기법 모두 우회가 가능합니다. 예제코드 2와 예제코드 3을 이용해서 테스트해보세요.

메모리 브레이크 포인트 사용
코드를 실행시키려면 당연히 코드를 읽어들어야 합니다. 따라서 메모리 브레이크 포인트도 인스트럭션 브레이크 포인트처럼 사용이 가능합니다. 또한 메모리 브레이크 포인트 역시 코드를 직접 수정하지 않으므로 두 가지 탐지 기법 모두를 우회할 수 있습니다. 역시 단순하므로 예제는 생략합니다. 예제코드 2와 예제코드 3을 이용해서 테스트해보시기 바랍니다.

메모리 브레이크 포인트 사용과 관련하여 또 하나의 좋은 브레이크 지점은 임포트 어드레스 테이블입니다. DLL에서 익스포트한 API를 호출하려면 IAT에 저장되어 있는 주소값을 읽어야 한다는 사실을 이용한 것입니다. ^^; 참고로 DLL에서 익스포트한 API등에 브레이크 포인트를 설정하고자 할 때 브레이크 포인트를 걸 수 있는 지점은 호출하는 부분, API 시작부분 그리고 IAT입니다. 앞의 두 지점은 메모리 브레이크 포인트와 인스트럭션 브레이크 포인트 모두 사용가능하며 IAT는 메모리 브레이크 포인트를 사용할 수 있습니다. IAT에 메모리 브레이크 포인트를 설정하는 예제만 살펴보도록 하겠습니다.

Ollydbg의 CPU 윈도우에서 Ctrl+N을 눌러 import 테이블을 확인합니다. MessageBox를 선택하고 마우스 오른쪽 버튼을 누르면 아래 그림과 같은 메뉴가 보이죠. 여기에서 breakpoint on import를 선택하면 됩니다. 간단하죠?
사용자 삽입 이미지

 
 
브레이크 포인트 설정 위치 조정
앞 서 언급했던대로 0xcc를 스캐닝하는 경우 처음 일부분만을 살피는 경향이 강합니다. 이 점을 이용하여 함수의 시작 부분에서 조금 떨어진 지점에 브레이크 포인트를 설정하면 0xcc를 스캐닝하는 방식의 탐지 기법은 간단히 우회가 가능할 것입니다. 예제는 생략합니다.
 
브레이크 포인트 설정 타이밍 조정
보호하려는 함수에 브레이크 포인트가 설정되어 있는지를 항상 검사할 수는 없습니다. 어느 순간에 한 번 검사하는 거죠. 따라서 브레이크 포인트 탐지 코드가 종료된 후 원하는 지점에 브레이크 포인트를 설정하면 쉽게 우회할 수 있습니다. 문제는 대부분 탐지 코드를 가비지 코드랑 섞어서 눈에 잘 띄지 않도록 한다는 것과 여기저기 탐지 코드를 두어 여러번 검사한다는데 있습니다. 어쩔 수 없습니다. 삽질해야죠. ^^;
 

브레이크 포인트 탐지 코드를 실시간으로 변경
브레이크 코드 탐지 루틴 자체가 보호되지 않는다면 NOP로 다 변경하는 방법을 사용할 수 있습니다. 또는 아래 예제와 같이 체크섬 계산 부분만을 변경해도 좋겠습니다.

루프 종료후 CMP로 체크섬 값을 확인하고 JE로 점프하고 있습니다. JE 부분을 선택한다음 스페이스 바를 누르면 아래 그림과 같이 Assemble 윈도우가 보입니다. 여기에서 JE를 JNE로 변경해 주시면 됩니다. 그 후 보호받는 코드 부분에 브레이크 포인트를 설정하면 원하는 지점에서 프로그램이 정지할 것입니다.
사용자 삽입 이미지
 
맺음말 
지금까지 소프트웨어 브레이크 포인트 탐지 및 우회 기법에 대해서 간단하게 알아보았습니다.
즐핵~ 하세요.  

아.. 메일 주소 변경했습니다. zesrever@xstone.org 입니다. ^^;


 

Posted by zesrever

개요

디버깅 절차를 단순화 시켜보면 아래와 같습니다.
 
  1. 디버기를 실행시킨다.
  2. 디버기를 정지시킨다.
  3. 메모리(데이터 및 코드)나 레지스터의 값을 조사한다.
  4. 다시 1부터 반복한다.

뭐.. 디버기를 어떻게 정지시키는가, 어디에서 정지시키는가 하는 문제가 디버깅의 효율성을 좌우하는 중요한 문제라는 것은 누구나 동의하는 이야기죠. 디버기를 정지시키는 방법 중 가장 주된 수단은 브레이크 포인트입니다. 코드나 데이터 영역에 브레이크 포인트를 설정해 놓고 해당 코드를 실행시키거나 메모리 영역에 접근할 때 디버기가 멈추도록 하는 것이죠. 잘 알려진 안티리버싱 기법들 중에는 이러한 브레이크 포인트를 무력화 시키는 것들이 있습니다. 알고나면 우회하는 것이 별것 아니지만 어쨌든 짜증나는 녀석들입니다. 그렇지 않아도 삽질할 일이 많은데... 매번 느끼는 것이지만 어떤 의미에서 리버싱은 제로섬 게임같다는 느낌이 듭니다. 어쨌든 이번에는 브레이크 포인트 무력화 기법에 대해서 알아보도록 하겠습니다.  시리즈 중  첫번째는 하드웨어 브레이크 포인트 무력화 기법에 대한 것이며, 두번째는 소프트웨어 브레이크 포인트 무력화 기법에 대한 내용입니다.


브레이크 포인트의 종류

브레이크 포인트는 분류 기준에 따라 아래와 같이 나눠볼 수 있습니다. (왜 티스토리에는  표를 삽입하는 기능이 없을까요...)

구현 방식에 따라 크게 2가지 유형으로 분류할 수 있습니다.
  - 하드웨어 브레이크 포인트
    IA32에서 제공하는 디버그 레지스터를 이용하는 브레이크 포인트입니다.
    IA32에는 DR0~DR3까지의 레지스터에 브레이크 포인트를 설정할 주소를 지정하고
    DR7를 브레이크 포인트를 제어하는 용도로 사용합니다. 보다 자세한 설명은 아래 부분에
    있는 [하드웨어 브레이크 포인트의 이해]부분을 참고하세요.  
    하드웨어 브레이크 포인트는 아무래도 아키텍쳐에서 직접 지원하는 기능이므로, 보다 빠르고
    정확하다는 장점이 있지만 레지스터의 개수가 제한되는 이유로 동시에 설정할 수 있는
    하드웨어 브레이크 포인트의 개수 역시 제한된다는 단점이 있습니다.
 
 - 소프트웨어 브레이크 포인트
    디버그 레지스터를 사용하지 않고 디버거가 직접 관리하는 브레이크 포인트입니다.  
    예를들어 소트웨어 방식으로 구현된 인스트럭션 브레이트 포인트(아래 참고)의 경우
    디버그 레지스터를 사용하는 대신에 명령어의 첫번째 바이트를 0xcc(INT 3)으로 변경하는
    방식을 사용합니다. INT 3는 디버그 인터럽트로 정의되어 있으며 INT 3을 전달받은 프로세스
    는 실행이 일시 정지됩니다.
 
기능에 따라 크게 2가지 유형으로 분류할 수 있습니다.
  - 인스트럭션 브레이크 포인트
    특정 번지의 명령어에 설정되는 브레이크 포인트입니다. 디버기는 실행되다가 인스트럭션
    브레이크 포인트를 만나면 명령어는 실행되지 않은 채로 일시 정지됩니다.  하드웨어/소프트
    웨어 방식으로 구현됩니다.

  - 메모리 브레이크 포인트
    흔히 개발자들 사이에서 워치 포인트라고 불리는 것이 바로 메모리 브레이크 포인트입니다.
    메모리 브레이크 포인트는 특정 메모리 영역에 위치한 데이터를 읽거나 변경할 때 디버기를
    멈추고자 할 때 사용합니다. 역시 소프트웨어와 하드웨어 브레이크 포인트 형태로 구현됩니다.
    코드 영역도 어차피 프로세스의 메모리 영역의 일부이므로 메모리 브레이크 포인트를 활용하여
    인스트럭션 브레이크 포인트와 동일한 효과를 얻을 수 있습니다.
 
그 밖에도 응용 방법에 따라서 조건부 브레이크 포인트, 로그 브레이크 포인트 등 다양한 형태의 브레이크 포인트가 존재합니다.

하드웨어 브레이크 포인트 탐지 및 무력화
널리 사용되는 브레이크 포인트 탐지/무력화 기법은 크게 소프트웨어 브레이크 포인트 탐지/무력화 방법과 하드웨어 브레이크 포인트 탐지/무력화 방법, 메모리 브레이크 포인트 무력화 방법 등으로 구분됩니다. 이번 글에서는 하드웨어 브레이크 포인트 탐지 및 무력화에 대해서 알아보도록 하겠습니다.

하드웨어 브레이크 포인트의 이해
하드웨어 브레이크 포인트는 IA32에서 제공하는 디버그 레지스터를 이용하여 구현된 브레이크 포인트를 지칭하는 용어입니다. 메모리 브레이크 포인트나 인스트럭션 브레이크 포인트 모두 하드웨어 브레이크 포인트로 구현이 가능합니다.  IA32에서는 DR0~DR7 까지 모두 8개의 디버그 레지스터를 제공합니다.

이 중 DR0~DR3까지 4개의 레지스터는 브레이크 포인트를 설정할 곳의 주소를 지정하는데 사용됩니다. 이 때문에 하드웨어 브레이크 포인트는 동시에 4개만 설정 가능합니다.

DR4와 DR5는 현재는 사용하지 않습니다.

DR7은 Debug Control Register입니다. 그림은 생략합니다만, 이 레지스터의 역할은 하드웨어 브레이크 포인트의 활성화 여부와 유형(인스트럭션,메모리)에 대한 정보를 담고 있습니다.하위 8bytes는 활성화 여부를 나타내는데, 이 중 0,2,4,6 번째 bit는 local enable 비트이며 1,3,5,7 번째 bit는 global enable 비트로 사용됩니다. 예를들어 0번 비트의 값이 1로 설정되어 있다면 DR0에 기록된 주소에 관련하여 하드웨어 브레이크 포인트가 활성화 되어 있다는 이야기입니다. 이 때 브레이크 포인트의 유형은 16~23번째 비트에서 결정됩니다. 이 값이 이진수 00이면 인스트럭션 브레이크 포인트임을 의미하며 01이면 메모리 브레이크 포인트(write), 11이면 메모리 브레이크 포인트(read/write)를 의미합니다. 메모리 브레이크 포인트인 경우 24~31번째 bit내에 위치한 2개의 bit가 모니터링할 사이즈를 지정하게 됩니다. (헉헉 그림은 나중에.. 궁금하시면 IA32 메뉴얼 참고하세요.)

DR6은 debug staus 레지스터로 브레이크 포인트가 히트되었는지에 대한 정보를 가지고 있다고 보면 됩니다.
 
하드웨어 브레이크 포인트 탐지 및 무력화
IA32 메뉴얼에는 디버그 레지스터에 대해서 다음과 같이 기술되어 있습니다.

The debug registers are privileged resources; a MOV instruction that access these registers can only be executed in real-address mode, in SIMM, or in protected mode at a CPL of 0. An attemp to read or write the debug regiters from any other privilege level generates a general protection exception (#GP).

IA32 메뉴얼에 의하면 디버그 레지스터는 일반적으로 Ring0 모드에서만 접근할 수 있겠군요. 다시 말해  일반 애플리케이션에서는 디버그 레지스터의 값을 읽거나 변경할 수 없다는 이야기죠. 그런데 MS 윈도우의 경우 SEH를 이용하면 Ring3에서도 디버그 레지스터의 값을 읽거나 변경하는 것이 가능합니다. 좀 더 자세하게 알아볼까요. 익셉션이 발생했을 때 실행되는 익셉션 핸들러의 프로토타입은 아래와 같습니다.

__cdecl  _except_handler (

    struct _EXCEPTION_RECORD *ExceptionRecord,

    void * EstablisherFrame,

    struct _CONTEXT * ContextRecord,

    void * DispatcherContext );

익셉션 핸들러의 프로토타입에 나타난 세번째 아규먼트 ContextRecord를 이용하면 익셉션이 발생했을 당시의 컨텍스트 즉 레지스터들의 값에 접근하거나 변경하는 것이 가능합니다. 익셉션이 발생했을 때 익셉션 핸들러가 익셉션 발생 당시의 레지스터의 값을 조사하고 수정이 필요한 경우 수정할 수 있도록 해주기 위해서 이렇게 디자인된거죠. 재미있는 사실은 _CONTEXT 구조체 안에 디버그 레지스터가 포함되어 있다는 사실입니다.

정리해보겠습니다. 하드웨어 브레이크 포인트 탐지는 디버그 레지스터의 값을 읽어야만 가능하고 하드웨어 브레이크 포인트를 무력화시키는 것은 디버그 레지스터의 값을 변경할 수 있을 때만 가능합니다. IA32 아키텍쳐에서는 Ring0 모드에서만 디버그 레지스터에 접근할 수 있도록 제한을 하고 있지만 윈도우즈의 경우 SEH를 이용하면 Ring3 모드에서도 디버그 레지스터의 값을 읽거나 변경하는 것이 가능하다는 거죠. 그 방법은 아래와 같습니다.

  1. 디버그 레지스터의 값을 읽고, 변경하는 즉 하드웨어 브레이크 포인트를 탐지하고 무력화 시키
     는 코드를 SEH 핸들러로 등록한다.
  2. 익셉션을 유발시킨다.
  3. 미리 등록해 놓은 SEH 핸들러가 실행된다. 핸들러는 세번째 아규먼트인 ContextRecord를
     이용하여 디버그 레지스터의 값을 조사하거나, 디버그 레지스터의 값을 변경하는 방법으로
     하드웨어 브레이크 포인트를 무력화 시킬 수 있다.  
 
비교적 단순하지 않습니까? 그럼 위의 내용을 바탕으로 간단한 코드를 구현해 보도록 하겠습니다. 코드 구현에는 MASM을 사용하였습니다.  주석을 달아놓았으니 참고하세요.^^;

[예제 코드 1] 하드웨어 브레이크 포인트 탐지
;
; Hardware Breakpoint Detection Example by zer0one (
zer0one@xstone.org)
;
;

   .586
   .model flat, stdcall
   option casemap :none

      include \masm32\include\windows.inc
      include \masm32\include\user32.inc
      include \masm32\include\kernel32.inc
      include \masm32\include\comdlg32.inc
     
      includelib \masm32\lib\user32.lib
      includelib \masm32\lib\kernel32.lib
      includelib \masm32\lib\comdlg32.lib
     
     
  .data
     HardwareBPXTitle db  "Hardware Detection Example by zer0one",0h
     DetectedMsg db "브레이크포인트가 탐지되었습니다",0h,0h
     NotDetectedMsg db "브레이크 포인트가 탐지되지 않았습니다",0h,0h
     SetBPXHere db "여기에 브레이크 포인트를 설정해주세요",0h,0h
          
    .code

start:

   ASSUME FS:NOTHING
 
   ;익셉션 핸들러 설치
   PUSH  offset @DetectHardwareBPX
   PUSH FS:[0]                   
   MOV  FS:[0], ESP
 
   ;하드웨어 브레이크포인트가 설정되었는지를 검사하기 위해 익셉션 유발
   ;보통 익셉션 유발은 Access violation을 유발시키거나
   ;INT 3을 사용하거나, 잘못된 코드를 삽입하거나..암튼 방법이 많습니다.
   ;아래의 코드는 Access violation을 유발시키는 코드의 한 예에 불과합니다.
   XOR  EAX, EAX
   XCHG [EAX], EAX

   ;보호되는 코드
   PUSH 40h
   PUSH offset HardwareBPXTitle
   PUSH offset SetBPXHere
   PUSH 0
   CALL MessageBox
 
   POP FS:[0]   ;Exception List 복구
   ADD ESP, 4h  ;스택 청소 쓱싹~쓱싹~
   
   PUSH 0
   CALL ExitProcess
   
 @DetectHardwareBPX :
    PUSH EBP  
    MOV  EBP, ESP  ;익셉션 핸들러를 위한 스택 프레임 오픈
                   ;사실 예제에서는 필요없음. ㅠ.ㅠ;
    
    PUSH ESI ;callee saved register
             ;ContextRecord를 가르키기 위해 사용할 것입니다.
    
    MOV  ESI, [EBP + 10h]  ;3번째 아규먼트(ContextRecord)의 값을 EAX에 저장
    CMP  DWORD PTR DS:[ESI+04h], 0h ;DR0의 값이 0인지 비교
    JNE  @Detected
    CMP  DWORD PTR DS:[ESI+08h], 0h ;DR1의 값이 0인지 비교
    JNE  @Detected
    CMP  DWORD PTR DS:[ESI+0Ch], 0h ;DR2의 값이 0인지 비교
    JNE  @Detected
    CMP  DWORD PTR DS:[ESI+10h], 0h ;DR3의 값이 0인지 비교
    JNE  @Detected
 
    PUSH 30h
    PUSH offset HardwareBPXTitle
    PUSH offset NotDetectedMsg
    PUSH 0
    CALL MessageBox
   
    JMP @EndOfDetectHardwareBPX
    
  @Detected :
    PUSH 30h
    PUSH offset HardwareBPXTitle
    PUSH offset DetectedMsg
    PUSH 0
    CALL MessageBox
   
  @EndOfDetectHardwareBPX :
   
    ; ContextRecord 상의 EIP값을 변경합니다.
    ; 변경 전 EIP는 XCHG [EAX], EAX 부분을 가르키지만
    ; 변경 후에는  PUSH 40h 를 가르키게 됩니다.
    ; 이 작업을 해 주지 않으면 계속 Exception이 발생하겠죠.
    ; 다른 방법도 있지만 SEH 안에서 EIP값을 변경할 수 있다는
    ; 것도 이야기 할 겸 겸사겸사 직접 EIP를 변경하였습니다.   
    MOV  ECX, DWORD PTR DS:[ESI+0B8h]
    ADD  ECX, 2 ;XCHG [EAX], EAX는 2bytes
    MOV  DWORD PTR DS:[ESI+0B8h], ECX
   
    XOR  EAX, EAX ;EAX=0 익셉션 핸들러의 리턴값이 0이면
                  ;컨텍스트를 다시 로드한 다음 실행을 계속합니다.
                 
    POP  ESI      ;ESI값 복구
    MOV  ESP, EBP ;지역변수 해제(쓴것도 없는데..ㅠ.ㅠ;)
    POP  EBP      ;프레임포인터 복구
    RET  

end start

다음은 예제 코드를 테스트하는 화면입니다. 먼저 [그림 1]에서와 같이 보호되는 코드중 메시지박스 호출 부분에 하드웨어 브레이크 포인트를 설정합니다.

사용자 삽입 이미지
[그림 1] 하드웨어 브레이크 포인트 설정

이제 [F9]를 눌러 코드를 실행시켜 보겠습니다. 아래 [그림 2]에서 처럼 00401015번지에서 익셉션이 발생하였습니다.

사용자 삽입 이미지
      [그림 2] 익셉션 발생

이 때 Ollydbg의 하단 메시지바를 살펴보면 [그림 3]과 같습니다.
사용자 삽입 이미지
      [그림 3] 익셉션 발생 시 하단 메시지바의 내용

시키는 대로 Shift+F9를 눌러 보겠습니다. 익셉션이 발생한 경우 위와 같이 Shift 버튼을 누른채 F7/F8/F9를 누르면 디버기로 익셉션이 전달되어 익셉션 핸들러가 실행됩니다. 잠시 후에 아래와 같이 메시지 박스가 출력될 것입니다.

사용자 삽입 이미지
      [그림 4] 브레이크 포인트 탐지

탐지가 잘 되는 군요.. ^^; 예제에서는 탐지된 후에 메시지박스를 띄웠지만 실제로는 리버싱을 방해할 만한 행동을 할 것입니다. 예를 들어 예제코드를 살짝 비틀어 EIP값을 엉뚱한 곳으로 변경한다든지 아니면 곧이어 설명할 것처럼 하드웨어 브레이크 포인트를 제거한다든지, 실행에 영향을 끼치는 레지스터의 값을 변경한다든지 뭐 여러가지 방법을 사용할 수 있겠습니다.

하드웨어 브레이크 포인트 무력화
하드웨어 브레이크 포인트를 제거하는 방법 역시 매우 간단합니다. SEH 핸들러 안에서 DR0~DR3에 저장되어 있는 값을 모두 0으로 조작하면 됩니다. 아래 예제 코드가 있습니다.

 @DetectHardwareBPX :
    PUSH EBP  
    MOV  EBP, ESP    
    PUSH ESI ;calle saved register
              ;ContextRecord를 가르키기 위해 사용할 것입니다.
    
    MOV  ESI, [EBP + 10h]  ;3번째 아규먼트(ContextRecord)의 값을 EAX에 저장
    MOV  DWORD PTR DS:[ESI+04h], 0h ;DR0의 값을 0으로 초기화
    MOV  DWORD PTR DS:[ESI+08h], 0h ;DR1의 값을 0으로 초기화
    MOV  DWORD PTR DS:[ESI+0Ch], 0h ;DR2의 값을 0으로 초기화
    MOV  DWORD PTR DS:[ESI+10h], 0h ;DR3의 값을 0으로 초기화
             
    PUSH 30h
    PUSH offset HardwareBPXTitle
    PUSH offset DetectedMsg ;메시지 내용 : 지웠지롱~
    PUSH 0
    CALL MessageBox
   
    MOV  ECX, DWORD PTR DS:[ESI+0B8h]
    ADD  ECX, 2 ;XCHG [EAX], EAX는 2bytes
    MOV  DWORD PTR DS:[ESI+0B8h], ECX
   
    XOR  EAX, EAX ;EAX=0 익셉션 핸들러의 리턴값이 0이면
                  ;컨텍스트를 다시 로드한 다음 실행을 계속합니다.
                 
    POP  ESI      ;ESI값 복구
    MOV  ESP, EBP ;지역변수 해제(쓴것도 없는데..ㅠ.ㅠ;)
    POP  EBP      ;프레임포인터 복구
    RET
   


하드웨어 브레이크 포인트 제거 기법 우회
하드웨어 브레이크 포인트 탐지나 제거 기법이 구현되어 있는 경우 우회는 어떻게 해야 할까요? 사실 정답은 없습니다. 우회 방법은 상황에 따라 다를 것이며 지금까지의 내용을 잘 이해하고 있다면 잠깐의 생각만으로도 충분히 우회 가능한 방법들을 찾아낼 수 있을 것입니다. 그 중 간단한 방법 몇 가지만 살펴보도록 하겠습니다.
 
* 소프트웨어 브레이크 포인트 사용
하... 다소 어처구니 없는 이야기처럼 들릴수도 있겠습니다만, 하드웨어 브레이크 포인트 탐지와 소프트웨어 브레이크 포인트 탐지 기법이 동시에 사용되지 않은 경우라면 제일 효율적인 방법이 될 수 있겠죠. ^^; 물론 소프트웨어 브레이크 포인트를 탐지/제거하는 루틴이 포함되어 있다면 당연히 무의미 하겠습니다.

* 브레이크 포인트 설정 타이밍 조정
이러한 방식의 브레이크 포인트 탐지 및 무력화 방법의 단점은 어느 한 시점에서 브레이크 포인트 탐지 및 제거를 수행한다는 것입니다. 따라서 이러한 코드를 식별해 낼 수 있다면 탐지/제거 코드 이후에 브레이크 포인트를 설정하는 방법을 통해 간단히 우회할 수 있겠습니다. 유저모드 애플리케이션에 구현되어 있는 하드웨어 브레이크 포인트를 탐지하는 하는 코드를 식별해 내는 방법 중 도움이 될 만한 사실은 아래와 같습니다. 유저모드 애플리케이션에서 하드웨어 브레이크 포인트를 탐지하거나 제거하려면 반드시 SEH를 호출해야 한다는 사실을 이용한 것들입니다.

- FS:[0]을 보거든 SEH를 떠올려라. ^^;
- 익셉션 핸들러에서 스택 프레임을 사용하는 경우 EBP+10h가 의미하는 것은 아규먼트로 전달받은 ContextRecord 값이다. 이러한 코드를 발견하면 ContextRecord를 전달받아 어떠한 일을 하는지 살펴볼 필요가 있다.
 - 익셉션 핸들러에서 스택 프레임을 사용하지 않는 경우에는 ESP+0ch가 ContextRecord를 의미한다.
-  리버싱 중 익셉션 리스트를 종종 살펴보라. (Ollydbg의 View->SEH chain, Windbg의 경우 아래 명령어 참고)

0:000> !list -t _EXCEPTION_REGISTRATION_RECORD.Next -x "dt   _EXCEPTION_REGISTRATION_RECORD @$extret" poi(fs:[0])
ntdll!_EXCEPTION_REGISTRATION_RECORD
 +0x000 Next    : 0x0012fcf0 _EXCEPTION_REGISTRATION_RECORD
 +0x004 Handler : 0x77368bf2 _EXCEPTION_DISPOSITION ntdll!_except_handler4+0

ntdll!_EXCEPTION_REGISTRATION_RECORD
 +0x000 Next    : 0xffffffff _EXCEPTION_REGISTRATION_RECORD
 +0x004 Handler : 0x77368bf2 _EXCEPTION_DISPOSITION ntdll!_except_handler4+0

ntdll!_EXCEPTION_REGISTRATION_RECORD
   +0x000 Next             : ????
   +0x004 Handler          : ????
Memory read error 00000003

cf.
악성코드 등이 커널모드 드라이버 형태로 구성되어 있는 경우라면 이야기가 좀 달라질 수 있습니다. 이 부분에 대해서는 나중에 다루도록 하겠습니다.(언제? -.-;; )

* Exception을 유발시키는 코드를 NOP로 변경
익셉션 핸들러가 실행되지 않아도 코드 실행에 문제가 없는 경우라면 익셉션을 유발시키는 코드를 NOP로 지워버리는 방법도 효과적일 수 있습니다. 아래 예제 그림은 익셉션을 유발시키는 코드 부분을 NOP로 처리하고 하드웨어 브레이크 포인트를 설정하여 실행한 화면입니다. 그림만으로는 실제 브레이크 포인트가 제대로 동작하였는지 파악하기 힘들겠지만 실제로 해보면 쉽게 확인할 수 있습니다. ^^

사용자 삽입 이미지


맺음말
지금까지 하드웨어 브레이크 포인트에 대해서 간단히 알아보았습니다. 내용상 오류가 개선사항/질문은 댓글 또는 zer0one@xstone.org로 메일 주세요. 즐핵~





 



Posted by zesrever
개요
악성코드나 패커 등의 프로그램에서 안티 리버싱 테크닉으로 많이 사용되는 것중 TLS callback이라는 것이 있습니다. 오늘 이야기할 주제는 바로 이 TLS callback에 관한 것입니다.

한마디로 TLS callback은 쓰레드가 생성될 때 로더에 의해서 실행되는 코드입니다. 보통 디버거를 이용하여 프로그램을 오픈하면 엔트리 포인트에서 프로그램이 멈추는데, TLS callback은 프로그램의 엔트리 포인트 이전에 실행되어 디버거로 분석하기가 쉽지 않습니다.(물론 몰랐을때 이야기입니다. TLS callback을 우회하거나 분석하는 것은 사실 매우 쉽습니다.) 악성코드 또는 패커와 같은 프로그램은 이러한 사실을 이용하여 TLS callback에 디버거 탐지 루틴이나 언패킹 루틴을 등록해 둡니다. TLS callback에 대해서 지식이 없는 리버서라면 귀신이 곡할 노릇아니겠습니까. 분석하려고 열면 죽고, 열면 죽고 또는 OEP를 찾아야하는데 도대체 어디서 언패킹을 하는지 알 수가 없다든지..

어쨌든 오늘은 이 녀석에 대해서 알아보도록 하죠.

TLS(Thread Local Storage)
사실 TLS callback을 이용한 안티 리버싱 테크닉을 이해하기 위해서 TLS 자체에 대한 이해가 필요한 것은 아닙니다. 단지 TLS callback 함수가 쓰레드가 생성될 때 실행된다는 것, ntdll.dll에 있는 LdrpRunInitializeRoutines()가 TLS callback으로 등록된 코드를 실행시킨다는 것 쯤을 기억하면 됩니다. 뭐 하지만 늘 그렇듯이 TLS callback을 공부하면서 덤으로 TLS 섹션에 대해서도 알아보는 것이 좋을 것 같습니다.  

먼저 첨부된 예제 프로그램을 OllyDbg를 이용하여 열어 보겠습니다.  

사용자 삽입 이미지

            [그림 1] sample.exe를 ollydbg로 오픈한 직후의 모습


[그림 1]에서 확인할 수 있는 것처럼 sample.exe를 디버거로 오픈한 직후 디버기(sample.exe)가 종료되어 정상적인 디버깅이 힘들게 되었습니다.  디버거가 디버기를 로드하고 브레이크하기 전에 실행되는 함수가 있다는 의미죠. 이에 대한 해답은 TLS 섹션에 있습니다.  TLS는 Thread Local Storage의 약자로 쓰레드별 전역 변수 공간 쯤으로 이해하면 됩니다. 보통 동일 프로세스 안에 존재하는 모든 쓰레드들은 전역 변수를 공유하지만 경우에 따라서 쓰레드간 전역 변수를 구분한 필요가 있기 때문에 고안된 공간입니다. TLS에 관련된 정보는 PE 구조 내의 Data Directory 테이블에서 찾을 수 있습니다. [그림 2]는 LordPE를 이용하여 sample.exe의 Data Directory 테이블을 살펴본 결과 입니다. TLS 테이블의 RVA값이 3060 이고 사이즈가 24bytes임을 알 수 있습니다.


사용자 삽입 이미지
                           [그림 2] sample.exe의  Data Directory 테이블

PE 파일 내에서 TLS 테이블의 내용을 확인하기 위해서는 파일 내 오프셋 값을 알아야 합니다.(사실 LordPE에서 H 버튼을 누르면 해당 테이블의 내용을 확인할 수 있습니다. 하지만 주소를 직접 계산해 보도록 하죠. PE에 대한 지식을 조금이라도 늘리는 것이 좋지 않겠습니까? )  불행하게도 TLS 테이블의 파일 내 오프셋에 대한 직접적인 정보는 PE 파일 내에 존재하지 않습니다. 이러한 이유로 TLS 테이블의 오프셋 값을 알아내기 위해서는 약간의 번거로운 계산이 필요합니다. 계산 방법은 아래와 같습니다.
  1. Data Directory 테이블에서 TLS 테이블의 RVA값을 확인한다.
  2. 섹션 테이블에서 TLS 테이블이 속한 섹션을 확인하고 섹션의 오프셋 값을 확인한다.
  3. 다음과 같이 계산한다.

     TLS 테이블 오프셋 = TLS 테이블이 속한 섹션의 오프셋
                              + (TLS 테이블의 RVA - TLS 테이블이속한 섹션의 RVA)


 [그림 3]은 LordPE를 이용하여 sample.exe의 섹션 테이블을 살펴본 결과 입니다.
사용자 삽입 이미지
       [그림 3] sample.exe의 섹션 테이블

.data 섹션의 RVA값이 3000입니다. 앞서 살펴본 TLS 테이블의 RVA값은 3060이었습니다. 따라서 TLS 테이블은 .data 섹션에 존재함을 알 수 있습니다. 일반적인 경우 TLS 테이블은 보통 .rdata 섹션에 존재하지만 예제 프로그램은 제가 직접 TLS 테이블을 구성한 것이라 컴파일러가 생성한 것과는 조금 차이가 있습니다. 어쨌든 RVA값이 3000인 경우 파일 내 오프셋이 800이므로 RVA값이 3060인 경우 파일내 오프셋은 860( 3060 - 3000 + 800 )이 될 것입니다.  이제 Hex 에디터를 이용하여 sample.exe를 열어보겠습니다. 앞서 잠깐 이야기한 것처럼 LordPE에서 "H" 버튼을 눌러도 됩니다.
TLS 테이블은 아래와 같이 정의되어 있습니다.

   typedef IMAGE_TLS_DIRECTORY32
   {
         DWORD    StartAddressOfRawData;
         DWORD    EndAddressOfRawData;
         DWORD    AddressOfIndex;
         DWORD    AddressOfCallBacks;
         DWORD    SizeOfZeroFill;
         DWORD    Characteristics;
   } IMAGE_TLS_DIRECTORY32;

위 구조체 중에서 4번째 멤버인 AddressOfCallbacks는 TLS callback 함수들의 주소를 담고 있는 벡터 테이블의 주소입니다. 예제 프로그램을 통해서 좀 더 자세하게 살펴보겠습니다. 먼저 sample.exe의 파일 내 오프셋 860 번지로 이동해 보겠습니다.
 
사용자 삽입 이미지
         [그림 4] sample.exe의 TLS 테이블

[그림 4]에서 볼 수 있듯이 AddressOfCallbacks 값이 00403084 입니다. RVA나 오프셋이 아닌 가상 주소가 그대로 사용된다는 사실을 알아두세용~. 이미 언급한대로 AddressOfCallbacks에 저장되어 있는 주소 값은  TLS callback 함수의 주소를 저장하고 있는 벡터 테이블을 가르킵니다. 따라서 TLS callback 함수의 주소를 알아내려면 00403084번지의 내용을 확인해 보아야 합니다. 00403084번지는 파일 내 오프셋이 884 입니다. sample.exe의 Image Base가 00400000이므로 00403084 번지의 RVA는 3084가 되고, 앞서 TLS 테이블의 시작 주소의 RVA 3060의 오프셋이 860이었음을 생각해보면 어렵지 않게 계산할 수 있을 것입니다. [그림 4]를 보면 오프셋 884에는 0040101B가 저장되어 있음을 확인할 수 있습니다. 바로 이 주소가 TLS callback 함수의 주소입니다.  IDA로 열어서 확인해 보겠습니다.


사용자 삽입 이미지

대충봐도 IsDebuggerPresent가 보이죠? 디버거 탐지 루틴이 구현되어 있음을 아실 수 있을 거라 믿습니다. sample.exe는 TLS callback 함수를 가지고 있으며, 그 안에는 디버거 탐지 루틴이 구현되어 있습니다. 따라서 아무 생각없이 디버거로 열면 정상적으로 디버깅할 수가 없는 것이지요. 설명 목적 상 디버거 탐지 루틴이 매우 직접적이고 단순하지만 PEB를 직접 접근하는 방식을 이용하거나 다른 방식의 디버거 탐지 루틴을 추가하고, 가비지 코드 많이 넣어주고, 발견되었을 때도 단순히 종료가 아니고 엉뚱한 주소로 점프하게 만들어 디버거가 코드를 잘못 해석하게 만들어 준다면 초보 리버서로서는 당황할 수 밖에 없겠죠.
 

TLS callback 디버깅 & 분석 방법

TLS callback 함수를 분석하거나 디버깅하는 방법은 매우 간단합니다.
IDA를 이용하여 실행시키기 전에 TLS callback 함수를 분석할 수도 있으며,
분석 결과 TLS callback 함수 내에 디버거 탐지 루틴 등 실행되지 않아도 분석에는 지장이 없는
(오히려 실행되지 않는 것이 더 좋은) 코드들이 구현되어 있다면 TLS 테이블 내의 AddressOfCallbacks의 값을 00000000으로 변경하여 우회하는 것도 가능합니다.

디버깅하는 방법도 역시 그리 어렵지 않습니다. TLS callback 함수가 실행되기 전에 브레이크가 걸리도록 디버거를 설정한 다음 TLS callback 함수의 시작점에 브레이크 포인트를 걸어 분석을 시작하면 됩니다. 보다 구체적인 방법은 아래와 같습니다.

TLS callback 함수 주소 알아내기 

TLS callback 함수의 주소는 앞에 설명한 방법으로 알아낼 수 있습니다. 또 다른 방법으로는 IDA를 이용하는 것인데, 개인적으로는 강의할 때가 아니면 후자의 방법을 사용합니다. IDA는 똑똑하여 TLS callback 함수를 식별해 낼 줄 알거든요. IDA로 분석할 파일을 오픈한 후 Ctrl+E(Entrypoint)를 눌러보면 쉽게 TLS callback 함수의 주소를 식별해 낼 수 있습니다. [그림 5]에 예제가 있습니다.       

사용자 삽입 이미지

                     [그림 5] IDA를 이용한 TLS callback 함수 식별



TLS callback 함수 디버깅
WinDbg를 이용하는 경우 TLS callback이 구현되어 있는 경우 TLS callback 호출 전에 알아서 멈춥니다. 멈추면 TLS callback 함수의 시작점에 브레이크 포인트를 설정하고 F9를 누르거나 'g(go)' 명령을 이용하여 분석을 시작하면 됩니다.

OllyDbg를 이용하는 경우 디버깅 옵션을 변경해 주어야 합니다.  아래와 같이 OllyDbg의 디버깅 옵션에서 Events 탭을 선택한 후 최초 정지 지점을 System Breakpoint로 설정해 주면 됩니다.

사용자 삽입 이미지


맺음말
지금까지 TLS callback 함수에 대해서 간단히 알아보았습니다. 두서없이 쓴 글이지만 리버싱을 공부하시는 분들에게 도움이 되었으면 합니다. 혹 내용 중에 잘못된 점이 있으면 zer0one@xstone.org로 이메일 주시거나 댓글 부탁드리겠습니다. 즐핵~

Posted by zesrever

BLOG main image
Slow but Steady, Broad and Deep ... by zesrever

공지사항

카테고리

분류 전체보기 (44)
Digital Forensics (4)
Reverse Engineering (21)
Vulnerability (2)
Secure Coding (0)
Book Story (1)
Digital Life (7)
My Life (7)
세미나자료 (1)
개인용 (0)
Musics (0)
Total : 229,596
Today : 2 Yesterday : 255