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

블로그 이미지
Malware Analyst
SanseoLab

태그목록

공지사항

Yesterday
Today
Total

달력

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

최근에 올라온 글

최근에 달린 댓글

글 보관함