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

블로그 이미지
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

최근에 올라온 글

최근에 달린 댓글

글 보관함