'분류 전체보기'에 해당되는 글 62건

  1. 2017.06.27 Windbg, Gdb 명령어 정리
  2. 2017.06.03 VC++ 옵션 정리
  3. 2017.05.16 다형성 바이러스 4
  4. 2017.05.12 API Sets
  5. 2017.05.09 윈도우의 예외 처리
  6. 2017.04.23 링크 및 책 정리
  7. 2017.04.23 윈도우의 드라이버 개발과 루트킷 그리고 AV
  8. 2017.04.23 간단한 패커 개발
  9. 2017.04.23 Yoda's Protector 분석
  10. 2017.04.23 패커들 분석 5



0. 개요

.... 0.1 Windbg (Cdb)

.... 0.2 Gdb

1. Control

.... 1.1 Session

........ 1.1.1 Restart

........ 1.1.2 Quit

.... 1.2 Basic

........ 1.2.1 Trace / Step

........ 1.2.2 Go

.... 1.3 ETC

........ 1.3.1 Symbols

2. Facility

.... 2.1 BreakPoint

........ 2.1.1 BP 기본 명령어

........ 2.1.2 BP 활용

.... 2.2 Memory Assemble / Edit

.... 2.3 Memory Fill / Copy / Compare

.... 2.4 Search

........ 2.4.1 Search memory

........ 2.4.2 Search for disassembly pattern

.... 2.5 Checkpoint

.... 2.6 Reverse Debugging

.... 2.7 Inferior

.... 2.8 Thread

.... 2.9 Signal

.... 2.10 Catch

.... 2.11 Event

.... 2.12 ETC

........ 2.12.1 Help

........ 2.12.2 Clear

........ 2.12.3 Loop

........ 2.12.4 Log

........ 2.12.5 ASLR

........ 2.12.6 Stop-on-solib-events

........ 2.12.7 Alias

........ 2.12.8 Define

........ 2.12.9 Etc

3. Data

.... 3.1 Unassemble

.... 3.2 Register

.... 3.3 Memory

........ 3.3.1 Display Memory

........ 3.3.2 Display words and symbols

........ 3.3.3 Display Referenced Memory

........ 3.3.4 Display Type

........ 3.3.5 Display Debugger Object Model Expression

.... 3.4 Call Stack

.... 3.5 ETC

........ 3.5.1 의사 레지스터

........ 3.5.2 Etc





0. 개요

  Windbg(또는 cdb 및 kd)나 Gdb를 다루는 대부분의 자료들은 커널 모드 디버깅이나 소스 코드 및 디버깅 정보 파일을 통해 디버깅을 하는 내용을 담고 있다. Gdb의 경우는 리눅스 플랫폼 자체가 오픈 소스가 많은 면이 있기 때문인 것으로 보이는데 물론 유저 모드 디버깅과 관련된 자료들 및 PEDA나 GEF 또는 evan's debugger 같은 Gdb의 불편한 인터페이스를 보완해주는 툴들도 존재한다. Windbg의 경우에는 Ollydbg의 영향이 매우 크다고 여겨지며 Ollydbg가 아직 지원하지 않는 64비트 리버싱의 경우에도 x64dbg 같은 디버거들의 개발로 인해 Windbg가 유저 모드 리버싱에서는 거의 사용되지 않는 것으로 보인다.

  Cdb는 CLI 형태의 유저 모드 디버거이며 Kd는 CLI 형태의 커널 디버거이다. Windbg는 Cdb 및 Kd를 통합함과 동시에 간단한 GUI 형태를 제공한다. Gdb는 유저 모드와 커널 모드를 모두 제공하면서 CLI 형태를 갖는다. 이 두 디버거의 공통점으로 윈도우 및 리눅스의 업그레이드와 함께 발전되며 많은 기능을 지원하는 장점이 있는 반면 두 디버거 모두 사용하기 매우 불편하다는 단점도 갖는다. 그렇기 때문에 특히 윈도우의 경우 x86 유저 모드 디버거 중에서 매우 사용자 친화적인 Ollydbg가 대표적으로 많이 쓰이고 있다.

  이 문서에서는 유저 모드 디버깅 즉 유저 모드 악성코드 분석을 위한 리버싱 용도로 Windbg(어떻게 보면 단지 Cdb)와 Gdb의 명령어를 정리하기로 한다. 유저 모드 디버거에서 이 디버거들보다 훨씬 사용자 친화적인 디버거 및 확장이 존재함에도 불구하고 굳이 이 문서를 만드는 이유는 관련 자료들이 많지 않아보이기 때문이다. 마지막으로 본문에서 프롬프트가 >로 시작하는 라인은 Windbg의 명령어를 의미하며 (gdb)로 시작하는 라인은 Gdb의 명령어를 의미한다. 그리고 명령어 옆에 괄호가 있는 경우에는 명령어의 약자를 의미한다.



0.1 Windbg (Cdb)

  Windbg에서 일반적으로 사용되는 명령은 "일반 명령"으로서 디버거 자체에 내장된 명령이다. 이 외에도 "메타 명령"이라고 "."으로 시작하는 명령 및 "!"로 시작하는 "확장 명령"도 존재한다. 

  참고 사항으로서 심볼 설정이 있는데 조금 자세히 설명해 보겠다. Ollydbg에서는 이 설정을 굳이 자세히 하지는 않았지만 이 디버거도 자체적으로 심볼 형태를 제공해 주기 때문에 API 함수의 이름이나 파라미터 이름 등의 정보를 보여줌으로써 우리가 쉽게 리버싱을 하도록 도와주었다. Windbg의 경우 심볼 설정을 해주지 않으면 앞에서 설명한 디버깅을 도와주는 정보를 하나도 얻을 수 없다. 이러한 디버깅 정보는 pdb 형태로 제공된다.

  pdb 파일 즉 심볼 파일은 우리가 직접 프로그램을 개발할 경우에도 디폴트로 만들어지는데 이 pdb 파일이 존재한다면 우리는 디버깅 시에 어셈블리 명령어에 상응하는 소스 코드를 보면서 디버깅을 할 수 있을 정도로 많은 정보가 들어있다. 물론 이것은 우리가 직접 개발하였고 심볼 파일을 생성한 후에 디버거로 읽어들인 경우에 해당하며 악성코드의 경우에는 심볼 파일과 같이 배포될 일이 없기 때문에 이런 기대를 할 수 없다.

  여기서 우리가 심볼 설정을 통해서 얻는 pdb 파일은 ntdll.dll이나 kernel32.dll 같은 시스템 DLL 파일들에 대한 정보이다. pdb 파일도 public 및 private 심볼로 나뉘는데 private 심볼의 경우 위에서 말한 소스 코드 등의 정보까지 많은 내용의 정보가 들어있으며 용량도 상당히 크다. 하지만 MS가 소스 코드를 공개할 일이 없을 것이며 이에 따라 제공되는 심볼 파일은 public 심볼인데 이것은 상당히 제한된 내용만 들어있다. 물론 없는것 보다는 훨씬 낫기 때문에 이렇게 심볼 설정을 하는 것이다. 여기서 얻을 수 있는 정보는 소스 코드는 아니더라도 API 함수의 이름은 얻을 수 있다.

  심볼 관련 설정은 두 가지로 나뉘는데 하나는 MS가 제공하는 모든 심볼 파일(public)들을 다운로드 받아서 특정 디렉토리에 두고 그 디렉토리의 경로를 설정하는 것이 있다. 다른 하나는 MS에서 제공하는 심볼 다운로드 링크 및 내 컴퓨터에 다운로드할 경로를 적어서 디버깅 시에 필요한 심볼 파일만(분석 도중 kernel32.dll로 진입했을 때 이 심볼 파일을 다운로드 받는 형태로) 다운로드 받을 수도 있다. 

