Win32 실행파일 (PE) 의구조 Windows 운영체제실행파일의구조에대하여알아보자. 2016.05.10 Kali-KM
2 목차 1. 개요... 5 2. PE 파일의전체구조... 6 3. PE 분석을위한개념정리... 8 (1) RVA (Relative Virtual Address)... 8 (2) Section... 8 (3) VAS (Virtual Address Space)... 10 4. IMAGE_DOS_HEADER & IMAGE_DOS_STUB... 13 5. IMAGE_NT_HEADER... 15 (1) IMAGE_FILE_HEADER... 15 (2) IMAGE_OPTIONAL_HEADER... 16 (3) IMAGE_SECTION_HEADER... 19 6. Section... 22 (1) Code Section... 22 (2) Data Section... 23 (3) Export Section... 24 (4) Import Section... 25 (5) Relocation Section... 28 7. API Hooking... 31 (1) DLL Injection... 31 (2) IAT Hooking... 33
3 그림 그림 1. HxD로본 PE 구조... 5 그림 2. 일반적인 PE 구조의형태... 6 그림 3. PE View로본 IMAGE_DATA_DIRECTORY... 7 그림 4. 대표적인섹션의종류... 10 그림 5. CreateFileMapping API... 10 그림 6. flprotect 항목... 11 그림 7. MapViewOfFile API... 11 그림 8. dwdesiredaccess 항목... 11 그림 9. UnmapViewOfFile API... 12 그림 10. CloseHandle API... 12 그림 11. 파일과프로세스에서의 PE 시작점... 12 그림 12. IMAGE_DOS_HEADER 구조체... 13 그림 13. 예제.exe의 IMAGE_DOS_STUB... 13 그림 14. 필수적이지않은요소제거... 14 그림 15. IMAGE_NT_HEADER 구조체... 15 그림 16. IMAGE_FILE_HEADER 구조체... 15 그림 17. Characteristics의주요 PE 특성... 16 그림 18. IMAGE_OPTIONAL_HEADER 구조체... 17 그림 19. IMAGE_DATA_DIRECOTRY 구조체... 18 그림 20. IMAGE_DATA_DIRECTORY ENTRY... 19 그림 21. IMAGE_SECTION_HEADER 구조체... 20 그림 22. IMAGE_SECTION_HEADER 속성값... 21 그림 23. 예제.exe의.text 섹션... 22 그림 24. 예제.exe의.text 섹션 어셈블리코드... 22
4 그림 25. 예제.exe의몇가지필드값... 23 그림 26. 예제.exe의.data Section... 23 그림 27. IMAGE_EXPORT_DIRECTORY 구조체... 24 그림 28. 예제.dll의.edata 섹션... 25 그림 29. IMAGE_IMPORT_DESCRIPTOR 구조체... 26 그림 30. INT와 IAT... 26 그림 31. IAT에함수주소기록과정... 27 그림 32. IMAGE_BASE_RELOCATION 구조체... 28 그림 33. TypeOffset 엔트리구조... 28 그림 34. a.dll의재배치섹션... 29 그림 35. 재배치해야할주소확인... 29 그림 36. 재배치할주소의명령어... 29 그림 37. CreateRemoteThread API... 31 그림 38. AppInit_DLLs Registry Key... 32 그림 39. 메시지전달방식... 32 그림 40. SetWindowsHookEx API... 32 그림 41. SetWindowsHookEx를이용한후킹... 33 그림 42. 일반적인 API 호출과정... 33 그림 43. 후킹된 API 호출과정... 34
5 1. 개요 우리가컴퓨터로무엇인가작업하기위해서는언제나특정프로그램을실행시킨다. 이러한실행파일또는응용프로그램이라불리는 EXE 파일말고도프로그램실행을위한 DLL 파일도프로그램실행시에같이물려메모리상에로드된다. 이러한 EXE 파일관련 DLL 파일들이메모리상에로드되면서비로소프로그램이라는것이사용가능하게되고이렇게로드된하나의 EXE와여러개의관련 DLL들이소위운영체제론에서이야기하는하나의프로세스를구성하게된다. 그림 1. HxD로본 PE 구조이러한실행파일들은항상 MZ라는식별가능한문자로시작하는데이는무의미한문자가아니라 PE(Portable Executable) 구조로된 PE 파일들을나타낸다. PE파일은이름과같이플랫폼에관계없이 Win32 운영체제가돌아가는시스템이면어디서든실행가능하다는의미를지니고있다. 따라서우리는이러한 PE파일의구조를중점적으로알아볼것이다
6 2. PE 파일의전체구조 PE 파일의전체적인구조에대하여알아보자. PE 파일은아래의그림과같은형태로 MZ 가위치하고있는 IMAGE_DOS_HEADER를시작으로프로그램의많은정보를구조체형태로포함하고있다. 도스헤더의경우 PE파일임을구별할수있도록시작부분에 MZ Signature(4D5A) 로시작한다. 그다음도스스텁이나오는데이는필수적이지않은존재로 16 Bit 환경에서출력될문자열인 This program cannot be run in DOS mode 라는문자열등을포함하고있다. 그림 2. 일반적인 PE 구조의형태그다음본격적인 PE Signature(5045) 가존재하고있는 IMAGE_NT_HEADERS로이부분은크게 IMAGE_FILE_HEADER와 IMAGE_OPTIONAL_HEADER 두부분으로나눌수가있다. FILE 헤더의경우 PE 파일에대한파일정보를나타내고 OPTIONAL 헤더의경우 PE 파일이메모리에로드될때
7 필요한모든정보들을담고있다. OPTIONAL_HEADER 내에는기본필드들과함께주요섹션들의 위치와크기를나타내는 IMAGE_DATA_DIRECTORY 구조체배열을담고있다. 이에대해선추후에 더자세히설명할것이다. 그림 3. PE View로본 IMAGE_DATA_DIRECTORY IMAGE_DATA_DIRECTORY를끝으로 IMAGE_SECTION_HEADER가여러개나오는데, 이는섹션테이블로각섹션의위치와크기등의정보를포함하고있다. MZ헤더부터섹션테이블까지 PE 파일헤더라하며, PE 헤더뒷부분부터는실제코드나데이터들이성격에맞게각각의섹션에위치하고있다.
8 3. PE 분석을위한개념정리 PE 파일구조를분석하기전에알아야할내용들에대하여언급할것이다. RVA, Section, MMF, VSA 에대하여알아보자. (1) RVA (Relative Virtual Address) RVA는상대적가상주소로파일 Offset과는다른개념이다. Offset은파일에서의위치를나타낼때사용하는개념이지만 RVA는가상주소공간상의위치를나타낼때사용하는개념으로메모리상에서의 PE의시작주소에대한오프셋으로생각하면된다. 그렇다면메모리에서는왜 Offset 이나 VA가아닌 RVA로나타낼까? 이는 PE 파일이지정된베이스위치 (ImageBase) 를기준으로로딩된다는보장이없기때문이다. EXE 파일의경우일반적으로파일이지정된위치에로드된다. DLL의경우일반적으로 ImageBase 값이 0x1000000으로설정되어있지만하나의프로세스에는여러개의 DLL이존재하고있기때문에 ImageBase를기준으로할경우중첩된다. 이러한중첩을방지하기위해 DLL Relocation이존재하고있으며이러한이유로인해절대주소가아닌상대주소를사용한다. 만약 ImageBase가 0x2000000이며 RVA 값이 0x1234라고한다면가상주소의값은 0x02001234이되는것이다. (2) Section PE 파일에서섹션은 PE가가상주소공간에로드된다음실제내용을담고있는블록들이다. 대 표적인내용으로는명령어코드와데이터이며, 그외에실행에관련된여러정보들이섹션에배 치된다. 대표적으로언급할만한섹션들에대하여간단히알아보자. 종류 이름 설명 코드.text 프로그램을실행하기위한코드를담고있는섹션으로, 명령포인터는이섹션내에존재하는번지값을담게된다. 데이터.data 초기화된전역변수들을담고있는읽고쓰기가능한섹션이다..rdata 읽기전용데이터섹션으로문자열표현이나 C++/COM 가상함수테이블등이.rdata에배치되는항목중의하나이다..bss 초기화되지않은전역변수들을위한섹션이다. 실제 PE 파일내에서는존재하지만가상주소공간에매핑될때에는보통.data 섹션에병합되어메모리상에서는따로존재하지않는다. Import API 정보.idata 임포트할 DLL과그 API들에대한정보를담고있는섹션이다. 대표적으로 IAT가존재한다.
9.didat 지연로딩 (Delay-Loading) 1 임포트데이터를위한섹션으로지연 로딩은 Windows 2000부터지원되는 DLL 로딩의한방식으로암시적인방식과명시적인방식의혼합이다. Export API 정보.edata 익스포트할 DLL과그 API들에대한정보를담고있는섹션이다. 보통 API나변수를익스포트할수있는경우는 DLL이기때문에 DLL PE에이섹션이존재한다. 리소스.rsrc 다이얼로그, 아이콘, 커서등의윈도우 APP 리소스관련데이터들이이섹션에배치된다. 재배치정보.reloc 실행파일에대한기본재배치정보를담고있는섹션이다. 재배치란 PE 이미지를원하는기본주소에로드하지못하고다른주소에로드했을경우코드상에서의관련주소참조에대한정보를갱신해야하는경우를말한다. 위에서언급한바와같이주로 DLL 파일에서재배치가일어난다. TLS.tls declspec(thread) 지시어와함께선언되는스레드지역저장소를위한섹션이다. 이섹션에는런타임이필요로하는부가적인변수나 declspec(thread) 지시어에의한데이터의초기값을포함한다. C++ 런타임.crt C++ 런타임 (CRT) 을지원하기위해추가된섹션으로정적 C++ 객체의생성자와소멸자를호출할때이용되는함수포인터가예이다. Short.sdata 전역포인터에상대적으로주소지정될수있는읽고쓰기가능한 Short 데이터섹션이다. IA-64 같은전역포인터레지스터를사용하는플랫폼을위해사용된다..srdata.sdata에들어갈수있는데이터들의읽기전용섹션이다. 예외정보.pdata IMAGE_RUNTIME_FUNCTION_ENTRY 구조체의배열을가지며예 외 정보를 담고 있는 섹션이다. 이 섹션의 위치는 IMAGE_DIRECTORY_ENTRY_EXCEPTION 슬롯을통해알수있으며, Table-base exception handling을사용하는플랫폼에서지원된다. 이를지원하지않는유일한플랫폼은 x86 계열의 CPU이다. 디버깅.debug$S OBJ파일 2 에존재하는가변길이코드뷰심벌레코드의스트림이다..debug$T OBJ파일에존재하는가변길이코드뷰심벌레코드의스트림이다. 1 프로그램실행시로드되는 DLL이많을경우초기화시간이길어지기에, 프로세스수행중해당함수가호출되는시점에서 DLL을로딩하도록하여초기화시간을단축하고자할때사용한다. 2 컴파일러에의해생성되는기계어명령문을 Object Code 라하며, 이를포함한디스크상의파일을 Object File 이라고한다.
10.debug$P 미리컴파일된헤더를사용했을때 OBJ 파일에만존재한다. Directives.drectve OBJ 파일에만존재하는섹션으로 Directives란링커명령라인을통해전달할수있는 ASCII 문자열을말한다. 그림 4. 대표적인섹션의종류 (3) VAS (Virtual Address Space) 마지막으로고려할것은파일로존재하는 PE구조와이것이메모리에올라올때주어지는가상주소공간 (VAS) 에서의 PE 구조에대한관계이다. 이를위해 MMF(Memory Mapped File) 에대하여먼저알아보자. 32비트환경에서프로세스는 4GB의 VAS를갖는데, 이가상공간을실제의물리적인기억장치와연결시켜주는것이가상메모리관리자 (Virtual Memory Manager, VMM) 이다. 여기서물리적기억장치는 RAM 뿐만아니라하드디스크상의특정파일 ( 기본적으로는 PageFile.sys) 을포함한다. 이와함께페이징기법을통해프로세스에게실제로 4GB의주소공간을가진것처럼사용할수있다. 페이징파일과 RAM 그리고 VAS는 VMM에의해관리되며프로세스에속한특정스레드가가상주소공간내의특정번지에접근하고자할때, VMM은해당번지의페이지를페이징파일과매핑시켜준다. 매핑된페이지는접근가능한상태가되며, 여기서이러한페이징은반드시 PageFile.sys하고만이루어져야하는것은아니라일반파일을메모리에맵핑하여이에대해페이징할수있다. 이처럼일반적인파일이 PageFile.sys의역할을대신하는경우를 MMF라고한다. 이제 MMF를사용하기위한함수에대하여알아보자. HANDLE CreateFileMapping( HANDLE hfile, LPSECRITY_ATTRIBUTES lpattributes, DWORD flprotect, DWORD dwmaximumsizehigh, DWORD dwmaximumsizelow, LPCTSTR lpname); 그림 5. CreateFileMapping API CreateFileMapping API는운영체제에게매핑을수행할파일의물리저장소를알려주기위한 API 이다. 이를통해지정파일을파일매핑오브젝트와연결시키며, 파일매핑오브젝트를위한충분한물리저장소가존재한다는것을확인시킨다. hfile의경우 CreateFile() 과같은 API를통해얻은파일의핸들로물리저장소로사용할파일의핸들을주어야한다. flprotect는 MMF의페이지속성을지정하는데 PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY와같은세가지보호속성을기본으로가진다. 이세가지보호속성외에다섯가지메모리매핑파일만의속성을추가로지정할수있다.
11 속성설명 SEC_NOCACHE 메모리매핑파일에대한캐싱을수행하지못하게한다. SEC_IMAGE 매핑한파일이 PE파일이미지임을알려주므로실행파일실행시사용 SEC_RESERVE 이두개는배타적으로사용되어야한다. 스파스메모리맵파일과관 SEC_COMMIT 련이있다. SEC_LARGE_PAGES 큰페이지할당기능과관련있다. 그림 6. flprotect 항목하지만파일매핑오브젝트를생성한다하더라도, 시스템은곧바로프로세스의주소공간상에영역을예약하지않는다. 그렇기에파일의데이터에접근하기위한영역을프로세스주소공간내에확보해야하며, 이영역에임의의파일을물리저장소로사용하기위한커밋단계를거쳐야하며이를위해사용하는 API가바로 MapViewOfFile이다. PVOID MapViewOfFile( HANDLE hfilemappingobject, DWORD dwdesiredaccess, DWORD dwfileoffsethigh, DWORD dwfileoffsetlow, DWORD dwnumberofbytestomap); 그림 7. MapViewOfFile API 첫번째인자는 CreateFileMapping으로얻은핸들을넘겨주면되고두번째인자에사용할수있는항목은아래와같다. 세번째와네번째인자의경우파일의어디부터매핑할것인지지정해주는것으로파일의오프셋값은반드시시스템의할당단위의배수여야한다. 마지막인자의경우얼마만큼해당할지설정하는것으로값이 0일경우오프셋으로부터파일의끝까지구성한다. 항목설명 FILE_MAP_READ CreateFileMapping에서 PAGE_READ_ONLY로설정한경우 FILE_MAP_WRITE CreateFileMapping에서 PAGE_READWRITE로설정한경우 FILE_MAP_ALL_ACCESS FILE_MAP_READ FILE_MAP_WRITE FILE_MAP_COPY와같다. FILE_MAP_COPY CreateFileMapping에서 PAGE_WRITECOPY로설정한경우로, 데이터를쓰면새로운페이지가생성된다. FILE_MAP_EXECUTE 데이터를코드로수행할수있다. 그림 8. dwdesiredaccess 항목이렇게 MMF를형성할수있으며프로세스주소공간내에매핑된데이터파일을더이상유지할필요가없다면 UnmapViewOfFile 함수를호출하여영역을해제해주어야한다. 사용되는인자는하나뿐이며해제할영역의주소를넘겨주면된다.
12 BOOL WINAPI UnmapViewOfFile( _In_ LPCVOID lpbaseaddress); 그림 9. UnmapViewOfFile API 마지막으로이전에얻어온파일오브젝트와파일매핑오브젝트가올바르게반환이이루어질수있도록 CloseHandle API를호출해주어야한다. 사용되는인자는역시하나로핸들을넘겨주어야한다. BOOL WINAPI CloseHandle( _In_ HANDLE hobject); 그림 10. CloseHandle API 이렇게 MMF를사용하는방법에대하여알아보았다. 사실이 MMF에대하여공부하며어느곳에사용해야하는것인지의문을가질수가있다. 이렇게 MMF에대하여자세히알아본이유가무엇일까? 바로 Windows는 EXE나 DLL 등의실행파일을로드할때 MMF를이용한다. 즉, PE 파일을페이징파일로복사하는것이아니라그파일자체를페이징파일로사용한다는것이다. <File> 00400000 4D 5A 6C 00 01 00 00 00 02 00 00 00 FF FF 00400010 00 00 00 00 11 00 00 00 40 00 00 00 00 00 00 00...@... 00400020 57 69 6E 33 32 20 50 72 6F 67 72 61 6D 21 0D 0A Win32 Program!.. 00400030 24 B4 09 BA 00 01 CD 21 B4 4C CD 21 60 00 00 00 $??? 퀽?`... 00400040 47 6F 4C 69 6E 6B 2C 20 47 6F 41 73 6D 20 77 77 GoLink, GoAsm ww 00400050 77 2E 47 6F 44 65 76 54 6F 6F 6C 2E 63 6F 6D 00 w.godevtool.com. 00400060 50 45 00 00 4C 01 05 00 BB F5 17 47 00 00 00 00 PE..L. 새G... <Process> 00000000 4D 5A 6C 00 01 00 00 00 00000010 00 00 00 00 11 00 00 00 40 00 00 00 00 00 00 00...@... 00000020 57 69 6E 33 32 20 50 72 6F 67 72 61 6D 21 0D 0A Win32 Program!.. 00000030 24 B4 09 BA 00 01 CD 21 B4 4C CD 21 60 00 00 00 $??? 퀽?`... 00000040 47 6F 4C 69 6E 6B 2C 20 47 6F 41 73 6D 20 77 77 GoLink, GoAsm ww 00000050 77 2E 47 6F 44 65 76 54 6F 6F 6C 2E 63 6F 6D 00 w.godevtool.com. 00000060 50 45 00 00 4C 01 05 00 BB F5 17 47 00 00 00 00 PE..L. 새G... 그림 11. 파일과프로세스에서의 PE 시작점위두개의바이너리를보자. 파일에서의바이너리와할당된가상주소에서의바이너리가주소만다르지내용은같다는것을확인할수있다. 이는해당파일자체가그대로가상주소공간에매핑된다는것으로, EXE나 DLL 와같은 PE 파일을실행할때 PE 파일내에정의된바와같이가상주소공간에매핑된다는것이다. 지금까지 PE의개략적인구조와 RVA, Section, 그리고 PE와 MMF의관계에대해알아보았다. 다음장부터는구체적으로 PE 파일에대하여알아보자.
13 4. IMAGE_DOS_HEADER & IMAGE_DOS_STUB PE 파일에서가장처음으로등장하는영역은바로도스헤더와도스스텁영역이다. 도스헤더에 는총 64 Bytes 로 19 개의필드를갖지만, 실제로중요한필드는단두개뿐이다. e_magic 필드는 MZ 헤더의시그니처가존재하는필드로 PE 파일이맞는지아닌지체크할때사용되며, 이는도 스헤더의시작을알리는코드라할수있다. e_lfanew 필드는 NT 헤더의시작위치를나타내는 값으로해당오프셋을확인해보면 NT 헤더의시그니처인 PE 가존재하고있다. typedef struct _IMAGE_DOS_HEADER { WORD e_magic; /* 00: MZ Header signature */ WORD e_cblp; /* 02: Bytes on last page of file */ WORD e_cp; /* 04: Pages in file */ WORD e_crlc; /* 06: Relocations */ WORD e_cparhdr; /* 08: Size of header in paragraphs */ WORD e_minalloc; /* 0a: Minimum extra paragraphs needed */ WORD e_maxalloc; /* 0c: Maximum extra paragraphs needed */ WORD e_ss; /* 0e: Initial (relative) SS value */ WORD e_sp; /* 10: Initial SP value */ WORD e_csum; /* 12: Checksum */ WORD e_ip; /* 14: Initial IP value */ WORD e_cs; /* 16: Initial (relative) CS value */ WORD e_lfarlc; /* 18: File address of relocation table */ WORD e_ovno; /* 1a: Overlay number */ WORD e_res[4]; /* 1c: Reserved words */ WORD e_oemid; /* 24: OEM identifier (for e_oeminfo) */ WORD e_oeminfo; /* 26: OEM information; e_oemid specific */ WORD e_res2[10]; /* 28: Reserved words */ DWORD e_lfanew; /* 3c: Offset to extended header */ } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; 그림 12. IMAGE_DOS_HEADER 구조체 IMAGE_DOS_STUB 은아래와같은형태를띄고있으며큰의미를갖지않는다. 아래의표를보면 식별가능한문자열이존재하고있는데이는 MS-DOS 나윈도우 3.1 에서실행하게되면바로이 문장을출력한다. MS-DOS 스텁은위문장을출력하기위한 16 비트도스용응용프로그램이라할 수있다. 00400050 54 68 69 73 20 70 72 6F 67 72 61 6D 20 6D 75 73 This program mus 00400060 74 20 62 65 20 72 75 6E 20 75 6E 64 65 72 20 57 t be run under W 00400070 69 6E 33 32 0D 0A 24 37 00 00 00 00 00 00 00 00 in32..$7... 그림 13. 예제.exe 의 IMAGE_DOS_STUB 이렇게도스헤더와도스스텁에대하여알아보았는데, 결국이두구조체에서필수적인항목은 단두개뿐인것이다. 다시말해, 다른필드의항목들은모두 NULL 이되어도상관없다는것이다. 한번직접코드를비교해보자. 아래는불필요한항목들을제거하지않은상태의코드와불필요한 항목들을제거한다음의코드를비교한것이다.
14 < 수정전 > 00000000 : 4D 5A 50 00 02 00 00 00 04 00 0F 00 FF FF 00 00 MZP... 00000010 : B8 00 00 00 00 00 00 00 40 00 1A 00 00 00 00 00...@... 00000020 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00... 00000030 : 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00... 00000040 : BA 10 00 0E 1F B4 09 CD 21 B8 01 4C CD 21 90 90...!..L.!.. 00000050 : 54 68 69 73 20 70 72 6F 67 72 61 6D 20 6D 75 73 This program mus 00000060 : 74 20 62 65 20 72 75 6E 20 75 6E 64 65 72 20 57 t be run under W 00000070 : 69 6E 33 32 0D 0A 24 37 00 00 00 00 00 00 00 00 in32..$7... < 수정후 > 00000000 : 4D 5A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 MZ... 00000010 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00... 00000020 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00... 00000030 : 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00... 00000040 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00... 00000050 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00... 00000060 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00... 00000070 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00... 그림 14. 필수적이지않은요소제거코드를이렇게수정을해도프로그램이정상적으로구동되는것을확인할수있을것이다. 각필드마다의미를가지고있기는하지만, PE 구조에서는이러한정보들이존재하지않더라도정상적으로구동하도록되어있다.
15 5. IMAGE_NT_HEADER IMAGE_DOS_HEADER 의 e_lfanew 필드값에해당하는위치에 IMAGE_NT_HEADER 가존재하고있 다. 해당구조체에는 PE 와관련된주요필드들이위치해있다. 우선해당위치에는 PE Signature, IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER 로분류할수있다. 아래의구조체를확인해보자. typedef struct _IMAGE_NT_HEADERS { DWORD Signature; /* "PE"\0\0 */ /* 0x00 */ IMAGE_FILE_HEADER FileHeader; /* 0x04 */ IMAGE_OPTIONAL_HEADER32 OptionalHeader; /* 0x18 */ } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; 그림 15. IMAGE_NT_HEADER 구조체 (1) IMAGE_FILE_HEADER IMAGE_FILE_HEADER 에는해당 PE 파일과관련된내용이존재하고있는 20 Bytes 로구성된구조 체이다. 첫번째필드에는 CPU 의 ID 를나타내는것으로세가지주요타입은다음과같이 Intel 386 의경우 0x014C, Intel 64 의경우 0x200, AMD64 의경우 0x8664 의값을갖는다. 두번째필드 의경우본파일에서섹션의수를나타내는것이며세번째필드 TimeDataStamp 는파일이 OBJ 형식의파일이면컴파일러가, EXE 나 DLL 과같은 PE 파일이라면링커가해당파일을만들어낸시 간을의미한다. PointerToSymbolTable 의경우 COFF 심벌의파일오프셋을나타내는것으로, 이필드는컴파일러 에의해생성되는 OBJ 파일이나디버그모드로만들어져 COFF 디버그정보를가진 PE 파일에서 만사용된다. 그다음 NumberOfSybols 는 PointerToSymbolTable 필드가가리키는 COFF 심벌테 이블내의심벌수를나타낸다. IMAGE_FILE_HEADER 다음에는 IMAGE_OPTIONAL_HEADER 가이어 서나오는데바로해당구조체의크기를나타내는것이 SizeOfOptionalHeader 필드이다. 마지막 으로 Chracteristics 필드는해당 PE 파일에대한특정정보를나타내는플래그로주요항목몇가 지가 [ 그림 17] 과같다. typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; 그림 16. IMAGE_FILE_HEADER 구조체 매크로명값의미 IMAGE_FILE_RELOCS_STRIPPED 0x0001 현재파일에재배치정보가없다.
16 IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 본파일은실행파일이미지이다. IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 본파일에라인정보가없다. IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0010 OS로하여금적극적으로워킹셋을정리할수있도록한다. IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 응용프로그램이 2GB 이상의가상주소번지를제어할수있도록한다. IMAGE_FILE_32BIT_MACHINE 0x0100 본 PE는 32비트워드머신을필요로한다. IMAGE_FILE_DEBUG_STRIPPED 0x0200 디버그정보가본파일에없고.DBG 파일에존재한다. IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 PE이미지가이동가능장치위에존재하면고정디스크상의스왑파일로카피해실행한다. IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 PE이미지가네트워크상에존재하면고정디스크상의스왑파일로카피해서실행한다. IMAGE_FILE_DLL 0x2000 본파일은동적링크라이브러리 (DLL) 파일이다. IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 본파일은하나의프로세서만을장착한머신에서실행된다. 그림 17. Characteristics의주요 PE 특성 (2) IMAGE_OPTIONAL_HEADER IMAGE_FILE_HEADER 의뒷부분에나오는 IMAGE_OPTIONAL_HEADER 구조체에는메모리에올라갈 때참조해야할주요한필드들이위치하고있다. 해당구조체는총 224 Bytes 의크기를갖으며많 은필드가위치해있다. 아래의구조체를보자. typedef struct _IMAGE_OPTIONAL_HEADER { /* Standard fields */ WORD Magic; /* 0x10b or 0x107 */ /* 0x00 */ BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; /* 0x10 */ DWORD BaseOfCode; DWORD BaseOfData; /* NT additional fields */ DWORD ImageBase; DWORD SectionAlignment; /* 0x20 */ DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; /* 0x30 */ WORD MinorSubsystemVersion;
17 DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; /* 0x40 */ WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; /* 0x50 */ DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; /* 0x60 */ /* 0xE0 */ } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; 그림 18. IMAGE_OPTIONAL_HEADER 구조체 첫번째필드인 Magic 은 IMAGE_OPTIONAL_HEADER 를나타내는 Signature 로 32 비트 PE 의경우 0x010B 이고, 64 비트 PE 의경우 0x020B, ROM 이미지파일에대해서는 0x0107 의값을갖고있는 것을확인할수있다. MajorLinkerVersion 과 MinorLinkVersion 는본파일을만들어낸링커의버전 을나타낸다. SizeOfCode 의경우코드섹션 (.text) 섹션의크기이며 SizeOfInitializedData, SizeOfUninitializedData 의경우각각코드섹션을제외한 초기화된데이터섹션의크기 와 초기 화되지않은데이터섹션의크기 를나타낸다. AddressOfEntryPoint 는로더가실행을개시할주소를나타낸다. 이주소는 RVA 로서보통.text 섹 션내의특정번지가된다. 이필드의값은프로그램이처음으로실행될코드를담고있는주소 이다. 즉, 프로그램이로드된후이프로세스의메인스레드문맥의 EIP 레지스터가가질수있는 최초의값이라할수있다. BaseOfCode, BaseOfData 의경우각각첫번째코드섹션이시작되는 RVA, 데이터섹션이시작되는 RVA 를의미한다. ImageBase 필드는해당 PE 가가상주소공간에매핑될때매핑시키고자하는메모리상의시작 주소이다. 일반적으로 EXE 파일의 ImageBase 값은 0x400000 이며 DLL 의경우 0x1000000 이지만, DLL 의경우하나의 VAS 에여러 DLL 이존재할수있으므로 DLL Relocation 을필요로하게된다. PE 파일은섹션으로나뉘어져있는데파일에서섹션의최소단위를나타내는것이 FileAlignment 이며메모리에서섹션의최소단위를나타내는것이 SectionAlignment 이다. 각섹션의시작주소 는언제나각필드의배수가되는주소가되도록보장해야한다. MajorOperatingSystemVersion, MinorOperatingSystemVersion 은해당 PE 를실행하는데필요한운 영체제의최소버전을의미한다. MajorImageVersion 과 MinorImageVersion 은유저가정의가능한 필드로, 제작할때 PE 파일에제작자가버전을기입할수있도록하는것이다. MajorSubsystem 과 MinorSubsystem 은본 PE 를실행하는데필요한서브시스템의최소버전을의미한다. Win32VersionValue 는이전엔예약필드였지만 VC++ 7.0 부터는이름을가지게되었다. 하지만거
18 의사용되지않으며보통 0으로설정된다. SizeOfImage는 PE 파일이메모리에로딩되었을때의전체크기를담고있으며이값은 SectionAlignment 필드값의배수가되어야한다. SizeOfHeaders는 PE 헤더의전체크기를나타내는것으로이값역시 FileAlignment의배수가되어야한다. CheckSum 필드는이미지의체크섬값을의미한다. PE 파일의체크섬값은 IMAGEHELP.DLL의 CheckSymMappedFile API를통해서얻을수있다. 체크섬값은커널모드드라이버나어떤시스템 DLL의경우요구된다. 그이외의경우라면보통 0으로설정된다. 그리고 Subsystem 필드의경우 sys 파일과같이디바이스드라이버같은경우 1의값을가지고 Windows GUI 프로그램의경우윈도우기반응용프로그램의경우 2, 마지막으로 CMD와같은콘솔기반응용프로그램은 3의값을갖는다. DllCharacteristics 필드는원래 PE가 DLL이라는전제하에어떤상황에서 DLL 초기화함수가호출되어야하는지를지시하는플래그였다. 하지만지금은대부분 0으로설정되어있는것을확인할수있다. SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve, SizeOfHeapCommit 필드에대하여알아보자. 프로세스는가상주소공간에자신만의스택과힙을별도로가진다. 따라서프로세스생성시시스템은언제나메인스레드를위한디폴트스택과프로세스를위한디폴트힙을해당프로세스내에생성시켜주는데, 이스택과힙의크기와속성에관계된설정을이필드들에지정하게된다. PE가메모리에로드될때시스템은이필드의값을참조하여해당프로세스에디폴트스택과힙을만들어준다. LoaderFlags 필드는이전에는디버깅지원에관계된목적으로존재하는것같지만, 현재는 0으로설정된다. NumberOfRvaAndSize 필드는바로뒤에나오는 IMAGE_DATA_DIRECTORY 구조체배열의원소개수를의미하는데, 이값은항상 16(0x10) 이다. typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; 그림 19. IMAGE_DATA_DIRECOTRY 구조체 IMAGE_DATA_DIRECTORY는구조체의배열로, 배열의각항목마다정의된값을가지게된다. 각항목은위와같이 VirtualAddress와 Size 필드로구성되어있으며각 16개의구조는다음과같은의미를가진다. ENTRY 설명 IMAGE_DIRECTORY_ENTRY_EXPORT Export Section의시작주소를가리킨다. IMAGE_DIRECTORY_ENTRY_IMPORT Import Section의시작주소를가리킨다
19 IMAGE_DIRECTORY_ENTRY_RESOURCE Resource Section의시작주소를가리킨다. IMAGE_DIRECTORY_ENTRY_EXCEPTION 예외핸들러테이블을가리킨다. IMAGE_DIRECTORY_ENTRY_SECURITY WinTrust.h에정의된 WIN_CERTIFICATE 구조체 들의리스트의시작번지를가리킨다. 이리스 트는메모리상에 매핑되지않기 때문에 VirtualAddress필드는 RVA가아닌 Offset이다. IMAGE_DIRECTORY_ENTRY_BASERELOC ImageBase를기준으로메모리에매핑되지않 을경우코드상의포인터연산과관련된주 소를다시갱신해야하는 재배치 가일어나야 하는데, 이를위한재배치섹션을가리킨다. IMAGE_DIRECTORY_ENTRY_DEBUG 해당이미지의디버그정보를기술하고있는 곳을가리킨다. IMAGE_DIRECTORY_ENTRY_ARCHITECTURE IMAGE_ARCHITECTURE_HEADER 구조체의배 열에대한포인터이다. x86 또는IA-64계열에서 는사용되지않는다. IMAGE_DIRECTORY_ENTRY_GLOBALPTR 글로벌포인터 (GP) 로사용되는 RVA를나타내 며 Size필드는사용되지않는다. IMAGE_DIRECTORY_ENTRY_TLS Thread Local Storage 초기화섹션에대한포 인터이다. IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG IMAGE_LOAD_CONFIG_DIRECTORY 구조체에 대한포인터이다. IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT DLL 바인딩과관련된정보를담고있는곳을 가리키는포인터이다. IMAGE_DIRECTORY_ENTRY_IAT 첫번째 IAT의시작번지를가리키며 Size 필 드는모든 IAT의전체크기를가리킨다. IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 지연로딩에대한정보를가리키는포인터다. IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR.NET 응용프로그램이나 DLL 용 PE를위한것 으로 PE 내의.NET 정보에대한최상위정보 의시작번지를가리킨다. NULL 마지막엔트리는항상 NULL 값이다. 그림 20. IMAGE_DATA_DIRECTORY ENTRY (3) IMAGE_SECTION_HEADER PE 헤더바로다음엔 IMAGE_SECTION_HEADER 가나온다. 섹션헤더는각섹션의속성이정의되 어있는구조체로각섹션헤더마다 40 Bytes 로구성된다. 각필드에대하여알아보자. typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
20 union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; 그림 21. IMAGE_SECTION_HEADER 구조체 Name 필드는섹션의아스키이름을나타내며만약섹션의이름이 8 Bytes 를넘을경우 8 Bytes 이후의문자열은잘린뒤이필드값을채운다. 또한이값은섹션의이름을참고할용도뿐이라 해당이름을바꾸어도프로그램의실행에는아무런지장이없다. PhysicalAddress 필드는이전엔 OBJ 파일에서섹션의물리적인번지를지정했지만, 지금은사용되 지않아 0 으로지정되어있다. VirtualSize 필드는메모리에서섹션이차지하는크기를나타낸다. VirtualAddress 는 PE 에서해당섹션을매핑시켜야할가상주소공간상의 RVA 를가지고있다. SizeOfRawData 는파일에서섹션이차지하는크기를나타낸다. 그리고 PointerToRawData 는파일 에서해당섹션의위치를나타낸다. PointerToRelocations 는본섹션을위한재배치파일오프셋으로 OBJ 파일에서만사용되고실행파 일에서는 0 이된다. PointerToLinenumbers 는본섹션을위한 COFF 스타일의라인번호를위한파 일오프셋이다. NumberOfRelocations 는 PointerToRelocations 필드가가리키는구조체배열의원소개수를나타 내며, NumberOfLinenumbers 는 PointerToLinenumbers 필드가가리키는구조체배열의원소개수 를나타낸다. 마지막으로 Characteristics 는해당섹션의속성을나타내는플래그의집합으로아래와같은속성 값이존재하고있다. 속성값 설명 IMAGE_SCN_CNT_CODE(0x20) 섹션이코드를포함하고있다. IMAGE_SCN_CNT_INITIALIZED_DATA(0x40) 섹션이초기화된데이터를포함하고있다. IMAGE_SCN_CNT_UNINITIALIZED_DATA(0x80) IMAGE_SCN_MEM_DISCARDABLE(0x2000000) IMAGE_SCN_MEM_NOT_CACHED(0x4000000) 섹션이초기화되지않은데이터 (ex.bss) 를가지 고있다. 이섹션은실행이미지가메모리에완전히매 핑되고난뒤버려질수있다. 해당섹션은페이지되지않거나캐쉬되지않
21 IMAGE_SCN_MEM_NOT_PAGED(0x8000000) 는다. 페이지되지않는다는것은페이지파일 로스왑되지않는다는것을의미하며이는항 상 RAM에존재하는섹션임을의미한다. IMAGE_SCN_MEM_SHARED(0x10000000) 이섹션은공유가능한섹션임을나타낸다. IMAGE_SCN_MEM_EXECUTE(0x20000000) 이섹션은실행가능하섹션임을나타낸다. IMAGE_SCN_MEM_READ(0x40000000) 이섹션은읽기가능한섹션이다. IMAGE_SCN_MEM_WRITE(0x80000000) 이섹션은쓰기가능한섹션이다. IMAGE_SCN_LNK_INFO(0x0x200) 해당섹션이링커에의해사용될주석이나다 른어떤종류의정보를가진다. IMAGE_SCN_LNK_REMOVE(0x800) 링크시에최종실행파일의일부가되지말 아야할섹션의내용들을지시한다. IMAGE_SCN_LINK_COMDAT 해당섹션의내용들은공용데이터이다. IMAGE_SCN_ALIGN_XBYTES _XBYTES의값으로 _1BYTES부터 _8192Bytes까 지의정렬단위를나타낸다. 특별히지정되지 않으면 디폴트로 16바이트에 해당하는 IMAGE_SCN_ALIGN_16BYTES가된다. 그림 22. IMAGE_SECTION_HEADER 속성값.code 섹션의경우주로 CNT_CODE, MEM_EXECUTE, 그리고 MEM_READ 속성값을가지며.data 섹션과.idata 섹션의경우 CNT_INITALIZED_DATA, MEM_READ, 그리고 MEM_WRITE의속성값을 가진다.
22 6. Section 각 PE 파일마다가지는섹션은다를수있지만, 대부분섹션은유사한기능을한다. 그러므로이 러한각섹션에대하여알아보자. (1) Code Section 코드섹션또는텍스트섹션은컴파일러나어셈블러가최종적으로생성하는일반목적코드가존재하는섹션으로실행명령어들이이곳에존재하고있다. 우선예제파일을통해실제코드섹션의내용을확인해보자. 아래의바이너리와같이우리가읽을수없는코드로이루어져있기때문에우리는기계어를해석하기위하여디스어셈블러와같은도구를사용하여야한다. 00000600 : 6A 00 68 00 20 40 00 68 12 20 40 00 6A 00 E8 4E j.h. @.h. @.j..n 00000610 : 00 00 00 68 94 20 40 00 E8 38 00 00 00 46 48 EB...h. @..8...FH. 00000620 : 00 46 46 48 3B C6 74 15 6A 00 68 35 20 40 00 68.FFH;.t.j.h5 @.h 00000630 : 3B 20 40 00 6A 00 E8 26 00 00 00 EB 13 6A 00 68 ; @.j..&...j.h (skip) 그림 23. 예제.exe의.text 섹션디스어셈블러를통해해당섹션의내용을확인하면아래와같은명령어가위치해있는것을알수있다. 이러한명령어들이하나하나실행되면서프로그램의정의된대로동작하게된다. 실행의흐름을위하여 EIP 레지스터에는실행할명령어의위치가담겨있다. 00401000. 6A 00 PUSH 0 ; /Style = MB_OK MB_APPLMODAL 00401002. 68 00204000 PUSH test1.00402000 ; Title = "abex' 1st crackme" 00401007. 68 12204000 PUSH test1.00402012 ; Text = "Make me think your HD is a CD- Rom." 0040100C. 6A 00 PUSH 0 ; howner = NULL 0040100E. E8 4E000000 CALL <JMP.&USER32.MessageBoxA> ; \MessageBoxA...(skip) 그림 24. 예제.exe의.text 섹션 어셈블리코드 그렇다면왜파일에서의위치 (Offset) 은 0x600인데메모리에서의위치 (RVA) 는 401000일까? 아무이유없이이렇게메모리에올라오는것이아니라, 이전에언급한바와같이 ImageBase나해당섹션의 RVA, PointerToRawData 등에의해메모리에올라오면서정의된대로위치하게되는것이다. 해당프로그램의필드값몇가지를확인해보자. 필드이름 ImageBase RVA PointerToRawData 필드값 0x400000 0x1000 0x600
23 그림 25. 예제.exe의몇가지필드값우선코드섹션의 PointerToRawData는 0x600으로파일에서해당섹션의위치가 0x600임을알려준다. 그렇기에해당위치를확인해보면실행할코드가존재하고있는것을확인할수있다. 메모리에서해당섹션의위치는 RVA인 0x1000으로이에 ImageBase 값을더하면위그림에서의주소인 0x401000임을알수가있다. 이처럼 RVA와 RAW(Offset) 의주소의관계는직접코드를파일에서수정하고자할때와같은경우에, 이를변환할줄알아야한다. 코드섹션에실행을위한명령어들이있다고하여.text 섹션의첫부분이프로그램의실행을위한첫명령어가아니다. 흔히디버거를통해프로그램의시작부분으로이동되는주소는 Entry Point로 IMAGE_OPTIONAL_HEADER의 AddressOfEntryPoint에 ImageBase를더한위치가프로그램의시작주소가된다. (2) Data Section 데이터섹션은그종류가여러가지이다. 일반적으로.data라는이름을가진섹션이존재하며이안에.idata나.edata, 또는.rdata 섹션이존재하기도한다. 이에대해서는뒤에서상세히다룰것이다. 데이터섹션은그속성이읽기 / 쓰기가능한섹션으로전역변수나정적변수를정의하게되면이러한변수들이이섹션에위치하게된다. 아래는실제예제프로그램의데이터섹션이다. 해당데이터섹션에는 ASCII 형태의문자열들이존재하고있는것을확인할수있다. 00000800 : 61 62 65 78 27 20 31 73 74 20 63 72 61 63 6B 6D abex' 1st crackm 00000810 : 65 00 4D 61 6B 65 20 6D 65 20 74 68 69 6E 6B 20 e.make me think 00000820 : 79 6F 75 72 20 48 44 20 69 73 20 61 20 43 44 2D your HD is a CD- 00000830 : 52 6F 6D 2E 00 45 72 72 6F 72 00 4E 61 68 2E 2E Rom..Error.Nah.. 00000840 : 2E 20 54 68 69 73 20 69 73 20 6E 6F 74 20 61 20. This is not a 00000850 : 43 44 2D 52 4F 4D 20 44 72 69 76 65 21 00 59 45 CD-ROM Drive!.YE 00000860 : 41 48 21 00 4F 6B 2C 20 49 20 72 65 61 6C 6C 79 AH!.Ok, I really 00000870 : 20 74 68 69 6E 6B 20 74 68 61 74 20 79 6F 75 72 think that your 00000880 : 20 48 44 20 69 73 20 61 20 43 44 2D 52 4F 4D 21 HD is a CD-ROM! 00000890 : 20 3A 70 00 63 3A 5C 00 00 00 00 00 00 00 00 00 :p.c:\... 그림 26. 예제.exe의.data Section 데이터섹션과유사.rdata 섹션은읽기전용데이터섹션으로해당섹션헤더를확인해보면 MEM_WRITE 속성이존재하지않는것을확인할수있다. 따라서이섹션에무엇인가기록하고자하면시스템은예외를나타내며프로그램이종료될것이다. 또한.rdata 섹션은이러한용도뿐만아니라다른섹션들이병합되는곳이기도하다. 이후에나올.edata나.idata 섹션이.rdata섹션에
24 병합되는경우도종종있다는것을잊지말자. (3) Export Section Export 섹션은주로 DLL에서나타나는섹션으로자신이가진함수의기능을외부프로그램이사 용할수있도록제공하는것이목적이다. 만약 A.exe와 B.exe라는프로그램이존재할때두프로 그램모두 TEST_Function() 이라는함수를정의하여사용하고있다고가정하자. 두프로그램에있 어 TEST_Function을각각써넣어주는것보단용량이나이후관리를위하여 TEST_Function() 을가 진 DLL을하나만든다음이를 Import하여사용할수있다. 반대로해당 DLL은 Export를제공하 는것이다. 먼저 Export Section의 IMAGE_EXPORT_DIRECTORY 구조체에대하여알아보자. 해당구조체는 Export Section에서가장중요한정보들을담고있는구조체이며, IAT와는다르게 PE 파일당하나 만존재한다. 해당필드의목록은아래의그림과같다. typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; DWORD AddressOfNames; DWORD AddressOfNameOrdinals; } IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY; 그림 27. IMAGE_EXPORT_DIRECTORY 구조체 첫번째부터필드는사용되지않으며두번째필드는해당파일이생성된시간을나타낸다. 그 다음버전과관련된필드역시사용되지않는다. 다섯번째 Name필드는해당 DLL의이름을나타 내는 ASCII 코드문자열의위치를지시하는 RVA이다. 파일에서해당 DLL의이름은 RVA를 RAW 로변환해주면해당 Offset에서이름을확인할수있다. Base 필드는 Export된함수들에대한서 수의시작번호이다. NumberOfFunctions는뒤에나오는 AddressOfFunctions 필드가가리키는 RVA 배열의원소개수 를나타낸다. AddressOfFunctions는 export된함수들의함수포인터를가진배열을가리킨 RVA 값으로이함수주소들은본모듈내에서각각 export된함수에대한엔트리포인터이다. NumberOfNames는 AddressOfNames 필드가 가리키는 RVA 배열의 원소 개수와 AddressOfNameOrdinals 필드가 가리키는 서수 배열의 원소 개수를 동시에 나타낸다. AddressOfNames 필드는 export된함수의심벌을나타내는문자열포인터배열을가리키는rva
25 값이고, AddressOfNameOrdinals는 export된모든함수들의서수를담고있는배열에대한포인 터이다. 여기서 NumberOfNames와 NumberOfFunctions은다를수있는데보통 NumberOfFunctions 필 드가더크거나같다. 하지만실제 export된함수의정확한개수는 NumberOfNames 필드의값이 다. 이렇게 IMAGE_EXPORT_DIRECTORY에대하여알아보았다. 아래는실제 DLL 파일의.edata 섹 션을분석한내용이다. 구조체 필드 값 IMAGE_EXPORT_DIRECTORY Characteristics 0000 TimeDateStamp 2009/07/13 23:38:00 UTC Major Version 0 Minor Version 0 Name RVA 10DA4(adsnsext.dll) Ordinal Base 1 Number Of Functions 2 Number Of Names 2 Address Of Functions 10D90 Address Of Names 10D98 Address Of Name Ordinals 10DA0 구조체 데이터 값 Export Address Table 2D6C DllCanUnloadNow 2D51 DllGetClassObject Export Name Pointer Table 10DB1 DllCanUnloadNow 10DC1 DllGetClassObject Export Ordinal Table 0001 DllCanUnloadNow 0002 DllGetClassObject 그림 28. 예제.dll의.edata 섹션 IMAGE_EXPORT_DIRECTORY 구조체외에 3개의 Export Table이존재하는것을확인할수있다. 하 나는 Export 함수포인터테이블이며다른하나는 Export 함수이름포인터테이블, 마지막으로 Export 함수서수테이블임을알수가있다. (4) Import Section DLL의입장에서는함수를 Export 해주었다면반대로그함수를사용하기위해선다른실행파일에서이를 Import 해주어야한다. 이렇게사용하고자 import 하는함수들과그 DLL에대한정보를가지고있는것이바로임포트섹션이다. 아래 IMAGE_IMPORT_DESCRIPTOR 구조체를확인해보자.
26 typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; // INT Address (RVA) } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; // IAT Address (RVA) } IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR; 그림 29. IMAGE_IMPORT_DESCRIPTOR 구조체 우선첫번째필드인 Characteristics 필드는더이상사용하지않고 OriginalFirstThunk 라는이름 의필드로사용한다. OriginalFirstThunk 필드는 INT(Import Name Table) 의 RVA 주소값을가지고 있다. 그다음 TimeDataStamp 는시간과날짜를나타내며, 바인딩되지않을경우항상 0 이다. ForwarderChain 필드는바인딩여부와관계되는필드로바인딩되지않은이미지의경우 0 이며 바인딩된경우이값은 0 이아니다. Name 필드는 import 된 DLL 의이름이존재하는 RVA 값을가 진다. 마지막으로 FirstThunk 필드는 IAT(Import Address Table) 의 RVA 주소값을가지고있다. 이러한구조는로드하는 DLL 의수만큼존재하며맨마지막에는 NULL 로채워진해당구조체가존 재하므로배열의끝을알려준다. IMAGE_IMPORT_DESCRIPTOR 의필드항목을통해알수있는 INT 와 IAT 의값을확인해보자. 배열이름 오프셋 데이터 값 0x00000A3C 0x0000307C GetDriveTypeA 0x00000A40 0x0000308C ExitProcess Import Name Table 0x00000A44 0x00000000 KERNEL32.DLL 0x00000A48 0x0000309A MessageBoxA 0x00000A4C 0x00000000 USER32.DLL 0x00000A50 0x0000307C GetDriveTypeA 0x00000A54 0x0000308C ExitProcess Import Address Table 0x00000A58 0x00000000 KERNEL32.DLL 0x00000A5C 0x0000309A MessageBoxA 0x00000A60 0x00000000 USER32.DLL 그림 30. INT와 IAT 여기서 INT와 IAT가동일한값을가리킨다는것을알수있다. 하지만메모리에올라오면서 PE 로더가 IAT엔실제함수의명령어위치를채워주게되며, INT에는파일에서와마찬가지로임포트 하는함수의이름을가리키고있다. 그렇다면메모리에서 IAT 기록되어있는함수의주소는어떻 게얻어오는것일까? 크게네단계로나눌수가있다. 아래의그림을보자.
27 그림 31. IAT에함수주소기록과정우선로더는 A.EXE 파일이필요로하는 DLL을로드하고자한다. 이를위해 Import 섹션의존재하고있는 IMAGE_IMPORT_DESCRIPTOR(IID) 를통해어떠한 DLL을필요로하는지이름을얻는다. 그리고해당 DLL들을 LoadLibrary API를통해메모리에올리고자한다. 해당 DLL을찾은프로세스는 DLL을매핑하기위한공간을확보한다음 ImageBase에지정된주소로매핑을시도하며, 만약해당주소에매핑하지못한경우재배치를하여다른주소에매핑을한다. 매핑이되었다면로더는 IID의 OriginalFirstThunk 필드를통해 INT에존재하고있는함수에대한정보를얻어온다. 그다음으로로더는해당함수들의함수포인터즉, 함수의시작주소를얻고자획득하고자한다. 로더는 dll의 Export 섹션에서 IMAGE_EXPORT_DIRECTORY 구조체를참고하여 AddessOfName 멤버를통해해당함수의이름을비교하여원하는함수의이름을찾는다. 이때몇번째인덱스에존재하는지확인을한다음 AddressOfNameOrdinals 필드를참조한다. Ordinal 배열에서해당인덱스번호에맞는값을찾은뒤, AddressOfFunctions 멤버를이용해 EAT에서해당인덱스번호에맞는함수의시작주소 (RVA) 를얻는다. 마지막으로해당함수의포인터를획득한다음로더는중요한과정을수행하게되는데, 바로위과정을통해얻은함수의포인터 ( 함수의시작주소 ) 를저장하는것이다. IID의 FirstThunk 필드값을통해 IAT의주소를얻을수가있고, 첫번째과정에서읽은것과같은함수에세번째과정에서얻은함수의시작주소를기록하게된다. 이러한과정을통해파일에서 IAT는 INT와같은곳을가리키지만, 메모리에서는 INT와는전혀다른실제함수의시작위치를가리키고있게된다. 다시말해, IAT는 PE 파일이미지로존재할때와실제로프로세스주소공간내로매핑되었을때의내용이달라진다. DLL 바인딩을할경우
28 DLL 을로딩하기전에, IAT 에실제함수의주소를고정시켜버리게되어프로그램이실행될때마다 이러한과정을거치지않게된다. (5) Relocation Section 앞서논의한바와같이로더는실행파일을로드할때지정된 ImageBase 에실행파일이미지를 로드하고자한다. 하지만해당주소에이미다른실행파일이미지가로드되어있는경우중첩되 어그주소를사용할수는없다. 이런경우로더는매핑가능한다른주소를찾아해당주소에 로드해야한다. 대부분의 DLL 은 0x10000000 영역이기본 ImageBase 로처음에로드되는 DLL 의경 우상관없지만두번째부터는다른주소를사용해야만한다. 로딩주소가바뀌게되면절대주소를사용한것은반드시바뀐주소에해당하는값으로고쳐주 어야한다. 그렇지않으면 0x10000000 에로딩된다른 DLL 의메모리영역을참조하게된다. PE 에 서는이렇게고쳐주어야하는곳을재배치섹션에모아서저장해두고있다. 재배치섹션의구 조를한번살펴보자. typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; /* WORD TypeOffset[1]; */ // 이후에해당배열이뒤에따라옴을알려줌 } IMAGE_BASE_RELOCATION,*PIMAGE_BASE_RELOCATION; 그림 32. IMAGE_BASE_RELOCATION 구조체 기준재배치섹션은단순한구조를가지고있다. VirtualAddress 필드는기준재배치가시작되어야 할메모리상의번지에대한 RVA 이다. 실제갱신할위치의표현은 기준 RVA+ 재배치 Offset 으로 구성되고이때기준 RVA 에해당하는것이이필드의값이다. 재배치섹션내의재배치블록은 4K 단위의구조체를포함하는블록이존재하기에나뉘어지는데, 이때 SizeOfBlock 필드의값은 자신을포함하고있는구조체의크기를말한다. 이뒤에는재배치가적용되어야할대상에해당하는가상주소에대한정보를담은 WORD 타입 의배열이온다. 해당배열의각엔트리는두필드로구성되는데하나는재배치타입이며, 다른 하나는재배치오프셋이다. Bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 WORD 재배치타입재배치오프셋 그림 33. TypeOffset 엔트리구조 재배치타입의경우거의큰의미가없는값으로 Win32 PE 의경우 3, Win64 의경우 10 이된다. 가끔재배치그룹의마지막엔트리에이필드의값이 0 인경우가있는데이는해당구조체가 4 바이트단위로정렬되기때문에이것을맞추어주기위한패딩으로사용될뿐이다.
29 다음으로재배치오프셋은재배치할대상의번지값에대한오프셋이다. 오프셋의기준은위에서 언급하였던 IMAGE_BASE_RELOCATION 구조체의 VirtualAddress 필드의값이된다. 따라서실제로 갱신되어야할위치의 RVA는 VirtualAddress 필드값에재배치오프셋값을더한결과가된다. 재배치오프셋이 12 Bit 밖에되지않아기준이되는 VirtualAddress로부터 4095만큼까지만접근 이가능하다. 그렇다면그이상으로떨어진지점은어떻게표현할까? 새로 VirtualAddress를지정 해주면된다. 즉하나이상의 VirtualAddress 필드가존재할수있으며이에따라각배열이뒤에 붙게된다. 재배치는다음의과정으로이루어진다. 만약 ImageBase의값과실제로드될주소가다른경우그 값의차이인델타값을구한다. 예로원래는 0x10000000에로드될 a.dll이 0x15000000에로드되 었다면델타값은 0x05000000이된다. 그다음아래와같은재배치섹션을확인해보자. pfile Data Description 0x000C1A00 0x00010000 VirtualAddress 0x000C1A04 0x0000001C SizeOfBlock 0x000C1A08 0x3E15 TypeOffset[0] 0x000C1A0A 0x3E41 TypeOffset[1] 그림 34. a.dll의재배치섹션 VirtualAddress 필드값이 0x10000인것을확인할수있다. 해당섹션은.text 섹션으로 TypeOffset를따라가보자. 해당 RVA 0x10000의 RAW는 0x600으로재배치오프셋인 0xE15과 0xE41를각각더하면 TypeOffset[0] 이나타내는파일에서의주소는 0x1215와 0x1241이된다. 해 당값을확인해보자. 00001210 : 1C 53 56 8B 35 18 03 E4 6D 57 8B 78 10 85 F6 0F.SV.5...}W.x... 00001220 : 85 0A 01 00 00 E8 B0 09 00 00 8B 40 2C 64 8B 0D...@,d.. 00001230 : 18 00 00 00 6A 44 5B 53 50 8B 41 30 FF 70 18 FF...jD[SP.A0.p.. 00001240 : 15 E4 05 D7 6D 8B F0 33 C0 3B F0 0F 84 5D 01 00...}..3.;...].. 그림 35. 재배치해야할주소확인 이위치에있는것이무엇을뜻하는지어셈블리어로확인해보자. 첫번째 TypeOffset[0] 은 MOV 명령어의오퍼랜드로사용되고두번째 TypeOffset[1] 은 CALL 명령어에사용되는것을확인할수 있다. 10001083 8B35 1803E47D MOV ESI,DWORD PTR DS:[6DE40318] (skip) 100010AF FF15 E405D77D CALL DWORD PTR DS:[6DD705E4] 그림 36. 재배치할주소의명령어 재배치해야할값을찾았으니이제이값에위에서구한델타값 0x05000000을각각더해주게
30 된다. 따라서 0x1215에있는값은 0x15000000에매핑된이후 [0x6DE40318+0x05000000] 이되며, 0x1241에있는값은매핑된이후 [0x6DD705E4+0x05000000] 이된다. 이와같이재배치섹션은어떠한값을바꾸어야하는지알려주는역할을한다. 하지만기준재배치를수행해야할상황이되었을때발생할수있는문제점또한존재한다. 크게두가지문제점이있는데첫째로, 로더는재배치섹션을스캔하면서재배치섹션내에존재하는각오프셋이가리키는위치의해당모듈의코드를모두수정해야한다. 이것은응용프로그램의초기화시간을더늘어나게만든다. 둘째로, 로더가재배치섹션의엔트리가지시하는해당번지값을수정할때발생하는문제가있다. 갱신되어야할해당주소공간의번지값은.text 섹션에존재하는데, 코드섹션의경우 Write 속성이없기때문에결국번지값을수정하기위해섹션의속성을변경해야만한다.
31 7. API Hooking 위과정에서본것과같이 IAT는읽을수있을뿐만아니라쓰기속성을가지고있다. 따라서이러한속성을이용해기존의 API에대한호출을자신이정의한 API로향하도록변경할수있다. 이를 API 후킹이라한다. 이번장에서는 DLL 인젝션을진행하는방법과 IAT 후킹에대하여알아보자. (1) DLL Injection 몇가지인젝션방법중세가지방법에대하여알아보자. 우선 DLL 인젝셕은다른프로세스에게강제로 DLL을로딩시키도록하는것으로, 원하는기능을수행하는 DLL을다른프로세스에매핑시켜원하는동작을수행하도록한다. 여러방법중우선 CreateRemoteThread API를이용하는방법에대하여알아보자. 흔히 DLL을자신의프로세스에로드하기위해서는 LoadLibrary API를사용한다. 하지만다른프로세스에게 LoadLibrary API를사용하여 DLL을로드시킬수가없으므로 CreateRemoteThread API 를통해다른프로세스에게스레드를실행시키도록하여 DLL을로드시킬수있다. HANDLE WINAPI CreateRemoteThread( _In_ HANDLE hprocess, // 프로세스핸들 _In_ LPSECURITY_ATTRIBUTES lpthreadattributes, _In_ SIZE_T dwstacksize, _In_ LPTHREAD_START_ROUTINE lpstartaddress, // 스레드함수주소 _In_ LPVOID lpparameter, // 스레드파라미터주소 _In_ DWORD dwcreationflags, _Out_ LPDWORD lpthreadid ); 그림 37. CreateRemoteThread API 위에는 CreateRemoteThread API의인자로어떠한것이있는지나타내는것으로중요한항목은바로네번째인자인 lpstartaddress이다. 해당파라미터는스레드가수행할함수의주소를넘겨주는것으로다른프로세스에서수행할함수의주소를말한다. 바로여기에 LoadLibrary API의주소를주고, 다섯번째인자에로드시키고자하는 DLL의이름을넘겨주면된다. 다음으로레지스트리를통해쉽게 DLL Injection하는방법에대하여알아보자. 바로 AppInit_DLLs 라는레지스트리키로여기에인젝션하고자하는 DLL의경로를기입해준뒤재부팅을시행하면, 재부팅하면서실행되는모든프로세스에해당 DLL을인젝션시켜준다. 위 CreateRemoteThread가하나의프로세스를지정해주는것과는다르게모든프로세스에시행된다는점이차이난다고할수있다. 해당경로는다음과같다. HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs
32 그림 38. AppInit_DLLs Registry Key 마지막으로알아본 DLL 인젝션방법은윈도우운영체제가제공하는 API를사용하는방법이다. 윈도우운영체제는사용자에게 GUI를제공해주고, 사용자는제공받은 GUI를이용하여원하는동작을수행할수있다. 동작을수행하는데있어마우스나키보드와관련된동작을수행하게되는데이러한동작은윈도우운영체제가 Event Driven 방식으로처리한다. 다시말해이러한동작을이벤트로발생시켜운영체제가그이벤트에맞는메시지를해당응용프로그램에게전달하여처리하는방식이다. 아래그림을보면메시지후킹이어떤지점에서이루어지는지볼수있다. 사용자가어떠한행위를했을때이벤트가발생되고, 이벤트발생으로인해 OS에서응용프로그램으로보낼메시지들이 OS Message Queue에추가된다. 운영체제는해당이벤트가어떤응용프로그램에서발생했는지파악한다음, OS 큐에서메시지를꺼내해당응용프로그램의메시지큐에전달한다. 해당응용프로그램은자신의응용프로그램메시지큐에해당메시지가추가된것을확인하고해당이벤트핸들러를호출한다. 이러한방식으로윈도우는메시지를전달한다. 그림 39. 메시지전달방식 윈도우운영체제어서는이러한메시지를후킹하기위한 API 인 SetWindowsHookEx() 를기본적으로 제공한다. 이 API 는훅체인에응용프로그램이정의한후크프로시저를설치하며이를통해사 용자는특정유형의이벤트를모니터링할수있다. HHOOK WINAPI SetWindowsHookEx( _In_ int idhook // 훅종류 _In_ HOOKPROC lpfn, // 지정한이벤트발생시처리하는프로시저주소 _In_ HINSTANCE hmod, // lpfn 이있는 DLL 의핸들 _In_ DWORD dwthreadid ); 그림 40. SetWindowsHookEx API 만약해당 API 를구현하는 HookKey.dll 이존재하며이를실행하기위한 HookMain.exe 를제작하
33 였다고가정하자. HookMain.exe 를실행하면 HookKey.dll 이해당프로세스의메모리에로드되며 SetWindowsHookEx() 가호출된다. 이렇게메시지후킹이걸린상테에서, 다른프로세스가해당이 벤트를발생시킨다면 HookKey.dll 은그프로세스에서도로딩된다. 그림 41. SetWindowsHookEx 를이용한후킹 이러한방법들을통해원하는 DLL 을프로세스에인젝션할수있다. (2) IAT Hooking IAT는위에서자세히설명한바와같이 Import Address Table로, 메모리에매핑되면서 PE 로더가 IAT에실제함수의주소를기록해준다. 다시말해파일에서의 IAT는실제함수의주소를가리키고있는것이아니며일반적으로 INT와같은곳을가리키고있다. 하지만메모리에올라온뒤에는해당프로그램이사용하고자하는함수의주소가기록되어있다. IAT 후킹은바로이 IAT에기록되어있는주소를바꿔원하는함수의주소로가도록하는것이다. 이를통해다양한파라미터나리턴값을조작하는등의작업을수행할수있다. 그렇다면일반적으로 API가호출되는상황에대하여먼저알아보자. 0040104A. 68 E8030000 PUSH 3E8 ; /Timeout = 1000. ms 0040104F. FF15 68B14300 CALL DWORD PTR DS:[43B168] ; Sleep() API 0043B168 > FF 10 34 76 0A 19 34 76 69 51 34 76 2F 44 34 76 4v.4viQ4v/D4v 763410FF > 8BFF MOV EDI,EDI 76341101 55 PUSH EBP 76341102 8BEC MOV EBP,ESP (skip) 그림 42. 일반적인 API 호출과정 위예에서 Sleep API를호출하고자할때바로 CALL 명령어를통해 763410FF(Sleep함수 ) 를호출
34 하는것이아니라 DS:[43B168] 을참조해해당주소에있는 763410FF라는주소를얻어이를호출한다. 그렇다면왜바로 CALL 763410FF라하지않을까? 이는 DLL의특성상운영체제버전이나언어, 서비스팩에따라 DLL의버전이다르며해당함수의위치가달라지기때문에 IAT에매핑된주소를참조하여함수를호출하도록하는것이다. 바로 43B168가 IAT의한부분으로 Sleep 함수의실제주소가메모리에올라오면서기록된것이다. 따라서 IAT를후킹한다는것, 좀더구체적으로 IAT에서 Sleep() 을후킹하는것은바로해당 API의실제주소를가지고있는 IAT의주소 (43B168) 에위치한주소값을바꾸는것이다. 후킹된모습은다음과같다. 0040104A. 68 E8030000 PUSH 3E8 ; /Timeout = 1000. ms 0040104F. FF15 68B14300 CALL DWORD PTR DS:[43B168] ; Sleep() API 0043B168 > 20 10 40 00 0A 19 34 76 69 51 34 76 2F 44 34 76 4v.4viQ4v/D4v 00401020 814424 04 001 ADD DWORD PTR SS:[ESP+4],1000 ; 인자값변조 00401028 - E9 D200F475 JMP 763410FF ; Kernel32.Sleep() 763410FF > 8BFF MOV EDI,EDI 76341101 55 PUSH EBP 76341102 8BEC MOV EBP,ESP (skip) 그림 43. 후킹된 API 호출과정 이전과똑같이 Sleep() 을호출하기위해 IAT를참조하게된다. 하지만해당 IAT는후킹되어기존의 Sleep() 함수의주소가아닌, 후킹함수의주소 (0x401020) 를가리키고있다. 결국프로세스는 Sleep() 을호출했지만후킹된주소로넘어가게되며, 후킹된주소에서파라미터를변조한후원래 의 Sleep() 함수로진입하게된다. 이와같은 IAT 후킹은간단하게이루어지면서도해당프로세스에서후킹한함수를호출할때마다 후킹함수를지나가게되므로강력하다고할수있다.