PE의구조 < 밑의내용은 kkamagui 님이쓰신글과 Msdn Magazine 의 An In-Depth Look into the Win32 Portable Executable File Format Part I, II와 msdn의 Debugging and Error Handling Technical Articles 에기재된 Peering Inside the PE: A Tour of the Win32 Portable Executable File Format 에서도움을많이받았습니다.> 1. PE 파일의전체구조다음은 PE 파일의전체구조이다. 위와같은형태이며 PE 파일포맷은 COFF 라는포맷을계승한파일포맷이다. COFF 라는것은수많은프로그램들은하나로합쳐메모리에옮기는과정에서매우복잡하고많은시간을요구하는등의문제점을개선하기위해미국 AT&T 사에서고안해낸규칙이다.
각부분을크게살펴보면다음과같이 4 부분으로크게나뉜다. IMAGE_DOS_HEADER : PE 파일의처음에위치하며뒷부분에 DOS 에서실행했을때, 에러메시지 (This program cannot be run in DOS mode) 를표시하는스텁 (Stub) 코드를포함하고있다. MAGIC Number 와다음에오는 IMAGE_NT_HEADER 의위치를표시한다. IMAGE_NT_HEADER : PE 파일포맷에대한정보를포함. 아래의두부분으로구성된다. IMAGE_FILE_HEADER : Section 의수및속성과같은정보포함한다. IMAGE_OPTIONAL_HEADER : PE 파일에대한속성또는이미지베이스와같은정보포함한다. Data Directory : 어떤영역의 Virtual Address 와 Size 정보를포함한다. IMAGE_SECTION_HEADER Array: 섹션에대한실질적인정보를포함한다. Section( 섹션 ) : 실제데이터가위치하는영역이다. 로더는섹션의내용만을메모리로로드한다. 다음은대표적인섹션들의설명이다. 1) 코드 2) 데이터.text : 프로그램을실행하기위한코드를담고있는섹션.data : 초기화된전역변수들을담고있는읽고쓰기가능한섹션.rdata : 읽기전용데이터섹션으로서문자열표현이나 C++/COM 가상함수테이블들이배치된다..bss : 초기화되지않은전역변수들을위한섹션이다. 가상주소공간에매필될때에는보통.data 섹션에병합되어메모리상에서따로존재하지않는다. VC++ 7.0 에서는.textbss 로나타난다. 3) 임포트 API 정보.idata : 임포트할 DLL 과그 API 들에대한정보를담고있는섹션. 대표적으로 IAT(Import Address Table) 이존재함..didat : 지연로딩임포트데이터를위한섹션이다. 지연로딩은 Win2000 부터지원되는 DLL 로딩의한방식이다. 릴리즈모드일때는다른섹션에병합된다. 4) 익스포트 API 정보.edata : 익스포트할 API 에대한정보를담고있는섹션. 보통 API 나변수를익스포트할수있는경우 5) 리소스 DLL 이기때문에 DLL PE 에이섹션이존재한다..rsrc : 다이얼로그, 아이콘, 커서들의윈도우 APP 리소스관련데이터들이배치된다. 6) 재배치정보.reloc : 실행파일에대한기본재배치정보를담고있는섹션. 재배치란 PE 이미지를원하는기본주소에로드하지못하고다른주소에로드했을경우코드상에서의관련주소참조에대한정보를해야하는경우. 7) TLS
.tls : declspec(thread) 지시어와함께선언되는스레드지역저장소 (Thread Local Storage) 를위한섹션 8) C++ 런타임.crt : C++ 런터임을지원하기위한추가된섹션. C++ 객체생성자와소멸자를호출하는데이용되는함수포인터. 9) Short.sdata : 전역포인터에상대적으로주소지정될수있는읽고쓰기가능한 "Short" 데이터섹션이다..srdata :.sdata 에들어갈수있는데이터들의읽기전용섹션. 10) 예외정보.pdata : 예외정보를담고있는섹션이다. IMAGE_RUNTIME_FUNCTION_ENTRY 구조체의배열을가지고있다. 이구조체는 CPU 플랫폼에의존한다. 11) 디버깅 12) Directives.debug$S : OBJ 파일에만존재한다. 가변길이코드뷰포맷심벌레코드의스트림이다..debug$T : OBJ 파일에만존재한다. 가변길이코드뷰포맷타입레코드의스트림이다..debug$P : 미리컴파일된헤더를사용했을때 OBJ 파일에만존재하는섹션이다..drectve : OBJ 파일에만존재한다. Directives 란링커명령라인을통해전달될수있는아스키문자열을말한다. 마지막 COFF Linue Numbers, COFF Symbols, CodeView Debug Information 은파일로드시쓰이지않는다. 구체적으로 PE 포멧에대해서공부하기전에알아야할것이있는데바로 RVA(Relative Virtual Address, 상대적가상주소 ) 이다.. 파일의오프셋과관련이없고, PE 파일내의번지에관계된값. 즉, 가상주소공간상의번지값들이라고생각하면된다. RVA 가상대적가상주소이기때문에실제주소번지로변환하기위해서는다음과같이계산을하면된다. 실제주소번지 ( 가상주소 ) = 이미지로드시작번지 + RVA 여기서이미지로드시작번지는 NT 의경우 EXE 가시작되는번지는 0x004000000 번지이다. IMAGE_DOS_HEADER IMAGE_DOS_HEADER 는 PE 실행파일첫부분에위치하며아래와같이 WinNT.h 에정의되어있다. #define IMAGE_DOS_SIGNATURE 0x4D5A // MZ #define IMAGE_OS2_SIGNATURE 0x4E45 // NE #define IMAGE_OS2_SIGNATURE_LE 0x4C45 // LE #define IMAGE_NT_SIGNATURE 0x50450000 // PE00
typedef struct _IMAGE_DOS_HEADER { // DOS.EXE header WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10]; LONG e_lfanew; // Magic number <MZ> // Bytes on last page of file // Pages in file // Relocations // Size of header in paragraphs // Minimum extra paragraphs needed // Maximum extra paragraphs needed // Initial (relative) SS value // Initial SP value // Checksum // Initial IP value // Initial (relative) CS value // File address of relocation table // Overlay number // Reserved words // OEM identifier (for e_oeminfo) // OEM information; e_oemid specific // Reserved words // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; 크게주의해서볼부분은실행파일인지판단하는데사용되는 e_magic 부분과다음에오는 IMAGE_NT_HEADER 의위치를표시해주는 e_lfanew 부분이다. IMAGE_NT_HEADER IMAGE_NT_HEADER 는실제 PE 파일포맷에대한정보를포함하는헤더로써 IMAGE_FILE_HEADER 와 IMAGE_OPTIONAL_HEADER 로구성된다. 1. typedef struct _IMAGE_NT_HEADERS { DWORD Signature; <PE00> IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
IMAGE_NT_HEADER 는위와같이 Signature 와 File 헤더, 그리고 Optional Header 로구성되어있다. Signature 는 IMAGE_NT_SIGNATURE 로 <PE00> 의값을가진다. 그럼첫번째에해당하는 FileHeader 를알아보자. IMAGE_FILE_HEADER 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; Machine : CPU ID 를나타내는데, 간단히보면 Intel 인지, MIPS 인지등등의정보가들어있음 NumberOfSections : PE 파일에포함된총섹션의수를나타냄 TimeDateStamp : 컴파일러또는링커가파일을생성한시간. 1970 년 1 월 1 일 GMT 기준으로지나온초 PointerToSymbolTable : COFF 파일의심볼테이블의오프셋을나타냄. 없는경우가대부분 NumberOfSymbols : 심볼의개수를나타냄 SizeOfOptionalHeader : 뒤에이어서나오는 Optional Header 의크기를나타낸다. 32Bit/64Bit 에따라서그크기가다름 Characteristics : 파일의특성 Machine 에대한매크로는 WinNT.h 에정의되어있는데아래와같다. Intel32 or Intel64 가대부분일테니까위의파란색만보면될것같다. #define IMAGE_FILE_MACHINE_UNKNOWN 0 #define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386. #define IMAGE_FILE_MACHINE_R3000 #define IMAGE_FILE_MACHINE_R4000 #define IMAGE_FILE_MACHINE_R10000 #define IMAGE_FILE_MACHINE_WCEMIPSV2 #define IMAGE_FILE_MACHINE_ALPHA 0x0162 // MIPS little-endian, 0x160 big-endian 0x0166 // MIPS little-endian 0x0168 // MIPS little-endian 0x0169 // MIPS little-endian WCE v2 0x0184 // Alpha_AXP #define IMAGE_FILE_MACHINE_SH3 #define IMAGE_FILE_MACHINE_SH3DSP #define IMAGE_FILE_MACHINE_SH3E 0x01a2 // SH3 little-endian 0x01a3 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 #define IMAGE_FILE_MACHINE_SH5 #define IMAGE_FILE_MACHINE_ARM #define IMAGE_FILE_MACHINE_THUMB #define IMAGE_FILE_MACHINE_AM33 #define IMAGE_FILE_MACHINE_POWERPC 0x01a6 // SH4 little-endian 0x01a8 // SH5 0x01c0 // ARM Little-Endian 0x01c2 0x01d3 0x01F0 // IBM PowerPC Little-Endian #define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1 #define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64 #define IMAGE_FILE_MACHINE_MIPS16 #define IMAGE_FILE_MACHINE_ALPHA64 #define IMAGE_FILE_MACHINE_MIPSFPU #define IMAGE_FILE_MACHINE_MIPSFPU16 #define IMAGE_FILE_MACHINE_AXP64 #define IMAGE_FILE_MACHINE_TRICORE #define IMAGE_FILE_MACHINE_CEF #define IMAGE_FILE_MACHINE_EBC #define IMAGE_FILE_MACHINE_AMD64 #define IMAGE_FILE_MACHINE_M32R #define IMAGE_FILE_MACHINE_CEE 0x0266 // MIPS 0x0284 // ALPHA64 0x0366 // MIPS 0x0466 // MIPS IMAGE_FILE_MACHINE_ALPHA64 0x0520 // Infineon 0x0CEF 0x0EBC // EFI Byte Code 0x8664 // AMD64 (K8) 0x9041 // M32R little-endian 0xC0EE 다음은 Characteristics 에대한부분인데 WinNT.h 에아래와같이정의되어있다. 역시나파란색부분만보면될것같다. #define IMAGE_FILE_RELOCS_STRIPPED #define IMAGE_FILE_EXECUTABLE_IMAGE 0x0001 // Relocation info stripped from file. 0x0002 // File is executable (i.e. no unresolved externel references). #define IMAGE_FILE_LINE_NUMS_STRIPPED #define IMAGE_FILE_LOCAL_SYMS_STRIPPED #define IMAGE_FILE_AGGRESIVE_WS_TRIM #define IMAGE_FILE_LARGE_ADDRESS_AWARE #define IMAGE_FILE_BYTES_REVERSED_LO 0x0004 // Line nunbers stripped from file. 0x0008 // Local symbols stripped from file. 0x0010 // Agressively trim working set 0x0020 // App can handle >2gb addresses 0x0080 // Bytes of machine word are reversed. #define IMAGE_FILE_32BIT_MACHINE #define IMAGE_FILE_DEBUG_STRIPPED 0x0100 // 32 bit word machine. 0x0200 // Debugging info stripped from file in.dbg file #define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file. #define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM #define IMAGE_FILE_DLL 0x1000 // System File. 0x2000 // File is a DLL. #define IMAGE_FILE_UP_SYSTEM_ONLY #define IMAGE_FILE_BYTES_REVERSED_HI 0x4000 // File should only be run on a UP machine 0x8000 // Bytes of machine word are reversed. IMAGE_OPTIONAL_HEADER typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage;
DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; Optional Header 는꽤나중요한정보를가지고있다. 위에서보면알수있듯이 PE 파일의전반적이내용들에대한정보를포함한다. 항목이꽤나많은데중요한정보만추리면아래와같다. Magic : Signature 로 32Bit 의경우 0x10b 를가짐 SizeOfCode : 섹션중에 IMAGE_SCN_CNT_CODE 속성을가진섹션들전체의합 SizeOfInitializedData : 섹션중에 IMAGE_SCN_CNT_INITIALIZED_DATA 속성을가진섹션들전체의합 SizeOfUninitializedData : 섹션중에 IMAGE_SCN_CNT_UNINITIALIZED_DATA 속성을가진섹션들전체의합 AddressOfEntryPoint : Entry Point 의주소. 실제로더가제일먼저실행할코드의시작점 BaseOfCode : 코드가시작되는상대주소 (RVA) BaseOfData : 데이터가시작되는상대주소 (RVA) ImageBase : 이미지가로딩되는메모리의 Base 주소. 일반적으로실행파일의경우 0x400000(4Mbyte) 위치에로딩 SectionAlignment : 섹션이정렬되는크기. PE 파일자체가메모리맵파일이기때문에 0x1000(4Kbyte) 보다크거나같아야함 SizeOfImage : 모든섹션들의합. 이미지실행을위해메모리를할당해야하는총크기 NumberOfRvaAndSizes : 뒤에오는 DataDirectory 의개수. 무조건 16 개 Data Directory : 총 16 개가있으며각항목은특정데이터에대한정보를가지고있음. 뒤에서설명 IMAGE_DATA_DIRECTORY
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; 데이터디렉토리는위와같은구조로이루어져있으며 IMAGE_OPTIONAL_HEADER 에총 16 개가있다. 각각에 Index 에대한매크로는 WinNT.h 에아래와같이정의되어있다. #define IMAGE_DIRECTORY_ENTRY_EXPORT #define IMAGE_DIRECTORY_ENTRY_IMPORT #define IMAGE_DIRECTORY_ENTRY_RESOURCE #define IMAGE_DIRECTORY_ENTRY_EXCEPTION #define IMAGE_DIRECTORY_ENTRY_SECURITY #define IMAGE_DIRECTORY_ENTRY_BASERELOC 0 // Export Directory 1 // Import Directory 2 // Resource Directory 3 // Exception Directory 4 // Security Directory 5 // Base Relocation Table #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory // IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage) #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR #define IMAGE_DIRECTORY_ENTRY_TLS 8 // RVA of GP 9 // TLS Directory #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers #define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor 이미지디렉토리정보는굉장히중요하다. 경우에따라서섹션이합쳐질수있기때문에통합된섹션에서원하는정보를찾는방법은이미지디렉토리에포함된정보를이용하는방법밖에는없다. 여러모로많이쓰이는인덱스는아래와같은역할을한다. 또로더가섹션을일일이돌아다닐필요없이필요한자기가필요한섹션의정보를없는데유용하다. IMAGE_DIRECTORY_ENTRY_EXPORT : Export 함수들에대한 Export Table 의시작위치와크기를나타냄 IMAGE_DIRECTORY_ENTRY_IMPORT : Import 함수들에대한 Import Table 의시작위치와크기를나타냄 IMAGE_DIRECTORY_ENTRY_RESOURCE : IMAGE_RESOURCE_DIRECTORY 구조체의시작위치를나타냄
IMAGE_DIRECTORY_ENTRY_TLS : Thread Local Storage 에대한포인터 IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG : IMAGE_LOAD_CONFIG_DIRECTORY 구조체애대한포인터 IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT : IMAGE_BOUND_IMPORT_DESCRIPTOR 구조체의배열을가리키는포인터 IMAGE_DIRECTORY_ENTRY_IAT : Import Address Table 의시작위치를나타냄 여기까지 IMAGE_NT_HEADER 에대해서알아보았다. 일단지금은특정영역의크기와위치를표시한다는정도만알아놓고다음으로넘어가자. IMAGE_SECTION_HEADER PE 헤더의뒷부분에연속해서 IMAGE_SECTION_HEADER 가위치하게된다. 섹션은뒤에올코드나데이터가위치하는영역에대한구체적인정보를포함하고있으므로굉장히중요하다. 섹션의개수는앞서 IMAGE_FILE_HEADER 에포함된 NumberOfSections 에서얻을수있으며해당개수만큼얻어오면된다. IMAGE_SECTION_HEADER 는 WinNT.h 에아래와같이정의되어있다. #define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; 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;
중요한항목에대한의미는아래와같다. VirtualSize : 실제코드나데이터영역의크기를표시 VirtualAddress : 메모리에로드되었을때 RVA 를표시 SizeOfRawData : VirtualSize 의크기를 IMAGE_OPTIONAL_HEADER 에포함된 FileAlignment 의단위로올림한크기 PointerToRawData : 실제섹션데이터가파일내에존재하는오프셋. Virtual Address 와같을수도있고다를수도있음 Characteristics : 섹션의속성표시. 자세한것은뒤를참조 위의 VirtualSize 와 SizeOfRawData 는영역의크기를나타낸다는공통점이있으나라운드업된크기와실제크기를나타낸다는차이가있다. 만약섹션의크기를조작했다면위의두부분모두손을봐야한다. Virtual Address 와 Pointer To Raw Data의값이다를수있다고했는데, 왜그럴까? 이것은실행파일의크기를줄이기위해서이다. 만약로드되었을때크기가 0x2000 정도인섹션이있다고하자. 그런데이섹션은메모리의값이초기화될필요도없고값도들어있지않다면? 실행시에영역만할당해주면끝이라면? 이런경우라면굳이이섹션이실행파일에서영역을가지고있을필요가없다. 따라서 Virtual Address는 0 이아닌값을갖겠지만파일내에위치를의미하는 Pointer To Raw Data 의값은 0이된다. 즉실제파일내에는존재하지않는영역이생김으로써 Virtual Address 와 Pointer To Raw Data 의값이달라질수있으며, 기타다른이유로도충분히다를수있다. 따라서실행파일을조작하기위해서는 Pointer To Raw Data 의값을위주로작업을해야한다. Characteristics 는해당영역의속성을나타내는데, WinNT.h 에정의되어있고아주흥미로운값을가지고있다. // IMAGE_SCN_TYPE_REG 0x00000000 // Reserved. // IMAGE_SCN_TYPE_DSECT 0x00000001 // Reserved. // IMAGE_SCN_TYPE_NOLOAD 0x00000002 // Reserved. // IMAGE_SCN_TYPE_GROUP 0x00000004 // Reserved. #define IMAGE_SCN_TYPE_NO_PAD 0x00000008 // Reserved. // IMAGE_SCN_TYPE_COPY 0x00000010 // Reserved. #define IMAGE_SCN_CNT_CODE 0x00000020 // Section contains code. #define IMAGE_SCN_CNT_INITIALIZED_DATA #define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000040 // Section contains initialized data. 0x00000080 // Section contains uninitialized data. #define IMAGE_SCN_LNK_OTHER #define IMAGE_SCN_LNK_INFO 0x00000100 // Reserved. 0x00000200 // Section contains comments or some other type of information.
// IMAGE_SCN_TYPE_OVER 0x00000400 // Reserved. #define IMAGE_SCN_LNK_REMOVE #define IMAGE_SCN_LNK_COMDAT 0x00000800 // Section contents will not become part of image. 0x00001000 // Section contents comdat. // 0x00002000 // Reserved. // IMAGE_SCN_MEM_PROTECTED - Obsolete 0x00004000 #define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 // Reset speculative exceptions handling bits in the TLB entries for this section. #define IMAGE_SCN_GPREL #define IMAGE_SCN_MEM_FARDATA 0x00008000 // Section content can be accessed relative to GP 0x00008000 // IMAGE_SCN_MEM_SYSHEAP - Obsolete 0x00010000 #define IMAGE_SCN_MEM_PURGEABLE #define IMAGE_SCN_MEM_16BIT #define IMAGE_SCN_MEM_LOCKED #define IMAGE_SCN_MEM_PRELOAD 0x00020000 0x00020000 0x00040000 0x00080000 #define IMAGE_SCN_ALIGN_1BYTES 0x00100000 // #define IMAGE_SCN_ALIGN_2BYTES 0x00200000 // #define IMAGE_SCN_ALIGN_4BYTES 0x00300000 // #define IMAGE_SCN_ALIGN_8BYTES 0x00400000 // #define IMAGE_SCN_ALIGN_16BYTES 0x00500000 // Default alignment if no others are specified. #define IMAGE_SCN_ALIGN_32BYTES 0x00600000 // #define IMAGE_SCN_ALIGN_64BYTES 0x00700000 // #define IMAGE_SCN_ALIGN_128BYTES 0x00800000 // #define IMAGE_SCN_ALIGN_256BYTES 0x00900000 // #define IMAGE_SCN_ALIGN_512BYTES 0x00A00000 // #define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000 // #define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000 // #define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000 // #define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000 // // Unused 0x00F00000 #define IMAGE_SCN_ALIGN_MASK 0x00F00000 #define IMAGE_SCN_LNK_NRELOC_OVFL #define IMAGE_SCN_MEM_DISCARDABLE #define IMAGE_SCN_MEM_NOT_CACHED #define IMAGE_SCN_MEM_NOT_PAGED 0x01000000 // Section contains extended relocations. 0x02000000 // Section can be discarded. 0x04000000 // Section is not cachable. 0x08000000 // Section is not pageable.
#define IMAGE_SCN_MEM_SHARED #define IMAGE_SCN_MEM_EXECUTE #define IMAGE_SCN_MEM_READ #define IMAGE_SCN_MEM_WRITE 0x10000000 // Section is shareable. 0x20000000 // Section is executable. 0x40000000 // Section is readable. 0x80000000 // Section is writeable. 중요한플래그별로의미를보면아래와같다. IMAGE_SCN_CNT_CODE : 섹션에코드가포함되어있음. IMAGE_SCN_MEM_EXECUTE 와보통같이지정됨 IMAGE_SCN_CNT_INITIALIZED_DATA : 섹션이초기화된데이터를포함하고있음 IMAGE_SCN_CNT_UNINITIALIZED_DATA : 섹션이초기화되지않은데이터를포함하고있음 IMAGE_SCN_MEM_DISCARDABLE : 섹션이버려질수있음. 한번사용되고필요없는섹션들 (relocation 데이터같은경우 ) 이이속성을가짐 IMAGE_SCN_MEM_SHARED : 섹션이이모듈을사용하는모든프로세스에의해서공유될수있음을의미 IMAGE_SCN_MEM_EXECUTE : 섹션이실행가능함 IMAGE_SCN_MEM_READ : 섹션이읽기가능함 IMAGE_SCN_MEM_WRITE : 섹션이쓰기가능함 위의값을보면섹션에대한속성이미리정의되어있다는것을알수있다. 즉데이터섹션같은경우 IMAGE_SCN_MEM_READ/WRITE 속성을가지고있으리라유추할수있고, 코드가포함된섹션의경우 IMAGE_SCN_MEM_EXECUTE 속성을가지고있다고유추할수있다. 섹션의경우섹션이름을가지고있는데, VC로실행파일을만들면.text,.data,.idata 와같은이름의섹션들이생긴다. 이름그대로코드, 데이터와같은정보가포함된섹션이라는것을알수있는데, 여기서속지말아야할것은섹션이름은권장값이므로섹션이름으로섹션이포함하는내용을판단하면안된다는것이다. 특히파일의크기를줄이는릴리즈옵션같은경우는섹션들이합쳐져서하나의섹션으로존재하는경우도있기때문에섹션이름을이용해서찾아서는안되며 IMAGE_NT_HEADER 에있는 Data Directory 의값을참조해서찾도록해야한다. 2. Import Section Import 영역은 PE 파일이실행될때외부로부터당겨와서사용하는함수들의목록이포함되어있으며, Export 영역은다른모듈이사용할수있게노출해놓은함수들이들어있다. Import 영역은 PE 파일내의특정섹션에자리잡고있으며, IMAGE_OPTIONAL_HEADER 의 DataDirectory 에서위치를찾을수있다. DataDirectory 내의인덱스는 0x01 이며 IMAGE_DIRECTORY_ENTRY_IMPORT 매크로로 WinNT.h 에
정의되어있다. Data Directory 가포함하는정보는해당위치의 RVA 값과크기정보이다. RVA 를통해파일내실제 Offset 을찾는알고리즘은다음과같다. DWORD CPEAnalyzer::GetPointerOfRawDataFromRVA( DWORD dwrva ) { int i; DWORD dwvirtualaddress; DWORD dwsize; DWORD dwpointerofrawdata; for( i = 0 ; i < m_pstimagentheader->fileheader.numberofsections; i++ ) { dwvirtualaddress = m_pstimagesectionheader[ i ].VirtualAddress; dwsize = m_pstimagesectionheader[ i ].SizeOfRawData; dwpointerofrawdata = m_pstimagesectionheader[ i ].PointerToRawData; if( ( dwvirtualaddress <= dwrva ) && ( dwrva < ( dwvirtualaddress + dwsize ) ) ) { return ( dwrva - dwvirtualaddress ) + dwpointerofrawdata; } } return 0xFFFFFFFF; } 우선 RVA 를통해해당섹션헤드 (IMAGE_SECTION_HEADER) 를찾는다. 그러면 Offset 은헤더의시작에서찾으려는주소의상대주소 + 헤더의파일내 Offset 이면된다. IMAGE_IMPORT_DESCRIPTOR Import 섹션은 IMAGE_IMPORT_DESCRIPTOR 구조체로시작한다. 해당구조체의배열형태로구성되며마지막은 NULL 구조체로되어있어끝을표시한다. IMAGE_IMPORT_DESCRIPTOR typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; // 0 for terminating null import descriptor // RVA to original unbound IAT (PIMAGE_THUNK_DATA) }; DWORD TimeDateStamp; // 0 if not bound, // -1 if bound, and real date\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR; OriginalFirstThunk : IMAGE_THUNK_DATA 구조체 RVA 주소를가리킴. 이때 IMAGE_THUNK_DATA 는 Import 하는함수이름이나서수 (Ordinal) 를포함하는구조체 IMAGE_IMPORT_BY_NAME 을가리킴 Name : Import 한 DLL 의이름을담고있는 ASCII 문자열의 RVA 주소 FirstThunk : OriginalFirstThunk 와같이 IMAGE_THUNK_DATA 구조체의 RVA 주소를가리키지만 PE 파일이메모리에맵핑되고나면 Import 한 DLL 내의함수주소가지고있는 IMAGE_THUNK_DATA 를가리킴 IMAGE_IMPORT_DESCRIPTOR OriginalFirstThunk 및 FirstThunk 가가리키는구조체인 IMAGE_THUNK_DATA 의구조는다음과같다. typedef struct _IMAGE_THUNK_DATA64 { union { ULONGLONG ForwarderString; // PBYTE ULONGLONG Function; // PDWORD ULONGLONG Ordinal; ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA64; typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64; typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; // PBYTE DWORD Function; // PDWORD DWORD Ordinal; DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA32; typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32; ForwarderString : 실제 Import 한함수가 Forwarding 된함수일경우.
Function : FirstThunk 의경우메모리에로딩이되면실제함수주소를가리키는 IMAGE_THUNK_DATA 의 RVA 값을표시한다고했는데, 이때실제 Function 의주소가포함된부분 Ordinal : Import 를함수명이아니라서수 (Ordinal) 로한경우. 0x80000000 값으로마스크하면최상위비트를구할수있는데이것이 1 로셋팅된경우서수로판단 AddressOfData : 실제 Import 된함수의이름이포함된 IMAGE_IMPORT_BY_NAME 구조체에대한 RVA 주소포함 여기서 ForwarderString 에대해서잠시알아보겠다. 이것은 import 할 dll 에서해당함수를또다시다른 dll 로포워딩할경우이변수가사용된다. 예를들어윈도우 NT, 윈도우 2000, 윈도우톄의경우 KERNEL32 의 HeapAlloc 함수는 NTDLL 의 RtlAllocHeap 로포워드되어있다. 그렇다면 ( 밑의 Export 섹션에서보겠지만 ) Kernel32 의 Export 섹션부분에서해당함수 AddressOfFunction 는 NTDLL.RtlAllocHeap 스트링심볼의 RVA 주소로셋팅되어있다. 참고로 Export 섹션에서 AddressOfFunction 이가르키는곳이함수의주소인지위와같이심볼의주소인지를판별하는방법은만약주소가 export 섹션내부를가르키고있다면이는심볼의주소이다. IMAGE_IMPORT_BY_NAME 이제살펴볼마지막은실제함수이름을포함하는 IMAGE_IMPORT_BY_NAME 구조체이다. typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; BYTE Name[1]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME; Hint : Import 시서수를사용하지않으면임의로설정되는 0 base 의숫자, Import 한함수가추가될때마다 1 씩증가. Name : NULL 로끝나는 Ascii 문자열. 실제함수이름을포함 아래는전체적인 Import 섹션의그림이다.
3. Export 섹션 Export 영역은내가다른프로그램을위한기능을제공하기위해노출한함수목록이들어있다. Import 영역과비슷한방법으로 IMAGE_NT_HEADER 의 Data Directory 를찾아서 0 번째인덱스 (IMAGE_DIRECTORY_ENTRY_EXPORT) 를찾으면 Export 섹션을구할수있다. 역시 RVA 를파일내의오프셋 (Pointer Of Raw Data) 로바꾸는작업이필요하다. IMAGE_EXPORT_DIRECTORY Export 섹션의첫번째는 IMAGE_EXPORT_DIRECTORY 구조체로되어있다. 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; // RVA from base of image // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY; Name : DLL 의이름을나타내는 ASCII 문자열. RVA 값 Base : 아래에오는 Address Of Name Ordinals 의시작서수. AddressOfNameOridianals 의값은 Base 의값을뺀형태로저장. NumberOfFunctions : AddressOfFunctions 가가리키는 RVA 배열의수 NumberOfNames : AddressOfNames 가가리키는 RVA 배열의수 AddressOfFunctions : 함수의실제주소가담긴 RVA 값의배열. 배열의인덱스는아래 AddressOfNameOrdinals 에서구한값이거나서수에서 Base 값을뺀값 AddressOfNames : 함수의실제 Ascii 문자열이담긴 RVA 위치값의배열 AddressOfNameOrdinals : 함수의서수 (Ordinal) 값의배열위치. RVA 값이며배열은 WORD 크기. 실제해당함수의서수는 AddressOfNameOrdinals 에서 WORD 의값을얻은것에 Base 의값을더해야함 Import 섹션보다비교적직관적이고간단한구조로되어있음을알수있다. 이것을그림으로보면아래와같다. < 주의 : 여기서서수는함수순서에따라 1,2,3 순서로순차적으로할당되었다고, 함수순서라고생각해서는안된다. 이것은어디까지나우연일뿐이다. 실제함수중에서는같은이름의함수도있으므로서수는이것들을구별해주는고유의숫자이다. 실제로랜덤으로숫자가할당될수도있다.> 4. Relocation 섹션일단들어가기전에재배치 (Relocation) 이무엇인지알아보자. 재배치는코드에특정값을더해줘서다른메모리주소에서실행가능하게해주는것을말한다. 말그대로코드를다시배치하는과정인데, 왜이런걸해야하는걸까?
EXE 파일의경우윈도우에서는굳이재배치를할필요가없다. 왜냐하면로더가프로그램을로딩할때제일먼저 EXE 파일을위한메모리를할당해주기때문이다. IMAGE_OPTIONAL_HEADER 에 ImageBase 라는필드를기억할지모르겠다. 이것이바로 PE 파일이로딩될 Base 주소를의미한다. EXE 파일의경우가장먼저메모리를할당받으므로 ImageBase( 일반적으로 0x400000) 에로딩가능하다. DLL 의경우는어떨까? 실행파일이사용하는 DLL 이어디한두개일까? 여러개가로딩이되면당연히그중에몇몇 DLL 은 ImageBase 에로딩하지못하는경우도발생한다. 이때어쩔수없이다른메모리주소에서실행해야하는데이과정을재배치과정이라고하고재배치섹션의정보가사용되는것이다. 재배치정보는다른정보와마찬가지로 IMAGE_OPTIONAL_HEADER 의 Data Directory 에서찾을수있고 0x05 인덱스 (IMAGE_DIRECTORY_ENTRY_BASERELOC) 에서그 RVA 를구할수있다. IMAGE_BASE_RELOCATION 재배치섹션의처음시작은 IMAGE_BASE_RELOCATION 구조체로시작한다. typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION; VirtualAddress : 재배치가수행될메모리상의 RVA. SizeOfBlock : 재배치영역의크기. IMAGE_BASE_RELOCATION 자신크기를포함한전체크기 Export 섹션보다더간단한구조를가진다. 이후에보면알겠지만재배치데이터같은경우 IMAGE_BASE_RELOCATION 구조체다음에 n 개의 WORD 형태로반복해서나타나고그중하위 0xFFF는 Offset 으로사용된다. 따라서재배치할영역이크고넓은경우 IMAGE_BASE_RELOCATION + n 개의 WORD 의형태가반복되어서나타나게된다. 마지막은역시데이터가 0 인가를이용하여판단한다. 그럼 IMAGE_BASE_RELOCATION 이후에존재하는 n 개의 WORD 는어떤식으로구성될까? 상위 4Bit 는재배치 Type 으로사용되며하위 12Bit 는 Offset 으로사용된다. 따라서코드를작성한다면아래와같이쓸수있을것이다. Type 과 Offset 은아래와같은의미를가진다.
Type : 재배치정보의타입. 사실거의의미가없고 0 일경우패딩데이터 Offset : 실제코드가재배치될영역. VirtualAddress 와더해져서수정해야할 RVA 값이됨 위에서보듯실제코드가수정되어야하는위치는 IMAGE_BASE_RELOCATION 의 VirtualAddress 와 Offset 을더한값이된다. 정말간단하다. 여기서알아두어야할점은 Offset 이최대 0xFFF 라는것이다. 즉 4Kbyte 까지커버가가능하므로 4Kbyte 이상이되면다시 IMAGE_BASE_RELOCATION 을만들어서접근해야한다. 재배치를수행하는과정은아주간단하다. 아래의순서대로수행하면된다. 1. 재배치가수행되야할곳의 DWORD 값을읽는다. 2. 읽은 DWORD 값에서현재 IMAGE_OPTIONAL_HEADER 의 ImageBase 를뺀다. 빼는순간코드는 0 을기본 Base 주소로하는코드로변한다. 3. 뺀값에실제로모듈이로딩된메모리주소를더한다. 더하는순간코드는실제모듈이로딩된위치에서정상적으로수행가능한코드로변한다. 단순한뺄셈과덧셈만으로재배치가가능하다. 5. Resource 섹션 PE 포멧내에서 resource 의구조는하드디스크의폴더구조와비슷한다. 폴더구조와똑같이항상 root 폴더가하나존재하고그밑으로서브폴더가존재한다. 그리고그런식으로내려다가보면맨마지막에는바로리소스데이터를찾을수있게된다. 이러한폴더구조체는다음과같다. IMAGE_RESOURCE_DIRECTORY typedef struct _IMAGE_RESOURCE_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; WORD NumberOfNamedEntries; WORD NumberOfIdEntries; // IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[]; } IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
Characteristics : 실제로는리소스에대한특성플래그를가지고있을수있지만대부분항상 0 이다. NumberOfNamedEntries : 이름을구별자로사용하는요소들의개수 NumberOfIdEntries : 정수 ID 를구별자로사용하는요소들의개수 DirectoryEntires[] : 배열의개수는 NumberOfNamedEntires 와 NumberOfIdEntires 의합이다. 폴더의하위폴더를가르키거나실제리소스데이터를가르킨다. 실제로이멤버는 IMAGE_RESOURCE_DIRECTORY 의멤버는아니지만이구조체뒤에곧바로위치해있다. IMAGE_RESOURCE_DIRECTORY_ENTRY typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY { union { struct { DWORD NameOffset:31; DWORD NameIsString:1; }; DWORD Name; WORD Id; }; union { DWORD OffsetToData; struct { DWORD OffsetToDirectory:31; DWORD DataIsDirectory:1; }; }; } IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY; Name : 만약폴더가이름으로구별될어질경우사용된다. 최상위 1 비트가 1 로셋팅되어있고, 나머지 31 비트는실제스트링문자열을가르키는오프셋이된다. 오프셋은이엔트리가속한 IMAGE_RESOURCE_DIRECTORY 의시작위치에서부터의오프셋이다. Id : 만약폴더가정수로구별질경우사용된다. 최상위 1 비트가 0 으로셋팅되고, 하위 16 비트가정수로사용된다.
OffsetToData : 만약구별자 (ID) 폴더 ( 아래설명 ) 일경우, 최상위비트는 0 으로셋팅되고, 나머지는오프셋으로실제리소스데이터를가르키게된다. 만약다른또다른폴더를가르킬경우, 최상외비트는 1 이고나머지오프셋은해당하위폴더를가르킨다. IMAGE_RESOURCE_DIRECTORY_STRING typedef struct _IMAGE_RESOURCE_DIRECTORY_STRING { WORD Length; CHAR NameString[ 1 ]; } IMAGE_RESOURCE_DIRECTORY_STRING, *PIMAGE_RESOURCE_DIRECTORY_STRING; 위구조체는폴더가이름으로구별될경우쓰이는스트링문자열구조체이다. 따로설명안해도이해할거라믿는다. 보통이러한 Resource 섹션구조에서는실제리소스데이터에접근하기위해서최소한 3 개의폴더구조를거쳐야한다. 첫번째는 Resource 섹션처음에찾을수있는 root 폴더이다. 다음이폴더의하위폴더로 PE 파일이가지고있는리소스를분류하는폴더들이있을수있다. 예를들어만약 PE 파일이몇몇의대화상자들와스트링테이블들과메뉴들을가지고있다면 3 개의하위폴더가있을수있겠다. 그리고이폴더밑에실제리소스데이터를가르키는구별자 (ID) 폴더가있을수있다. 각각의리소스는이러한구별자 (ID) 폴더와 1 대 1 대응이된다. 예를들어 3 개의대화상자가있다면대화상자폴더 (2 번째계층 ) 는 3 개의 ID 폴더를가지고있을것이다. 이러한 ID 폴더는각각의리소스를구별해줄문자열이름 ( MyDialog 같은것 ) 이나정수 ID 를가지고있을것이다. 아래그림은이때까지의내요을설명해준다.
6. TLS(Thread Local Storage) 섹션 섹션 TLS(Thread Local Storage) 는스레드별로고유한저장공간을가질수있는방법이다. 각각의스레드는고유한스택을갖기때문에스택변수 ( 지역변수 ) 는스레드별로고유하다. 예를들어서각각의스레드가같은함수를실행한다고해도그함수에서정의된지역변수는실제로서로다른메모리공간에위치한다는의미한다. 그러나정적변수나전역변수의경우에는프로세스내의모든스레드에의해서공유된다. TLS 를사용하는방법은 2 가지가있다. 한가지는 API 에서지원해주는방식을사용하는것이고, 다른하는 compiler 에서지원하는방식을사용하는것이다. 다들알겠지만 compiler 에서지원하는방식을 storage-class modifier 라하는데이방식을이용해야만 PE 포멧내에 TLS 섹셕이만들어진다. API 는그대로함수코드를이용해서스레드고유의공간을할당하는방식이다. compiler 에서 TLS 를사용하는방법은다음과같다. _declspec( thread ) int nwindows; 위와같이선언한경우변수 nwindows 는스레드고유변수가된다.
로더는 PE 파일로드시 DataDirectory 의 IMAGE_DIRECTORY_ENTRY_TLS 엔트리를검사한후, 크기가 0 이아니면 TLS 섹션의존재로간주하고힙에서스레드고유공간을위한메모리를할당하고 TLS 섹션의값들로초기화시킨다. TLS 섹션은다음과같은하나의 IMAGE_TLS_DIRECTORY 로이루어져있다. typedef struct _IMAGE_TLS_DIRECTORY64 { ULONGLONG StartAddressOfRawData; ULONGLONG EndAddressOfRawData; ULONGLONG AddressOfIndex; // PDWORD ULONGLONG AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *; DWORD SizeOfZeroFill; DWORD Characteristics; } IMAGE_TLS_DIRECTORY64; typedef IMAGE_TLS_DIRECTORY64 * PIMAGE_TLS_DIRECTORY64; typedef struct _IMAGE_TLS_DIRECTORY32 { DWORD StartAddressOfRawData; DWORD EndAddressOfRawData; DWORD AddressOfIndex; // PDWORD DWORD AddressOfCallBacks; // PIMAGE_TLS_CALLBACK * DWORD SizeOfZeroFill; DWORD Characteristics; } IMAGE_TLS_DIRECTORY32; typedef IMAGE_TLS_DIRECTORY32 * PIMAGE_TLS_DIRECTORY32; StartAddressOfRawData : 새로운스레드의 TLS 공간을채우기위한데이터의시작번지. EndAddressOfRawData : 새로운스레드의 TLS 공간을채우기위한데이터의마지막번지. AddressOfIndex : 실행파일이로드되고 TLS 섹션이존재할경우로더는 TLS 핸들을이변수에할당한다. 스레드고유데이터를찾기위해이핸들이사용된다. AddressOfCallBacks : PIMAGE_TLS_CALLBACK 함수포인터의배열이다. 스레드가생성되고파괴될때함수들이차례차례호출된다. 배열의마지막은 0 으로되어있다. 보통비주얼 C++ 로생성된실행파일은이부분이비어있다. SizeOfZeroFill : 위의 TLS 범위뒤에 0 으로초기화시킬바이트크기. Characteristics : 예약된멤버.
여기서주소는 RVA 가아니라가상주소라는점을주의해야한다. 만약실행파일이원하는위치에로드를실패하면가상주소는수정될것이다. 그리고 IMAGE_TLS_DIRECTORY 구조체자체는.tls 섹션에있는것이아니라.rdata 섹션에위치한다.