[ SRV*c:\symbols*http://msdl.microsoft.com/download/symbols ]

  Windbg의 File -> Symbol File Path에 위와 같은 값을 입력하면 MS가 제공하는 심볼 서버에서 필요할 때마다 해당 심볼 파일을 c:\symbols 디렉토리에 다운로드 받아서 저장해 준다. 물론 미리 심볼 파일들 전체를 다운로드 받은 경우에는 디렉토리 경로만 적어주어도 된다. cdb를 사용하거나 커맨드 명령어를 통해 심볼 관련 설정을 하는 방식은 아래에서 명령어를 소개할 때 설명하도록 하겠다.

  실행 중인 프로세스를 어태치(Attach)하기 위해서는 cdb 실행 시 인자로 -p와 pid를 주면 된다.



0.2 Gdb

  Gdb로 디버깅하는 경우 특정 주소(일반적으로 EP)에 BP를 걸고 run 명령어를 사용할 수도 있지만 보통은 start 명령어를 이용한다.

(gdb) start

  이 명령어는 main()에 temporary breakpoint를 걸고 실행을 시작한다. 일반적으로 디버깅을 시작할 때 가장 먼저 사용하는 명령어로서 이 명령어를 통해 EP로 이동하여 디버깅을 시작할 수 있다.

  또 다른 참고 사항으로 디버기를 인자와 함께 실행시키는 경우에는 다음과 같이 사용한다. aaa는 실행 파일 이름이고 arg1과 arg2는 인자들로서 --args 옵션과 함께 사용한다.

$ gdb --args aaa arg1 arg2

  실행 중인 프로세스를 어태치(Attach)하기 위해서는 attach 명령어에 인자로 pid를 주면 된다.

(gdb) attach [pid]







1. Control

1.1 Session

1.1.1 Restart

> .restart

(gdb) run

  디버깅을 재시작한다.


1.1.2 Quit

> q

(gdb) quit    /    q

  디버깅을 종료한다.





1.2 Basic

1.2.1 Trace / Step

> t

(gdb) stepi    /    si

  Trace. Ollydbg의 Step Into와 같다.


> p

(gdb) nexti    /    ni

  Step. Ollydbg의 Step Over와 같다.


> wt [WatchOptions] [= StartAddress] [EndAddress]

> wt 0040107f

  Trace and watch data. EndAddress까지 실행하면서 여러가지 통계 정보를 보여준다. Start Address를 지정하지 않을 경우 현재 EIP를 기준으로 한다. 보여주는 통계 정보들로는 어떤 함수들이 호출되었는지와 그 횟수, 몇 개의 명령어들이 실행되었는지 그리고 몇 개의 시스템 호출이 실행되었는지 등이 있다.


> ta 0x<addr>    /    pa 0x<addr>

> ta 00401020

> pa 00401020

  Trace / Step to address. Ollydbg에서 F4 키와 같다고 할 수 있다. 즉 해당 주소까지 진행한다. ta의 경우에는 trace 방식을, pa의 경우에는 step 방식을 통해 진행한다.


> tc    /    pc

  Trace / Step to next call. 다음 call 명령어가 올 때까지 진행한다.


> tt    /    pt

  Trace / Step to next return. 다음 ret 명령어가 올 때까지 진행한다.


> tct    /    pct

  Trace / Step to next call or return. 다음 call 또는 ret 명령어가 올 때까지 진행한다.


> th    /    ph

  Trace / Step to next branching instruction. 다음 분기 명령어가 올 때까지 진행한다. 분기 명령어로는 call이나 ret 뿐만 아니라 jump 명령어도 포함된다.



1.2.2 Go

> g

  Go. 디버기 애플리케이션을 실행시키는 명령어로서 올리디버거의 F9와 같다. 일반적으로 BP를 걸어놓고 실행해야하며 그렇지 않는 경우에는 끝까지 실행되서 종료되어 버린다.

(gdb) continue    /    c

  Go. Windbg의 g 명령어와 같이 다음 BP를 만날때 까지 계속 진행한다.


> gc

  Go from conditional breakpoint. Conditional BreakPoint와 관련해서 사용되는 명령어이다. 이것은 뒤에 "2.1.2 BP 활용" 절에서 자세히 설명한다.


> gu

  Go up. 현재 함수가 반환할 때 까지 실행. 즉 현재 함수가 끝나는 ret까지 실행해서 caller로 복귀한다. 실수로 step over할 함수 내부로 step into한 경우에 이 명령을 사용해 간단하게 복귀할 수 있다.

(gdb) finish

(gdb) return

  현재 스택 프레임을 종료한다. 즉 현재 스택 프레임(함수)이 return할 때까지 실행하여 caller로 복귀한다. Windbg의 gu 명령어와 같다.


> gh

  Go with Exception Handled. 이것은 프로그램의 예외 핸들러를 호출하지 않고 직접 수동으로 예외 상황을 고칠 때 사용한다. 즉 예외가 발생되는 명령어를 실행해서 first chance 예외가 발생한 경우에 해당 명령어를 고치고 난 후에 다시 재개할 때 사용한다. first chance 예외 발생 시 고치지 않고 gh 명령어를 사용하면 계속 다시 first chance 예외가 발생한다.


> gn

  Go with Exception not Handled. 예외 발생 시 디버거가 받은 제어를 프로그램의 예외 핸들러가 처리하도록 프로그램에게 넘긴다. Ollydbg에서 Shift+F7 같이 다시 프로그램에게 넘겨주는 것이다.

  예를들어 보자면 발생할 예외의 핸들러에 미리 bp를 걸고 g 하면 first chance 예외가 발생하게 된다. 이 때 다시 g 하면 핸들러에 도착해서 bp에 걸린다. 하지만 애초에 gn을 하면 first chance 예외에 걸리지 않고 바로 핸들러까지 가서 이곳에서 bp에 걸린다.


* First Chance Exception / Second Chance Exception

  디버거의 구조 상 프로그램에서 발생한 이벤트를 받게 된다. 만약 예외가 발생한다면 당연히 디버거가 예외를 받게 된다. 이 때 first chance 예외가 발생한다. First chance 예외 발생 이후 프로그램에 예외 핸들러가 존재한다면 예외를 프로그램에게 넘길 수 있고(gn) 그렇지 않다면 디버거로 예외가 발생한 부분을 직접 수정해서 예외가 발생하지 않게 만든 이후 다시 실행을 재개(gh)할 수 있다. 어쨌든 프로그램에서 적절한 핸들러가 없어서 예외를 제대로 처리하지 못하면 다시 예외가 발생하게 되고 디버거는 이러한 unhandled 예외를 다시 잡게 되는데 이것을 second chance 예외라고 한다. 만약 디버거가 존재하지 않는다면 프로그램은 crash할 것이다.


* 예외 분석

  예를들어서 분석 시에 SEH를 이용한 안티 디버깅을 만났다고 가정해보자. 이 경우에 보통 SEH 핸들러의 주소에 BP를 걸고 실행할 것이다. 이 때 예외를 일으키는 명령어를 만나면 first chance 예외가 발생한다. 그리고 다시 g나 gn 명령어를 실행하면 BP에 도착할 것이다. 물론 gh 명령어를 사용한다면 계속 first chance 예외가 발생할 것이다.





1.3 Etc

1.3.1 Symbols

> .symfix c:\symbols

  심볼이 존재하는 디렉터리 경로를 지정한다.


> .reload

  경로 지정 후 심볼을 다시 로드한다.







2. Facility

2.1 BreakPoint

2.1.1 BP 기본 명령어

> bp 00407725

  Breakpoint. 0x00407725 주소에 BreakPoint를 건다.

> bp $exentry

  EP에 브레이크포인트를 설정할 때 사용한다. Windbg의 경우 디폴트 옵션에서는 처음 바이너리를 불러오면 ntdll.dll의 LdrInitializeThunk()에 BP가 걸리기 때문에 이 명령어를 통해 EP에 BP를 걸고 실행함으로써 간단하게 EP로 바로 이동할 수 있다. 

  참고로 Ollydbg의 경우에는 옵션에서 EP에 BP를 걸게 해주는 옵션이 있고 이것이 디폴트 옵션이라서 바이너리를 불러오면 자동으로 EIP가 EP에 위치하고 있기 때문에 처음 Windbg를 사용할 때 익숙치 않아서 당황할 수 있다. 

  Windbg로 바이너리를 불러오자 마자 이 명령어를 사용하면 아마 에러가 뜰 것이다. 그렇기 때문에 이 명령어는 한 번 실행 후 프롬프트가 돌아올 때 까지 약간 기다린 후에 한번 더 입력한다. 그리고 g 명령어를 사용하면 EP에 위치하는 것을 볼 수 있다.


(gdb) break *0x407725    /    b *0x407725

  Set breakpoint.


(gdb) tbreak

  Temporary breakpoint. 한 번 사용되면 자동으로 삭제되는 BP이다.


(gdb) hbreak

  Hardware breakpoint. Windbg는 하드웨어 BP라는 이름을 갖는 기능은 없다. 대신 ba를 이용하는데 뒤에서 ba 항목을 정리한다.


(gdb) thbreak

  Temporary hardware breakpoint.


> bl

(gdb) info break

  Breakpoint list. BP들의 목록을 볼 수 있다. 뒤의 명령어를 보면 알겠지만 목록에 나온 BP의 번호를 가지고 삭제하거나 활성화/비활성화를 할 수 있다.


> bc 0

(gdb) delete 0    /    d 0 

  Breakpoint clear. 0번 bp를 삭제한다.


> be 1

(gdb) enable 1

  Breakpoint enable. 1번 bp를 활성화한다.


> bd 2

(gdb) disable 2

  Breakpoint disable. 2번 bp를 비활성화한다.


> ba r4 00407725

  Break on access. 메모리 BP로서 해당 BP가 걸린 주소를 읽거나 쓰거나 실행할 경우에 발동한다. windbg의 경우 이것을 통해 하드웨어 BP와 같은 기능을 제공한다. 옵션에서 r은 read를 의미하며 w는 write, e는 execute이다. 뒤의 숫자는 BP가 걸리는 메모리의 크기로서 dword 즉 4바이트의 메모리에 BP를 걸려면 4를 입력한다. 참고로 e(execute) 옵션에서는 반드시 1바이트여야 한다. 그리고 메모리의 boundary를 맞추어야 하는데 예를들면 0x00407740의 주소에는 4바이트를 지정할 수 있지만 0x00407742의 경우에는 2바이트만 지정할 수 있다. 다시 0x00407744는 4바이트를 지정할 수 있고 0x00407746은 2바이트를 지정할 수 있다. 물론 1바이트는 아무 곳이나 지정할 수 있다.


(gdb) watch *0x00407725

(gdb) rwatch *0x00407725

(gdb) awatch *0x00407725

(gdb) info watch


  Watchpoint. Gdb에서는 메모리 BP를 watchpoint라는 개념을 통해 사용한다. 그래서 목록 확인도 info breakpoint 대신 info watch를 사용한다. 그냥 watch의 경우에는 write, rwatch는 read 그리고 awatch는 read/write를 의미한다. 



2.1.2 BP 활용

- windbg

  Conditional breakpoint. 예를들어 0x00401020에서 ecx 레지스터의 값이 2인 경우에 BP가 걸리게 하는 방식은 다음과 같다. 참고로 조건부 BP 명령어에는 j 명령어도 있지만 여기에서는 .if ~ .else만 고려하기로 한다.

> bp 00401020 ".if (@ecx == 0x2) {} .else {gc}"

  여기서 g 대신 gc를 사용하는데 이것은 만약 조건이 맞지 않는 경우에 g를 사용한다면 끝까지 실행되어 버리기 때문이다. 이것은 p 같은 명령어로 한 줄씩 트레이싱 하고 있는 경우에도 해당된다. 대신 gc를 사용한다면 조건이 맞지 않아도 다음 줄에서 브레이크가 걸린다. 즉 gc는 이러한 조건부 BP에서만 사용된다. 만약 bp가 걸린 경우에도 특정 명령어를 수행하고 싶다면 아래와 같이 사용할 수도 있다.

> bp 00401020 ".if (@ecx == 0x2) {lm ; u} .else {gc}"

  이 경우에는 조건 bp가 걸리면 lm 명령어와 u 명령어가 자동으로 실행된다. 물론 else 구문에도 명령어를 실행시킬 수 있다.


- gdb

  Conditional breakpoint. 예를들어 0x08043214에서 eax 레지스터의 값이 0인 경우에 BP가 걸리게 하는 방식은 다음과 같다.

(gdb) break *0x08043214 if $eax == 0

또는 다음과 같이 사용할 수도 있다.

(gdb) break *0x08043214

(gdb) info b

2 breakpoint ...

(gdb) condition 2 $eax == 0

  commands 명령어는 BP가 걸릴 때 실행시킬 명령어를 지정해준다.


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

(gdb) break *0x40081d

Breakpoint 2 at 0x40081d

(gdb) commands 2

Type commands for breakpoint(s) 2, one per line.

End with a line saying just "end".

>pwd

>end

(gdb) c

Continuing.

Breakpoint 2, 0x000000000040081d in main (0

Working directory /root/hw

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


  위와 같이 BP 2번 명령어를 인자로 넣고 commands 명령어를 실행하면 실행시킬 명령어를 받을 수 있다. 여기서는 pwd 명령어를 넣었다. 이후 실행시킬 명령어를 다 썼으면 end를 입력하여 끝낸다. 이제 continue 명령어를 입력하면 실행되다가 BP에 걸리고 동시에 pwd 명령어가 실행된 결과를 볼 수 있다.





2.2 Memory Assemble / Edit

> a 00407725

  Assemble. 명령어 실행 즉 Enter 후에 새로운 명령어의 입력을 받아들이는데 이 때 push 18 같은 방식으로 입력하면 해당 주소가 자동으로 수정된다. 이후에는 다음 주소에서 또 어셈블을 시도하는데 공백으로 입력 즉 Enter를 다시 한 번 입력하면 자동으로 종료된다.


> e 004077aa 6a 6d

> ea 00417000 "hello world"

  Edit. Assemble과 달리 명령어가 아닌 메모리를 수정한다. 디스어셈블리를 수정할 경우에도 edit 명령어를 사용할 수 있겠지만 assemble 명령어가 훨씬 편할 것이고 일반적인 메모리를 수정할 경우에는 edit 명령어를 사용한다. e만 사용할 경우에는 eb와 같아서 byte 즉 1바이트의 바이너리 값을 수정할 수 있고 뒤에 붙는 값에 따라 여러가지 방식으로 수정할 수 있다. 뒤에 붙는 w는 word, d는 dword, q는 qword, f는 부동 소수점(single precision - 4bytes)이며 D도 부동 소수점(single precision - 8bytes)이다. 이 외에도 위와 같이 a를 이용해 아스키 문자열을, za를 통해 NULL 종료 아스키 문자열, u는 유니코드 문자열, zu는 NULL 종료 유니코드 문자열 값으로 수정할 수 있다.

  참고 사항으로 e나 eb의 경우에 입력으로 11 22 33 44를 받는다고 하자. 그렇다면 해당 주소를 기준으로 "11 22 33 44" 이대로 각각의 바이트가 수정된다. ew의 인자로 "11 22 33 44"를 받는 경우는 좀 다르다. 이 때는 해당 주소부터 시작해서 "11 00 22 00 33 00 44 00" 이런식으로 변경된다. 즉 word는 2바이트이므로 1바이트만 입력받으므로 나머지는 "00"으로 채우는 것이다. word 형태로 바꾸고 싶다면 "1122 3344" 형태로 입력해야 한다. 그러면 원하는대로 1122 3344 형태로 수정된다. 마찬가지로 ed를 저런 식으로 입력한다면 "11 00 00 00 22 00 00 00 ..." 형태로 입력되며 "11223344" 형태로 입력해야 "11 22 33 44"와 같이 수정된다.


(gdb) set (*0x400820) = 0x6c

(gdb) set {char}0x001d3bb4 = 0x33

(gdb) set {int}0x001d3bb5 = 0x33333333

  gdb에서는 마땅한 명령어가 없는것으로 보인다. 즉 위와 같은 불편한 방식으로 사용할 수 밖에 없다.





2.3 Memory Fill / Copy / Compare

> f 00403000 l20 41 42 43

  Fill memory. 0x00403000부터 20바이트를 "0x41, 0x42, 0x43"의 연속으로 채운다.


> m <Start Range> <End Range> <Addr>

  Move memory. 

> m 00403000 00403003 00403010

  0x00403000부터 0x00403003의 메모리 값을 0x00403010에 옮긴다. 즉 복사 및 삽입이라고 할 수 있다. 참고로 처음 두 인자로부터 범위를 구할 수 있으므로 굳이 복사할 위치 즉 목적지 메모리의 끝 주소를 받을 필요는 없다.


> c <Start Range> <End Range> <Addr>

  Compare memory.

> c 00403000 00403001 00403010

  0x00403000부터 0x00403001의 메모리와 0x00403010부터 0x00403011의 메모리를 비교한다. 마찬가지로 처음 두 인자로부터 범위를 구할 수 있으므로 굳이 비교 대상 메모리의 끝 주소를 받을 필요는 없다.





2.4 Search

2.4.1 Search memory.

- windbg

s -[d|w|b|a] [ Range ] [ Pattern ]

> s [시작주소] [끝주소] [값]

> s 0019ffe0 0019fffc 'H' 'e'

  주소 사이에서 문자 H와 e가 들어간 것을 찾는다.

> s esp l200 77 d3

  esp 주소를 시작으로 200의 범위만큼 "77 d3" 값이 들어간 메모리를 찾는다.

> s -w esp l200 77d3

  또는 위와 같이 77d3이 word이므로 -w 옵션을 사용할 수 있다.

> s -d 00400000 00403000 00000000

  00000000인 값을 찾으므로 dword인 -d 옵션을 사용한다.

> s -a 0019fae0 l200 "hi"

  -a는 아스키를 말하며 l은 길이를 말하므로 0019fce0 까지이다. "hi" 문자열을 찾는다.

> s -sa 00417000 00418000

  아스키 스트링들을 찾아준다.


- gdb

(gdb) find [/sn] start_addr, +len, val1 [, val2, …]

(gdb) find [/sn] start_addr, end_addr, val1 [, val2, …]

  /b 옵션은 byte, /h는 harfwords(2 bytes), /w는 words(4 bytes), /g는 giant words(8 bytes)를 의미한다. 두 번째 인자는 시작 주소이며 세 번쨰 인자는 끝 주소 또는 길이를 의미한다. 마지막 인자가 찾을 메모리이다.


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

(gdb) find /b 0x400814, 0x40083a, 0xe5

0x400814 <main>

1 patterns found

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


  위와 같이 사용하면 주소 0x400814부터 0x40083a까지 0xe5 값을 찾는다.


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

(gdb) find /w 0x400814, +1000, 0xe5894855

0x400814 <main>

0x40083b <_Z41__static_initialization_and_destruction_0ii>

0x40087b <_GLOBAL__I_main>

0x400930 <__do_global_ctors_aux>

4 patterns found.

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



2.4.2 Search for disassembly pattern

> # [Pattern] [Address [ L Size ]] 

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

00401011 cmp ecx, dword ptr [edx+0ch]

00401307 add edx, eax

00401344 mov eax, dword ptr fs:[00000018h]

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

> # cmp*ecx 00401000 l30

> # cmp 00401000

> # ecx 00401000

> # 00401011 00401000 l30

> # ecx*word

> # ecx*dword*ptr

  위의 명령어들은 모두 다음 디스어셈블리를 찾기 위한 것이다. 참고로 4번째 예제는 주소도 검색 결과에 포함된다는 것을 보여준다.

00401011 cmp ecx, dword ptr [edx+0ch]

> # edx*eax 00401300

00401307 add edx, eax

> # dword*ptr*fs

00401344 mov eax, dword ptr fs:[00000018h]





2.5 Checkpoint

  Gdb는 체크포인트라는 기능을 제공한다. 이것은 현재 상태를 스냅샷으로 저장한 이후에 진행하다가 이전에 저장한 스냅샷으로 복귀할 수 있는 기능을 제공함으로써 디버깅을 편리하게 해준다. 실질적으로는 fork()를 통해 똑같은 프로세스를 만드는 것이다.

(gdb) checkpoint

  디버기의 현재 상태를 스냅샷으로 저장한다. 

(gdb) info checkpoint

  체크포인트 정보를 보여준다. 이후에는 여기서 보이는 체크포인트 번호를 사용한다.

(gdb) restart [ 체크포인트 번호 ]

  다른 체크포인트로 변경할 수 있다. 즉 이전에 저장한 체크포인트로 복귀할 수 있다.

(gdb) delete checkpoint [ 체크포인트 번호 ]

  체크포인트를 지운다.





2.6 Reverse Debugging

  Gdb는 역디버깅을 제공한다. 즉 디버깅 과정 중 저장을 시작한 후부터 다시 역으로 되돌아갈 수 있다는 것이다.

(gdb) target record

  역디버깅을 시작한다. 즉 상태 및 명령어 로그 저장은 여기서부터 시작한다.

(gdb) info record

  record 상태를 볼 수 있다. 저장된 명령어들이 몇 개인지 같은 정보를 볼 수 있다.

(gdb) ni

  ni나 si 등의 명령어를 사용하며 디버깅을 진행할 것이다.

(gdb) rc

  c 즉 continue 명령어의 반대이다. 반대 방향으로 c 명령어를 진행한다.

(gdb) rsi

  si 명령어의 반대이다.

(gdb) rni

  ni 명령어의 반대이다.

(gdb) record stop

  record를 중단한다. stop한 경우에는 지금까지 저장했던 로그가 모두 삭제된다. 즉 그냥 없었던 일이 되는 것이다.

(gdb) record save

  현재 record를 로그 파일로 저장한다.

(gdb) record restore [ 로그 파일 이름 ]

  로그 파일에서 복구한다. 참고로 복구하면 record의 begin 즉 record를 시작한 곳에서 시작한다.

(gdb) record goto end

  record의 마지막으로 간다. 여기서 마지막은 record로 저장된 마지막을 의미한다.

(gdb) record goto [ n ]

  record의 번호 n으로 간다.

(gdb) record goto begin

  record의 시작으로 간다.

(gdb) record delete 

  현재 위치 이후부터 end까지를 삭제한다. 현재 위치를 record의 end로 만든다.

(gdb) show exec-direction

  rsi, rni, rc 같은 명령어가 있어서 큰 의미는 없어 보이지만 실행 방향을 설정하는 모드도 존재한다. reverse와 forward가 있다. 

(gdb) set exec-direction reverse

  reverse로 설정한 경우에는 ni, si, c 같은 명령어가 rni, rsi, rc 처럼 반대로 진행된다. 참고로 "target record"를 통해 역디버깅을 시작한 후에 이 모드를 설정할 수 있다. 그냥 rni 같은 명령어를 사용하면 되므로 큰 의미는 없어보이는 모드이다.

(gdb) set exec-direction forward

  모드를 foward로 설정한다.

(gdb) set pagination off

  출력 결과가 화면을 넘어가는 경우 전체를 보여주지 않고 다음과 같이 부분만 출력한다. pagination 옵션을 off하면 화면을 넘어가는 긴 출력 결과도 전체를 보여준다.

---Type <return> to continue, or q <return> to quit---





2.7 Inferior

  리눅스에서는 fork()를 통해 자식 프로세스를 생성할 수 있다. Gdb의 경우 디버깅 도중 fork()로 인해 자식 프로세스가 생성된 경우 부모 프로세스 뿐만 아니라 자식 프로세스도 디버깅할 수 있는 기능을 제공한다.

  먼저 이것과는 상관 없지만 배경 지식을 위해 다음 모드를 살펴보겠다. 다음은 fork() 이후 각각 parent 또는 child 프로세스를 디버깅하겠다는 설정이다. 즉 부모 프로세스를 디버깅하는 도중 자식 프로세스가 생성된 경우에 둘 중 하나를 선택하여 그것을 디버깅하겠다는 것이다. 참고로 fork() 외에도 exec()도 마찬가지이다. same은 그대로 디버깅을 계속하고 new는 exec()을 통해 생성된 프로세스를 디버깅하겠다는 의미이다.

(gdb) set follow-fork-mode parent

(gdb) set follow-fork-mode child

(gdb) show follow-fork-mode

(gdb) set follow-exec-mode same

(gdb) set follow-exec-mode new

(gdb) show follow-exec-mode

  detach-on-fork 모드를 off시키면 fork() 시에 부모 또는 자식 프로세스를 선택하고 나머지를 detach 시키는 것을 막는다. 이에 따라 inferior는 부모 프로세스 와에도 자식 프로세스도 포함된다.

(gdb) show detach-on-fork

(gdb) set detach-on-fork off

  이제 inferior의 개념을 알게 되었으니 어떤 방식으로 inferior를 디버깅할지 살펴보겠다.

(gdb) info inferiors

  위의 명령어를 통해 inferior들의 목록을 구할 수 있다. 이 결과로 나온 번호를 통해 다음 명령어를 실행한다.

(gdb) inferior [ inferior 번호 ]

  이 명령은 디버깅 대상을 다른 inferior로 옮기는 역할을 한다. 만약 자식 프로세스의 번호를 사용하는 경우 부모 프로세스를 그대로 둔 상태에서 이제 자식 프로세스를 디버깅할 수 있는 것이다. 그리고 다시 부모 프로세스의 번호를 입력하여 부모 프로세스로 돌아갈 수도 있다.

(gdb) detach inferior [ inferior 번호 ]

  쓸모가 없는 경우에는 inferior를 detach 시킬 수 있다.

(gdb) kill inferior [ inferior 번호 ]

  또는 위의 명령을 사용해 inferior를 종료시킬 수 있다.





2.8 Thread

(gdb) info threads

  모든 스레드 리스트 및 번호를 보여준다.

(gdb) thread [ thread 번호 ]

  현재 스레드에서 다른 스레드로 변경한다.

(gdb) thread apply [ thread 번호 | all ] [ command ]

  기본적으로 명령어를 사용하면 현재 스레드에만 적용되겠지만 인자로 받은 thread 번호를 통해 명시된 스레드에도 동일한 command를 적용할 수 있다. 인자가 all인 경우 모든 스레드가 입력된 명령어를 수행한다.

(gdb) thread apply all bt

  위와 같은 명령어를 사용하면 모든 스레드의 콜스택을 보여준다(bt 명령어).





2.9 Signal

  리눅스에만 존재하는 signal과 관련된 명령어들이다.

(gdb) info signals

(gdb) info handle

  모든 종류의 시그널 목록과 이 시그널 발생 시 GDB각 각각의 항목에 따라 어떠한 행위를 할지에 대한 설정을 보여준다. 이것은 다음 명령어를 통해 각각에 맞는 키워드를 넣어서 설정할 수 있다.

(gdb) handle [ signal 이름 ] [ keyword ]

  키워드는 세 종류가 있다. 시그널 발생 시 프로그램을 중지시킬지에 따라 nostop, stop이 존재하며, 시그널 발생 시 메시지를 표시할지 여부에 따라 print, noprint가 존재하고 마지막으로 받은 시그널을 프로그램으로 다시 넘겨줄지 여부에 따라 pass(noignore), nopass(ignore)가 존재한다. 즉 예를 들면 다음과 같이 사용한다.

(gdb) handle SIG117 nostop

  signal 명령어는 프로그램에 인자로 받은 시그널을 넘김과 동시에 continue한다. 실제로 사용되는 예시를 들어보겠다. 만약 특정한 시그널이 발생하였고 거기에 대해 stop이 설정되어 있다면 프로그램은 정지한다. 이 때 다시 프로그램을 진행시키면 프로그램은 이 시그널을 받은 상태로 진행되기 때문에 만약 그 시그널이 프로그램을 종료시키는 중대한 에러였다면 프로그램은 종료된다. 이런 경우에 받은 시그널을 없애고 진행(continue)하고 싶은 경우에 다음 명령어를 사용한다.

(gdb) signal 0

  마지막으로 catch 명령어를 통해 signal에 대한 캐치포인트를 지정할 수도 있다. 이것은 Catchpoint 항목에서 설명한다.

(gdb) catch signal [ 시그널 번호 ]





2.10 Catch

  Gdb는 Catchpoint를 제공한다. 캐치포인트는 이벤트들에 대한 BP와 비슷한 개념이다. 즉 특정한 이벤트가 발생했을 때 BP를 걸어주는 것이다. 이벤트들로는 throw, catch, exception, exec, fork, vfork, syscall 등이 있다. 예를들어 간단하게 "catch syscall"을 사용함으로써 모든 시스템 호출에 catchpoint를 걸 수 있으며 fork()나 exec()도 마찬가지이다. 이 외에도 여러 C++ 예외에도 유용하게 사용할 수 있다.

(gdb) catch [ event ]

  여러가지 이벤트들에 대한 catchpoint를 설정한다. 

(gdb) info break

  catchpoint도 breakpoint와 같이 이 명령어를 통해 확인할 수 있다.

(gdb) tcatch [ event ]

  catch와 같지만 temporary로서 한 번만 처리된다.





2.11 Events and Exceptions for Windows

  sx* 명령어들은 디버기에서 예외 또는 이벤트가 발생할 때 디버거가 취할 행동을 제어한다. 즉 Break Status와 Handling Status를 설정한다.

> sx

  이 명령어는 현재 프로세스의 예외 목록 및 비-예외 이벤트들의 목록을 보여줌과 동시에 각 예외 및 이벤트에 대한 디버거의 디폴트 행위를 보여준다.

  먼저 Break Status부터 보겠다. sxe(이제부터 괄호 안은 cdb에서 사용되는 명령어로 하겠다) 명령어는 Break Status를 Break 즉 Enabled를 의미한다. 이것은 예외가 발생할 때 즉시 디버거로 넘겨주는 것을 의미하며 first-chance exception이라고도 불린다. sxd(-xd) 명령어는 Second chance break로 설정한다. 즉 Disabled를 의미한다. 이것은 first-chance exception 발생 시 메시지만 표시하고 break하지 않으며 에러 핸들러가 이 예외를 처리하지 못해 다시 예외가 발생했을 때 실행을 멈추고 다시 디버거로 넘겨주는 것이다. sxn(-xn) 명령어는 Output으로 설정한다. 즉 Notify를 의미한다. 이것은 예외 발생 시 메시지만 표시하는 것이다. sxi(-xi) 명령어는 ignore로 설정한다. 이것은 예외 발생 시 디버기 프로그램 자체에서 처리하지 디버거로 넘겨주지 않는다는 것을 의미한다.

  이제 Handling Status를 살펴보겠다. sxe -h 명령어는 Handling Status를 Handled로 설정한다. 이것은 실행을 재개할 시에 이벤트가 처리되었다고 여기게 해준다. sxd(또는 sxn, sxi) -h 명령어는 Not Handled로 설정한다. 이것은 실행이 재개되었을 때 이벤트가 처리되지 않았다고 여기게 해준다. 참고로 Single-step exception 같이 디버기와 디버거간의 통신에 사용되는 예외들은 Handled로 설정되어 있으며 디버거가 알아서 처리해 주지만 다른 예외들은 모두 Not Handled로 설정되어 있으며 일반적으로 그래야 한다. 특별한 경우가 아닌 경우에 일반적인 예외들을 Handled로 설정한다면 모든 first-chance 예외 및 second-chance 예외 발생 시에 모든 예외 처리 루틴들을 건너뛰기 때문이다.

  이렇게 sx* 명령어를 통해 Break Status를, sx* -h 명령어를 통해 Handling Status를 설정할 수 있다. 참고로 특별한 몇몇 이벤트 코드들은 -h 옵션을 사용하지 않고 sx* 명령어를 통해 Break Status 대신 Handling Status를 설정한다. 예를들면 아래에 나오는 hc의 경우에는 sx* hc 처럼 -h 옵션이 없어도 Handling Status를 설정한다는 것을 의미한다.다음은 일반적인 이벤트들이다.

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

asrt : Assertion failure.

av : Access violation.

dm : Data misaligned.

dz : Integer division by zero.

c000008e : Floating point division by zero.

eh : C++ EH exception.

gp : Guard page violation.

ii : Illegal instruction.

iov : Integer overflow.

ip : In-page I/O error

isc : Invalid system call.

lsq  Invalid lock sequence.

sbo : Stack buffer overflow.

sov : Stack overflow.

wkd : Wake debugger.

aph : Application hang.

3c : Child application termination.

ch, hc : Invalid handle. 참고로 ch는 Break Status, hc는 Handling Status를 설정할 때 사용된다.

Number : Any numbered exception.

dbce : Special debugger command exception.

vcpp : Special Visual C++ exception.

wos : WOW64 single-step exception.

wob : WOW64 breakpoint exception.

sse, ssec : Single-step exception. sse는 Break Status, ssec는 Handling Status를 설정할 때 사용된다.

bpe, bpec : Breakpoint exception. bpe는 Break Status, bpec는 Handling Status를 설정할 때 사용된다.

cce, cc : CTRL+C or CTRL+BREAK. cce는 Break Status, cc는 Handling Status를 설정할 때 사용된다.

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

  다음 이벤트들은 예외가 아니므로 Handling Status는 상관이 없기 때문에 Break Status만 설정할 수 있다.

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

ser : System error.

cpr : Process creation.

epr : Process exit.

ct : Thread creation.

et : Thread exit.

ld : Load module.

ud : Unload module.

out : Target application output.

ibp : Initial breakpoint.

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

  예들들어 보겠다. 다음은 ld 이벤트와 관련된 예제이다. ld 이벤트 코드는 예외가 아니므로 Break Status만 설정할 수 있다.

ld [ module, driver 이름 ] : 모듈(.dll)이나 드라이버(.sys)가 로드되었을 때의 이벤트 설정.

ud [ module, driver 이름 ] : 모듈(.dll)이나 드라이버(.sys)가 언로드되었을 때의 이벤트 설정.

  올리디버거의 옵션에는 모듈 로드 시에 자동으로 BP를 걸어주는 옵션이 있다. 즉 악성코드가 LoadLibraryA() API를 사용해 특정한 모듈을 로드할 시에 BP를 거는데 사용될 수 있는 편리한 기능이다. 이 기능을 Windbg에서 구현해보도록 하겠다. 

  Windbg에서는 ld라는 이벤트 코드가 있는데 이것은 모듈이 로드되었을 때 발생하는 이벤트이다. 이 이벤트를 sxe 명령어를 사용해 Break Status를 Break로 걸어준다. 이를 통해 특정 모듈이 로드된 경우에 BP를 자동으로 걸어주는 원리이다. 


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

> sxe ld user32.dll kernel32.dll

ld - Load module - break

(only break for user32.dll kernel32.dll)

> g

ModLoad: 749c0000 74afc000   C:\WINDOWS\SysWOW64\User32.dll

eax=00000000 ebx=00000000 ecx=000028b4 edx=0041a6e3 esi=00000000 edi=002d0000

eip=77731fec esp=0019d12c ebp=0019d180 iopl=0         nv up ei pl nz na po nc

cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202

ntdll!NtMapViewOfSection+0xc:

77731fec c22800          ret     28h

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


  위에서는 두 dll을 사용했는데 이 명령어는 user32.dll과 kernel32.dll을 차례대로 입력하면 통하지 않기 때문이다. 즉 마지막에 입력된 명령어로 수정된다. 그래서 여러개를 입력하고 싶은 경우에는 한 번에 위처럼 여러개를 같이 써야 한다. 만약 모든 모듈을 설정하고 싶다면 " sxe ld *.dll "과 같이 사용할 수도 있다. 비슷한 원리로 ud 이벤트 코드를 사용해 모듈이 언로드되었을 때 BP가 걸리게 할 수 있다. 참고로 Gdb에서도 이와 동일한 기능을 제공하는데 이것은 2.12.6 Stop-on-solib-events 절에서 설명하겠다.

  이번에는 sx 명령어를 입력하여 디폴트로 설정된 것들을 살펴보겠다. 보니까 ibp 이벤트 코드 즉 Initial breakpoint가 break로 설정되어 있다. 이 설정을 보면서 우리가 Windbg로 새로운 실행 파일을 로드한 경우에 자동으로 초기 BP가 걸려서 거기에서 멈춰있던 것을 생각할 수 있다.

  또한 위의 리스트에서 dbce부터 cce의 항목까지 보면 모두 디폴트로 Break Status는 break, Handling Status는 Handled인 것을 볼 수 있다. 이것은 당연하게도 디버거와 디버기가 통신하는데 사용되는 예외이기 때문이다. 단적인 예로 sse는 Single-step exception을 의미한다.





2.12 ETC

2.12.1 Help

> ?

  모든 명령어들과 연산자들의 목록을 보여준다. 그것 외에는 없다.

> .help

> .help /D wake

  meta-command help. 메타 명령어들에 대한 설명을 보여준다. 디폴트로 사용할 시 사용 가능한 모든 메타 명령어들의 목록을 보여준다. 두 번째 줄은 .wake 메타 명령어에 대한 간단한 설명을 볼 수 있다. 물론 각 알파벳을 클릭하여 해당 알파벳으로 시작하는 모든 메타 명령어들에 대한 간단한 설명을 볼 수 있다. .hh를 통해 자세한 설명을 보는 것을 추천한다.

> !help

  확장 명령어들의 목록을 보여준다.

> .hh

> .hh !gle

  Open html help file. HTML help 문서 파일을 열어준다. 인자로 명령어를 넣는다면 해당 명령어에 관한 설명을 찾아주며 일반 명령어든 메타 명령어든 확장 명령어든 문서에 있는 명령어는 모두 찾아준다. 참고로 왼쪽에서 검색 후 체크된 항목을 더블클릭해야 볼 수 있다.


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

(gdb) help

List of classes of commands:


aliases -- Aliases of other commands

breakpoints -- Making program stop at certain points

data -- Examining data

files -- Specifying and examining files

internals -- Maintenance commands

obscure -- Obscure features

running -- Running the program

stack -- Examining the stack

status -- Status inquiries

support -- Support facilities

tracepoints -- Tracing of program execution without stopping the program

user-defined -- User-defined commands

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


  인자 없이 사용하면 명령어들의 클래스를 보여준다. 즉 모든 명령어들을 보여주는 것이 아니라 각 명령어들이 속한 분류인 클래스를 보여준다.

(gdb) help data

  여기서는 data 클래스를 선택하였는데 이 data 클래스에 속한 모든 명령어들을 간단한 설명과 함께 보여준다. data 클래스의 명령어 중 dump 명령어를 보려면 아래와 같이 입력한다.

(gdb) help dump

  위와 같이 인자로 명령어를 넣으면 명령어에 대한 조금 더 자세한 설명을 볼 수 있다. 

(gdb) apropos breakpoint

  인자로 받은 문자열과 관련된 명령어들을 보여준다. 즉 위와 같은 경우에는 breakpoint 관련 명령어들의 목록이 결과로 나온다.


2.12.2 Clear

> .cls

  화면을 clear해 준다.

(gdb) shell clear

  gdb의 명령어도 아니고 특정한 환경(xterm)에 통하는 명령이지만 화면을 clear해 준다. 직접적으로 제공되는 gdb 명령어는 없다.


2.12.3 Loop

> z(expression)

  Execute while. 일종의 반복문으로서 괄호 안이 참일때 까지 반복한다. 다음과 같이 사용하는데 이렇게 사용하면 ecx가 0이될 때 까지 p 명령어를 실행한다.

> p; z(ecx != 0)


2.12.4 Log

- Windbg

  Windbg에서는 로그 파일을 만들어서 텍스트 형태로 기록할 수 있다. 즉 로그 파일을 오픈한 이후부터 닫을 때 까지 입력한 명령어 및 결과가 해당 텍스트 파일에 모두 기록된다. 


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

> .logopen /t C:\Users\monto\Desktop\log.txt

Opened log file 'C:\Users\monto\Desktop\log_252c_2017-06-15_10-58-50-192.txt'

// Open log file

> ...

> .logfile

Log 'C:\Users\monto\Desktop\log_252c_2017-06-15_10-58-50-192.txt' open

// Display log file status

> .logclose

Closing open log file C:\Users\monto\Desktop\log_252c_2017-06-15_10-58-50-192.txt

// Close log file

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


- Gdb

(gdb) set logging on

  로깅을 시작한다. 디폴트로 gdb.txt 파일에 저장된다.

(gdb) set logging off

  로깅을 중지한다.

(gdb) show logging

  현재 로깅 설정을 보여준다. 설정들은 뒤에서 나열하겠지만 어떤 이름의 파일에 저장할지, 로그를 append 즉 이어쓸지 아니면 overwrite할지, 마지막으로 출력을 터미널에도 보여주고 로그 파일에도 저장할지 아니면 로그 파일에만 저장할지이다.

(gdb) set logging file [ 파일 이름 ]

  로그 파일의 이름을 새로 지정할 수 있다. 디폴트는 gdb.txt 파일이다.

(gdb) set logging overwrite [ on|off ]

  로그를 overwrite할지 여부를 결정한다. 디폴트는 append이다.

(gdb) set logging redirect [ on|off ]

  로그의 출력을 로그 파일에만 보낼지 지정한다. 디폴트는 로그 파일 및 터미널 모두로 보내는 것이다.


2.12.5 ASLR

  디버기 실행 시에 커널에서 제공되는 ASLR 기능을 활성화할지 여부를 결정한다.

(gdb) show disable-randomization

  disable-randomization의 on/off 설정을 보여준다.

(gdb) set disable-randomization on 

  디버기 프로세스의 ASLR을 비활성화시킨다.

(gdb) set disable-randomization off

  디버기 프로세스의 ASLR을 활성화시킨다.


2.12.6 Stop-on-solib-events

(gdb) show stop-on-solib-events

  위에서 설명하였듯이 Gdb에서도 공유 라이브러리가 로드 또는 언로드 될 시에 BP를 걸어주는 기능을 제공한다. 위의 명령어는 현재 그 기능이 켜져있는지 여부를 보여준다. 

(gdb) set stop-on-solib-events 0

  Stop-on-solib-events 기능을 끈다.

(gdb) set stop-on-solib-events 1

  Stop-on-solib-events 기능을 켠다.


2.12.7 Alias

> as 또는 aS

  Set Alias. 아래의 예에서는 !teb 명령어의 alias로 !t를 만들었다. 이후에는 !teb 외에 !t 명령어만 사용해도 디버거는 !teb와 동일하게 받아들인다.

> as !t !teb

> !t


(gdb) alias -a di = disas

(gdb) di

  gdb에서는 alias 명령어가 존재한다.


2.12.8 Define

(gdb) define adder

> print $arg0 + $arg1 + $arg2

> end

(gdb) adder 1 2 3

$1 = 6

  gdb에서는 사용자 정의 명령어를 정의하기 위해 define 명령어를 사용한다. define 명령어 이후 새로운 명령어의 이름을 적으면 명령어의 내용을 받을 수 있다. 이후 마지막으로 end를 입력하면 종료된다.


2.12.9 Etc

> .printf

  c언어의 printf 문과 같다고 생각하면 된다. 유용하게 사용할 수 있다.


> poi()

  포인터 참조 명령어로서 c언어에서 *와 같다. 즉 인자로 주소를 받는다고 여기고 그 주소의 값을 반환한다.


> .printf "%y \n", poi(00413014)

KERNEL32!GetVersionExAStub (747456d0)


  00413014는 IAT 영역에 위치한 주소로서 GetVersionExAStub()의 주소가 들어가 있다. 조금 더 자세히 말하면 현재 PE가 로드되어 있으므로 IAT에는 각 임포트하는 API들의 주소가 들어가 있다. 코드 영역에서 api를 호출할 경우 이 IAT의 주소를 사용해 call dword ptr [ 00413014 ] 같은 방식으로 호출한다. 이 00413014가 어떤 함수인지를 보기 위해서 poi()를 이용해 해당 주소에 들어있는 값을 알 수 있다. 즉 747456d0이 .printf로 넘어온다.

  .printf의 포맷스트링을 보면 %y이다. 이것은 인자로 디버거 심볼의 주소를 받아서 해당 심볼의 이름을 보여준다. 즉 현재 kernel32.dll의 심볼이 있고 이 중에서 747456d0의 주소를 넘겨 받았으므로 이 주소에 해당하는 심볼인 KERNEL32!GetVersionExAStub 문자열을 출력해 준다. 물론 뒤에 실제로 받은 값이 747456d0도 보여주며 만약 심볼이 없다면 이 값만 보여준다. 이렇게 poi()와 %y를 통해 간단하게 IAT 주소를 가지고 api 이름까지 알아낼 수 있다.


> .echo [ string ]

  일반적인 echo와 같다고 생각하면 된다.

> .echo windbg echo

windbg echo







3. Data

3.1 Unassemble

> u

  Unassemble을 보여준다.

> u 00401000 0040107A

  위에서 지정한 주소만큼 Unassemble을 보여준다.

> u eip l30

  Unassemble을 30라인 만큼 보여준다.

> u ntdll!LdrUnloadDLL

  심볼이 존재한다면 해당 api 함수의 주소를 구하는데 u 명령어를 사용할 수 있다. 예를들면 Ollydbg에서 "Ctrl + G" 명령어를 통해 찾는 것과 같은 방식이다.


> ub

  u가 특정 주소 이후를 보여준다면 ub는 반대로 이전을 보여준다.

> ub eip l20


(gdb) disas

(gdb) disas $eip

  gdb의 경우 ub 같은 명령어가 없다.


(gdb) show disassembly-flavor

  디스어셈블리 스타일이 AT&T인지 아니면 Intel인지를 보여준다.

(gdb) set disassembly-flavor [ intel ]

  디스어셈블리 스타일을 Intel로 설정한다.





3.2 Register

> r

  기본 레지스터들의 값을 보여준다.

> r $proc

  현재 프로세스의 PEB 주소. 위와 같은 "의사 레지스터"는 항목 "3.5.1 의사 레지스터"에서 설명한다.

> r @eip=0x00401000

> r $ip=0x00401000

  두 명령어는 모두 EIP 값을 바꾸는 명령이다. 이것도 "의사 레지스터" 항목을 참고하자.


(gdb) info reg    /    i r

  Gdb의 경우 위와 같이 사용한다.

(gdb) set $eip = 0x00401000

(gdb) set $rip = 0x00401000

  두 명령어는 레지스터 즉 eip 또는 rip의 값을 변경하는 명령이다.


  GDB와 Windbg 모두 EFLAGS 레지스터 관련한 정보는 다음 링크를 확인한다. [ http://sanseolab.tistory.com/44 ]





3.3 Memory

3.3.1 Display Memory

- windbg

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

> d* <addr>

- *

a : ascii chars

u : unicode chars

b : byte + ascii (디폴트 d와 같다)

w : word

W : word + ascii

d : dword

c : dword + ascii

q : qword

f : floating point (single precision - 4b)

D : floating point (double precision - 8b)

b : binary + byte

d : binary + dword

s : string struct

S : unicode string

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


> dc 00401700

  Ollydbg의 디폴트 메모리 창처럼 메모리를 dword 형태로 아스키 값과 함께 출력한다. dW 명령어를 사용해도 띄어쓰기 즉 보여주는 단위의의 차이만 있지 실질적인 차이는 없다.


- gdb

(gdb) x/[Length][Format] [Address expression]

  x/ 바로 뒤에오는 숫자는 길이를 나타낸다. 아래의 예를 보면 알겠지만 10개 단위를 보여준다. 즉 여기서는 word 10개 단위를 보여준다. 두 번째는 출력 포맷을 의미한다. i는 인스트럭션(disas 명령어처럼), x는 익숙한 16진수 값, c는 char, s는 문자열이다. 세 번째는 출력 포맷의 크기이다. 즉 b(byte), h(halfword, 2bytes), w(word, 4bytes), g(giant word, 8bytes)가 있다. 다음 예제는 x86 아키텍처의 예이며 Ollydbg처럼 메모리를 4바이트 단위로 16진수 포맷을 통해 보여준다. 하지만 Ollydbg나 Windbg처럼 아스키 또는 유니코드 문자열을 보여주지는 못하고 메모리 값만 보여준다.


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

(gdb) x/10xw 0x400814

0x400814 <main>:        0xe5894855  0x400988be  0x0d20bf00  0xc1e80060

0x400824 <main+16>:    0xbefffffe    0x00400708  0xe8c78948  0xfffffec4

0x400834 <main+32>:    0x000000b8  0x55c3c900

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


(gdb) x/30i 0x08048320

  위와 같이 사용하면 disas 명령과 같이 인스트럭션들을 보여준다.

(gdb) x/16wx $sp

(gdb) x/16gx $sp

  첫 번째 예제는 x86 아키텍처에서 스택 창을 보여주며 두 번째 예제는 x64 아키텍처에서 보여준다. 마찬가지로 Ollydbg나 Windbg와는 다르게 값만 보여주고 심볼 또는 문자열 같은 정보는 보여주지 않는다.



3.3.2 Display words and symbols

- windbg

> d*s <addr>

  dds는 dword, dqs는 qword, dps는 아키텍처 표준 단위로 보여주며 특정 단위와 연관된 심볼도 함께 보여준다. 즉 해당 메모리 주소에 들어있는 값 및 이 값과 연관된 심볼을 보여준다. 예를들어 들어있는 값이 특정 함수의 주소인 경우에는 관련 함수의 이름 같은 심볼을 보여준다.

> dds esp

> dqs rsp

  x86 아키텍처에서 위와 같이 사용하면 esp를 dword 단위로 보여줌과 동시에 각 dword 주소 별로 값을 보여주며 만약 그 값과 연관된 심볼이 존재하는 경우 심볼도 보여주어서 Ollydbg의 스택 메모리 창처럼 보여줄 수 있다. x64의 경우에는 dqs 명령어를 사용한다.



3.3.3 Display Referenced Memory

> d** <addr>

  인자로 받은 해당 주소 영역에서 포인터를 보여주고 해당 포인터로 참조되는 메모리를 보여준다. 예를들어 현재 eax에 0x00685ce8가 들어있다고 하자. 이 때 "ddp eax" 명령을 실행한다면 결과는 다음과 같다.


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

00685ce8    006853f8    554c4c41

00685cec    00685be8    44505041

...

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


  즉 0x00685ce8을 포인터가 위치한 주소로 여기고 여기에 들어있는 값인 0x006853f8을 포인터로 생각한다. 마지막 3번째는 해당 포인터에서 참조되는 메모리이다. 이 명령어를 이렇게만 사용한다면 큰 의미가 없을테지만 다음과 같은 기능을 제공한다. "dds eax"를 실행해보면 다음과 같다.


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

00685ce8  006853f8 "ALLUSERSPROFILE=C:\ProgramData"

00685cec  00685be8 "APPDATA=C:\Users\monto\AppData\Roaming"

...

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


  즉 세번째 문자를 a로 넣는다면 참조되는 메모리를 아스키 문자열로 보여준다. 참고로 Ollydbg의 경우에 스택 메모리 창을 보면 함수 이름 및 파라미터 이름 같은 심볼 외에도 문자열로 여겨지는 경우에는 문자열도 보여준다. 그렇기 때문에 스택에 "dda esp" 명령도 유용하게 사용할 수 있다. 다음은 두번째 및 세번째 문자열로 들어갈 수 있는 값들이다.


dd* : 해당 값을 32비트 포인터로 생각한다. 즉 x86 아키텍처에서는 dd*를 사용한다.

dq* : 해당 값을 64비트 포인터로 생각한다. 즉 x64 아키텍처에서는 dq*를 사용한다.

dp* : 해당 아키텍처에 맞는 표준 사이즈를 자동으로 설정한다.

d*a : 참조되는 메모리를 아스키 문자열로 보여준다.

d*u : 참조되는 메모리를 유니코드 문자열로 보여준다.

d*p : 참조되는 메모리를 보여준다.



3.3.4 Display Type

  지역 변수, 전역 변수 또는 데이터 타입에 대한 정보를 보여준다.

> dt [-DisplayOpts] [-SearchOpts] [module!]Name [[-SearchOpts] Field] [Address] [-l List] > dt [-DisplayOpts] Address [-l List] > dt -h


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

> dt _PEB 0x0026a000

> dt ntdll!_PEB


   +0x000 InheritedAddressSpace : 0 ''

   +0x001 ReadImageFileExecOptions : 0 ''

   +0x002 BeingDebugged    : 0x1 ''

   +0x003 BitField         : 0 ''

   +0x003 ImageUsesLargePages : 0y0

   +0x003 IsProtectedProcess : 0y0

   +0x003 IsImageDynamicallyRelocated : 0y0

   +0x003 SkipPatchingUser32Forwarders : 0y0

   +0x003 IsPackagedProcess : 0y0

   +0x003 IsAppContainer   : 0y0

   +0x003 IsProtectedProcessLight : 0y0

   +0x003 IsLongPathAwareProcess : 0y0

   +0x004 Mutant           : 0xffffffff Void

   +0x008 ImageBaseAddress : 0x00400000 Void

   +0x00c Ldr              : 0x77757be0 _PEB_LDR_DATA

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



3.3.5 Display Debugger Object Model Expresison

  NatVis 확장 모델을 이용하여 C++ Expression을 보여준다.

> dx


  자세한 r# 옵션은 recursion Level이다. 다음과 같이 사용하여 _PEB_LDR_DATA 구조체 0x77757be0에 대한 정보를 확인하였다.

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

> dx -r1 ((ntdll!_PEB_LDR_DATA *)0x77757be0)


    [+0x000] Length           : 0x30 [Type: unsigned long]

    [+0x004] Initialized      : 0x1 [Type: unsigned char]

    [+0x008] SsHandle         : 0x0 [Type: void *]

    [+0x00c] InLoadOrderModuleList [Type: _LIST_ENTRY]

    [+0x014] InMemoryOrderModuleList [Type: _LIST_ENTRY]

    [+0x01c] InInitializationOrderModuleList [Type: _LIST_ENTRY]

    [+0x024] EntryInProgress  : 0x0 [Type: void *]

    [+0x028] ShutdownInProgress : 0x0 [Type: unsigned char]

    [+0x02c] ShutdownThreadId : 0x0 [Type: void *]

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





3.4 Call Stack

  여기서는 스택 프레임 즉 콜스택과 관련된 정보를 볼 수 있는 명령어들을 다룬다. 

- Windbg

> k

  Display stack backtrace.

> kb

  인자도 같이 보기.


- Gdb

(gdb) backtrace

(gdb) bt

(gdb) info stack

(gdb) where

  위의 명령어 모두 전체 call stack을 보여준다. 

(gdb) bt [ n ]

  특정 순서의 프레임만 보여준다.

(gdb) bt full

  전체 콜 스택에 대하여 로컬 변수를 포함해서 보여준다. 물론 심볼이 있어야 하기 때문에 여기서는 의미가 없다.

  다음에 설명하는 명령어들은 각 스택 프레임에 대한 정보를 보여주는 명령어들이다. 참고로 info args나 info locals 같은 명령어들은 심볼이 있는 경우에 정보를 보여줄 수 있다.

(gdb) info frame

  현재 선택된 스택 프레임에 대한 정보를 보여준다.

(gdb) info args    /    info locals

  심볼이 있는 경우에 각각 현재 프레임의 인자와 로컬 변수를 보여준다. 심볼이 있어야 하기 때문에 여기서는 의미가 없다.

  다음에 설명하는 명령어들은 콜 스택에서 각각의 스택 프레임을 선택하는 명령들이다.

(gdb) frame [ frame 번호 ]

  프로그램의 진행과 관련 없이 현재 스택 프레임을 변경한다.

(gdb) select-frame [ frame 번호 ]

  frame 명령어와 같지만 별도로 내용을 출력하지 않는다.

(gdb) up / down

  up 명령어는 현 프레임이 호출한 스택 프레임을 선택하고 down 명령어는 현 프레임을 호출한 스택 프레임을 선택한다. 간단히 말해서 bt 명령어를 통해 볼 수 있는 콜스택에서 위 아래로 선택하는 명령들이다.





3.5 ETC

3.5.1 의사 레지스터

  Windbg는 특정한 값을 갖는 의사 레지스터들을 지원한다. 이것은 dollar ($) 기호를 앞에 붙임으로써 구별 가능하다. 참고로 특정한 레지스터를 의미하는 at (@) 기호와 호동될 수 있다. 앞에서 특정한 레지스터를 표현할 때 다음과 같이 사용하였다.

> bp 00401020 ".if (@ecx == 0x2) {} .else {gc}"

  즉 의사 레지스터는 $ 기호를, 구체적인 레지스터는 @ 기호를 붙인다. 다음은 의사 레지스터들의 예시이다.


- $ip : 현재 디버거 타겟 아키텍처의 명령 포인터 레지스터의 이름을 나타낸다. 즉 x86에서는 eip 레지스터이며 @eip와 같은 의미이고 x64에서는 rip 레지스터 즉 @rip와 같은 의미이다.

- $ra : 현재 함수의 복귀 주소.

- $retreg : 주요한 값을 가지는 레지스터를 나타낸다. 즉 x86에서는 eax 레지스터(@eax), x64에서는 rax 레지스터(@rax)와 같은 의미이다.

- $csp : 현재 스택의 포인터. x86에서는 esp 레지스터(@esp), x64에서는 rsp 레지스터(@rsp)를 의미한다.

- $proc : 현재 프로세스. 유저 모드에서 PEB의 주소나 커널 모드에서 현재 프로세스의 EPOCESS 구조체의 주소.

- $thread : 현재 스레드. 유저 모드에서 TEB의 주소나 커널 모드에서 현재 스레드의 ETHREAD 구조체의 주소

- $tpid : PID

- $tid : TID

- $t0 ~ $t9 : 사용자 정의 의사 레지스터.


> ? <expression>

> ? $ip

> ? @eip

  ? <expression> 명령어는 해당 표현식의 결과를 보여준다. 바로 아래에 나온 예제를 보면 x86 환경에서는 두 명령어 모두 같은 값 즉 eip 레지스터의 값을 의미하며 ? 명령어의 결과도 동일하다.



3.5.2 Etc

3.5.2.1 Module

> lm

  List loaded modules. 참고로 윈도우의 심볼 서버를 설정한 경우를 가정하고 처음 이 명령어를 사용하면 ntdll만 심볼이 보이고 나머지는 deferred로 된다. 이것은 지금까지 분석한 모듈이 ntdll 밖에 없고 그래서 심볼 서버로 ntdll만 심볼을 다운로드 받았기 때문이다. 추후에 kernel32.dll 같은 곳에 들어가게 되면 우니도우의 심볼 서버에서 그것을 다운받기 때문에 심볼이 있는 환경에서 분석을 할 수 있게 된다. 물론 여기서는 악성코드 분석을 가정하기 때문에 실행 파일에 대한 심볼은 없을 수 밖에 없다. 


(gdb) info share

  로드된 공유 라이브러리들을 보여준다.


> !lmi ntdll.dll

  모듈 상세 정보


> !dlls

  로드된 모든 DLL 출력


> !dh -f [imgbase]

  해당 이미지의 헤더를 분석해서 보여준다. (-f는 파일 헤더, -s는 섹션 헤더, -a는 모든 헤더 정보)


3.5.2.2 Memory Map

> !address

  Memory Map을 보여준다.

> !vadump

  !address가 이것의 결과를 포함한 훨씬 많은 정보를 보여준다.

> !vprot 0013ff34

  인자로 받은 특정 메모리 주소의 속성을 보여준다.

(gdb) info program

(gdb) shell cat /proc/24084/maps

  뒤에서 나오지만 "info program" 명령어를 통해 PID를 얻는다. 이렇게 얻은 PID가 24084라고 하자. 그리고 shell 명령어도 뒤에 나오지만 셸 명령어를 실행시켜주는 역할을 한다. 위와 같이 사용하면 프로그램의 Memory map 정보를 얻을 수 있다.



3.5.2.3 etc

> !peb

  PEB 관련 정보

> !teb

  TEB 관련 정보

> !handle

  핸들 값 출력.

> !heap

  힙 관련 정보를 보여준다.

> !exchain

  SEH들을 보여준다

> !gle

  API의 마지막 에러 코드. 즉 GetLastError().

> .tlist

  현재 실행 중인 모든 프로세스들의 목록 및 PID를 보여준다.

> ~.

  현재 스레드 정보.

> ~*

  프로세스의 모든 스레드

> .imgscan

  Find image headers. 로드된 모듈들의 이미지 헤더 주소를 찾아준다.

> !tls [Slot]

  명시한 TLS slot을 보여준다.

> .shell

  셸 명령어를 사용할 수 있다. 인자 -x는 해당 프로세스 실행 이후 디버거에서 detach하는 옵션이다.


(gdb) pwd

  현재 작업중인 디렉토리를 보여준다.

(gdb) shell

  셸 명령어를 사용할 수 있다.

(gdb) show commands [ 번호 ]

  명령어 사용 히스토리를 보여준다.

(gdb) show env

  환경변수를 보여준다.

(gdb) show args

  인자를 보여준다.

(gdb) info proc [ keyword ]

  현재 프로세스의 /proc 프로세스 정보를 보여주며 디폴트로는 PID 등 같단한 정보만 보여준다. 키워드로는 mappings(매핑된 메모리 영역 목록), stat(프로세스 정보), status(프로세스 정보), all(/proc을 통해 얻을 수 있는 모든 정보)이 있다.

(gdb) info program

  프로그램의 실행 상태. PID, 현재 BP 정보, 현재 정지한 위치의 주소. 아주 간단한 정보만 보여준다.

(gdb) info files

  로드된 이미지들 즉 바이너리 및 모듈들의 섹션 별 주소 정보.

(gdb) info functions

  로드된 이미지들 즉 바이너리 및 모듈들의 함수들 목록 및 주소 정보.



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

악성코드가 감염되기까지  (2) 2017.08.13
리눅스 안티바이러스 구현에 관한 정리  (0) 2017.07.04
VC++ 옵션 정리  (0) 2017.06.03
다형성 바이러스  (4) 2017.05.16
API Sets  (0) 2017.05.12
Posted by SanseoLab



0. 개요

1. C/C++

... 1.1 General

... 1.2 Optimization

... 1.3 Code Generation

... 1.4 Language

... 1.5 Advanced

2. Linker

... 2.1 Manifest File

... 2.2 Debugging

... 2.3 Advanced





0. 개요

  이 문서에서는 VC++의 옵션들에 대해서 다룰 것이며 특히 리버싱과 관련된 즉 생성되는 어셈블리 코드들을 변화시키는 옵션들 위주로 정리한다. 





1. C/C++

1.1 General

1.1.1 Debug Information Format [ 디버그 정보 형식 ] :  /Z7  /Zi  /Zl

  " /Zi "를 설정하면 디버거에서 사용할 형식 정보와 기호 디버깅 정보를 포함하는 프로그램 데이터베이스인 .pdb 파일이 생성된다. 그리고 자동으로 링커 옵션의 /debug가 설정된다. 빌드 후 확인해 보면 실행 파일과 같은 폴더에 pdb 파일이 생성된 것을 알 수 있다.


* PDB

  심볼 파일(.pdb)은 함수, 변수 이름 등의 정보를 갖으며, 디버깅 시에 어셈블리와 함께 이러한 정보들을 같이 볼 수 있음으로서 디버깅을 훨씬 쉽게 해준다. 즉 클래스, 메서드 및 기타 코드의 소스 파일을 만드는 식별자를 프로젝트의 컴파일된 실행 파일에 사용되는 식별자에 매핑(소스 코드의 문을 실행 파일의 실행 명령에 매핑)한다.



1.1.2 SDL checks :  /sdl  /sdl-

  권장되는 Security Development Lifecycle 검사를 추가한다. 추가되는 것은 2가지가 있는데 첫 째는 오류와 같은 추가 보안 관련 경고이고 둘 째는 추가 보안 코드 생성 기능이다. 즉 첫번째는 컴파일 시 여러 경고를 오류로 활성화 한다. 예를들면 위험한 CRT 함수를 사용한 경우에 경고가 뜨면서 컴파일을 할 수 없을 때가 있는데 (C4996 경고) 이것은 이 옵션 때문이며, 프로젝트 생성시에 SDL 검사를 해제하면 된다. 


  다른 하나는 런타임 검사의 경우 여러 검사를 런타임에 수행하기 위한 코드를 생성한다. MSDN에 다르면 다음과 같다.


- /GS로 컴파일할 때와 동일한 #pragma strict_gs_check(push, on) 런타임 버퍼 오버런 검색에 대해 strict 모드를 활성화합니다.

- 제한된 포인터 삭제를 수행합니다. 역참조를 포함하지 않으며 사용자 정의된 소멸자가 없는 식에서 포인터 참조는 delete에 대한 호출 이후 유효하지 않은 주소로 설정됩니다. 이렇게 하면 오래된 포인터 참조가 재사용되지 않습니다.

- 클래스 멤버 초기화를 수행합니다. 개체 인스턴스화 시(생성자 실행 전) 모든 클래스 멤버를 자동으로 0으로 초기화합니다. 이렇게 하면 생성자가 명시적으로 초기화하지 않는 클래스 멤버와 연관된 초기화되지 않은 데이터를 사용하지 않도록 방지할 수 있습니다.




1.2 Optimization

1.2.1 Optimization [ 최적화 ] :  /Od  /O1  /O2  /Ox

  /Od의 경우 최적화 기능을 사용하지 않으면 디폴트 옵션이다. 참고로 디버깅 시에 어셈블리 명령어들의 양이 늘어나 있는 것을 볼 수 있다. 어떤 면에서는 최적화되지 않고 풀어써져 있어서 이해하기 어려운 코드들이 줄어들지만 또 어떤 면에서는 왜 삽입되어 있는지 모를 명령어들이 보이기도 한다. 더 분석해봐야 겠다.


  /O1의 경우 크기 최적화로서 다음 옵션들(/Og /Os /Oy /Ob2 /Gs /GF /GY)이 자동으로 포함된다. 일반적으로 가장 작은 크기의 코드를 생성한다. /O2의 경우 속도 최적화로서 다음 옵션들(/Og /Oi /Ot /Oy /Ob2 /Gs /GF /GY)이 자동으로 포함된다. Release 빌드의 디폴트 설정이며 일반적으로 가장 속도가 빠른 코드를 생성한다.


  /Ox의 경우 최대 최적화로서 다음 옵션들(/Ob2 /Og /Oi /Ot /Oy)이 자동으로 포함된다. 일반적으로 /Ox 대신 /O2를 사용한다고 한다. 



1.2.2 Inline Function Expansion [ 인라인 함수 확장 ] :  /Ob0  /Ob1  /Ob2

  기본적으로 컴파일러는 자신의 판단에 따라 함수를 인라인 확장할지를 결정한다. 함수를 호출하는 경우에는 인자를 push하고 함수를 call하는 등 함수 호출 오버헤드가 존재하는데 만약 함수의 크기가 작거나 하는 경우 따로 함수를 호출하는 대신 코드를 통합함으로써 이러한 오버헤드를 줄일 수 잇는 것이다.


  /Ob0은 인라인 확장을 사용하지 않도록 설정한다. /Ob1은 inline, __inline, __forceinline으로 표시된 함수나 클래스 선언된 C++ 멤버 함수만 확장한다. /Ob2는 디폴트 옵션으로서 inline, __inline, __forceinline으로 표시된 함수 외에도 컴파일러가 판단하여 선택한 기타 함수들을 인라인 확장한다. 또한 위에서 살펴보았듯이 /O1, /O2, /Ox를 사용할 때도 적용된다. 



1.2.3 Enable Intrinsic Functions [ 내장 함수 사용 ] :  /Oi

  응용 프로그램이 더 빨리 실행될 수 있도록 일부 함수 호출을 내장 함수나 특정한 형태의 함수로 교체한다. 즉 특정한 함수 호출을 컴파일러의 내장 함수로 대체하는 것이다. 참고로 인라인 함수와 헷갈릴 수 있는데 인라인 함수의 경우 특정 함수를 호출하는 형태에서 호출 대신 자체적으로 통합시키는 것이며 이것은 컴파일러가 특정 함수를 대체시킨다는 면에서는 비슷하다. 하지만 대체하는 내장 함수는 컴파일러가 자체적으로 그것에 대한 지식이 있으므로 상응하는 내장 함수가 존재한다면 그 함수 호출을 더 나은 방식으로 통합시킬 수 있다. 



1.2.4 Favor Size Or Speed [ 크기 또는 속도 ] :  /Os  /Ot

  /Os는 코드 크기 우선으로서 속도보다 크기를 우선적으로 처리하도록 컴파일러에 지시하며 /Ot는 코드 속도 우선으로서 크기보다는 속도를 우선적으로 처리하도록 컴파일러에 지시한다. 이 옵션들도 각각 /O1과 /O2에 포함된다.



1.2.5 Omit Frame Pointers [ 프레임 포인터 생략 ] :  /Oy  /Oy-

  호출 스택에서 프레임 포인터를 생성하지 않으며 이에 따라 함수 호출 속도가 빨라진다. 이것은 x86 컴파일러에서만 사용할 수 있다. /Oy를 사용하면 프레임 포인터가 생략되며 /Oy-를 사용하면 프레임 포인터 생략이 비활성화된다.


  사실 리버싱을 공부할 때 스택 프레임을 배우면서 EBP를 이용한 방식의 메커니즘을 배우게 된다. 이 옵션을 사용하면 즉 /Oy가 설정되면 이 EBP를 스택 프레임으로서 사용하지 않고 General Purpose로 사용하게 된다. 이렇게 됨으로써 몇 개 되지 않은 범용 목적의 사용 가능한 레지스터가 하나 더 추가될 수 있지만 리버싱하는 입장에서는 문제점이 더 많이 발생하게 된다.


  먼저 Call Stack을 확인할 필요가 있는 경우 디버거의 명령어나 기능을 통해서든 아니면 직접 확인하여 찾든지 간에 이 EBP를 이용해야 할 것인데 이것이 사용되지 않으므로 콜 스택을 구별할 수가 없어진다. 참고로 x64의 경우에도 레지스터를 이용하므로 이런 방식을 통한 콜 스택 추적이 불가능하다. 또한 PDB 형식도 이 EBP를 이용하여 로컬 변수 같은 정보들이 저장되어 있다. 물론 윈도우에서 제공되는 함수들을 확인해 보면 디버깅을 지원하기 위하여 이 옵션을 사용하지 않아서인지 프레임 포인터가 계속 쓰이고 있는것으로 보인다.



1.2.6 Whole Program Optimization [ 전체 프로그램 최적화 ] :  /GL

  전체 프로그램 최적화를 사용하지 않으면 모듈(컴파일)별로 최적화가 수행된다. 




1.3 Code Generation

1.3.1 Enable C++ Exceptions :  /EHa  /EHs  /EHsc

  기본적으로 SEH는 운영체제 차원에서 지원된다. 즉 __try, __except 구문을 사용하면 된다. 아래의 옵션들은 C++ 문법의 catch문에서 SEH나 extern "C"로 선언된 함수의 예외를 catch할 수 있는지의 여부를 판단한다.


  /EHa를 설정하면 C++ EH 뿐만 아니라 SEH도 catch 구문에서 catch할 수 있다. /EHs를 설정하면 catch 구문에서 C++ EH를 catch하지만 SEH는 받을 수 없다. 또한 컴파일러에 extern "C"로 선언된 함수가 예외를 throw할 수 있다. /EHsc를 설정하면 catch 구문에서 C++ EH를 catch하지만 SEH는 받을 수 없다. 또한 컴파일러에 extern "C"로 선언된 함수가 예외를 throw할 수 없다.



1.3.2 Runtime Library [ 런타임 라이브러리 ] :  /MT  /MTd  /MD  /MDd  /LD

  /MT는 런타임 라이브러리의 다중 스레드 정적 버전을 사용한다. 즉 런타임 라이브러리의 API 함수를 사용할 경우 실행 파일에 정적으로 링크한다. /MTd는 디버그 버전을 링크한다. /MD는 런타임 라이브러리의 다중 스레드 별 및 DLL 별 버전을 사용한다. 즉 실행 파일이 런타임 라이브러리의 DLL을 임포트하여 필요한 함수를 호출하여 사용하는 방식으로서 이것을 동적 링크라고 한다. /MDd는 디버그 버전을 링크한다. /LD는 DLL 개발 시의 옵션이다.



1.3.3 Security Check [ 보안 검사 ] :  /GS  /GS-

  /GS는 스택 버퍼를 위한 보안 검사를 추가한다. 즉 함수의 반환 주소나 예외 핸들러의 주소에 쿠키를 삽입하여 버퍼가 오버플로우 되었는지를 검사한다.



1.3.4 Control Flow Guard [ 행 가드 제어 ] :  /guard:cf

  제어 흐름 보호 (CFG : Control Flow Guard). vtable을 이용한 가상 함수 호출이나 콜백 함수의 경우 특정 실행 시점에서 실행될 함수가 컴파일 시에 정적으로 결정되는 것이 아니라 런타임 시에 함수 포인터를 이용해 결정된다. 그렇기 때문에 이러한 함수 호출을 간접 호출이라고 한다. 어셈블리 루틴으로 보자면 다음과 같다.


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

MOV ESI, DWORD PTR DS:[ESI]

MOV ECX, ESI

PUSH 1

CALL ESI

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


  제어 흐름 보호는 이러한 간접 호출을 보호하기 위한 기법으로서 간접 호출 직전에 검사하는 것이다. 생성되는 코드는 다음과 같이 __guard_check_icall_fptr이라는 래퍼 함수 호출이 추가되어 있다.


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

MOV ESI, DWORD PTR DS:[ESI]

MOV ECX, ESI

PUSH 1

CALL DWORD PTR DS:[__guard_check_icall_fptr]

CALL ESI

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


  이 래퍼 함수는 /guard:cf 옵션이 꺼져 있다면 단지 retn만 존재하는 루틴을 가리키지만 옵션이 켜져 있다면 LdrpValidateUserCallTarget()를 가리킨다. 또한 IMAGE_LOAD_CONFIG_DIRECTORY 구조체에 관련 값들이 추가된다. 컴파일 시에 생성되는 이 값들과 호출할 함수 포인터를 이용해 유효한 함수 포인터인지 비교하여 검사한다.




1.4 Language

1.4.1 Enable Run-Time Type Information [ 런타임 형식 정보 사용 (RTTI) ] :  /GR

  런타임에 개체 형식을 검사하는 코드를 추가한다. 일반적으로 코드에서 dynamic_cast 연산자 또는 typeid를 사용할 경우 이 옵션이 필요하다. 이것을 사용하면 .rdata 섹션 크기가 증가한다. 그러므로 dynamic_cast, typeid를 사용하지 않는 경우 /GR-를 사용해서 이미지 크기를 줄일 수 있다.




1.5 Advanced

1.5.1 Calling Convention [ 호출 규칙 ] :  /Gd  /Gr  /Gz  /Gv

  /Gd는 __cdecl, /Gr는 __fastcall, /Gz는 __stdcall, /Gv는 __vectorcall을 의미한다.



1.5.2 Compile As [ 컴파일 옵션 ] :  /TC  /TP

  /TC는 C 코드로 컴파일하며 /TP는 C++ 코드로 컴파일한다.





2. Linker

2.1 Manifest File

2.1.1 Generate Manifest [ 메니페스트 생성 ] :  /MANIFEST

  xml 파일로 생성된다고 하지만 일반적으로 리소스 섹션에 통합되는 것으로 보인다. 여기서는 UAC 관련된 내용만 다루겠다.



2.1.2 Enable User Account Control (UAC) [ 사용자 계정 컨트롤 사용 ] : /MANIFESTUAC

  프로그램 매니페스트에 UAC 정보를 포함할지 여부를 지정한다.



2.1.3 UAC Execution Level [ UAC 실행 수준 ] :  /level='asInvoker' or 'highestAvailable' or 'requireAdministrator'

  레벨에는 asInvoker (응용 프로그램을 시작한 프로세스와 동일한 권한으로 응용 프로그램 시작), highestAvailable (최대한 높은 권한 수준으로 응용 프로그램 실행), requireAdministrator (관리자 권한으로 실행)이 있다.


* UAC

  리소스 섹션을 보면 xml 형태의 문자열이 보인다. 여기서 requestedExecutionLevel level="highestAvailable" 같은 형태의 문자열을 볼 수 있다. 파일을 실행할 때 참고할 UAC 값이 여기에 존재한다.




2.2 Debugging

2.2.1 Generate Debug Info [ 디버그 정보 생성 ] :  /DEBUG  /DEBUG:FASTLINK

  참고로 /Zi가 설정되어 있으면 자동으로 /DEBUG가 설정되며 실행 파일들에 디버그 정보를 넣는다. 실제로 생성된 실행 파일을 분석해 보면 디버그 섹션이 추가되어 있고 이 섹션에 디렉토리들의 주소가 들어가 있어서 소스 코드라던지 .pdb 파일의 위치라던지 하는 정보가 들어가 있다. 참고로 디버그 섹션은 .rdata 섹션에 통합되는 경우가 많다.


  사족으로 프로그램을 직접 작성한 후 리버싱하려고 할 때 pdb 파일 없이 분석하려고 한다면 이 pdb 파일을 지우거나 이동시키고 바이너리 이름을 변경해야 한다. pdb 파일을 지우거나 이동시킨다는 것은 바이너리 내부에 pdb의 경로가 저장되어 있으므로 실행 파일만 옮긴다고 해서 pdb를 읽어오지 못하는 거이 아니라는 점이다. 아예 그 경로에 pdb 파일이 없어야 읽어올 수 없다. 그리고 바이너리 이름을 변경해야 한다는 것은 해당 바이너리를 처음 읽어올 때 대표적인 디버거들의 경우 이것을 저장해 놓는 경우가 많다. 그래서 pdb가 이미 없더라도 처음 읽어왔을 때 저장한 정보를 가지고 pdb를 이용해 분석된 내용이 계속 존재할 것이다. 그래서 바이너리의 이름을 변경함으로써 이전에 저장된 분석 내용을 불러오지 않게 하는 것이다. 



2.2.2 Strip Private Symbols [ 전용 기호 제거 ] :  /PDBSTRIPPED

  빌드할 때 두 번쨰 PDB가 만들어지는데 여기에는 고객에게 제공하지 않을 기호가 생략된다. 즉 여기에는 공용 기호, 개체 파일의 목록과 개체 파일에서 제공하는 실행 파일의 일부, 스택을 통과시키는데 사용된 FPO(프레임 포인터 최적화) 디버그 레코드가 생략되며 포함되지 않는 것으로는 형식 정보, 줄 번호 정보, 개체 파일별 CodeView 기호(함수, 지역 및 정적 데이터에 대한 기호 등)이 있다.


  참고로 실행 파일 및 원래 pdb 파일이 생성되는 release 폴더가 아니라 프로젝트 폴더에 생성된다. 그리고 비교해 보면 두 번째 PDB의 크기가 훨씬 작다는 것을 확인할 수 있다.



2.2.3 Generate Map File [ 맵 파일 생성 ] :  /MAP

  맵 파일이 생성된다. 이것도 실행 파일 및 원래 pdb 파일이 생성되는 release 폴더가 아니라 프로젝트 폴더에 생성된다. 그리고 pdb와는 달리 텍스트 파일의 형태이다. 개인적으로 pdb의 경우 디버거를 통해 읽혀져 많은 디버깅 정보를 얻을 수 있는 유용한 파일로 알고 있지만 map 파일은 굳이 pdb 파일이 있는데 어디에 필요한지 잘 모르겠다. 어쨌든 pdb가 없는 경우라면 텍스트 파일로 되어 있는 map 파일을 직접 읽어서 유용하게는 사용할 수 있을 것 같다. 


  MSDN에 따르면 맵 파일에 들어있는 정보는 다음과 같다.

- 파일의 기본 이름인 모듈 이름

- 파일 시스템이 아니라 프로그램 파일 헤더의 타임스탬프

- 프로그램의 그룹 목록. 각 그룹의 시작 주소(section:offset), 길이, 그룹 이름 및 클래스가 함- 께 표시됩니다.

- 공용 기호 목록. 각 주소(section:offset), 기호 이름, 플랫 주소, 기호가 정의된 .obj 파일이 함께 표시됩니다.

- 진입점(section:offset)


* 추가

  pdb와 map 파일 간의 차이점에 대해서 찾아보았는데 "John Robbins"가 "Debugging Applications"라는 책에서 한 설명을 보면 맵 파일은 프로그램의 전역 심볼들과 소스 그리고 줄 번호에 관한 정보를 텍스트 형태로 보여주는 파일이라고 한다. 이것은 마이크로소프트가 심볼 테이블 형식을 주기적으로 바꾸게 됨으로써 오래된 프로그램의 경우 오래된 심볼 엔진 버전을 찾기가 힘들 수 있는데 맵 파일이 텍스트 형태로 존재함으로써 분석 시에 더 용이함을 줄 수 있다는 것이다.

[ http://stackoverflow.com/questions/14640676/why-should-we-need-the-map-file-when-pdb-file-is-available-in-windows-platform ]




2.3 Advanced

2.3.1 [ 기준 주소 ] : /BASE

  ASLR을 사용하지 않을 경우 바이너리의 ImageBase 주소는 보통 0x00400000이다. 아래의 "임의 기준 주소" 옵션을 비활성화하고, 이 옵션을 사용하며 주소를 0x01000000으로 설정한다면 바이너리의 ImageBase 주소가 0x01000000으로 설정된 것을 볼 수 있다. 거의 사용되지 않겠지만 가끔은 사용할 수도 있는 옵션으로 보인다.



2.3.2 Randomized Base Address [ 임의 기준 주소 ] :  /DYNAMICBASE

  ASLR 기능을 활성화한다.



2.3.3 Data Execution Prevention (DEP) [ 데이터 실행 방지 ] :  /NXCOMPAT

  데이터 실행 방지 (DEP) 기능 활성화



2.3.4 Image Has Safe Execption Handlers [ 이미지에 안전한 예외 처리기 포함 ] :  /SAFESEH

  SEH 오버플로우 공격을 방어하는 기법으로 /GS 옵션도 있지만(SEH 프레임에 GS 쿠키 사용) /SAFESEH 옵션도 존재한다. 이것은 간략하게 말해서 핸들러가 유효한 핸들러인지를 검사해 준다. 


  /SAFESEH 설정 시에 PE 헤더에 IMAGE_LOAD_CONFIG_DIRECTORY 구조체가 생성된 것을 볼 수 있다. 이 구조체의 여러 멤버 중에서 여기에 사용되는 것은 SEHandlerTable 필드와 SEHandlerCount 필드이다. SEHandlerCount 필드는 프로그램에서 설치되는 SEH 핸들러의 개수이며 SEHandlerTable 필드는 핸들러들의 주소들을 담는 __safe_se_handler_table의 주소이다.


  로더는 PE가 로드될 때 IMAGE_LOAD_CONFIG_DIRECTORY 구조체의 SEHandlerTable 필드의 값인 __safe_se_handler_table에 이 프로그램에서 사용되는 핸들러들의 주소를 설정하고 그 개수는 IMAGE_LOAD_CONFIG_DIRECTORY 구조체의 SEHandlerCount 필드에 설정한다. 이후 예외가 발생하면 KiUserExceptionDispatcher()를 지나 RtlDispatchException()에서 호출할 특정 핸들러와 __safe_se_handler_table의 엔트리들을 비교해서 상응하는지를 검사한다.



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

리눅스 안티바이러스 구현에 관한 정리  (0) 2017.07.04
Windbg, Gdb 명령어 정리  (0) 2017.06.27
다형성 바이러스  (4) 2017.05.16
API Sets  (0) 2017.05.12
윈도우의 예외 처리  (0) 2017.05.09
Posted by SanseoLab



  다형성 바이러스에 대한 개념 및 분류가 너무 헷갈리는것 같아 정리하기로 했다. 물론 이 내용이 모든 것을 포함하지도 않고 틀린 부분도 있을 수 있다. 각 개념에 대한 검색용으로 참고하길.


  기본적으로 바이러스이기 때문에 파일을 감염시키는 개념이고 이것은 File Infection과 관련있다. 특정 행위를 수행하는 루틴이 삽입되는 방식은 다음과 같다.


1. 기존 섹션의 빈 공간에 삽입

2. 기존 섹션의 크기를 늘려서 삽입

3. 새로운 섹션을 추가하여 그곳에 삽입


  AV는 검사 과정에서 감염된 부분이 있는지 여부를 판단하기 위해 파일을 검사할 것이다. 감염된 파일은 정상 파일과 비교해 보자면 먼저 하나 이상의 바이러스 루틴이 삽입되어 있을 것이며 그 루틴으로 이동하는 분기문이 존재할 것이다. 일반적으로 File Infector는 PE 헤더의 EP를 수정하여 감염된 루틴이 있는 주소로 바꾸어 시작할 때부터 감염 루틴에서 시작하게 하거나 EP 부분에 위치한 분기문의 이동 주소(api를 호출하는 call이나 jmp 등)를 감염된 루틴이 존재하는 주소로 변경할 것이다. 바이러스 루틴은 적합하다고 판단된 영역에 삽입되어 있을 것이다.


  시그니처 기반 AV의 경우 바이러스로 인해 감염된 부분의 패턴을 검사할 것이며 일반적인 바이러스의 경우 이 패턴은 항상 같기 때문에 AV에 의해 발견될 수 있다. 이에 따라 다형성 바이러스가 나오게 되었는데 이것은 이러한 시그니처에 잡히지 않게 하기 위한 방식으로 감염 시마다 매번 동일하지 않은 패턴을 생성해낸다.


1. 사용 명령어 변경 : 하는 행위는 동일하지만 어셈블리 루틴은 차이가 난다.

2. Garbage 명령 삽입

3. 사용 레지스터 변경

4. 감염 루틴들의 위치나 순서 바꾸기

5. EPO (EntryPoint Obscuring) : 위에서 언급하였듯이 바이러스 루틴으로 분기하도록 분기문을 수정하는데 이 분기문을 랜덤으로 선택한다. 즉 바이너리에 존재하는 call이나 jmp 명령어 중에서 랜덤으로 골라 감염 루틴으로 분기하는 분기문을 생성하는 것이다.


  이 같은 방식들을 통해 매번 바이러스에 감염될 때 마다 생성되는 감염 루틴은 동일하지 않게 되지만 행위는 같게 유지될 수 있다.


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

Windbg, Gdb 명령어 정리  (0) 2017.06.27
VC++ 옵션 정리  (0) 2017.06.03
API Sets  (0) 2017.05.12
윈도우의 예외 처리  (0) 2017.05.09
링크 및 책 정리  (0) 2017.04.23
Posted by SanseoLab

2017. 5. 12. 23:56 악성코드 분석

API Sets



1. 개요

  윈도우 7부터는 MinWin 즉 Minimal Windows kernel라는 개념을 통해 커널에 변화가 생겼다. 과거부터 발전되어 감에 따라 그리고 동시에 하위호환을 만족시켜야 함에 따라 kernel32.dll이나 advapi32.dll 같은 DLL은 수 많은 API들을 포함함으로써 크기나 성능, 구조적으로 경제적이지 않게 되었다. 이에 따라 이러한 DLL들을 기능 및 목적에 따라 재구성할 필요가 생긴 것이다.





2. Virtual DLL과 Logical DLL

  예를들어 최신 VC++로 개발한 애플리케이션을 분석해보면 과거와 달리 api-ms-win-crt-heap-l1-1-0.dll 같은 api-ms-win으로 시작하는 DLL들이 보인다. 이러한 DLL들을 Virtual DLL이라고 하며 실제 kernel32.dll이나 advapi32.dll 같은 DLL들을 Logical DLL이라고 한다. 이런식으로 함수들이 기능 및 목적에 따라 각각의 Virtual DLL들로 재구성되어 있다. 실질적으로 Virtual DLL은 실제 함수 루틴이 존재하는 Logical DLL에 대한 스텁 DLL이라고 할 수 있다. api-ms-win-crt로 시작하는 것들은 CRT 라이브러리들의 Virtual DLL이고 또 다른 종류로는 api-ms-win-core로 시작하는 것들이 있는데 이것은 뒤에서 보겠지만 kernelbase.dll 등으로 매핑된다. 


  이렇게 Virtual DLL만 만들어진 것이 아니라 Logical DLL 즉, 실제 시스템 DLL들에 대해서도 변화가 생겼다. 첫 번째는 kernelbase.dll과 sechost.dll인데 kernelbase.dll은 kernel32.dll과 advapi32.dll의 api들 중에서 필요한 부분을 가져온 것이다. 정확히 말하자면 kernelbase.dll은 MinWin을 위한 핵심 서비스를 제공하는 함수들이 포함된 라이브러리이다. 즉 kernel32.dll과 advapi32.dll 같은 무겁고 큰 라이브러리 대신 필요한 핵심 함수들만 제공하는 이 kernelbase.dll만으로 MinWin을 지원할 수 있는 것이다. 참고로 이렇게 가져온 함수들은 kernel32.dll이나 advapi32.dll에서도 여전히 호출할 수 있다. 실제 구현은 kernelbase.dll에 존재하지만 똑같은 이름의 함수가 존재하며 내부적으로 간단한 처리를 거쳐서 kernelbase.dll에 존재하는 함수를 호출하기 때문이다. 이에 따라 레거시 애플리케이션은 계속 kernel32.dll 등의 함수를 호출할 수 있고 최근 VC++로 만들어진 애플리케이션은 직접 kernelbase.dll의 함수를 호출할 것이다. 물론 더 자세히 해보자면 애플리케이션은 api-ms-win-core 같은 Virtual DLL을 임포트할 것이고 앞의 과정을 통해 결국 kernelbase.dll 내부의 함수를 호출할 것이다. 참고로 sechost.dll에는 advapi32.dll 중에서 kernelbase.dll로 이동한 함수들을 제외한 다른 함수들이 이동되어 있다.


  다른 하나는 CRT 라이브러리이다. msvcrt.dll 같은 CRT 라이브러리는 두 개로 나뉘어 졌다. 하나는 vcruntime140.dll로써 프로세스 스타트업이나 예외 핸들링 같은 컴파일러 지원에 요구되는 기능들(함수들)이 존재한다. 다른 하나는 ucrtbase.dll로써 이것은 범용 CRT 즉 순수한 CRT 라이브러리이다. 예를들면 api-ms-win-crt-stdio나 api-ms-win-crt-math 같은 Virtual DLL들이 이것과 매핑된 것이다. 일반적으로 PEview로 보면 vcruntime140.dll은 그대로 보이지만 ucrtbase.dll은 보이지 않고 api-ms-win-crt를 접두사로 갖는 Virtual DLL들만 보일 것이다. 물론 내부적으로 이런 Virtual DLL들은 ucrtbase.dll의 함수를 호출하게 된다.


  정리해 보자면 특정 DLL들의 구조가 Virtual DLL과 Logical DLL로 나뉘게 되었다. Logical DLL은 실제 함수 루틴이 존재하는, 과거부터 있어왔던 DLL들이지만 변화가 생겼는데 kernelbase.dll이 만들어 졌으며 이것은 kernel32.dll과 advapi32.dll의 함수들 중에서 필요한 부분을 가져와서 만들었다. 이 외에도 sechostd.dll도 만들어 졌다. 물론 레거시 애플리케이션을 위해 이 두 DLL에서도 kernelbase.dll로 이동한 함수들을 호출할 수 있는데 같은 이름의 스텁 함수가 존재해서 내부적으로 실제 함수 루틴이 존재하는 kernelbase.dll의 함수를 호출해 주기 때문이다. 이 외에도 CRT 라이브러리에도 변화가 생겼다. vcruntime140.dll과 ucrtbase.dll로 나뉜 것이다. 


  올리디버거로 api들을 분석해 보면 어느 DLL에서 함수 루틴이 진행되는지를 직접 확인할 수 있다. Virtual DLL은 PEview 같은 도구를 통해 임포트 테이블을 확인하면 차이점을 발견할 수 있을 것이다. 확인해 보면 애플리케이션이 익숙치 않은 api-ms-win으로 시작하는 DLL들의 함수들을 임포트하는 것을 볼 수 있다. 이 Virtual DLL들은 실제로는 Logical DLL을 호출한다. 예를들면 api-ms-win-crt-runtime-l1-1-0.dll의 _initterm_e 함수를 호출할 경우 실제로는 ucrtbase.dll의 _iinitterm_e를 호출하게 된다.





3. 원리

  여기서는 Virtual DLL과 Logical DLL이 어떻게 매핑되는지를 알아본다. 부팅 과정 중에 ApiSetSchema.dll이 로드되며 이 DLL의 .apiset 섹션 내의 데이터가 메모리에 로드된다. 이후 이 DLL은 언로드되고 데이터는 PEB의 ApiSetMap에 매핑된다. 이제 DLL을 로드하게 된다면 LoadLibrary() 내부에서 PEB에 저장된 ApiSetMap 구조체를 참조해서 Virtual DLL의 이름을 Logical DLL의 이름으로 리다이렉트 시킨다.





4. 분류

  참고로 아래에서 kernel32.dll 및 advapi32.dll로 설명한 것은 kernelbase.dll도 포함하며 advapi32.dll로 설명한 것은 sechost.dll도 포함할 수 있다. 자주 볼 수 있는 것들 위주로 정리한 것이며 이 외에도 다수 존재한다.


- API-MS-Win-Core : 대부분 kernel32.dll의 함수들이며 advapi32.dll도 약간 존재한다.

- API-MS-Win-Security : kernel32.dll, advapi32.dll, sechost.dll의 함수들.

- API-MS-Win-Service : advapi32.dll 및 sechost.dll의 함수들

- API-MS-Win-CRT : ucrtbase.dll



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

VC++ 옵션 정리  (0) 2017.06.03
다형성 바이러스  (4) 2017.05.16
윈도우의 예외 처리  (0) 2017.05.09
링크 및 책 정리  (0) 2017.04.23
윈도우의 드라이버 개발과 루트킷 그리고 AV  (0) 2017.04.23
Posted by SanseoLab



0. 서론

1. 어셈블리 SEH

2. SEH

3. C++ EH

4. SetUnhandledExceptionFilter()

5. Unwind

6. VEH

7. 64비트

8. 기타





0. 서론

  윈도우의 예외 처리에 대한 기본적인 내용은 인터넷 자료들만 봐도 충분히 정리할 수 있다. 여기서는 종류별로 정리와 함께 조금 더 깊게 정리하고자 한다. 먼저 간단한 어셈블리 루틴을 이용한 SEH를 시작으로 기본적인 SEH를 사용할 때 그리고 C++ 방식의 예외 처리, 마지막으로 SetUnhandledExceptionFilter()에 대해 알아볼 것이다. 그리고 VEH 및 64비트와 간단하지만 Unwind 부분을 거쳐서 예외와 관련된 여러 사항들을 살펴보겠다.





1. 어셈블리 SEH

1.1 소스 코드

  여기서는 어셈블리를 이용해서만 만들 수 있는 아주 간단한 형태의 SEH를 통해 기본적인 분석을 수행할 것이다. 기본적인 내용은 알고 있을 것이므로 간단한 설명과 함께 분류하고 정리하는 식으로 설명할 것이다.


  간단한 형태의 SEH는 스택에 핸들러 주소와 다음 SEH 체인을 가리키는 포인터만을 PUSH한다. SEH를 설치하는 코드는 다음과 같다.


push [ 핸들러 주소 ] ; 핸들러 주소를 push

mov eax, large fs:0

push eax ; 다음 SEH 체인을 가리키는 포인터를 push

mov large fs:0, esp ; fs:0에 현재 SEH 체인을 넣는다. 즉 toplevel을 이것으로 변경.


  기본적으로 알다시피 핸들러의 주소를 PUSH하고 fs:[0]에 위치한 다음 SEH 체인의 주소를 PUSH하여 포인터로서 다음 SEH 체인을 가리키게 한다. 이후에 fs:[0]의 값을 즉 SEH 체인의 toplevel을 방금 설치한 SEH로 설정한다.



1.2 분석

  이렇게 설치를 한 후에 INT3 즉 0xCC로 예외를 발생시켜 보자. 예외 발생 시 커널에서의 과정을 거쳐 유저 모드로 복귀할텐데 가장 먼저 복귀되는 부분은 ntdll.KiUserExceptionDispatcher() 함수이다. 즉 일반적인 사용자 모드 디버거를 사용해서 분석하는 경우 예외 발생 이후 처음으로 분석할 수 있는 부분이 이 함수의 시작 루틴이다. 


  이 함수는 크게 3가지 부분으로 나뉘어져 있다. 가장 처음에는 (Ollydbg에서는 이름이 보이지 않지만) RtlDispatchException() 함수이며 이 함수의 결과에 따라 NtContinue() 또는 NtRaiseException()이 호출된다. 조금 더 자세히 말하자면 RtlDispatchException()를 수행하다가 복귀한 경우(정상적인 경우 대부분 복귀하지 않는다) 이 함수의 반환값은 DISPOSITION_DISMISS 즉 0이 될 수 있는데 이 경우에는 NtRaiseException()이 호출되며 DISPOSITION_CONTINUE_SEARCH 즉 1이 반환되면 NtContinue()가 호출된다.


  그리고 추가적으로 말하자면 커널에서 호출될 때 인자를 두 개 받는데 하나는 예외와 관련된 정보 즉 ExceptionRecord이며 다른 하나는 스레드 문맥 정보인 ContextRecord이다. ContextRecord는 이후에 NtContinue()에서 예외를 실행한 코드로 복귀할 때 사용되며 ExceptionRecord는 예외를 처리 시에 필요한 정보를 담고 있다.


  ContextRecord 구조체에 대해서 조금 더 설명해 보겠다. 다른 부분보다는 실질적으로 NtContinue()를 통해 실행되는데 사용되는 eip 멤버가 중요할 것이다. 예를들어서 KiUserExceptionDispatcher()가 호출될 때 인자로 받은 포인터 중 하나이자 NtContinue()을 호출할 때의 인자인 ContextRecord 구조체에 대한 포인터 주소가 0x0019E41C라고 하자. 여기에서 0xB8만큼 더한 값 즉 0x0019E4D4 주소에 들어 있는 값인 0x0041A7C1이 eip 즉, 예외 핸들러에서 복귀한 후에 갈 주소이다.


  다시 본론으로 와서 실질적으로 모든 예외 처리를 하는 함수가 RtlDispatchException()이기 때문에 뒤에서 분석하도록 하고 먼저 결과값에 따른 뒷 부분을 분석해 보겠다. 앞에서 말했듯이 NtContinue()가 호출되는 True인 경우는 예외 발생 시 받았던 스레드 문맥인 ContextRecord를 통해 예외를 일으킨 명령어 즉 INT3의 다음 명령어부터 다시 실행하게 된다. 참고로 뒤에서 이야기하겠지만 __except 구문의 필터 값으로 EXCEPTION_CONTINUE_EXECUTION을 넣어도 지금처럼 NtContinue()가 호출된다. 물론 eip는 발생한 그 명령어 주소이다. 그렇기 때문에 예외를 발생시키는 조건을 수정하는 특별한 처리를 수행하지 않는 이상 예외는 계속 일어날 것이다. NtRaiseException()은 예외 핸들러를 찾지 못해 디버거 또는 윈도우의 예외 핸들러를 위해 예외를 다시 일으키는 것이다.


  RtlDispatchException()는 내부에서 여러 과정을 거치며 RtlpExecuteHandlerForException()를 즉 앞에서 등록했던 핸들러를 호출하고 이 함수의 결과 값에 따라 DISPOSITION_DISMISS나 DISPOSITION_CONTINUE_SEARCH 같은 결과를 반환한다.


  조금 정리해서 설명하자면 먼저 예외가 발생할 것이고 현재 Context는 저장된다. 제어는 커널로 넘어가며 커널에서 유저 모드로 제어가 넘어올 때 즉 유저 모드 디버거로 처음 잡을 수 있는 부분은 예외 디스패처 즉 KiUserExceptionDispatcher()이다. 이곳에서 결과적으로 RtlpExecuteHandlerForException()을 호출하여 핸들러를 호출해 주는데 중요한 것은 인자를 넣어주고 호출해 준다는 것이다. 첫 번째 인자는 ExceptionRecord 구조체로서 ExceptionCode, ExceptionAddress 등 예외 발생과 관련된 내용이 있으며 두 번째 인자는 현재 예외 프레임을 가리키는 포인터이다. 따라가 보면 현재 핸들러 주소가 등록되어 있는 그 예외 프레임의 주소라는 것을 확인할 수 있다. 마지막으로 세 번째 인자는 CONTEXT 구조체이다. 앞에서 설명한 0xB8이 EIP라던 그 구조체이다. 이를 통해 현재 핸들러에서도 이 구조체를 확인하여 어디로 복귀할 것인지를 확인할 수 있을 뿐더러 세 번째 인자 즉 ESP + 0x0C를 통해 이 구조체의 주소를 얻어서 EIP 뿐만 아니라 EAX 등 여러 Context를 핸들러에서 수정할 수도 있다.


  정리해 보자면 지금까지 설명한 것처럼 간단하게 어셈블리 루틴으로 SEH를 설치한 경우 예외 발생 시에 커널을 통해 KiUserExceptionDispatcher()로 이동하며 여기에서 RtlDispatchException()를 호출하여 등록한 핸들러를 호출하며 이후 ContextRecord를 인자로 받아 다시 실행을 복귀시켜 주는 NtContinue()를 통해 예외를 일으켰던 명령어의 바로 다음 명령어로 다시 복귀한다.





2. SEH

2.1 소스 코드

  윈도우에서는 SEH라는 구조적 예외 처리 방식을 제공한다. SEH는 Termination Handler와 Exception Handler 이 두개로 나뉜다. Termination Handler는 __try와 __finally를 사용하는 것으로서 __try 블록을 한 줄이라도 실행하면 반드시 __finally 블록을 실행해야 한다는 것이다. Exception Handler는 __try와 __except(예외 필터)로 나뉜다. 즉 예외 필터를 인자로 받는 것이다. 


  예외 필터는 __except (EXCEPTION_EXECUTE_HANDLER) 같이 사용할 수도 있지만 조건문을 지정해서 상응하는 예외를 고를 수 있고 필터 함수를 지정할 수도 있다. 참고로 그냥 방금과 같이 사용하면 예외 "필터"라는 의미가 없다. 


* EXCEPTION_EXECUTE_HANDLER(1)

예외 발생 시 예외 핸들러를 실행한다. 핸들러는 콜스택에서 가장 위쪽의 핸들러이다. 위에서 설명한 내용과 같이 기본적인 형태이다. 참고로 핸들러를 실행하게 해주는 필터는 이것 뿐이다.

* EXCEPTION_CONTINUE_EXECUTION(-1)

예외 발생 시 예외를 발생 시킨 그 코드로 이동해서 계속 실행한다.

* EXCEPTION_CONTINUE_SEARCH(0)

예외를 여러개 설정한 경우 다음 예외를 찾는다.


  즉, 어떻게 사용하든지 앞에서 설명한 3개의 예외 필터 중 하나가 __except() 내에 반환되어야 하며 그것을 받아서 __except()의 내부를 실행하던지 다시 넘기던지 하게 된다.


  추가로 기본적인 API들을 설명하겠다. RaiseException(), GetExceptionInformation(), GetExceptionCode()가 존재하는데 RaiseException()은 예외를 발생시키며 GetExceptionInformation()은 예외가 발생했을 때의 정보를 LPEXCEPTION_POINTERS 라는 포인터로 넘겨준다. GetExceptionCode()는 간단한 예외 코드를 반환한다.


      
// MSDN
BOOL SafeDiv(INT32 dividend, INT32 divisor, INT32 *pResult)
{
	__try
	{
		*pResult = dividend / divisor;
	}
	__except (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ?
		EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
	{
		return FALSE;
	}
	return TRUE;
}

// 다음은 조건문을 사용한 예외 필터이다.
__except (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ?
	EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)

// 다음은 필터 함수를 사용한 예외 필터이다.
	int seh_filter(unsigned int code, struct _EXCEPTION_POINTERS* ep) {
	return EXCEPTION_EXECUTE_HANDLER;
}
__except (seh_filter(GetExceptionCode(), GetExceptionInformation())) {}



2.2 분석

  기본적으로는 어셈블리를 통해 만든 SEH와 비슷하지만 추가된 내용들이 많이 존재한다. 먼저 SEH를 설치하는 어셈블리 루틴이 어떤 형식인지와 그에 따른 스택의 형태를 알아보겠다.


PUSH -1 ; Try Level (FFFFFFFF)

PUSH OFFSET xxxx ; Scope table

PUSH _except_handler3 ; handler

MOV EAX, DWORD PTR FS:[0]

PUSH EAX ; Pointer to next chain

MOV DWORD PTF FS:[0], ESP ; installs SE handler _except_handler3


Pointer to Next Chain

__except_handler3

Scope table

Try Level


  다음 SEH 체인을 가리키는 포인터는 같지만 핸들러는 직접 설정한 핸들러 함수의 주소가 아니라 __except_handler3라는 함수이다. 이것은 윈도우에서 제공되는 함수이며 뒤에서 자세히 설명하겠다. 이 외에도 Scope Table 및 TryLevel이 존재한다.


  사실 소스 코드를 설명한 부분을 보면 알겠지만 앞에서 설명했던 어셈블리 루틴으로 만든 SEH와의 가장 큰 차이점이란 필터 함수의 존재라고 할 수 있다. 즉 앞에서는 예외 발생 시에 핸들러를 호출한다는 것이 중요하였지만 C나 C언어로 만들어 사용하는 SEH는 필터 함수의 도움을 통해 예외 발생 시에 어떤 행위를 할지 설정할 수 있는 것이다. 그래서 SEH도 그냥 핸들러의 주소가 아니라 필터와 관련된 여러 행위가 필요하므로 더 확장된 것으로 보인다.

  앞 부분에서 설명한 것처럼 KiUserExceptionDispatcher() 및 RtlDispatchException()를 거쳐서 핸들러 함수를 호출하는 것 까지는 같다. 차이점은 이 핸들러 함수가 윈도우에서 제공되는 _except_handler3() 함수라는 것이다. 


  사실 VC++에서 /GS 옵션을 사용하면 _except_handler3() 대신 _except_handler4()가 사용되는데 TryLevel의 초깃값이 0xFFFFFFFE라는 것과 쿠키 검사를 한다는 것 등을 제외하면 차이가 없기 때문에 더 간단한 _except_handler3() 함수를 대상으로 분석하겠다.


  _except_handler3()는 위에서 언급했던 Scope Table 구조체를 통해 실제 핸들러 주소 및 필터링을 위한 정보를 얻어서 거기에 맞는 일을 수행한다. 예를들면 필터링의 결과가 EXCEPTION_EXECUTE_HANDLER라면 실제 핸들러를 수행하는 것이다.


  필터 부분도 필터링을 위한 함수를 호출하는데 예를들어 간단하게 __except (EXCEPTION_EXECUTE_HANDLER) 같은 방식으로 되어 있다면 그 함수는 EAX에 1을 넣고 반환하는 함수가 된다. 그리고 앞에서 언급하였듯이 필터 값으로 EXCEPTION_CONTINUE_EXECUTION을 넣는 경우에는 NtContinue()가 호출되서 예외가 발생한 그 코드로 돌아가서 다시 실행한다.





3. C++ EH

3.1 소스 코드

  try와 catch 구문을 사용하는 방식이다. 예외는 try 구문에서 throw를 사용해서 던져진다. 이것을 받아서 catch 구문 즉, 예외 핸들러에서 예외 처리가 수행된다. 간단하게 EH라고도 불린다.


// MSDN

#include "stdafx.h"
#include   
#include   
#include   

using namespace std;

int main()
{
	try
	{
		throw invalid_argument("MyFunc argument too large.");
	}

	catch (invalid_argument& e)
	{
		cerr << e.what() << endl;
		return -1;
	}

	return 0;
}



3.2 분석

  이것도 기본적인 내용은 같다. 차이점을 설명하자면 throw 구문을 사용한 경우 CxxThrowException() 함수가 사용된다는 점이다. 물론 이것도 내부적으로는 RaiseException()를 사용해 구현된다. 또한 EH에서는 C++의 예외 처리 규약과 맞게 EXCEPTION_CONTINUE_SEARCH나 EXCEPTION_EXECUTE_HANDLER 필터만 사용한다. 그리고 가장 중요한 차이점으로 핸들러가 CxxFramehandler() 류의 함수이다. 





4. SetUnhandledExceptionFilter()

4.1 소스 코드

  이것은 Callback 함수를 등록하는 방식으로서 따로 예외 영역을 지정하지 않는다. 그러므로 Unhandled Exception이 발생했을 때 호출할 Handler를 등록할 수 있게 된다. 즉 핸들러가 존재하지 않거나 존재하는 모든 핸들러가 EXCEPTION_CONTINUE_SEARCH를 반환한 경우에 사용된다. 


// https://www.codeproject.com/Articles/30815/An-Anti-Reverse-Engineering-Guide

#include "stdafx.h"
#include  
#include  
#include  

LONG WINAPI UnhandledExceptFilter(PEXCEPTION_POINTERS pExcepPointers) { 
	SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER) pExcepPointers->ContextRecord->Eax); 
	// EAX was 0 and because of this, exception occurs. So we set this function's parameter to 0, it means default handling within UnhandledExceptionFilter.
	pExcepPointers->ContextRecord->Eip += 2; 
	return EXCEPTION_CONTINUE_EXECUTION; 
} 

int main(int argc, char **argv) { 
	SetUnhandledExceptionFilter(UnhandledExceptFilter); 
	__asm{xor eax, eax} 
	__asm{div eax} 
	printf_s("No Debugger! \n"); 
	return 0; 
}


  EAX를 0으로 설정하고 div 함으로써 예외를 발생시킨다. main에는 미리 등록된 SEH가 없기 때문에 예외가 발생하면 Unhandled되며 이에 따라 등록된 UnhandledExceptFilter()가 호출된다. 이 콜백 함수를 보면 파라미터로 0을 넣고 SetUnhandledExceptionFilter()를 호출함으로써 앞에서 설정한 UnhandledExceptFilter() 대신 디폴트 Unhandled 예외 처리기를 설정하고 예외가 발생했던 명령어의 주소를 2만큼 증가시켜 다시 실행을 재개할 때 예외가 발생하지 않게 해준다.


  참고로 위의 예제는 이렇게 단지 Unhandled 예외 처리기를 삽입함으로써 안티 디버깅을 수행한다. 왜냐하면 일반적으로 디버깅 중이라면 우리가 등록한 UnhandledExceptFilter() 함수가 실행되지 않기 때문이다. 이것은 예외 발생 후 KiUserExceptionDispatcher()에서 UnhandledExceptionFilter()를 거쳐서 실행되는 도중에 NtQueryInformationProcess()의 인자에 ProcessDebugPort를 파라미터로 넣고 실행하는 부분이 있는데 디버깅 중인 경우 이 함수 호출 시에 이 부분이 -1 즉 FFFFFFFF 값이 나오게 되며 이 값을 받은 경우에는 프로세스를 종료해 버린다. 그렇기 때문에 이 부분을 0으로 직접 수정해주지 않으면 우리가 등록한 UnhandledExceptFilter() 함수가 실행될 수 없다. 


  참고로 Ollydbg 2.01 버전에서는 Debugging - Exceptions 옵션에서 Pass unprocessed exceptions to Unhandled Exception Filter를 체크해 주면 위처럼 귀찮게 NtQueryInformationProcess()에 BP를 걸고 ProcessDebugPort를 인자로 넣을 때까지 실행해보는 귀찮음을 덜 수 있다. 즉 이 옵션을 체크하면 아무 설정 없이도 위의 예제가 디버깅 중에도 정상적으로 동작하는 것을 볼 수 있다.


4.2 분석

  일반적인 예외처럼 KiUserExceptionDispatcher()를 지나 RtlDispatchException()를 호출한다. 이후 등록된 SEH가 있다면 호출하고 해당하지 않거나 없는 경우에는 kernel32.UnhandledExceptionFilter()를 호출하고 궁극적으로 우리가 SetUnhandledExceptionFilter()로 등록했던 UnhandledExceptFilter() 함수가 호출된다.





5. Unwind

  Unwind 즉 해제는 __try 블록 내부에 return, continue, break 등의 처리를 하는 경우에 필요하다. 예를들면 __try, __finally 구문이 있으며 __try 내부에 return이 존재한다면 return 호출 이전에 __finally를 실행하고 마지막에 return이 호출되어야 한다. 위와 같은 __finally 구문의 특징 때문에 Unwind가 필요한 것이다. 참고로 __try와 __finally 모두에 return 구문이 있는 경우 __try에서는 return 1, __finally에서는 retrun 2라면 2가 반환된다. 왜냐면 처음에 1이 반환 후보였지만 __finally에 의해서 2가 결정되고 반환되기 때문이다.


  사실 지금까지 Unwind나 __finally 부분을 다루지 않았다. 앞에서 RtlDispatchException() 함수를 설명하며 각 예외 체인에 대한 RtlpExecuteHandlerForException() 함수를 호출한다고 하였다. 하지만 __finally 부분에서는 Unwind를 다루는 RtlpExecutehandlerForUnwind() 함수가 호출된다.





6. VEH

6.1 소스 코드

  VEH는 SEH보다 더 높은 우선 순위를 갖는다. 즉 어떤 SEH 보다도 먼저 실행된다. 그렇기 때문에 다음의 코드를 보면 VEH 핸들러가 더 먼저 실행되고 이후에 SEH 핸들러가 실행되는 것을 볼 수 있다.


#include "stdafx.h"
#include 
#include 


LONG WINAPI  VEHandler( struct _EXCEPTION_POINTERS *ExceptionInfo )  {
	printf_s("VEH Handler \n");
	return 0;
}

void main() {
	PVOID forVEH;
	forVEH = AddVectoredExceptionHandler(1, VEHandler);

	__try {
		RaiseException(1, 0, 0, NULL);
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		printf_s("SEH handler \n");
	}

	RemoveVectoredExceptionHandler(forVEH);
}



6.2 분석

  일반적인 예외처럼 KiUserExceptionDispatcher()를 지나 RtlDispatchException()를 호출한다. RtlDispatchException()에서 일반적인 SEH를 검사하고 호출하는 루틴 이전의 초반부에 RtlpCallVectoredHandler()를 호출한다. 여기에서 등록한 VEH 핸들러가 호출된다. 즉 SEH 핸들러 이전에 VEH 핸들러가 호출되는 것이다. 



6.3 기타

  다음 내용에 대해서 더 찾아봐야 할 것 같다. [ To decode addresses of VEH handlers, OllyDbg hacks NTDLL.RtlAddVectoredExceptionHandler(), therefore process must be started from the OllyDbg ]





7. 64비트

7.1 개요

  64비트에서는 예외와 관련된 정보가 PE 헤더의 .pdata 섹션에 존재한다. 이 섹션은 IMAGE_RUNTIME_FUNCTION_ENTRY 구조체들로 이루어져 있으며 각 구조체의 멤버는 각각 BeginAddress, EndAddress, UnwindInfoAddress이다. 64비트에서는 이런 방식으로 모든 함수들의 시작 주소와 끝나는 주소가 PE 헤더에 삽입되어 있다. 예외와 관련된 정보는 UnwindInfoAddress를 살펴본다.


  예를들어 간단한 SEH 예제를 64비트로 컴파일해서 분석해 보겠다. main 함수는 0x1000에 존재하며 .pdata를 보니 첫 번째 구조체가 0x1000으로 시작하는 함수이므로 이것이 main 함수에 상응하는 IMAGE_RUNTIME_FUNCTION_ENTRY 구조체이다. 즉 시작 주소는 0x1000이며 끝나는 주소는 1049이다. UnwindInfoAddress 값에는 0x26A0이 들어있다. 이 말은 main 함수의 예외와 관련된 정보 즉 UNWIND_INFO 구조체가 .rdata 섹션에 위치한 0x26A0 주소에 들어있다는 것을 말한다. 


  .rdata 섹션에 위치한 UNWIND_INFO 구조체에는 SCOPE_TABLE 구조체가 존재한다. 이 구조체는 BeginAddress(__try 구문의 시작), EndAddress(__try 구문의 끝), HandlerAdress(필터), JumpTarget(__except 구문의 시작)로 이루어져 있다. BeginAddress 즉 __try 구문의 시작 주소를 보니 0x00001012이다. EndAddress 즉 __try 구문의 끝은 주소가 0x00001029이며 JumpTarget 또한 0x00001029이다. 사실 EndAddress 주소는 __try 구문의 마지막 부분 이후의 첫 명령어의 주소이며 이에 따라 __try 구문인 어셈블리 루틴 바로 다음에 __except 구문이 시작되는 것을 알 수 있다. HandlerAddress 멤버는 필터 함수가 존재할 경우에는 그 RVA 값이 들어가지만 만약 필터가 EXCEPTION_EXECUTE_HANDLER 즉 1을 갖는다면 값은 그냥 1이 된다. 즉 내부에서 이 값이 1인 경우 바로 __except 구문(JumpTarget에 주소가 들어있다)을 실행하며 아닌 경우에는 등록된 필터 함수를 호출하고 결정하는 것이다.


7.2 분석

  64비트에서도 예외가 커널을 지나 KiUserExceptionDispatcher() 및 RtlDispatchException()로 진행되는 것은 같다. 이 내부에서 가장 중요한 함수는 _C_specific_handler() 함수이다. 위에서 설명한 필터 함수나 __except 구문의 실행을 이 함수가 담당한다.





8. 기타

8.1 올리디버거와 예외

* Debugging

Set permanent breakpoints on system calls

; KERNEL32.UnhandledExceptionFilter(), NTDLL.KiUserExceptionDispatcher(), NTDLL.ZwContinue(),  NTDLL.NtQueryInformationProcess() 같은 시스템 호출에 INT3, 즉 브레이크포인트를 설정한다.


* Exceptions

Ignore memory access violations in KERNEL32

; kernel32.dll 모듈에서 발생하는 access violation 예외를 무시한다.

Step over INT3 breaks in MSCORWKS

; .NET 엔진이 디버거에게 이벤트를 알린다.

Ignore (pass to debugged program) following exceptions : [INT3 breaks, Single-step breaks, Memory access violations, Integer division by 0, Invalid or privileged instructions, All FPU exceptions, All service exceptions]

; 해당하는 예외를 무시한다. 이것은 디버거로 넘어온 예외를 Shift+Run/Step을 하지 않고 자동으로 다시 디버기에게 넘기겠다는 것을 의미한다.

Ignore also the following custom exceptions or ranges

; 나머지 예외들을 직접 설정할 수 있다.

Pass unprocessed exceptions to Unhandled Exception Filter

; 처리하지 않은 예외들을 Unhandled Exception Filter에 넘긴다. 일반적으로 SetUnhandledExceptionFilter() 함수를 사용해 Unhandled Exception을 처리하는 경우에는 디버깅 시에 등록된 Unhandled Exception Handler가 실행되지 않고 중간에 프로세스가 종료되어 버린다. 이것은 예외 발생 시 KiUserExceptionDispatcher() 및 RtlDispatchException()를 지나 등록한 Unhandled Exception Handler를 실행하기 이전에 ProcessDebugPort를 인자로 넣는 NtQueryInformationProcess()가 호출되는데 이 때 결과가 FFFFFFFF이기 때문이다. 그래서 이 값을 0으로 설정해 주어야 디버거에서도 정상적으로 실행될 수 있다. 이 옵션을 설정하면 NtQueryInformationProcess()에 BP를 걸고 확인하는 수고로움 없이 정상적으로 디버깅을 수행할 수 있게 된다.

Report ignored exceptions to log

; 무시한 예외들을 로그에 기록한다.


* SFX

Pass exceptions to SFX extractor

; extract 하는 동안 모든 예외를 extractor에 보냄으로써 무시한다.



참고] 올리디버거에서 예외 처리

  명령어를 실행시키는 중 예외가 발생할 경우 맨 아래를 보면 "Shift+Run/Step to pass exception to the program"이라는 내용이 보인다. 이 때 Shift+F7/F8/F9를 누름으로써 예외 처리 루틴으로 넘어갈 수 있다. 이것은 원래 디버거라는 것이 디버기의 제어를 갖음으로 인해 당연히 예외 발생 시에도 디버거에게 제어가 넘어오기 때문에 일어나는 일인데 이것을 누름으로써 예외를 다시 프로그램으로 넘겨주는 것이다. 


  이것을 리버싱하는 방법은 SEH의 경우에는 등록된 핸들러의 주소에 BP를 거는 방법이 있겠고 내부까지 들어가서 직접 분석하기 위해서는 ntdll.KiUserExceptionDispatcher()의 진입점에 BP를 걸면 예외 발생 후 커널로 넘겨진 제어가 커널로부터 복귀한 그 시점부터 바로 분석할 수 있다. 또는 Debugging 옵션에서 Set permanent breakpoints on system calls를 선택하면 이후 예외 처리 루틴으로 넘길 때 자동으로 ntdll.KiUserExceptionDispatcher()의 진입점에서 멈추게 된다. 



8.2 VC++ 예외 관련 옵션

* 일반의 코드생성 항목

- C++ 예외 처리 가능

기본적으로 SEH는 운영체제 차원에서 지원된다. 즉 __try, __except 구문을 사용하면 된다. 아래의 옵션들은 C++ 문법의 catch문에서 SEH나 extern "C"로 선언된 함수의 예외를 catch할 수 있는지의 여부를 판단한다.


=> /EHa

  C++ EH 뿐만 아니라 SEH도 catch 구문에서 catch할 수 있다. 

=> /EHs

  catch 구문에서 C++ EH를 catch하지만 SEH는 받을 수 없다. 또한 컴파일러에 extern "C"로 선언된 함수가 예외를 throw할 수 있다.

=> /EHsc

  catch 구문에서 C++ EH를 catch하지만 SEH는 받을 수 없다. 또한 컴파일러에 extern "C"로 선언된 함수가 예외를 throw할 수 없다.


* 링커의 고급 항목.

- 이미지에 안전한 예외 처리기 포함

=> /SAFESEH : 예

  SEH 오버플로우 공격을 방어하는 기법으로 /GS 옵션도 있지만(SEH 프레임에 GS 쿠키 사용) /SAFESEH 옵션도 존재한다. 이것은 간략하게 말해서 핸들러가 유효한 핸들러인지를 검사해 준다. 


  /SafeSEH 설정 시에 PE 헤더에 IMAGE_LOAD_CONFIG_DIRECTORY 구조체가 생성된 것을 볼 수 있다. 이 구조체의 여러 멤버 중에서 여기에 사용되는 것은 SEHandlerTable 필드와 SEHandlerCount 필드이다. SEHandlerCount 필드는 프로그램에서 설치되는 SEH 핸들러의 개수이며 SEHandlerTable 필드는 핸들러들의 주소들을 담는 __safe_se_handler_table의 주소이다. 


  로더는 PE가 로드될 때 IMAGE_LOAD_CONFIG_DIRECTORY 구조체의 SEHandlerTable 필드의 값인 __safe_se_handler_table에 이 프로그램에서 사용되는 핸들러들의 주소를 설정하고 그 개수는 IMAGE_LOAD_CONFIG_DIRECTORY 구조체의 SEHandlerCount 필드에 설정한다. 이후 예외가 발생하면 KiUserExceptionDispatcher()를 지나 RtlDispatchException()에서 호출할 특정 핸들러와 __safe_se_handler_table의 엔트리들을 비교해서 상응하는지를 검사한다.



8.3 First Chance Exception / Second Chance Exception

  Windbg로 예외를 처리하는 상황에서 설명해 보겠다. 디버거의 원리 상 프로그램에서 발생한 이벤트를 받게 된다. 만약 예외가 발생한다면 당연히 디버거가 예외를 받게 된다. 이 때 first chance 예외가 발생한다. 이후 프로그램에 예외 핸들러가 존재한다면 예외를 프로그램에게 넘길 수 있고(gn 명령어) 그렇지 않다면 디버거로 예외가 발생한 부분을 수정해서 예외가 발생하지 않게 한 후에 예외가 발생한 부분을 다시 실행할(gh 명령어) 수 있다. 어쨌든 프로그램에서 적절한 핸들러가 없어서 예외를 제대로 처리하지 못하면 다시 예외가 발생하게 되고 디버거는 이러한 unhandled 예외를 다시 잡게 되는데 이것을 second chance 예외라고 한다. 만약 디버거가 존재하지 않는다면 프로그램은 crash할 것이다.



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

다형성 바이러스  (4) 2017.05.16
API Sets  (0) 2017.05.12
링크 및 책 정리  (0) 2017.04.23
윈도우의 드라이버 개발과 루트킷 그리고 AV  (0) 2017.04.23
간단한 패커 개발  (0) 2017.04.23
Posted by SanseoLab

링크



I. Major

1. 레딧

https://www.reddit.com/r/ReverseEngineering/ ]

리버스 엔지니어링 : 매 주 질문을 할 수 있는 스레드가 올라온다.

https://www.reddit.com/r/Malware/ ]

악성코드

https://www.reddit.com/r/lowlevel/ ]

Low Level 프로그래밍 및 보안 (윈도우 리눅스)


2. StackExchange

http://reverseengineering.stackexchange.com/ ]

스택 오버플로우처럼 리버스 엔지니어링과 관련된 질문 및 답변


3. MSDN


4. 깃허브


5. 위키백과


6. VirusBulletin

https://www.virusbulletin.com/virusbulletin/ ]

https://www.virusbulletin.com/blog/ ]



II. Minor

1. Yobi Wiki

http://wiki.yobi.be/wiki/Reverse-Engineering ]

리버스 엔지니어링 관련 자료가 많다.


2. 0x00SEC

https://0x00sec.org/ ]

만들어진지 얼마 안된 곳이지만 괜찮은 내용들도 많이 올라오고 있다.



III. Twitter

보안뉴스  @boannews

SecurityWeek  @SecurityWeek

PHR34K  @unpacker

Evilcry_  @Blackmond

Ivan  @aszy

red plait  @real_redp

Giuseppe 'N3mes1s'  @gN3mes1s











I. 리버스 엔지니어링

* 리버싱 핵심원리 : 악성코드 분석가의 리버싱 이야기 - 이승원

* 리버스 엔지니어링 바이블 : 코드 재창조의 미학 - 강병탁

* 리버싱 : 리버스 엔지니어링 비밀을 파헤치다 - Eilam, Eldad

* Practical Reverse Engineering (역공학) : X86, X64, ARM, 윈도우 커널, 역공학 도구, 그리고 난독화 - Dang, Bruce

* 해킹의 꽃 디스어셈블링 : 보안 분석에 유용한 리버스 엔지니어링 - Kaspersky, Kris

* 리눅스 바이너리 분석 - 라이언 오닐

* Reverse Engineering for Beginners (실전 연습으로 완성하는 리버싱) : x86/x64 윈도우, 리눅스부터 모바일 ARM iOS까지 - 데니스 유리체프

* 리버스 엔지니어링 : 역분석 구조와 원리 - 박병익

* 즐거운 리버싱 : 리버스 엔지니어링 입문 - 아이코우 켄지

* 그레이햇 해킹 - 다니엘 레갈라도 등



2. 자료

* (Windows 시스템 실행파일의) 구조와 원리 - 이호동

* 리버스 엔지니어링 1,2 - 이호동

* The IDA pro book - Eagle, Chris

* Windows Internals 6th

* 리버싱 윈도우 : 장애, 해킹, 성능, 운영에 관한 실전 교과서 - 한주성



3. 악성코드

* 악성코드와 멀웨어 포렌식 - Aquilina, James M

* 실전 악성코드와 멀웨어 분석 - Sikorski, Michael

* 악성코드 분석가의 비법서 - Ligh, Michael Hale

* 악성코드, 그리고 분석가들 - 이상철

* 표적형 공격 보안 가이드 - 이와이 히로키

* DBD 공격과 자바스크립트 난독화로 배우는 해킹의 기술 - 최우석

* Cuckoo 샌드박스를 활용한 악성코드 분석 - 디지트 오크타비안토, 이크발 무하르디안토



4. 기타

* The Antivirus Hacker's Handbook - Joxean Koret

* 윈도우 시스템 해킹 가이드 : 버그헌팅과 익스플로 - 김현민


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

API Sets  (0) 2017.05.12
윈도우의 예외 처리  (0) 2017.05.09
윈도우의 드라이버 개발과 루트킷 그리고 AV  (0) 2017.04.23
간단한 패커 개발  (0) 2017.04.23
Yoda's Protector 분석  (0) 2017.04.23
Posted by SanseoLab

-1. 현재

http://sanseolab.tistory.com/33 ] 해당 글과 같이 공부 및 실습 중이며 간단한 프로젝트를 해볼 생각이다.





0. 서론

  커널 관련 공부를 시작하면서 드라이버를 직접 개발해보는 것이 공부하는데 많은 도움이 될 것이라고 생각해 왔다. 루트킷의 원리를 이해하고 안티바이러스나 게임 보안 등의 프로그램을 개발해 보는 것도 관련 지식을 쌓는데 큰 도움이 될 것 같아서 드라이버 개발과 관련된 내용을 공부하고 정리하고 있다. 하지만 가장 큰 단점 중 하나는 인터넷에서 떠도는 대부분의 자료들이 너무 오래된 것들이고 심지어는 최근에 올라오는 자료들도 이러한 SSDT 후킹 같은 기술들 위주로 올라오는 것이 개인적으로는 마음에 들지 않았다.

  안티바이러스를 보더라도 국내 뿐만 아니라 외국의 자료들도 마찬가지인 것이 대부분 안티바이러스 개발과 관련된 자료는 스캐너 뿐이며 이것도 사용자 영역에서 시그니처를 기반으로한 것이 대부분이다. 하지만 분명히 내가 모르는 수많은 기술들과 자료들이 있을 것이라는 생각으로 많이 찾아보았지만 능력의 한계로 개략적인 방식과 관련된 자료들만을 겨우 얻을 수 있었다.





1. 드라이버 개발과 관련된 변화

  현재는 32비트 운영체제 보다는 64비트 운영체제가 주류를 이루고 있다. 윈도우 비스타 x64부터 시작해서 64비트 운영체제에는 많은 변화가 추가되었고 더 추가되고 있는 중이다. 눈여겨 볼 것 중 하나는 필터 관리자가 커널에 추가되었다는 점이다. 이것으로 인해 특히 안티바이러스 업체의 경우에 큰 변화가 생기게 되었는데 미니필터 드라이버를 개발함으로써 과거에 했던 방식보다 더 진보된 방식의 개발이 가능해지게 되었다고 한다. 커널의 필터 관리자가 제공해주는 기능을 사용함으로써 더 쉽고 빠르게 더 안전한 안티바이러스를 개발할 수 있게 되었다는 것이다.

  또한 드라이버와 관련된 정책에서도 여러 변경 사항이 추가되었다. 첫 번째는 KPP(Kernel Patch Protection)이다. 이것은 다른 말로 패치 가드라고도 불리는데 x64 버전의 비스타부터는 이 보호 정책으로 인하여 SSDT 후킹 같은 커널 영역의 수정이 불가능해 졌다. 즉 이것을 우회하지 않고서는 과거처럼 SSDT 후킹을 이용한 루트킷이 존재할 수 없으며 이것은 안티바이러스 프로그램에게도 마찬가지이다. 대신 앞에서 말한 미니필터 드라이버 외에도 후술할 MS에서 제공하는 다른 기능들을 사용할 수 있게 되었다.

  두 번째 변화는 KMCS(Kernel Mode Code Signing)이다. 이것도 x64 버전의 비스타부터 시행되는 정책으로서 간단히 말해서 모든 드라이버는 서명이 필요하다는 것이다. 즉 테스트 모드로 설정을 하지 않는 이상 모든 드라이버는 서명이 되어 있지 않다면 커널에 설치할 수 없게되었다. 

  지금까지 언급한 것들 이외에도 많은 변화들이 있을 것이며 추가될 것으로 여겨진다. 확실한 사실은 과거와는 달리 이제는 윈도우의 보안 정책들을 우회하는 취약점을 사용하지 않고서는 과거처럼 간단하게 루트킷을 만들고 감염시킬 수 없다는 점이다. 

  참고로 이런 저런 시도를 해보는 도중에 알게된 것은 윈도우 7의 32비트 버전의 경우에 32비트이기 때문에 KPP와 KMCS가 적용되지 않는다는점 외에도 필터 관리자도 커널에 존재한다는 장점이 있다. 하지만 추후에 살펴볼 PsSetCreateProcessNotifyRoutineEx()나 ObRegisterCallbacks() 같은 확장된 함수나 새로 추가된 함수들을 사용하기 위해서는 32비트 윈도우 7에서도 드라이버에 서명이 필요한 것은 마찬가지라는 것이다.

  개인적으로 KPP 같은 강력한 보안 및 코드 서명 등으로 인한 루트킷 수의 감소와 (물론 더 복잡하고 강력한 방식으로 계속되겠지만) 드라이버 관련 개발과 연구가 아마추어 개발자 입장에서는 쉽게 이루어질 수 없는 현실(코드 서명을 통해)로 인해서 루트킷의 원리를 공부하고자 하는 것 뿐만 아니라 안티 바이러스나 게임 보안 프로그램의 원리를 공부하려고 했던 계획이 잘 진행이 되지 않고 있다. 또한 SSDT 후킹 같은 과거의 기술을 공부해야 하는지에 등의 생각 때문에 아직도 혼란스럽다.





2. 안티바이러스 개발에 대한 정리

  비스타에서 여러 API들과 필터 관리자 등의 변화가 생기기 전까지만 해도 대부분의 AV들은 루트킷처럼 만들어져서 감시할 API들을 후킹하였다고 한다. 하지만 MS에서 기능들을 지원함에 따라 후킹 기반 모니터에서 이제는 미니 필터 드라이버와 커널 통지 콜백들을 사용한다.


2.1 자가 보호

  안티바이러스 프로그램은 특성상 자기 자체를 보호할 필요가 있다. 안티바이러스에 대한 공격으로는 프로세스 종료 외에도 메모리 변조 같은 공격이 있을 수 있다. 과거에는 SSDT 후킹을 사용해서 프로세스 관련 함수를 후킹하여 자가 보호를 구현하였다면 KPP에 의한 커널 변경 방지로 인해 최근에는 관련 함수가 제공되고 있다. 

  ObRegisterCallbacks() 함수는 비스타부터 제공되는 함수로서 프로세스 및 스레드 객체에 대한 특정한 사전 / 사후 동작에 관한 통지를 받는 콜백 함수를 등록하는 함수이다. 즉 각 객체에 대한 핸들의 생성 및 복사 등의 동작에 대해 콜백 함수를 등록해 주는 것이다. 예를들면 이로 인해 유저 모드에서 프로세스나 스레드에 접근하려고 하는 경우 이 콜백이 먼저 호출되고 만약 이러한 접근을 차단하거나 권한을 수정하는 내용을 등록했다면 유저 모드에서는 접근할 수 없게되는 것이다.


2.2 파일 스캐닝

  확실하진 않지만 일반적인 스캐너에서 사용하는 방식이라기 보다는 실시간 감시 같은 기능에서 파일이 생성될 때 하는 검사에서 사용되는것으로 보인다. 이것은 파일 시스템 미니필터 드라이버를 사용해서 구현한다. 과거에는 파일 생성과 관련된 API들에 대한 SSDT 후킹을 통해서 구현했던 것으로 보인다.

  조금 더 자세히 설명하자면 커널의 필터 관리자를 이용하는 함수들을 통해 구현하는데 예를들면 FltRegisterFilter()는 필터링 할 IRP에 해당하는 콜백 정보를 구성하고 필터 관리자에게 이 정보를 등록하도록 요청하며 FltStartFiltering()은 등록된 미니필터 드라이버의 필터링을 시작한다. 이 외에도 많은 함수들이 지원된다. 

[ FltRegisterFilter(), FltBuildDefaultSecurityDescriptor(), FltCreateCommunicationPort(), FltFreeSecurityDescriptor(), FltStartFiltering(), FltReadFile(), FltSendMessage(), FltCancelFileOpen(), FltCloseCommunicationPort(), FltUnregisterFilter() ]

  참고로 MS는 이것과 관련된 드라이버 예제 샘플도 제공하는데 Scanner는 AV에서 사용되는 파일 데이터 스캐너 관련 예제이며 AVScan은 transaction-aware 파일 스캐너와 관련된 에제이다. 


2.3 프로세스 / 스레드 보호

  예전부터 이것과 관련된 함수들이 지원되어 왔다. PsSetCreateProcessNotifyRoutine()은 프로세스의 생성과 소멸을 모니터링하기 위한 콜백 루틴을 등록해주는 함수이다. 비스타 이후에서는 이것의 확장형인 PsSetCreateProcessNotifyRoutineEx() 함수가 제공되며 최신 AV들도 이 함수를 사용하고 있다. 이 확장된 함수는 이전 버전과 달리 프로세스의 객체 등의 많은 정보들 뿐만 아니라 결과적으로 프로세스의 생성을 막을 수 있는 기능도 제공된다.

  이것과 비슷한 함수로 PsSetCreateThreadNotifyRoutine()이 있으며 대상은 스레드이다. 예를들면 외부 코드를 일반 프로세스의 내부에서 스레드로 실행 못하게 하는데 사용될 수도 있다. 윈도우 10부터는 PsSetCreateThreadNotifyRoutineEx() 함수가 추가되었다.

  마지막으로 PsSetLoadImageNotifyRoutine() 함수가 있는데 이것은 이름처럼 이미지가 로드되었을 때 통지해주는 콜백을 등록할 수 있다. 예를들면 이미지가 올라온 경우 아직 프로세스가 시작되지 않았다고 하더라도 어떤 dll을 또는 어떤 드라이버를 로드할지 알 수 있기 때문에 유용하게 사용될 수 있다. 


2.4 레지스트리 보호

  레지스트리 또한 자가 보호에서나 시스템 보호에서나 중요한 보호 대상 중 하나이다. MS는 레지스트리에 대해서도 레지스트리 필터링 드라이버를 제공한다. 옛날부터 RegistryCallback 루틴을 등록하는 CmRegisterCallback() 함수가 제공되어 왔다. 이 콜백 루틴은 프로세스가 레지스트리 관련 동작을 수행하기 전에 각 레지스트리 관련 동작에 관한 통지를 받을 수 있다. 최근 비스타부터는 확장된 버전인 CmRegisterCallbackEx() 함수를 사용할 수 있다.


2.5 네트워크 보호

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



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

윈도우의 예외 처리  (0) 2017.05.09
링크 및 책 정리  (0) 2017.04.23
간단한 패커 개발  (0) 2017.04.23
Yoda's Protector 분석  (0) 2017.04.23
패커들 분석  (5) 2017.04.23
Posted by SanseoLab



Source Code : https://github.com/101196/SimplePacker





0. 개요

  사실 패커 개발과 관련된 자료는 흔치 않게 찾아볼 수 있지만 제대로 된 설명도 부족하고 처음 배우기에는 너무 많은 것을 다루는 경향이 있어보인다. 그래서 공부를 하면서 동시에 이렇게 정리해 보기로 하였다. 여러가지 패커들을 분석하면서 직접 개발을 해보는 것도 나쁘지 않겠다는 생각이 들었기 때문이다. 


  이미 오픈 소스로 제공되는 라이브러리들도 존재하고 PE를 다루는것도 직접 해보고 싶기도 해서 기본적인 목적만 달성할 수 있을 정도의 패커를 만들어 보았다. 원래는 다른 오픈 소스 패커들을 보면서 공부하는 것으로 끝내려 했지만 대부분의 것들이 코드가 방대하고 공부할 수 있을 정도로 제대로 정리되어 있다고 보기도 힘들었기 때문이다.


  개인적으로 간단하게 끝날 수 있을 거라고 생각했지만 이 정도 간단한 것을 만드는 데에도 상당한 시간이 들었다. PE를 다루는 것이 제공되는 구조체를 사용한다 하더라도 굉장히 까다로웠고 IAT를 복구하기 위한 과정들도 생각과 달랐다. 이것을 개발하면서 메모리를 다루는 일이 많았고 그래서인지 C 언어가 왜 고급 언어인지도 새삼 깨닫게 되었다. 소스 코드 뿐만 아니라 많은 자료들을 포함해서 여러 부분을 다른 곳에서 참고하였지만 개발에 익숙하지 않았기 때문에 겨우 간단한 패커를 만들 수 있었다. 참고로 다시 볼 때마다 수정할것이 그리고 추가하고 싶은 부분이 본인 눈에도 계속 보일 정도이므로 직접 해보았다는 것에 중점을 두고자 한다.




1. 정리

  기본적으로 패커는 원본 실행 파일을 읽어서 압축을 하고 그 내용을 새롭게 만들어질 실행 파일에 쓴다. 새롭게 만들어질 이 실행 파일에는 압축한 부분을 디코딩할 디코딩 루틴이 추가되어야 한다. 또한 압축 해제된 실행 파일은 정상적인 PE 로더가 처리하는 과정을 거치지 않았기 때문에 임포트 테이블 부분이 일반 파일과 같이 그대로 존재하므로 PE 로더가 수행하는 과정을 직접 수행할 수 있도록 디코딩 루틴에 추가해 주어야 한다. 이 외에도 이 모든 것들을 처리하려면 PE 헤더를 적절하게 수정해 주어야 한다.


  소스 코드를 보면 크게 디코딩 루틴, 몇몇 함수들(여러 곳에서 참조하였다) 마지막으로 main 함수가 보일 것이다. 각 장에서 main 함수를 기준으로 차례대로 정리해 보겠다. 그보다 먼저 패킹된 파일을 보면서 결과물을 기준으로 설명한 후에 소스 코드를 설명하겠다. 사용 방법은 다음과 같이 간단하며 옵션도 없다.


> packer.exe main.exe


  또한 이 패커는 콘솔 프로그램만 지원하는데 이것은 .text 섹션, .rdata 섹션 그리고 .data 섹션만 인식하고 나머지는 버릴 것이기 때문이다. 물론 콘솔 프로그램 중에서도 몇몇 추가적인 섹션들도 들어가 있는 경우가 있지만 실행하는데 크게 의미가 없는 경우도 있기 때문에 간단한 프로그램 정도는 동작할 것이다.


  어쨌든 패킹된 파일을 보면 섹션이 3개 존재하는 것을 볼 수 있다. 이것은 UPX의 특징을 살려서 만들었다. .nothing 섹션은 UPX의 UPX0 섹션과 같이 바이너리 상에서는 크기가 0이지만 메모리에 올라올 경우 실제 PE의 역할을 하는 섹션이다. 즉 뒤에 나올 압축된 내용이 여기에 풀리게 된다. 두 번째 섹션인 .packedc 섹션은 압축된 내용이 들어가고 뒤에 디코딩 루틴이 추가된다. 즉 UPX1 섹션과 같다. 사실 압축도 그냥 하는 것이 아니라 미리 특별한 처리를 해놓고 하기 때문에 그것은 뒤에서 알아볼 것이다. 마지막으로 UPX2 섹션과 비슷한 .importt 섹션이 존재한다. 여기에는 디코딩 루틴에서 사용할 API들의 임포트 테이블이 들어간다.


  이렇게 기본적인 사항을 알아보았지만 다음에 설명하듯이 소스 코드를 보다 보면 생각보다 많은 부분을 고려해야 한다는 것을 알 수 있을 것이다. 위에서도 언급하였듯이 최대한 간단한 패커를 만들려고 했고 콘솔 프로그램이라는 제한까지 있는데도 불구하고 말이다.




2. 소스 코드 분석 - 디코딩 루틴 및 여러 함수들

  가장 먼저 aplib.lib 라이브러리를 사용한다는 것을 알 수 있다. 이 라이브러리는 Ibsen Software[ http://ibsensoftware.com/products_aPLib.html ]에서 제공하는 압축 알고리즘 라이브러리이다. 실제로 여러 패커들에서 이 라이브러리를 사용한다. 이것을 사용함으로써 우리는 압축 알고리즘을 직접 만든다거나 할 필요없이 간단하게 제공되는 함수를 사용해서 개발을 할 수 있다. 또한 뒤에서 알아보겠지만 디코딩 루틴도 어셈블리어로 제공되기 때문에 약간 수정을 해서 사용할 수도 있다. 어쨌든 다운로드한 디렉토리를 열면 lib/coff/ 디렉토리에 aplib.h와 aplib.lib 파일이 존재하는 것을 볼 수 있으므로 이것을 소스 코드가 있는 디렉토리에 옮겨 넣는다.


  다음에 나오는 부분은 디코딩 루틴이다. 정확히 말하자면 APLib에서 제공되는 src/32bit/depack.asm을 수정한 것이다. 참고로 제공되는 어셈블리 프로그램은 fasm 문법으로 되어있기 때문에 약간 수정을 가한 것이다. 이 외에도 추가된 부분이 여럿 있는데 하나는 0xAAAAAAAA (추후에 압축된 내용이 들어갈 .packedc 섹션의 시작 주소가 들어갈 위치), 0xBBBBBBBB (압축 해제된 내용이 들어갈 .nothing 섹션의 시작 주소가 들어갈 위치), 0xCCCCCCCC(실제 OEP 주소가 들어갈 위치)가 보인다. 이 부분은 뒤에서 실제 주소로 수정할 것이다. 즉 나중에 수정할 때 찾기 쉽게 하기 위하여 이런 값을 사용한 것이다. 또한 nop을 여러개 추가한 것도 각 값을 읽어올 때 32비트 단위로 읽어오기 때문에 단위를 맞추기 위한 것이다. 나중에 직접 리버싱을 해보면 무슨 의미인지 알 수 있을 것이다. 물론 다른 방법도 있겠지만 여기에까지 많은 시간을 쏟을 수 없어서 참조한 곳의 소스를 최대한 따를 수 있을 만큼 따른 결과물이다.


  디코딩 루틴 다음에는 0xDDDDDDDD (추후에 보겠지만 수정된 임포트 테이블의 시작 주소가 들어갈 위치), 0xEEEEEEEE (임포트 테이블 복구 루틴에서 사용할 LoadLibraryA() 및 GetProcAddress() 함수를 사용하기 위한 주소가 들어갈 위치)가 보이고 임포트 테이블 복구 루틴이 보인다. 임포트 테이블 복구 루틴은 fsg나 kkrunchy 등에서 사용되는 루틴이다. 사실 이 루틴을 사용하기 위해서는 파일에 존재하는 임포트 테이블 부분을 그대로 사용하면 안되고 이것도 수정해 주어야 한다. 뒤에서 살펴볼 것이다.


  다음에 나오는 함수들은 PE 헤더를 다룰 수 있게 도와주는 함수이다. align_to_boundary() 함수는 PE의 특성상 Alignment 즉 경계를 맞추어야 하기 때문에 사용되는 함수이다. 뒤에서 사용될 때 설명할 것이다. my_int 함수는 union의 경우에 바이트를 다룰 때 사용된다. 이것도 직접 사용될 때 설명할 것이다.




3. 소스 코드 분석 - Main 함수

  처음은 전형적인 메모리 맵 파일 방식을 통해 바이너리를 읽어온다. CreateFile()로 패킹할 파일의 파일 오브젝트를 오픈해서 핸들을 얻고 CreateFileMapping() 함수를 통해 파일 매핑 커널 오브젝트를 생성한 후에 MapViewOfFile()로 파일 매핑 오브젝트를 프로세스의 주소 공간에 매핑시킨다. 반환 값은 매핑된 view의 시작 주소가 되며 소스 코드에서는 lpFile 변수를 사용했다.


  이후에는 PE 구조체를 정의하는 코드가 나온다. 여기서 이용하는 PE 헤더 구조체들로는 IMAGE_NT_HEADERS, IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER, IMAGE_SECTION_HEADER 등이 있다. PE 구조에 익숙하다면 이 구조체를 통해 쉽게 PE 헤더를 사용할 수 있을 것이다. 에를들면 GetFirstSectionHeader() 함수의 경우에는 간단하게 첫 번째 섹션 헤더를 구할 수 있게 해준다.


a. OEP 저장 및 임포트 테이블 수정 

  가장 먼저 OEP 즉 원본 파일의 엔트리 포인트를 미리 저장해 놓는다. 이제부터 상당히 까다로운 부분이 존재한다. 위에서 임포트 테이블 복구 루틴을 설명했는데 저 루틴은 파일 상태 그대로의 임포트 테이블을 복구하는 루틴이 아니다. 미리 특별한 형태로 저장된 데이터를 이용해서 임포트 테이블을 복구하는 것이다. 그러므로 우리는 그 특별한 형태로 임포트 테이블을 저장해 놓아야 한다. 즉 여기서 할 일은 현재 원본 바이너리 상태로 있는 임포트 테이블 부분을 다음과 같은 형태로 변경하는 것이다.


01 [첫 번쨰 DLL의 IAT 주소] [첫 번째 DLL 이름] 00 [api 이름] 00 [api 이름] 00 ... [api 이름] 00 01 [두 번째 DLL의 IAT 주소] [두 번째 DLL 이름] 00 [api 이름] 00 [api 이름] 00 ... 00 02


  참고로 여기서 api의 이름도 약간 다른데 이름의 첫 문자에 0x02만큼 값을 추가한 것이다. 이것은 예제를 들면서 이 루틴이 어떻게 수행되는지를 설명하겠다.


01 34 20 40 | 00 56 43 52 | 55 4E 54 49 | 4D 45 31 34   | 4 @ VCRUNTIME14

30 2E 64 6C | 6C 00 6F 65 | 6D 73 65 74 | 00 61 5F 74   | 0.dll oemset a_t

65 6C 65 6D | 65 74 72 79 | 5F 6D 61 69 | 6E 5F 72 65   | elemetry_main_re

74 75 72 6E | 5F 74 72 69 | 67 67 65 72 | 00 61 65 78   | turn_trigger aex

63 65 70 74 | 5F 68 61 6E | 64 6C 65 72 | 34 5F 63 6F   | cept_handler4_co

6D 6D 6F 6E | 00 61 5F 74 | 65 6C 65 6D | 65 74 72 79   | mmon a_telemetry

5F 6D 61 69 | 6E 5F 69 6E | 76 6F 6B 65 | 5F 74 72 69   | _main_invoke_tri

67 67 65 72 | 00 01 B0 20 | 40 00 61 70 | 69 2D 6D 73   | gger ° @ api-ms

2D 77 69 6E | 2D 63 72 74 | 2D 73 74 64 | 69 6F 2D 6C   | -win-crt-stdio-l

31 2D 31 2D | 30 2E 64 6C | 6C 00 61 5F | 70 5F 5F 63   | 1-1-0.dll a_p__c

6F 6D 6D 6F | 64 65 00 61 | 5F 73 74 64 | 69 6F 5F 63   | ommode a_stdio_c

6F 6D 6D 6F | 6E 5F 76 66 | 70 72 69 6E | 74 66 5F 73   | ommon_vfprintf_s

00 61 5F 61 | 63 72 74 5F | 69 6F 62 5F | 66 75 6E 63   |  a_acrt_iob_func

...

00 02 00 00 .. 


  가장 먼저 01이 있다. 이것은 다음에 올 것이 DLL의 IAT 주소라는 것을 알려준다. 즉 0x01만큼 뺐을 때 0이 된다면 다음이 DLL의 IAT 주소라는 것이다. 그리고 다음에 오는 00 40 20 34를 이 DLL의 IAT 시작 주소로 설정한다. 바로 뒤에는 00으로 끝나는 DLL의 이름 문자열이 온다. 즉 이 널 종료 문자열은 DLL의 이름이 되고 이 문자열을 LoadLibraryA()에 넣고 DLL의 핸들을 얻어온다. 그리고 다음 값을 검사하는데 0x01만큼을 빼고 0이 아닌 경우가 된다. 이것은 다음에 올 것이 DLL이 아니라는 것이다. 다시 0x01만큼 또 뺀다. 그래서 00이 아니면 이것은 API의 이름으로 여기고 GetProcAddress()로 API 이름과 LoadLibraryA()에서의 핸들 값을 통해 이 API의 주소를 얻어오고 아까 설정한 DLL의 IAT 시작 주소에 써 넣는다. 위에서 보면 API 이름이 oemset인 것을 볼 수 있다. 여기서 첫 번째 문자 o에서 0x02만큼 뺀다면 memset인 것을 알 수 있다. 만약 0x01을 빼고 다시 한 번 0x01을 뺐을 때 즉 0x02만큼 뺀 경우에 값이 0x00이 되면 이 루틴은 종료하게 된다. 그래서 마지막 부분을 보면 값이 0x02로 끝난 것을 볼 수 있다. 어쨌든 이 부분은 원본 바이너리의 임포트 테이블을 가지고 이런 식으로 만들어주는 역할을 한다.


b. 메모리에 정리해서 올리기

  바이너리가 파일 상태로 있는 것과 메모리에 올라온 것은 서로 다를 수 밖에 없다. 즉 파일은 파일 오프셋 주소를 통해 보면 알겠지만 File Alignment 단위로 나뉘어서 붙어있고 메모리에 올라온 경우에는 Section Alignment 단위로 올라온다. 그래서 PE 로더처럼 이것들을 메모리에 올라온 것 처럼 위치를 바꾸어줄 필요가 있다.


  참고로 더 설명해 보자면 이 상태로 압축을 할 것이고 이에 따라 압축이 해제된 모습도 이 상태가 될 것이다. 또한 압축한 이후에는 모든 섹션들과 안의 내용이 의미가 없기 때문에 모든 부분을 새로 만들 것이다. 왜냐하면 .nothing 섹션은 아무것도 없으며 .packedc 섹션도 곧 만들 압축된 내용과 뒤에서 추가할 디코딩 루틴으로만 이루어져 있고 마지막으로 .importt 섹션도 직접 만들 것이기 때문이다. 즉 버퍼에 압축된 내용을 넣은 후로 이제 새로운 내용을 써 넣어갈 것이다.


c. 섹션 압축하기

  aPLib에서 제공하는 aP_pack() 함수를 사용해서 지금까지 설정한 메모리의 범위를 압축한다. 참고로 이 함수의 반환값은 패킹한 데이터의 결과물의 크기이다. 이 함수를 사용하기 이전에 malloc()을 이용해 여러 버퍼를 할당하였는데 workmem 버퍼의 경우 패킹이 수행되는 동안 필요한 작업 영역이며 이것은 aP_workmem_size() 함수를 통해 크기를 미리 구할 수 있다. 즉 인자로 패킹할 대상의 크기를 넣으면 반환값으로 작업 영역의 최대 크기가 반환된다. 이 값을 malloc()으로 할당한다. 나머지는 패킹된 결과가 들어갈 버퍼로 이것은 aP_max_packed_size() 함수로 구하는데 이 함수는 인자로 들어온 크기의 데이터를 패킹할 경우 결과물의 최대값을 반환한다. 


d. 섹션 3개 처리하고 나머지 부분 지우기

  1에서 언급한 것처럼 3개의 섹션을 설정해 준다. 그리고 나머지 섹션은 그냥 지워버린다. 참고로 위에서 설명하였듯이 Section Alignment와 File Alignment를 고려해서 정리해야 한다. 섹션 헤더의 속성(Characteristics)에 대해서 좀 더 알아보자. 다음은 주요 속성들이다.


00000020 : IMAGE_SCN_CNT_CODE

00000040 : IMAGE_SCN_CNT_INITIALIZED_DATA

20000000 : IMAGE_SCN_MEM_EXECUTE

40000000 : IMAGE_SCN_MEM_READ

80000000 : IMAGE_SCN_MEM_WRITE


  예를들면 코드 섹션의 경우 속성은 IMAGE_SCN_MEM_READ, IMAGE_SCN_MEM_EXECUTE, IMAGE_SCN_CNT_CODE가 더해진 0x60000020가 된다. 하지만 실질적으로 우리가 생각해야할 것은 IMAGE_SCN_MEM_EXECUTE, IMAGE_SCN_MEM_READ, IMAGE_SCN_MEM_WRITE이다. PE 로더가 신경쓰고 우리에게 영향을 미칠만한 속성은 앞의 3개가 전부이다. .nothing 섹션의 경우 언패킹된 데이터가 쓰여져야 하며 추후에 실행되어야 할 섹션이기 때문에 0xE0000020을 준다. .packedc 섹션의 경우 압축된 데이터가 저장되기 때문에 메모리를 읽을 뿐만 아니라 여기에 디코딩 루틴도 쓰여져 있으므로 0xE0000040을 주기로 한다. 마지막으로 .importt 섹션의 경우 실행할 코드가 있지 않고 단지 .rdata 섹션과 같으므로 0xC0000040을 주기로 한다.


e. .packedc 섹션에 쓰기

  압축했던 것을 .packedc 섹션에 써 넣는다.


f. 데이터 디렉토리 처리하기

  데이터 디렉토리의  두 번째 즉 Import Table 부분의 크기와 위치를 설정한다. 참고로 .importt 섹션을 아직 처리하지도 않았는데 어떻게 설정하냐고 물을 수 있지만 이 부분은 미리 정해놓아서 즉 크기까지 구해놓았기 때문에 미리 설정할 수 있었다.


g. 디코딩 루틴 처리하기

  디코딩 루틴의 주소 부분을 정리한 후에 (0xAAAAAAAA 이런거) .packedc 섹션에서 패킹된 데이터의 뒤에 붙인다.


h. 기타 섹션 헤더 처리하기

  EP를 이 디코딩 루틴의 시작 주소로 놓고 새로 만들어질 바이너리의 크기 같은 기타 섹션 헤더들을 맞게 수정해 준다.


i. .importt 섹션 처리하기

  UPX와 비슷한 방식의 임포트 섹션을 순수하게 직접 만들어서 넣어준다. UPX로 패킹된 바이너리의 UPX2 섹션과 거의 비슷하므로 이것을 보면서 이해하면 될 것이다.


j. 파일 줄이기

  SetFilePointer()와 SetEndOfFile() 함수를 사용해서 파일의 크기를 줄인다. 즉 뒤에 0x00으로 채워지는 부분들을 아예 삭제하는 것이다. 참고로 나머지 뒷 부분이 모두 0으로 되어 있어야 통하는 것으로 보인다.




4. 결론

  사실 이것은 매우 단순화한 패커일 뿐이다. 패킹할 수 있는 대상도 한정되어 있고 여러가지 문제점이 존재할 수 있다. 예를들면 UPX의 경우 upx0, upx 섹션의 write 속성을 제거한다. 임포트 테이블에서 VirtualProtect()도 포함하였기 때문에 UPX처럼 write 속성을 제거해도 된다. 그렇지 않으면 매우 취약한 프로그램이 될 것이다. 이것은 UPX의 디코딩 루틴을 참고하여 간단하게 몇 줄의 어셈블리어를 추가하면 될 것이다.




5. 참고

http://ibsensoftware.com/products_aPLib.html : aPLib

- StackOverflow : align_to_boundary() 및 my_int() 함수

https://0x00sec.org/t/pe-file-infection/401 : 어셈블리 루틴 다루기



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

링크 및 책 정리  (0) 2017.04.23
윈도우의 드라이버 개발과 루트킷 그리고 AV  (0) 2017.04.23
Yoda's Protector 분석  (0) 2017.04.23
패커들 분석  (5) 2017.04.23
윈도우의 자료형 정리  (1) 2017.04.23
Posted by SanseoLab



0. 프로필

1. 개요

2. Packed PE

3. 알고리즘

4. MUP




0. 프로필

라이선스 : 프리웨어

상태 : v1.03

주소 : https://sourceforge.net/projects/yodap/files/Yoda%20Protector/



1. 개요

  Yoda's Protector는 Ashkbiz Danehkar이 Visuaal C++ 환경에서 제작하었다. 이것은 기존의 Yoda's Crypter에 데이터와 코드 섹션을 압축하는 기능을 추가한 버전이다. 기본적으로 현재 프로그램이 디버깅되는지를 판단해 안티 디버깅을 수행하고 다형성 코드 방식의 암호화와 압축 기술(LZO라고 한다)을 사용한다.


  안티 디버깅 기법으로 "Jumping into the middle of an instruction" 방식과 IsDebuggerPresent(), BlockInput() 그리고 SoftICE의 드라이버를 검사하는 것 등이 있으며 부모 프로세스를 검사해서 Explorer.exe가 아닌 경우에는 중단시키는 기술도 존재한다. 또한 코드의 다형성을 위해 다형성 암호화와 압축 기술을 사용한다. 


옵션을 보면

- Protection

Anti-SoftICE Protection (default)

Checksum Protection (default)

API Redirection (default)

Anti-Dump Protection (default)

Clear Import Information (default)

Clear PE header


- Advanced

Remove .reloc section (default)

Remove debug information (default)

Eliminate MS-DOS header

Optimize MS-DOS header


- Other

Create backup copy (BAK-file) (default)

Auto run after loading

Exit when done

Section's Name


- Compress Option

1 - 10



2. Packed PE

  일반 섹션들은 이름이 지워지고 .rsrc 섹션은 그대로 있다. .yP 섹션이 추가되는데 여기에는 Import Table과 stub 코드가 있다. Import Table을 보면 기본적으로 LoadLibraryA()와 GetProcAddress()가 필요하다. EP는 .yP 섹션의 Import Table 바로 뒤에 존재한다.



3. 알고리즘

  처음에는 CALL과 예외를 이용한 안티 디스어셈블리 방식이 존재한다. 일반적으로 "Jumping into the middle of an instruction"라고도 불리는데 CALL 명령어를 사용해 디스어셈블러의 분석이 실패하도록 주소를 호출한다. 물론 Ollydbg의 경우에는 Ctrl+A 단축키 또는 "Analyse Code"를 통해 깔끔하게 정리된 디스어셈블을 볼 수 있다. 정확히 표현하자면 명령어 중간에 "DB E9" 즉 무의미한 바이트를 써 넣고 call이나 jmp 명령어로 이 주소 다음 주소로 분기하도록 하여 실제 흐름에는 영향이 없지만 디스어셈블러가 명령어를 분석하는데 실패하도록 하는 방식이다. 이것도 여기서는 간단하게 사용되서 감당할만 한데 과하게 사용되는 경우에는 Ollydbg를 사용해도 분석하기 매우 까다롭다. Ollydbg의 경우 분석 이후 화면이 전환되기도 하고 그 부분만 분석하는 기능도 한계가 있기 때문이다. 더 좋은 디스어셈블러들의 경우 깔끔하게 정리된 것을 보여주기도 한다. 또한 동시에 다음 EIP 주소를 스택에 PUSH하는 CALL의 특징을 사용하는데 예외 핸들러를 설치할 때 이 PUSH된 주소가 예외 핸들러의 주소로 가게 된다. 그리고 INT 3을 이용해 예외를 발생시킨다. 이러한 부분이 5번 존재한다. 이 부분은 Step Into를 사용해 진행하다가 SEH가 설치되면 그 핸들러의 주소에 BP를 걸고 INT 3 명령어로 인한 예외를 디버거가 받으면 Shift+F9 키를 사용해 예외를 프로그램에게 돌려준다. 물론 옵션을 사용해 이 예외를 받지 않게할 수도 있다.


  사실 특이한 점으로는 예외 발생 시에 예외 핸들러가 호출되고 이것이 반환된 후에 다음 코드가 실행되는 것이 아니고 예외 핸들러 실행 도중에 다른 예외가 발생하여 다른 예외 핸들러가 호출되는 방식이 5번이나 존재한다는 것이다.


  어쨌든 이후에 복호화 루틴이 존재한다. 진행하다 보면 LOOP로 만들어진(즉 LOOP로 끝나는 프로시저) 복호화 루틴이 있는데 이 부분에 주의해야 할 것이 이 부분도 CALL과 JMP를 이용한 "Jumping into the middle of an instruction" 기법이 사용되기 때문에 코드가 상태가 좋지 않아서 조심스럽게 Step Into로 진행해야 한다. 이곳은 LOOP 명령어에 Conditional BP를 걸면 되는데, "ECX == 2"를 입력하고 실행(F9)시켜야 한다. 그러면 쉽게 LOOP 부분을 빠져나올 수 있다. 참고로 이런 루프가 하나 더 존재한다. 여기서 LOOP 명령어 뒤의 명령어에 F4를 눌러서 하는 방식은 통하지 않기 때문에(왜냐하면 이 LOOP 명령어 바로 뒤의 명령어가 현재 루프의 복호화 과정으로 인해 수정되기 때문에) 조건부 BP를 걸어야 하는 것이다.


  자세히 살펴보면 이 복호화 루프는 복호화해서 넣는 주소가 바로 다음 주소부터 0x3031만큼이라는 것을 알 수 있다. 이후 복호화가 끝나면 다음 명령어를 수행하게 된다. 그리고 앞에서 언급하였듯이 다음에도 또 다른 복호화 루프가 존재한다. 이 루프가 하는 행위를 보면 파일에 존재했던 Import Table 말고 스텁 루틴에서 사용할 Import Table을 복구한다. 복구되는 위치는 이미 존재하는 Import Table의 바로 뒤이다. 


  이 루프가 끝나면 예외를 이용한 안티디버깅이 하나 더 존재한다. 평소와 다른 차이점은 예외 관련 지식이 있으면 알겠지만 앞 부분과 달리 일반적인 방식처럼 호출한 예외 핸들러가 종료하여 반환을 하게 되는데 그렇기 때문에 KiUserExceptionDispatcher() 내부에서 NtContinue()까지 실행된다. 여기서 저장된 Context를 통해 복귀할 주소를 알아내는 방법을 보겠다. NtContinue()에서 받는 pContext 인자의 주소를 보면 스택에 존재하는 것을 볼 수 있다. 이 주소가 0x0019E41C라고 하자. 여기에서 0xB8만큼 더한 값 즉 0x0019E4D4 주소에 들어있는 값인 0x0041A7C1이 예외 핸들러에서 복귀한 후에 갈 주소이다. 


  사실 일반적인 경우라면 예외 핸들러가 호출된 후에 다음 명령어로 복귀될 것이다. 그래서 이렇게 복잡한 방식 대신 그냥 다음 명령어에 BP를 걸면 될 것이다. 하지만 이 부분도 복호화되서 변하는 부분이므로 미리 BP를 걸어놓고 실행하면 종료되어 버리기 때문에 이렇게 복잡한 방식을 사용하였다.


  이 부분을 지나면 API들의 주소를 찾는다. 참고로 원본 프로그램에서 사용할 API들이 아니라 Yoda가 압축 해제와 보호용으로서 사용할 API들이다. 전형적인 방식으로서 LoadLibraryA()와 GetProcAddress()를 통해 처리한다. 아까 구했던 이름을 가지고 주소를 얻어서 이름 다음 부분에 차례로 집어넣는다. 이후 스텁에서는 이 주소를 가지고 API들을 호출하게 된다. 패커가 아닌 프로텍터여서 그런지 사용하는 API들이 상당히 많다. 다음은 그 목록이다.


- Kernel32.dll

GetModuleHandleA(), VirtualProtect(), GetModuleFileNameA(), CreateFileA(), GlobalAlloc(), GlobalFree(), ReadFile(), GetFileSize(), CloseHandle(), IsDebuggerPresent(), CreateToolhelp32Snapshot(), GetCurrentProcess(), GetCurrentProcessId(), Process32First(), Process32Next(), Module32First(), Module32Next(), Thread32First(), Thread32Next(), OpenThread(), OpenProcess(), TerminateProcess(), SetPriorityClass(), GetPriorityClass(), ExitThread(), GetWindowsDirectoryA(), CreatWindowA(), GetCurrentThread(), SetThreadPriority(), SuspendThread(), Resumethread(), GetLastError(), GetSystemTime(), GetTickCount(), GetVersion(), DebugActiveProcess(), DebugActiveProcessStop()


- User32.dll

MessageBox(), SendMessageA(), WaitForInputIdle(), BlockInput(), GetWindowLongA(), SetWindowLongA(), GetForegroundWindow(), FindWindowA(), GetTopWindow()


- Advapi32.dll

RegCreateKeyExA(), RegOpenKeyExA(), RegCloseKey(), RegSetValueExA(), RegQueryValueExA(), CryptAcquireContextA(), CryptReleaseContext(), CryptCreateHash(), CryptDestroyHash(), CryptHashData(), CryptDeriveKey(), CryptDestroyKey(), CryptEncrypt(), CryptDecrypt()


  참고로 CALL을 통한 "Jumping into the middle of an instruction" 기법으로 인해 스택은 계속 쌓여 가지만 큰 의미는 없어보인다. 그리고 언패킹을 하면서 상황에 맞게 Code Analysis를 수행했다 말았다를 해야할 것이다. 


  어쨌든 이 부분이 끝나면 여러 안티 디버깅 기법들이 나온다. 먼저 GetVersion(), GetForegroundWindow(), FindWindowA(), GetTopWindow()를 사용하는 프로시저가 나온다. 이후 프로시저에서는 GetCurrentProcess(), GetPriorityClass(), SetPriorityClass()를 사용하고 BlockInput()을 사용한다. 참고로 이 API는 안티디버깅에 사용되는데 해결하는 방법은 Step Into로 따라 들어가서 RETN 4를 제외하고 모든 부분을 NOP으로 바꾸면 된다. 그리고 암호화와 관련한 루프문이 나온다. CryptAcquireContextA(), CryptCreateHash(), CryptHashData(), CryptDeriveKey(), CryptDestroyHash() 등의 API들을 사용한다.


  다음 프로시저에서는 SoftICE와 관련한 것이 나온다. 먼저 CreateFileA()로 "\\.\SICE"를 오픈하고 다음으로 "\\.\NTICE"를 오픈한다. 만약 이 파일들이 존재한다면 즉, 반환 값이 -1이 되면 정상적으로 진행되는데, 아닌 경우에는 CloseHandle()과 RtlExitUserThread()를 수행하여 종료한다.


  다음 프로시저에서는 GetCurrentProcessId()를 호출하는데 이것도 안티디버깅에 사용되므로 처리가 필요하다. Step Into로 들어가서 FS:[18]을 통해 PID를 구하는 부분을 직접 올리디버거의 PID를 구해서 이 값을 넣는다. (작업관리자에서 올리디버거의 PID를 찾은 후 이 깂은 10진수이므로 계산기에서 이 값을 16진수로 변환해서 얻는다) 즉 다음과 같다.


  MOV EAX, DWORD PTR FS:[18}

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

  RETN


이것을


  MOV EAX,14AC ; 참고로 16진수로.

  NOP

  NOP

  NOP

  NOP

  RETN


으로 바꾼다. 이후에 CreateToolhelp32Snapshot(), Process32First(), Process32Next() 등의 여러 API들의 결합을 이용해서 모든 실행중인 프로세스들의 PID 번호를 구하고 현재 실행된 프로세스를 검색한다. 타겟 프로세스의 PID와 스스로의 PID를 비교한 다음 서로의 PID가 다르면 그 프로세스를 종료시킨다.


  지겨울 정도로 진행하다 보면 PE 헤더부터 복구화 루틴이 진행되는 것을 볼 수 있다. 마지막 즈음에 IsDebuggerPresent()를 통한 안티디버깅이 존재하는데 이것은 그냥 이 API 호출 후에 EAX를 0으로 바꿔주면 된다. 이후에도 PID 관련한 부분이 반복되고 CryptDestroyKey()와 CryptReleaseContext() 등을 호출한다. 다음으로는 실제 Import Table을 복구한다. 즉 원본 프로그램에서 사용하는 API들을 위한 Import Table이 복구된다.


  이제 GetTickCount()를 통한 안티 디버깅 기법이 나온다. 결과 값이 들어올 EAX의 값을 아래의 비교문에서 비교할 값보다는 적게 맞추어야 한다.


  이제 끝이 다가오면서 BlockInput()을 또 수행하고(이미 이 API 내부를 수정하였기 때문에 추가적으로 할 일은 없다) GetCurrentProcess(), SetPriorityClass(), SetWindowLongA()를 수행한다. 이후에도 이전에 나왔던 IsDebuggerPresent() 외에도 GetCurrentProcessId(), SoftIce 관련 안티디버깅 등이 나오고 어느 정도 진행되다가 핸들러의 주소로 OEP를 넣고 예외를 발생시킨다. 이제 핸들러로 가게 되면 그곳이 OEP가 된다.



4. MUP

  특이점으로는 MUP가 끝난 바이너리는 원본 파일과 같다는 점이다. 다른 상용 프로텍터들과는 달리 Stub을 지난 후에도 안티 디버깅 기법들이 삽입되어 있거나 수정되어 있지 않다는 점에서 일반적인 패커와 비슷한 느낌이 든다.

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

윈도우의 드라이버 개발과 루트킷 그리고 AV  (0) 2017.04.23
간단한 패커 개발  (0) 2017.04.23
패커들 분석  (5) 2017.04.23
윈도우의 자료형 정리  (1) 2017.04.23
Ollydbg [올리디버거] 2.01 매뉴얼  (1) 2017.04.23
Posted by SanseoLab



  패커와 관련된 대부분의 문서에서는 단지 MUP 방식만 간단하게 설명되어 있는것 같다. 물론 리버스 엔지니어링을 하는 입장에서는 대부분의 패커가 프로텍터처럼 안티 디버깅 방식을 사용하는 것도 아니고, 압축 알고리즘까지 공부할 필요는 없다고 생각하기 때문일 것으로 여겨진다. 하지만 압축 알고리즘은 그렇다고 하더라도 전체적인 흐름과 방식에 대해서는 기본적으로 알아야할 필요는 있다고 생각하였기에 이렇게 정리를 하려고 한다.


  본문은 약 11개의 유명 패커들 각각을 기준으로 설명하고 있다. 물론 패커라는 소프트웨어들은 전체적으로 동작하는 방식이 유사한 점이 많기 때문에 처음에는 자세히 설명되어 있지만 다음 패커로 넘어갈수록 내용은 줄어들 것이다. 또한 하나하나 세부적으로 설명하기도 어렵고 능력도 안되며 압축 알고리즘에 대해서는 관심조차 없기 때문에 그냥 정리된 문서라고 여기고 읽어보면 될 것이다.


  각 패커는 기본 정보 외에도 라이선스와 업데이트 상태 그리고 관련 정보나 다운로드를 받을 수 있는 사이트 주소가 정리되어 있다. 또한 패킹된 실행 파일에 대한 설명과 전체적인 알고리즘 그리고 결론적으로 MUP 방식에 대해서도 설명한다.


  압축 알고리즘에 대해서는 조금도 알지 못하지만 그래도 어떤게 있는지 정도는 정리해 보도록 하겠다. 가장 많이 사용되는 것으로서 aPlib이라는게 있는데 이것은 라이브러리의 이름이다. 자세히 보자면 LZ77 알고리즘 기반의 무손실 데이터 압축 라이브러리로서 프리웨어이며 상업용으로도 사용 가능해서 많은 패커들이 이 라이브러리를 사용해서 압축을 구현하고 있다. 이 외에 많이 사용되는 알고리즘으로는 LZMA 알고리즘이 있다. 그리고 UPX에서 사용되는 상용 알고리즘인 NRV와 이것을 오픈 소스로 구현한 UCL, MPRESS 패커에서 사용되는 LZMAT 같이 많이 사용되지 않는 알고리즘도 있고 상용 프로그램에서 만든 경우 방식을 공개하지 않은 여러 알고리즘이 존재할 것이다.


  기본적으로 패커의 동작 원리를 미리 설명하자면 다음과 같다. 먼저 레지스터 Context를 저장한다. 이를 위해 일반적으로 PUSHAD를 사용한다. 그리고 코드와 데이터 섹션을 디코딩하고 압축을 해제한다(디코딩 루프). 이후에는 사용할 라이브러리를 로드하고 그 라이브러리에서 사용되는 API들의 주소를 가져온다(임포트 테이블 복구). 이제 처음에 EP에서 저장시켰던 레지스터 Context를 복구한다. 일반적으로 POPAD를 사용한다. 마지막으로 실제 OEP로 이동하여 실행된다.




목록

1. UPX

2. Upack

3. ASPack

4. Petite

5. MEW

6. MPress

7. KKrunchy

8. RLPack Basic

9. FSG 1.33

10. FSG 2.0

11. nPack




1. UPX

라이선스 : GPL (소스 코드 존재)

상태 : 최신(version 3.91 - 2013.9)

주소 : https://upx.github.io/ (소스 코드 : https://github.com/upx/upx)


1.1 개요

  UPX는 Ultimate Packer for Executables의 약자로서 1998년도에 시작되어 최근까지도 업데이트가 이루어지고 있는 패커이다. 또한 GPL 라이선스를 가지고 있으며 소스 코드도 공개되어 있다. 또한 패킹 외에도 "-d" 옵션을 통해 언패킹도 수행할 수 있는 특징이 있다.

  UPX는 UCL이라고 불리는 알고리즘을 사용하는데 이것은 상용 알고리즘인 NRV(Not Really Vanished)를 오픈 소스로 구현한 것이다. 이 외에도 최신 버전에서는 LZMA(Lempei-Ziv-Markov chain-Algorithm) 알고리즘을 사용할 수 있다. 


  옵션은 여러 방식들이 존재하는 것으로 여겨지는데 --best, --brute, --ultra-brute 등이 있다. 하지만 기본적인 방식은 디폴트인 UCL 알고리즘과 LZMA 알고리즘 이렇게 두 가지로 여겨진다.



1.2 Packed PE

  패킹된 바이너리를 보면 기본 헤더는 바뀌지 않았다는 것을 알 수 있다. 하지만 섹션을 보면 크게 UPX0, UPX1, UPX2로 나뉘어 있는 것을 알 수 있다. 참고로 .rsrc 즉 리소스 섹션이 존재하는 경우에는 UPX2 섹션 대신 .rsrc 섹션이 그대로 있는 것을 볼 수 있다.


  UPX0 섹션의 RawDataSize을 보면 값이 0인데 이것은 파일에서의 크기가 0이라는 것을 의미한다. 하지만 VirtualSize의 값은 크게 나온 것을 알 수 있다. 사실 UPX는 실행되면서 UPX1에 있는 인코딩 즉 압축된 내용을 디코딩해서 UPX0 섹션에 써넣는다. 즉 디코딩 루틴이 끝난 후에는 이 섹션이 원래의 코드 섹션의 역할을 한다. 


  UPX1 섹션은 위에서 설명하였듯이 압축된 내용이 들어있다. 그리고 마지막 부분에는 디코딩 루틴이 존재한다. 즉 우리가 처음 실행하면 EP는 이 디코딩 루틴의 시작이다. 추후에 디코딩이 끝나고 OEP로 점프할 때 원래 패킹하기 전의 바이너리의 EP로 이동하게 된다. 


  UPX는 리소스 부분을 압축하지 않는다. 그렇기 때문에 .rsrc 섹션은 그대로 존재하는 것을 알 수 있다. 하지만 차이점이 존재하는데, 디코딩 루틴에서도 기본적으로 필요한 API들이 존재한다. 이 API들을 사용하기 위해서는 Import Table이 필요한데 이 Import Table이 .rsrc 섹션에 추가된다. 물론 .rsrc 섹션이 존재하지 않는 경우에는 UPX2 섹션이 만들어져서 이곳에 들어가게 된다. 


  그럼 원래의 Import table은 어디로 가는걸까. UPX가 암호화하는 섹션은 코드 섹션과 데이터 섹션이다. 이것은 .text, .data 뿐만 아니라 일반적으로 Import Table이 존재하는 섹션인 .rdata 섹션도 마찬가지이다. 그렇기 때문에 디코딩 루틴이 끝난 후에야 이 섹션이 복구된다.


  조금 더 자세히 알아보자면 UPX의 디코딩 루틴에서 기본적으로 사용하는 API들은 다음과 같다. LoadLibraryA(), GetProcAddress(), VirtualProtect(), VirtualAlloc(), VirtualFree(), ExitProcess()이 그것이다. LoadLibraryA()와 GetProcAddress(), VirtualProtect()는 알고리즘 부분에서 설명할 것이고, ExitProcess()는 디코딩 루프를 돌다가 잘못된 경우에 프로그램을 종료시킬 때 사용된다. 그리고 UCL이나 LZMA 알고리즘에서도 모두 VirtualAlloc(), VirtualFree()는 사용되지 않는 것으로 보이는데 확실치 않다.



1.3 알고리즘

  디코딩 루틴은 위에서 설명한 것과 같다. 하지만 조금은 더 자세히 설명해 보겠다. EP는 다음과 같이 시작한다.


PUSHAD

MOV ESI, 00410000 // 두 번째 섹션 시작 주소(UPX1)

LEA EDI, [ESI+크기] // 첫 번째 섹션 시작 주소(UPX0)


  당연히 알겠지만 PUSHAD로 시작한다. 그리고 자세히 보면 Source를 나타내는 ESI에는 두 번째 섹션의 시작 주소를 넣고, Destination을 나타내는 EDI에는 첫 번째 섹션의 주소를 넣는다. 위에서 설명하였듯이 두 번째 섹션의 내용을 디코딩하여 첫 번째 섹션에 넣는다는 것을 유추할 수 있다.


  이것을 지나 디코딩 루프가 나온다. 루프가 돌아가는 동안 디코딩이 계속된다. 몇몇 루프가 끝난 후에는 마지막으로 loop로 끝나는 루프가 나온다. 이 루프는 위의 디코딩 루프와는 약간 다르다. 이것은 CALL/JMP 명령어의 주소를 복원시켜주는 역할을 한다. 직접 살펴보면 이 루프를 돌기 전까지는 CALL/JMP 명령이 원래와 다르다가 이 루프를 지나서 복구된다는 것을 알 수 있다.


  이제 Import Table을 복구하는 루프가 나온다. 먼저 디코딩된 라이브러리의 이름을 가지고 LoadLibraryA()로 라이브러리를 로드하고 마찬가지로 디코딩된 API들의 이름을 가지고 GetProcAddress()로 API들의 주소를 구한 후에 Import Table에 저장한다. 이렇게 Import Table의 복구가 끝난다. 참고로 말해보자면 원래 바이너리를 보면 섹션별로 .text, .data, .rdata 이런 식으로 나누어져 있어서 올리디버거로 보면 .text 섹션만 CPU 패널에 보일 것이다. 하지만 UPX는 이 섹션들이 모두 통합이되서 UPX0, UPX1 섹션이 모두 올리디버거의 CPU 패널에 보인다. 그렇기 때문에 Import Table이 복구되는 과정도 좀 더 편하게 직접 지켜볼 수 있다. 


  사실 다른 패커들도 마찬가지이기 때문에 Import Table을 복구하는 것에 대해서는 조금 더 설명이 필요하다. 확실한 이유인지는 모르겠지만 실제 IAT에서 함수의 이름을 가지고 Import Table을 복구하는 어셈블리 루틴은 상당히 복잡한 것으로 생각된다. 그래서인지 대부분의 패커들은 패킹하기 전에 api들의 이름과 dll들의 이름 및 IAT 주소를 따로 정리해서 저장해 놓고 이것을 패킹한다. 언패킹 루틴은 순서로 정리된 api와 dll의 이름과 이것을 위치시킬 IAT 주소를 가지고 간단하게 Import Table을 복구할 수 있게 된다.


  이제는 VirtualProtect() API를 사용할 차례이다. 이것을 사용해서 헤더 부분을 쓰기 가능하게 만든 후에 수정하고 다시 쓰기 가능한 속성을 없앤다. 수정하는 내용은 UPX0, UPX1 섹션을 쓰기 가능에서 읽기로 바꾸는 것이다. 참고로 말하자면 원래 .text 섹션은 읽기만 가능한게 일반적이다. 코드 섹션에서 코드를 읽고 실행을 하지 수정할 일은 잘 없기 때문이다. 하지만 디코딩 루틴이 이 섹션에 디코딩된 내용을 써야하므로 이 섹션이 쓰기 가능이었던 것이다. 그것을 이제 다시 쓰기 불가능하게 만드는 과정이다.


  LZMA 알고리즘으로 압축한 경우에는 디코딩 루틴만 다를 뿐 이후에 Import Table을 복구한다는 것 같이 기본적으로 하는 일은 동일하기 때문에 진행하다 보면 충분히 구별할 수 있다.



1.4 MUP

  전형적인 방법으로서 PUSHAD 이후에 디코딩 루틴을 수행하고 임포트 테이블을 복구하고난 후에 마지막으로 POPAD 명령어를 실행하는데 이 바로 다음의 JMP 명령어를 실행하면 OEP로 들어간다. 아니면 H/W BP로 ESP에 BP를 거는 방법도 상관없다.





2. Upack

라이선스 : 프리

상태 : 2006년 (WinUpack v0.39)

주소 : http://www.softpedia.com/get/PORTABLE-SOFTWARE/Compression-Tools/Windows-Portable-Applications-Portable-WinUpack.shtml


2.1 개요

  UPack은 유명한 패커이기도 하고 그만큼 오래된 패커이기도 하다(최근 업데이트가 약 10여년 전이다). 이것은 수정된 LZMA 알고리즘을 사용하는 것으로 알려져 있다. 옵션은 다음과 같다. 


- Reserve extra data

- Strip export table

- Strip base relocation table

- Relocate base address to ..

- Preserve original file date and time



2.2 Packed PE

  다음은 [ revealing Packed malware ( Published by the ieee ComPuter soCiety

- ieee seCurity & PrivaCy) ]에서 참고한 내용이다. 

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

| PE header                     |

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

| OEP                             |            // Original code section

 ------------------------------ // EP   -------------------------------

| LZMA decompression      |

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

| E8/E9 decompression      |

 ------------------------------      // .Upack section

| Import table rebuilding    |

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

| OEP Jumping                 |

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

| Compressed data            |            // .rsrc section

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


  UPack은 수정된 LZMA 압축해제, E8/E9(JMP나 CALL) 압축해제, import table 복구, OEP 점프로 나뉜다. 그러나 UPack은 압축 성능에 영향을 미치지 않게 normal LZMA 디코더를 수정하기 위해 LZMA 파라미터를 바꾼다. Import Table 복구 단계에서 UPack은 DLL 이름들과 thunk table 및 API들의 주소를 extract한다. UPack은 재배치 데이터 블록들의 RVA를 저장해서 재배치 테이블이 재배치가 필요한 경우 재배치된다. 즉 간단히 설명해서 LZMA 압축 해제를 하고 CALL/JMP문 복구 그리고 Import Table을 복구한 후에 OEP로 점프한다. 


  패킹된 바이너리를 보면 헤더부터 깨져있어서 PEView로는 정보를 얻을 수 없다. 그리고 안티바이러스가 악성코드로 인식해서 그냥 삭제해 버린다. 그나마 정적 분석으로 얻을 수 있는 정보는 임포트하는 함수가 LoadLibraryA()와 GetProcAddress()라는것 정도이다.



2.3 알고리즘

  EP는 그냥 일반 섹션에서 시작되며 디코딩 루프가 시작된다. 실행하다 보면 JB로 끝나는 루프를 볼 수 있는데 이 루프가 LZMA 디코딩 루프라고 할 수 있다. 이 비교문을 자세히 보자면 CMP/JB 명령어를 통해서 EDI 값이 ESI+34에 있는 값이 될 때까지 계속 루프를 돈다. EDI는 쓸 섹션의 처음부터 시작하다가 끝까지 가는거고 끝 주소는 ESI+34에 들어있다는 것이다. 디코딩 루틴을 조금 더 자세히 보면 


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


그리고


STOS BYTE PTR ES:[EDI]


이런 명령이 있는데 이것은 압축을 해제한 후에 EDI 값이 가리키는 메모리에 쓰라는 것이다. 위에서 설명했듯이 EDI는 복구할 원래 섹션의 처음 위치의 주소부터 시작해서 끝 주소까지 점점 증가하는 값을 갖는다.


  그 다음에는 바로 LOOP 명령어를 이용한 루프가 나오는데 이것은 CALL/JMP 명령어를 복구하는 루프이다. 이후 LoadLibraryA()와 GetProcAddress()로 Import Table을 복구하고 RETN하면 OEP로 간다.



2.4 MUP

  가장 처음 부분에서 EAX를 PUSH할 때 그 EAX가 바로 OEP이다. 이 OEP에 HW BP를 걸고 실행시키면 된다.


MOV ESI, 주소

LODS DWORD PTR DS:[ESI] ; ESI가 가리키는 주소에서 4바이트를 읽어서 EAX에 넣으라는 의미.

PUSH EAX





3. ASPack

라이선스 : 상용

상태 : 최신(version 2.39 - 2016.3)

주소 : http://www.aspack.com/


3.1 개요

  상용 프로그램으로서 전형적인 패커이다. 매년 수 회씩 꾸준히 업데이트 중이며 가장 유명한 상용 패커일 것이다. 이 외에도 프로텍터로는 ASProtect가 있다. 어느 문서에서는 isDebuggerPresent() API와 INT3을 통한 안티 디버깅 기법이 존재한다고 하는데 직접 사용해본 버전은 평가판 버전이기 때문인지 안티 디버깅 기법이 적용되어 있지 않았다. 


  그리고 옵션을 살펴보면 리소스 섹션도 압축할지 여부와 Max 압축 여부 등을 선택할 수 있고 섹션 이름도 변경할 수 있다는 것을 알 수 있다. 리소스 섹션 압축은 기본으로 설정되어 있어서 사용했지만 평가판 버전에서는 Max 압축이 불가능했기 때문에 이것을 제외하고 사용하였다.



3.2 Packed PE

  패킹된 바이너리를 보면 섹션들은 그대로 존재하고 대신 .aspack과 .adata 섹션이 추가되어 있다. 물론 존재하던 섹션들은 당연히 압축되어 있고 .aspack 섹션은 디코딩 루틴과 새로운 Import Table이 들어가 있다. 사용되는 API들은 다음과 같다.


GetProcAddress(), GetModuleHandle(), LoadLibraryA()


  참고로 메모리에 올라왔을 때 올리디버거의 Memory map을 보면 .adata 섹션을 제외한 다른 섹션들은 하나로 여겨지는 것을 볼 수 있다. 그리고 원본 파일과 크기를 비교해보면 .aspack과 .adata 섹션은 전체 섹션 마지막에 덧붙여진 것을 알 수 있다. 어찌되었든 EP를 보면 마지막 섹션이 끝난 바로 뒤인 .aspack 섹션의 시작 주소가 디코딩 루틴이라는 것을 알 수 있다. Import Table은 디코딩 루틴의 마지막에 존재한다. 참고로 진행하다보면 알겠지만 API 이름들이 중간중간에 껴있어서 진행하면서 몇몇 API들을 미리 임포트하기도 한다.



3.3 알고리즘

  시작부터 디버거로 분석하기 귀찮게 만드는 CALL/JMP를 통한 트릭이 사용된다. 이것은 안티 디스어셈블리 방식으로서 디스어셈블러가 명령어들을 제대로 해석하지 못하게 만들어 준다. 올리디버거의 경우 Ctrl + A 또는 "Analyse Code"를 이용하여 어느 정도 해석할 수 있다. 물론 간편하고 확실하게 되지는 않지만 이 부분이 그렇게 심하지 않으므로 감당할 정도는 된다. 정확한 이름은 존재하지 않는 것 같고 일반적으로 영어권에서 "Jumping into the middle of an instruction" 방식으로 불리는 것 같다. 우리는 PUSHAD로 시작한 것을 알았으니 POPAD로 복구할 수 있겠다는 예상을 할 수 있다. 차근차근 분석해 보면 GetModuleHandleA() 함수로 KERNEL32.dll의 핸들을 얻어오고 여기서 GetProcAddress()로 VirtualAlloc(), VirtualFree(), VirtualProtect()를 임포트한다. 


  ASPack에서는 UPX와는 다르게 압축 해제를 위해서 메모리 할당이 필요하다. VirtualAlloc()으로 메모리를 할당하고 디코딩하고 VirtualFree()로 할당된 메모리를 해제하는 것을 반복한다. 직접 확인해보고 싶다면 Ollydbg의 Memory map 패널을 켜놓고 VirtualAlloc()을 실행하면 새로운 영역이 생기는 것이 눈에 띌 것이다. 


  이제 다른 패커들과 같이 GetProcAddress()와 GetModuleHandle()로 Import Table을 복구한다. 그리고 VirtualProtect()로 헤더 속성을 변경하고 섹션들의 속성도 변경한다. 예를들어서 .text 섹션의 write 속성을 제거하고 read, execute만 남긴다던지 같이. 마지막으로 복귀할 OEP의 주소를 PUSH하고 RETN함으로써 OEP로 점프한다.



3.4 MUP

  POPAD 찾아서 그다음 부근에서 RETN 명령어를 실행한다. 그러면 OEP로 가게 되는데 참고로 Ctrl+A로 분석해야 제대로된 코드를 볼 수 있다.





4. Petite

라이선스 : 프리웨어

상태 : 최신(version 2.4 - 2015.10)

주소 : http://www.un4seen.com/petite/


4.1 개요

  Petite는 과거 수 년 동안 업데이트가 되지 않았지만 어떤 이유에선지 최근에 2.4로 업데이트가 되었다. 프리웨어이지만 압축 알고리즘 조차 알려져 있지 않다. 옵션을 보면 압축 수준을 1부터 9까지 정할 수 있고 "Virus detection" 기능을 사용하면 로드 시에 자체 검사하여 메시지를 보여준다. 이 외에도 "Strip debug info", "Compress exports", "Compress exports and EXE table", "Mangle imports", "Strip relocations" 등의 옵션도 존재한다.



4.2 Packed PE

  패킹된 바이너리를 보면 이름없는 섹션과 .rsrc 섹션, .petite 섹션이 존재한다. 이것은 .text, .rdata, .data, .idata 같은 섹션들이 압축되어 이름 없는 섹션에 하나로 통합되어 있고 리소스 섹션은 건드리지 않으며, Import Table은 .petite 섹션에 존재한다는 것을 보여준다.


  다른 패커보다는 임포트하는 함수들이 많은데 기본적으로 kernel32.dll에서는


ExitProcess(), GetModuleHandleA(), GetProcAddress(), VirtualProtect(), VirtualAlloc(), VirtualFree(), LoadLibraryA()


를 임포트하고, user32.dll에서는 다음을 임포트한다.


MessageBoxA(), wsprintfA()



4.3 알고리즘

  상당히 특이하다. 먼저 EP는 임포트 섹션인 .petite 섹션의 주소를 EAX에 넣고 PUSHAD 함으로써 시작한다. 그리고 VirtualAlloc()으로 메모리를 할당하고 현 프로시저의 바로 아래에 위치한 디코딩 루프를 호출한다. 이 디코딩 루프는 할당된 영역에 진짜 디코딩 루틴을 쓰기 위한 디코딩 루틴이다. 이것이 반환되면 RETN하게 되는데 이 때 스택에 아까 할당했던 주소가 들어가 있어서 EIP가 할당한 메모리로 이동되며 다른(실제) 디코딩 루프가 수행된다.


  이 디코딩 루프는 원래의 각 섹션 별로 VirtualProtect()로 쓰기 속성을 추가한 후에 디코딩을 수행하고 디코딩 된 내용을 써 넣는다. 그리고 디코딩 루프가 끝나면 일반적인 패커와 같이 Import Table을 복구한다. 이 때 사용되는 API들도 일반적인 GetModuleHandleA()(또는 LoadLibraryA())와 GetProcAddress()이다. 그리고 VirtualProtect()로 쓰기 속성을 제거한 후에 POPAD를 하고 다음 명령어를 수행한다.


00460308   FF60 20     JMP DWORD PTR DS:[EAX+20]           ; KERNEL32.VirtualFree


  참고로 이 때 인자로 4개를 스택에 넣는데 마지막에 넣는 인자(EDX)가 OEP이다. 그래서 특이하게도 (JMP 명령어이므로) 이 API의 내부로 들어가게 되고 내부에서 일이 다 끝나서 retn을 수행하면 아까 넣었던 마지막 인자(OEP)로 인해 OEP로 리턴하게 된다.



4.4 MUP

  할당된 영역(실제 디코딩 루틴)에서 +308이 VirtualFree()를 호출하는 JMP이다. 그러므로 EP가 존재하는 프로시저의 마지막 RET으로 간 후에 Step Into하면 할당된 영역으로 가는데 그 주소보다 +308인 곳에 JMP 명령어가 있다. 이 명령어를 실행하기 전에 인자를 보면 마지막 인자가 OEP이다. 즉 EDX에 들어가 있는 인자를 말한다.





5. MEW

라이선스 : 프리웨어

상태 : 2004년 (MEW11 SE v1.2)

주소 : http://www.softpedia.com/get/Programming/Packers-Crypters-Protectors/MEW-SE.shtml


5.1 개요

  MEW는 어느 정도 알려진 패커 중 하나이지만 그만큼 오래되엇기도 하다. 이것은 LZMA와 aPlib을 기반으로 만들어졌다. 사실 이건 정확히는 파악할 수 없었던게 옵션 중에서 "use LZMA algorithm too"와 "Special LZMA (E8 E9)"가 있고 이 중 첫 번째는 디폴트로 체크되어 있기 때문이다. 참고로 여기서 분석은 디폴트 옵션으로 하였다. 이 외에도 둘 중 아무것도 선택하지 않을 수도 있고 둘 다 선택할 수도 있다. 참고로 후자만 선택하는 것은 불가능하고 전자가 선택되어야 후자도 선택할 수 있다. 


  다음은 제공하는 특징이다. 


- TLS support

- strip reloc tables

- strip Delphi resources

- strip unused resources

- no antivirus warning (normal header)

- imports handeling but work with no-import files too

- aPPack & LZMA compression

- overlays support(Flash,Multimedia Builder)

- special windows gui

- command line support

  그리고 다음은 알려진 버그들이다.

- no .NET support (work in process)

- no export and delay import support

- no DLL support (work in process)

- no reloc handle support (just strip)

- no 64 bit support

  참고로 위에서 aPPack은 aPLib을 이용한 패커이다.



5.2 Packed PE

  패킹된 바이너리를 보면 섹션이 2개 존재한다는 것을 알 수 있다. 첫 번쨰 섹션은 MEW 섹션인데 이것은 크기가 0이지만 메모리에 올라오면 큰 크기를 갖는데 일종의 UPX0 섹션과 같다고 여기면 된다. 또 하나의 섹션은 이름이 깨져 있는데 여기에 인코딩된 코드와 데이터, 그리고 Import Table까지 들어가 있다. 참고로 Import Table은 마지막 부분에 붙어있다.


  올리디버거에서 메모리에 올라간 것을 보면 앞 부분은 0으로 채워진 MEW 섹션이 차지하고 있고 뒷부분에는 인코딩된 데이터들, 그뒤에 바로 Import Table이 있는 것을 볼 수 있는데 특이한 점은 이 Import Table 바로 다음에 EP가 존재한다는 것이다. 이 EP는 단지 JMP문(헤더 부분으로 가는) 한 개이다.


  Import Table을 보면 임포트 하는 함수는 LoadLibraryA()와 GetProcAddress() 두 개이다. 



5.3 알고리즘

  위에서도 설명했지만 EP에는 JMP문이 있는데 신기하게도 헤더 영역으로 점프한다. 간단한 예를 들면 보통 코드 섹션이 00401000에 시작하는 일반적인 프로그램일 경우 헤더 부분은 00400000에서 시작한다. MEW로 패킹된 바이너리는 기본 헤더들 바로 뒤에 디코딩 루틴이 붙어있는 것이다. 참고로 Ollydbg 2의 경우 "Analysis Code"를 해제하여야 코드를 볼 수 있다.


  이 부분을 수행하다 보면 헤더 부분까지 가서 몇몇 명령어를 실행하는 것을 볼 수 있는데 헤더에서 쓸모없는 작은 부분까지도 명령어를 저장할 위치로 사용할 정도로 개발자가 프로그램의 크기를 줄이는데 큰 관심을 보인 것을 알 수 있다. 디코딩을 수행하다 보면 깨진 이름의 섹션으로 이동해서 디코딩을 수행하기도 하는데 마지막에는 Import Section을 복구하고 RETN을 하면 OEP로 이동한다.


  옵션이 추가될 때마다 용량은 더 줄어들지만 알고리즘은 옵션들에 따라 큰 차이는 나지 않는 것으로 보인다.



5.4 MUP

  헤더 영역으로 JMP해서 명령어 몇 개 실행하다 보면 PUSH EAX를 수행하는데 여기에 들어있는 값이 OEP이다. 또는 헤더 영역으로 점프했을 때의 그 프로시저의 가장 마지막에 RETN을 수행하면 OEP로 이동할 수 있다.





6. MPress

라이선스 : 프리웨어

상태 : 2012년 (v2.19)

주소 : https://autohotkey.com/mpress/mpress_web.htm


6.1 개요

  MPress는그래도 윈도우 7까지는 지원하는 나름대로 최신 패커 중 하나이다. 이것은 LZMAT라는 알고리즘을 사용한다. 이것은 LZ77 알고리즘과 매우 비슷하지만 몇몇 이점이 있다고 하는데 Custom-made라서 문서화가 되어있지 않다고 한다. 어떤 문서에 따르면 작은 파일에는 LZMAT를, 큰 파일에는 LZMA를 사용한다고 하며 이것들의 차이는 EP를 보면 구별할 수 있다고 한다. 옵션을 보면 "force to use LZMAT"라는 옵션이 존재한다.



6.2 Packed PE

  섹션을 보면 .MPRESS1와 .MPRESS2가 있으며 리소스 섹션이 존재할 시에는 .rsrc 섹션도 그대로 존재한다. .MPRESS1 섹션에는 인코딩된 데이터가 존재하며, .MPRESS2 섹션에는 Import Table과 바로 뒤에 디코딩 루틴이 존재한다. 임포트하는 함수는 GetModuleHandleA(), GetProcAddress() 두 가지이다. 



6.3 알고리즘

  EP는 .MPRESS2 섹션의 Import Table 바로 뒤에서 시작하며 첫 명령어는 PUSHAD이다. 그냥 분석해대던 패커들과는 달리 CALL 명령어를 사용해서 디버깅하기 은근히 귀찮고 까다롭게 만들어 놓았다. 


  조금 자세히 말하자면 초반 부분에 나오는 큰 루프는 그냥 Step Over하면 된다. 이 부분은 언패킹 루틴으로써 상당히 크다. 이 부분을 넘어서 진행하다 보면 윗 부분으로 멀리 JMP하는 부분을 볼 수 있다. 여기서부터는 VirtualProtect()를 임포트하고 헤더 영역을 수정하는데 .MPRESS1과 .MPRESS2 섹션에 쓰기 가능 속성을 제거한다. 또한 이후 Import Table을 복구하고 POPAD를 한 후에 JMP 명령어로 OEP로 이동한다. 


  생각보다는 간단하게 분석할 수 있지만 처음부터 Ollydbg 2의 분석이 먹히는 것도 아니고(후반부에는 Analyse Code가 통해서 조금 더 보기 편해진다) CALL을 이용한 "Jumping into the middle of an instruction" 기법도 있고 해서 은근히 불편하다. 하지만 디코딩 루틴만 건너 뛴다면 그다지 난이도 높은 기술은 필요 없다.


  LZMAT 옵션을 사용해서 패킹한 것도 그다지 눈에 띄는 차이점은 없는것으로 보인다.



6.4 MUP

PUSHAD한 위치에 H/W BP를 걸면 된다.





7. Kkrunchy

라이선스 : BSD (소스 공개)

상태 : 2006년 (v0.23a, v0.23a2)

주소 : http://www.farbrausch.de/~fg/kkrunchy/, 소스 : https://github.com/farbrausch/fr_public

기타 정보 : https://fgiesen.wordpress.com/2011/01/24/x86-code-compression-in-kkrunchy/http://www.pouet.net/prod.php?which=26088


7.1 개요

  kkrunchy는 현재 두 버전이 있는데 v0.23a와 v0.23a2가 그것이다. 알파 버전은 매우 기본적인 LZ + arithmetic 알고리즘(LZMA가 아니다)을 사용한다. 이것은 압축이 느리지만 더 빨리 압축 해제를 수행한다. 알파2 버전은 PAQ, crinkler와 비슷한 context mixing 기반 알고리즘이다. 이것은 알파 버전보다는 압축이 빠르지만 압축 해제는 좀 더 느리다. 옵션을 보면 디폴트는 good packer frontend를 사용하는 것이고 best packer frontend를 사용할 수도 있다. 이것은 더 느리다고 한다. 



7.2 Packed PE

  기본적으로 PEview에서 잘 인식하지 못하지만 리소스 섹션을 포함해서 모든 섹션들이 따로 구분되어 있지는 않다. 임포트하는 API들은 LoadLibraryA()와 GetProcAddress()이다.



7.3 알고리즘

  먼저 알파 버전부터 설명한다. EP는 전체 섹션의 가장 처음 부분이다. 처음 프로시저는 먼저 특정 영역에 디코딩 루틴을 생성한다. 진행하다 보면 이 루틴이 끝나기 전에 LoadLibraryA()와 GetProcAddress()로 Import Table을 복구한다. 이 처음 프로시저가 RETN하면 지금까지 썼던 영역으로 이동한다. 이곳에서 디코딩 루프를 진행하다 보면 (사실 직접 살펴봐도 될 정도로 거의 바로 아랫 부분에 존재한다) 루프들 중에서 아래와 같은 명령어가 있을 것이다.


005F0A29   > \3B75 00           CMP ESI, DWORD PTR SS:[EBP]

005F0A2C   .  0F84 07CB0100    JE 0060D539


저 JE의 주소값이 OEP이다.


  알파2 버전은 처음 부분이 상당히 다르다. EP부터 굉장히 큰 프로시저가 존재한다. 그렇기 때문에 올리디버거2의 Analyse Code를 수행하고 이후 명령어들을 살펴보다 보면 MOV EBX, <&KERNEL32.LoadLibraryA> 부분이 있고 아래에 CALL DWORD PTR DS:[EBX] 부분이 있는걸로 봐서 여기가 디코딩 루틴 수행 후에 실행되는 임포트 테이블 복구 루틴인 것을 알 수 있다. 이 부분을 실행하면서 분석하다 보면 마지막에 POP EBP와 RETN이 나온다. 이제 알파 버전과 같이 생성된 디코딩 루틴으로 가게 되고 여기서도 똑같이 JE 부분을 보면 알 수 있다.



7.4 MUP

  처음 디코딩 루틴을 생성하는 부분에서 복잡한 부분이 존재하는데 LoadLibraryA()와 GetProcAddress()를 호출할 만한 부분을 찾아서 BP를 걸어야 한다. 이후 Import Table 복구 루프를 지나 ret을 하고 이후 부터는 쉽게 찾을 수 있다.





8. RLPack Basic

라이선스 : GPL (소스 공개)

상태 : 2008 (v1.21)

주소 : http://www.softpedia.com/get/Programming/Packers-Crypters-Protectors/RLPack-Basic-Edition.shtml


8.1 개요

  패커를 다운로드 받으면 소스 코드도 같이 들어있다. 알고리즘은 패킹할 때 선택할 수 있는데 aPlib 0.43과 LZMA 4.30이 있다. 옵션을 보면 이 외에도 "Strip TLS", "Strip relocations", "Strip export table", "Don't strip unimportant resources", "Use Windows DLL loader", "Preserve overlay" 등이 존재한다. 특징으로는 Upack처럼 V3가 바로 제거해 버린다.



8.2 Packed PE

  패킹된 바이너리를 보면 .packed 섹션과 .RLPack 섹션으로 나뉜다. .packed 섹션은 크기가 0인 UPX0 섹션과 같은 형태이고 .packed 섹션에는 리소스와 Import Table이 존재한다. 임포트하는 API들은 다음과 같다. 이 중에서 VirtualProtect()는 사용되지 않는 것으로 보이는데 확실치 않다.


LoadLibraryA(), GetProcAddress(), VirtualAlloc(), VirtualFree(), VirtualProtect()


  기본적인 형태는 aPlib 0.43과 LZMA 4.30 모두 동일하다.



8.3 알고리즘

  aPlib 0.43 알고리즘부터 보겠다. EP는 PUSHAD로 시작한다. 이후 .RLPack 섹션의 내용을 디코딩하여 .packed 섹션으로 풀어 쓴다. 그리고 VirtualAlloc()으로 메모리를 할당하고 이곳에 Import Table의 인코딩된 버전을 쓴다. 이후 할당된 메모리에서 라이브러리의 이름을 읽어와(라이브러리 이름은 인코딩되어 있지 않고 각 API 이름만 인코딩되어 있다) LoadLibraryA()로 로드한 후에 인자가 2개인 함수(이 함수 내부에서 GetProcAddress()를 호출하며 인자로는 받는 값은 할당된 메모리의 값으로서 인코딩된 API 이름이다)를 이용해 Import Table을 복구한다. VritualFree()로 할당된 메모리를 해제한다. 이후 POPAD 후 JMP하면 OEP로 이동한다.


  LZMA 4.30 알고리즘은 aPlib 0.43 알고리즘과 디코딩 루틴에서만 차이가 있다. 즉 PUSHAD로 시작한다는 것이나 Import Table을 복구하는 것 부터는 모두 동일하다. 디코딩 루틴은 VirtualAlloc()를 통한 할당된 메모리를 사용한다는 점에서 차이가 있다.


  디코딩 루틴을 호출하는 부분이 CALL로 구별하기 쉽게 보이고 기본적인 프로시저가 깔끔하므로 디코딩 루틴을 깊게 들어가지 않는 이상 간단하게 언패킹 과정을 분석할 수 있다.



8.4 MUP

  전형적인 PUSHAD와 POPAD를 이용한 방식을 사용하면 된다.





9. FSG 1.33

라이선스 : 프리웨어

상태 : X

주소 : http://freestyler03.uw.hu/tools.html (공식 사이트는 아니지만 유일하게 찾은 링크이다. 압축을 풀면 이상한 파일들도 존재하는데 설치하지 말고 안에 있는 폴더에 들어가면 독립된 실행 파일이 존재한다.)


9.1 개요

  FSG는 2002년 1월 14일 dulek와 bart가 만들었으며 2002년 11월 15일 발표된 것이 1.33버전이다. FSG는 aPLib을 사용하는 것으로 알려져 있다. 특이사항은 최근 VC++로 컴파일한 실행 파일들에는 제대로 동작하지 않는다는 점이 있다. 따로 사용할 수 있는 옵션은 없다.



9.2 Packed PE

  특별한 이름의 섹션을 갖지는 않고 PEview로 보면 그냥 SECTION 안에 인코딩된 부분과 Import Table이 존재하는 것을 알 수 있다. 참고로 EP 즉 복호화 루틴은 저 통합된 섹션의 끝 부분 쯤에 존재한다. 임포트하는 함수는 평범하게 LoadLibraryA(), GetProcAddress()가 있다.



9.3 알고리즘

  JMP로 끝나는 몇 겹의 복호화 루틴이 존재하는데 다른 것들과 비교해서 상당히 구분하기 쉬운 편이다. 기본적인 방식은 ESI의 주소에 있는 인코딩된 내용을 디코딩해서 EDI의 주소에 쓰는 것이다. Import Table 복구 루틴도 똑같이 매우 간단하데 복호화 루틴의 거의 바로 뒤에 나온다.



9.4 MUP

  특이하게 이것은 OEP로 가는 명령어가 하드코딩되어 있다. Import Table 복구 루틴 중간 부분에 JZ 명령어로 존재하며(복호화 루틴 내의 주소가 아니라서 쉽게 구분할 수 있다) 복구를 마친 후에 바로 이동하게 된다. 이 명령어가 뛰는 곳에 BP를 걸면 된다.





10. FSG 2.0

라이선스 : 프리웨어

상태 : X

주소 : http://freestyler03.uw.hu/tools.html (공식 사이트는 아니지만 유일하게 찾은 링크이다. 압축을 풀면 이상한 파일들도 존재하는데 설치하지 말고 안에 있는 폴더에 들어가면 독립된 실행 파일이 존재한다.)


10.1 개요

  FSG 2.0은 2004년 5월 24일 bart가 발표한 버전이다. 이것도 마찬가지로 최근 버전의 VC++에서 만든 실행 파일에는 제대로 동작하지 않는다. 이것도 마찬가지로 따로 사용할 수 있는 옵션은 없다.



10.2 Packed PE

  FSG 1.33 버전과 같다. 임포트하는 함수들도 같다.



10.3 알고리즘

  1.33 버전과 달리 섹션 헤더 부분에 EP 즉 복호화 루틴이 존재한다. 예를들면 나의 경우 EP는 00400154이다. 복호화 루틴은 1.33 버전보다 더 간단해 보인다. 기본적으로 ESI와 EDI를 이용한 방식이다. 또한 JMP 명령어로 끝나는 몇 겹의 루프문으로 둘러싸여 있다. 이후에 거의 바로 Import Table 복구 루틴이 존재한다.



10.4 MUP

  이것도 Import Table 복구 루틴 안에 OEP로 가는 분기문이 존재한다. 나의 경우는 다음과 같다.


004001CD JS SHORT 004001C2

004001CF JNE SHORT 004001D4

004001D1 JMP DWORD PTR DS:[EBX+0C]

004001D4 PUSH EAX


  이런 식으로 되어 있어서 저 JNE가 PUSH EAX 명령어로 뛰지 않고 JMP 명령어가 실행될 때 가는 곳이 OEP이다. 그러므로 그냥 간단하게 저 JMP 명령어에 BP를 걸면 된다.





11. nPack

카피라이트 : Underground InformatioN Center (url : uinc.ru)

상태 : 2008.3.3 (v1.1.800.2008)

주소 : 소스코드 구입(http://www.shareit.com/product.html?productid=300161907)



11.1 개요

  NEOx(neox@petools.org.ru)가 만들었으며 소스 코드는 위의 주소에서 부가세 포함 11달러에 구매할 수 있다. 옵션에 나온 기능을 제외하고 기본적인 특징은 다음과 같다. 제공되는 자료에 따르면 윈도우 비스타 x32까지 지원된다고 한다.


- exe, dll, ecx 등 모든 종류의 PE 파일 지원

- 코드, 데이터, 리소스 압축

- 섹션 이름 짓기 제공

- 빠른 복호화 루틴

- Relocation 지원

- TLS 지원

- Strip debug information

  지원하는 옵션은 다음과 같다.

- Compress resources

- Create backup copy(.bak file)

- Strip Relocations

- Rebuild file

- Save overlay (for installers)

- Skip Shared Sections



11.2 Packed PE

  PE를 보면 기본 섹션들은 그대로 있는데 안의 내용은 거의 지워져 있다. 추가적으로 디폴트 옵션인 경우 .nPack 섹션이 생성되며 여기에 인코딩된 내용들과 Import Table, 그리고 복호화 루틴이 있다. 즉 EP도 .nPack 섹션에 존재한다. 임포트하는 함수는 다음과 같다.


kernel32.dll : LoadLibraryA(), ExitProcess(), GetProcAddress()

user32.dll : wsprintfA(), MessageBoxA()


  최신 VC++로 컴파일한 실행 파일에는 제대로 동작하지 않는다.



11.3 알고리즘

  Ctrl+A로 분석해보면 쉽게 구조를 파악할 수 있다. 현재 EP가 존재하는 프로시저를 보면 마지막에 2개의 RETN이 존재하는 것을 볼 수 있다. 이 중에서 첫 RETN이 실행되면 OEP로 이동한다. 이 사이에는 약 6개의 CALL문이 존재한다.


  첫 호출문은 LoadLibraryA()로 kernel32.dll을 로드하고 GetProcAddress()로 VirtualAlloc(), VirtualFree()의 주소를 얻어온다. 두번째 호출문에서는 VirtualAlloc()으로 메모리를 할당하고 복호화 루틴을 수행하며 마지막으로 할당한 메모리를 VirtualFree()로 회수한다. Import Table은 4번째 호출문에서 복구된다.



11.4 MUP

  첫 프로시저의 끝 부분에 존재하는 2개의 RETN 중에서 첫번째 RETN을 실행하면 OEP로 이동한다.



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

블로그 이미지
Malware Analyst
SanseoLab

태그목록

공지사항

Yesterday
Today
Total

달력

 « |  » 2025.2
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

최근에 올라온 글

최근에 달린 댓글

글 보관함