PE(Portable Executable) 구조
리버싱 2017. 1. 10. 16:20Win32의 기본적인 파일 형식
PE(Portable Executable) 구조
오늘은 PE(Portable Executable)에 대해서 알아보도록 하겠습니다. PE라는 말을 들어보신 적이 있나요? 아마 이 글을 보고 계시는 분들 중에 리버싱 경험이 있으신 분들은 PE란 말을 이미 알고 계실겁니다. 우리가 쓰고 있는 윈도우즈 환경의 실행 파일 포맷을 PE라고 하며, "Portable"의 단어 뜻 그대로 의식성이 있으며 플랫폼에 독립적입니다. PE 파일은 굳이 확장자가 EXE인 파일만 일컫는게 아니며 SCR, SYS, DLL, OCX 등도 포함이 되고, 중간 파일인 OBJ 파일도 PE 파일이라고 간주합니다. PE 구조의 이해는 API 후킹, 압축 실행 파일 등과 같은 고급 리버싱 기법의 기본 바탕이 됩니다. 자, 이제는 PE의 전체적인 구조를 살펴보고 이해하도록 합시다!
PE(Portable Executable)의 전체적인 구조 살펴보기
우선은 헥스 에디터로 계산기(calc.exe) 파일을 열어보도록 합시다.
위 그림에서 보이는 부분이 calc.exe의 시작 영역이자, PE 헤더(PE Header) 영역이라고 할 수 있습니다. 위의 PE 헤더에는 calc.exe와 같은 실행 파일을 실행하기 위한 여러가지 정보가 기록이 되어 있으며, PE의 내용을 가지고 DLL를 로드하거나 여러가지 리소스를 할당하는 등 상당히 많은 정보가 PE 헤더에 저장되어 있습니다. 중요한 정보가 담긴 만큼, 이 영역의 일부가 누락되거나 손상되는 경우가 있으면 정상적으로 해당 파일을 실행할 수 없습니다. 파일을 백업해두고 HEX 코드 일부를 수정하시고 저장하셔서 실행을 하여 어떠한 결과를 초래하는지 보세요.
PE 구조는 차례대로 DOS Header, Stub Code, PE Header(File Header, Optional Header 포함), Section Header로 나뉘며 그 뒤에는 보통 코드를 포함하는 코드(.text) 섹션, 전역 변수 혹은 정적 변수를 포함하고 있는 데이터(.data) 섹션, 문자열이나 아이콘 같은 리소스 데이터를 포함하는 리소스(.rsrc) 섹션으로 나뉘어 등장합니다. 이러한 섹션은 1개 이상 존재하며, 섹션들 사이에 HEX 코드가 00(NULL)으로 나타나는 부분은 정렬 규칙에 의해 크기를 버리고 처리 효율을 높이기 위해 사용하는 영역으로 패딩(padding) 영역이라고 할 수 있습니다.
참고로 위 그림에서 섹션의 왼쪽에 있는 16진수 값은 해당 섹션의 크기를 말하는 겁니다.
위 그림에서는 우선 VirtualSize(가상 메모리에서 해당 섹션이 차지하는 크기), VirtualOffset(가상 메모리 오프셋, VA), RawSize(파일에서 해당 섹션이 차지하는 크기), RawOffset(파일 오프셋) 요 부분은 한번 보도록 합시다. (그림 내에 있는 RVA와 RAW란 녀석은 PE 헤더에 대해 알아볼때 다시 얘기를 할 것입니다.)
약간 살펴보시면, 파일이 메모리에 적재되고 나서는 섹션의 크기 혹은 형태 등이 달라졌음을 확인하실 수 있습니다. 또한 파일은 offset, 메모리에는 address라고 되어 있는데, 이는 파일이 오프셋(offset)으로 위치를 표현하고 메모리는 address, VA(Virtual Address, 가상 메모리의 절대 주소)로 위치를 표현한다고 할 수 있습니다. 이제는 PE 헤더에 대해서 조금씩 알아가면서 PE 구조를 이해하도록 하겠습니다.
PE 헤더(Portable Executable Header)
우리가 알아볼 헤더라는 것은 사실은 여러 가지 필드로 이루어진 하나의 구조체라고 말할 수 있습니다. PE에는 여러가지 헤더가 앞에 자리잡고 있으므로 이는 PE가 여러가지 구조체로 구성이 되어있다고 말할 수 있습니다. 아래에서는 도스 스텁을 제외한 나머지의 헤더를 크게 3개(도스 헤더, NT 헤더, 섹션 헤더)로 나누어 해당하는 헤더의 구조체를 가지고 설명합니다. PE를 구성하는 영역을 도스 헤더부터 섹션 헤더까지 차례대로 살펴보도록 합시다.
1. IMAGE_DOS_HEADER
가장 처음으로 등장하는 영역은 도스 헤더의 영역입니다. 아래의 코드는 winnt.h 헤더의 일부(나머지 헤더 구조체와 매크로 상수 역시 이 헤더에서 가져옴)로 도스 헤더의 구조체입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #define IMAGE_DOS_SIGNATURE 0x4D5A // MZ typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; |
위 구조체는 데이터 타입이 WORD인 필드가 총 16개, WORD[]인 필드가 2개, LONG인 필드가 1개로, 해당 구조체의 크기는 총 64 바이트(16진수로는 0x40)이며 필드는 총 19개임을 알 수 있습니다. 위 코드를 보시면 필드가 상당히 많은데, 겁먹을 필요 없이 여기서는 딱 두가지의 필드만 보시면 됩니다. e_magic 필드와 e_lfanew 필드만 보시면 됩니다.
e_magic: e_magic 필드는 PE 파일이 맞는지 아닌지 체크할 때 사용되며, PE 파일 처음부터 2바이트까지 보시면 4D 5A(IMAGE_DOS_SIGNATURE)로 시작하는 부분이 e_magic이 차지하는 공간입니다. 여기서 MZ는 DOS의 설계자인 마크 즈비코프스키(Mark Zbikowski)에서 이름을 따온 것이며, 도스 헤더의 시작을 알리는 코드라 할 수 있습니다. PE 파일의 맨 앞에 위치한 e_magic은 MZ(4D 5A)라는 코드로 고정되어 있으며 가장 앞에서 2바이트를 읽어온 후에 IMAGE_DOS_SIGNATURE와 비교를 하여 서로 다르다면 그것은 PE 파일 구조가 아니라고 할 수 있습니다.
e_lfanew: e_lfanew 필드는 IMAGE_NT_HEADER의 시작 오프셋을 가지며, 고정되어 있는 값이 아닌 파일에 따라 가변적인 값을 지닙니다. 즉, PE 헤더(NT 헤더)의 주소는 도스 헤더의 e_lfanew 필드를 참조하여 알아낼 수 있다는 것이 됩니다. 위 그림에서 e_lfanew 필드의 값은 000000D8 입니다. 주의하실 점은 값이 D8000000이 아니라는 겁니다. 이는 리틀 엔디언 표기법때문에 그런데, 여기서 잠깐 리틀 엔디언 표기법에 대해 잠시 알아보도록 합시다.
1-1. 리틀 엔디언(Little Endian) 표기법
리틀 엔디언(Little Endian) 표기법에 대해 간단히 알아보도록 합시다. 리틀 엔디언은 무엇일까요? 리틀 엔디언 표기법은 Intel 계열 CPU에서 사용하는 표기법으로 낮은 주소부터 시작하여 하위 바이트를 기록하는 것을 말합니다. 만약 0x075BCD15라는 값을 어떠한 저장 공간에 기록하고 싶다면, 하위 바이트부터 시작하여 차례대로 15 CD 5B 07으로 저장되게 됩니다. 바이트의 순서는 이렇지만, 실제 값이 0x15CD5B07이 아니라 0x075BCD15라는 것입니다.
위 그림에서도 0x11223344라는 값이 메모리 공간에 저장된다고 하면, 11 22 33 44 그대로 저장되는게 아니라, 낮은 주소부터 시작하여 하위 바이트를 기록하니 44 33 22 11과 같은 식으로 기록이 됩니다. 반대로 낮은 주소부터 시작하여 상위 바이트를 기록하는 방법은 빅 엔디언(Big Endian) 표기법이라고 합니다.
2. Stub Code
스텁 코드(Stub Code)가 무엇인지 잠시 아래 그림을 봐보도록 합시다.
위의 영역이 바로 도스 스텁 영역입니다. 저 스텁 영역을 자세히 보시면 "This program cannot be run in DOS mode"라는 문자열을 볼 수 있으며, 도스 모드에서 이 파일이 실행되는 것을 막기 위한 것입니다. (16비트 환경에서 실행되는 영역이며, 32비트 환경에선 실행되지 않는 영역입니다) 예전에 쓰던 MS-DS를 구해 가상머신 등으로 설치한 뒤에 윈도우 프로그램을 실행하려 한다면 저 메시지를 볼 수 있습니다. 하위 호환성을 위해서 만든 메시지이라고 생각하시면 됩니다.
도스 헤더 구조체에서의 e_lfanew 필드가 고정적인게 아닌 가변적인 것도 도스 스텁 영역의 크기가 가변적이라서 그렇습니다. e_lfanew 필드는 IMAGE_NT_HEADER의 시작 오프셋을 가진다고 했었고, 도스 스텁 영역의 다음이 IMAGE_NT_HEADER 구조체가 위치하니 도스 스텁 영역의 크기가 변하면 e_lfanew 필드의 값도 변합니다. 도스 스텁 영역은 크게 신경 안쓰셔도 되는 영역이라고 생각하시면 됩니다.
3. IMAGE_NT_HEADER
IMAGE_DOS_HEADER 구조체에 이어 IMAGE_NT_HEADER 구조체를 살펴보도록 하겠습니다.
1 2 3 4 5 6 7 | #define IMAGE_NT_SIGNATURE 0x00004550 // PE00 typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; |
Signature: _IMAGE_NT_HEADERS의 필드를 살펴보시면 Signature 필드가 가장 처음으로 등장하는데, 데이터 타입이 DWORD니 4바이트를 차지하며, 이 Signature의 값을 가지고 PE 파일 구조인지 아닌지 체크할 수 있습니다. Signature의 값은 IMAGE_NT_SIGNATURE 상수 그대로 PE 00 이라는 값을 지닙니다. 저장되는 순서는 50 45 00 00 이며, 한번 직접 Signature 필드가 어디에 위치하여 있는지 확인해보도록 합시다.
위 그림에 표시된 영역이 바로 Signature 필드의 공간입니다. 우리가 생각하는 값이 나왔네요. 한번, 시험삼아 파일을 백업해두고 도스 헤더 영역의 e_magic 필드나 IMAGE_NT_HEADERS의 Signature 필드를 임의의 값으로 수정하여 실행해보시면 엉뚱한 값이 들어감을 확인하여 올바른 PE 파일이 아니라고 에러 처리를 할 것입니다.
그리고, Signature 말고도 FileHeader와 OptionalHeader 필드가 있는데 이는 IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER를 설명하면서 같이 알아보도록 하겠습니다.
3-1. IMAGE_FILE_HEADER
파일 헤더 구조체인 IMAGE_FILE_HEADER에는 PE 파일에 대한 기본적인 내용이 담겨 있습니다. 우선은 아래의 코드를 보도록 합시다.
1 2 3 4 5 6 7 8 9 | 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; |
IMAGE_DOS_HEADER만큼은 아니지만 그래도 필드가 좀 많아보이죠? 여기에서는 Machine과 NumberOfSections, TimeDateStamp, SizeOfOptionalHeader, Characteristics 이 5개의 필드에 대해서 알아보도록 하겠습니다. 먼저 Machine을 보도록 합시다.
Machine: Machie 필드는 이 파일이 어떤 CPU에서 동작할 수 있는지, 실행될 수 있는 CPU의 타입을 정합니다. 아래는 winnt.h에 정의된 Machine 상수 입니다.
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 | #define IMAGE_FILE_MACHINE_UNKNOWN 0 #define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386. #define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian #define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian #define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian #define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2 #define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP #define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian #define IMAGE_FILE_MACHINE_SH3DSP 0x01a3 #define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian #define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian #define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5 #define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian #define IMAGE_FILE_MACHINE_THUMB 0x01c2 // ARM Thumb/Thumb-2 Little-Endian #define IMAGE_FILE_MACHINE_ARMNT 0x01c4 // ARM Thumb-2 Little-Endian #define IMAGE_FILE_MACHINE_AM33 0x01d3 #define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian #define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1 #define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64 #define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS #define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64 #define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS #define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS #define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64 #define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon #define IMAGE_FILE_MACHINE_CEF 0x0CEF #define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code #define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8) #define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R little-endian #define IMAGE_FILE_MACHINE_CEE 0xC0EE |
예를 들면, Machine 필드의 값이 014C라면 Intel x86 CPU와 호환이 된다는 것입니다. 한번 calc.exe의 Machine 필드의 값을 확인해보도록 합시다.
저기 보시면 4C01로 기록되어 있고, 값은 014C로 IMAGE_FILE_MACHINE_I386 상수와 값이 일치합니다.
NumberOfSections: 이 필드는 PE 파일을 구성하는 섹션(Section)의 수로 섹션이 추가되면 이 값이 늘어나고 섹션이 줄어들면 이 값 역시 줄어듭니다. 이 값을 보고 이후에 등장할 섹션의 수를 알아낼 수 있으며 이 필드의 값은 0보다 커야 합니다. 이는 섹션이 한개라도 없는 경우가 존재하지 않는다는 것입니다. 한번 NumberOfSection 필드의 값을 보도록 해봅시다.
앞에서 보았듯이 .text, .data, .rsrc, .reloc 섹션이 존재하며, 섹션의 수는 4개입니다. 위 그림에서 NumberOfSections의 값이 0004인 것을 확인하실 수 있으며, 이는 섹션의 수가 4개라는 것으로 실제 섹션의 수와 일치한다는 것을 확인하실 수 있습니다.
TimeDateStamp: TimeDateStamp 필드는 PE 파일이 만들어진 시간, 즉 이 파일이 빌드된 날짜가 타임스탬프 형식으로 기록됩니다. 그러나 이는 확실히 신뢰할 수 있는 값이 아니며 변조가 가능하니 대략적인 값으로 생각을 하셔야 합니다. 한번 TimeDateStamp 필드의 값도 확인을 해보도록 합시다.
TimeDateStamp의 값은 4CE7979D라고 할 수 있고, 이 16진수 값을 10진수 값으로 바꾸면 1290246045 입니다. 이를 다시 표준 시각으로 바꾸면 "Saturday, November 20th 2010, 09:40:45 (GMT)"로, 2010년 11월 20일 9시 40분 45초에 빌드되었다고 대략 예상할 수 있습니다.
SizeOfOptionalHeader: SizeOfOptionalHeader 필드에는 옵셔널 헤더(IMAGE_OPTIONAL_HEADER32)의 크기가 담깁니다. IMAGE_OPTIONAL_HEADER의 크기는 정해져 있는것 같지만, 운영체제마다 크기가 가변적이기 때문에 PE 로더가 이 SizeOfOptionalHeader 필드의 값을 확인하고 IMAGE_OPTIONAL_HEADER의 크기를 처리합니다. 이것 역시 한번 직접 값이 어떤지 보도록 합시다.
위 그림에서 SizeOfOptionalHeader 필드의 값을 확인하실 수 있으며, 값은 00E0으로 10진수로는 224 바이트 만큼을 자치한다고 할 수 있습니다. 이 필드의 값은 32비트에선 0xE0, 64비트에서는 0xF0의 크기를 지닌다고 합니다.
Characteristics: Characteristics 필드는 현재 파일의 형식을 알려주는 역할을 하며, 이 필드의 값을 가지고 실행 가능한 파일인지, DLL 파일인지, 시스템 파일인지, 재배치 여부 등에 대한 정보가 들어있다고 할 수 있습니다. 아래는 winnt.h에 정의된 Characteristics 상수입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file. #define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references). #define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file. #define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file. #define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set #define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses #define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed. #define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine. #define IMAGE_FILE_DEBUG_STRIPPED 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 0x1000 // System File. #define IMAGE_FILE_DLL 0x2000 // File is a DLL. #define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine #define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed. |
위 상수의 값은 비트 플래그를 사용한 것으로, 2진수 형식으로 증가합니다. 우선은 직접 파일의 Characteristics 필드를 확인하여 보도록 합시다.
보시면 Characteristics 필드의 값이 0102라는 것을 알 수 있으며, 이는 0100과 0002를 합한 값과 같습니다. 0x0100은 IMAGE_FILE_32BIT_MACHINE이며, 0x0002는 IMAGE_FILE_EXECUTABLE_IMAGE 인 것을 확인하실 수 있습니다. 이는 즉, 32비트 머신을 필요로 하며, 실행 가능한 파일임을 알 수 있습니다. 파일 헤더의 구조체에 대한 설명은 여기서 마치고, 다음으로는 옵셔널 헤더의 구조체에 대해 알아보도록 하겠습니다.
3-2. IMAGE_OPTIONAL_HEADER
이번에는 IMAGE_OPTIONAL_HEADER 구조체에 대해서 간단히 알아보도록 하겠습니다. 우선 아래의 코드를 보도록 합시다.
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 31 32 33 34 35 36 37 38 | typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; 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; |
IMAGE_OPTIONAL_HEADER 구조체는 PE 구조체 중에서도 가장 크기가 큰 구조체로, 필드의 수가 상당히 많아보이죠? 크기가 큰 만큼 중요한 값도 많이 지니고 있습니다. 총 31개의 필드를 지니고 있고, 이 중에서도 11개의 필드에 대해서 간략히 알아볼 생각입니다. 순서대로 나열하면 Magic, SizeOfCode, ImageBase, AddressOfEntryPoint, BaseOfCode, SectionAlignment, FileAlignment, SizeOfImage, SizeOfHeaders, Subsystem, DataDirectory에 대해 알아볼 것이며, 가장 처음으로 Magic 필드부터 시작하여 차근차근 알아보도록 하겠습니다.
Magic: 32비트(IMAGE_OPTIONAL_HEADER32)인 경우에는 값이 10B이며, 64비트(IMAGE_OPTIONAL_HEADER64)인 경우에는 20B라는 값을 가집니다.
위 그림에서 Magic 필드의 값이 010B임을 확인하실 수 있으며, 이는 32비트(IMAGE_OPTIONAL_HEADER32) 구조체임을 확인할 수 있습니다.
SizeOfCode: 코드 영역의 전체 크기가 이곳에 들어갑니다. 이는 .text 섹션의 크기가 들어간다는 것입니다. SizeOfCode의 값을 확인해보도록 하고, 이어서 .text 섹션의 크기와 SizeOfCode의 값이 일치하는지 비교도 해보도록 합시다.
위 그림에서 SizeOfCode의 값을 확인해보면 00052E00이며, 이는 .text의 크기와 정확히 일치합니다.
RawSize는 파일에서 해당 섹션이 차지하는 크기로 .text 섹션이 52E00만큼 공간을 차지한다고 볼 수 있습니다.
ImageBase: PE 파일이 메모리에 로드될 때의 시작 주소를 가리킵니다. 기본적으로 EXE 파일의 경우에는 0x400000 번지가, DLL 파일인 경우에는 0x10000000 번지로 지정되어 있으며 그렇다고 해서 항상 이 번지로 고정되어 있는게 아니라는 점을 주의하셔야 합니다. (이는 링커 옵션을 통해서 시작 주소를 지정할 수 있습니다) DLL의 경우는 기본 ImageBase의 값이 0x10000000 번지로 지정되어 있지만, 다른 DLL이 이 번지를 차지하고 있을 경우에는 다른 곳에 배치되는 재배치가 이루어집니다.
이 ImageBase는 RVA의 기준이 되며, 여기서 RVA(Relative Virtual Address, 상대 가상 주소)란 ImageBase를 기준으로 하여 어느만큼 떨어져 있는지를 나타내는 값으로 파일의 오프셋(Offset)과 같은 개념이라고 할 수 있습니다. 다만, RVA는 파일이 아닌 메모리 공간에서의 상대적인 값으로 예를 들면, ImageBase가 0x400000 번지일 경우 .text 섹션의 RVA 값이 0x3000 이라면 실제로 .text 섹션이 로드되는 위치는 0x403000이 되는 것입니다.
우선은 calc.exe의 ImageBase의 값을 보도록 하겠습니다.
위 그림에서 ImageBase의 값은 01000000이며, 0x1000000이 시작 주소임을 확인할 수 있습니다.
AddressOfEntryPoint: 프로그램이 메모리에서 실행 되는 시작 지점이며, 이는 진입점(Entry Point)를 말하는 것입니다. 위치는 RVA 값으로 저장되어 있으며, WinMain 혹은 DllMain의 번지라고 생각할 수 있습니다. (정확하게는 Start up의 번지라고 할 수 있습니다.) 실제로 다음 실행할 명령이 들어있는 메모리의 번지를 가지는 EIP 레지스터의 값을 파일이 메모리에 로딩되고 나서 ImageBase + AddressOfEntryPoint로 지정합니다. (올리디버거 같은 디버거를 통해서 파일을 실행시키고 나면 디버거가 처음 실행할 위치를 ImageBase + AddressOfEntryPoint로 잡습니다.)
위 그림에서 AddressOfEntryPoint의 값을 보시면 00012D6C 인 것을 확인할 수 있습니다. ImageBase + 12D6C의 값은 진입점의 주소라고 생각할 수 있습니다.
BaseOfCode: 코드 영역이 시작되는 상대 주소(RVA)가 담깁니다. BaseOfCode가 RVA니 ImageBase + BaseOfCode의 값은 실제 코드 영역의 주소가 됩니다.
위 그림을 보시면 BaseOfCode의 값이 00001000(0x1000)으로, 만약 ImageBase가 0x400000이라면 0x401000이 실제 코드 영역의 주소입니다.
SectionAlignment: 메모리에서 섹션의 최소단위를 나타냅니다. 메모리에서 섹션의 크기는 반드시 SectionAlignment의 배수가 되어야 합니다.
위 그림에서 SectionAlignment의 값은 00001000(0x1000) 입니다. 이는 메모리 공간에서 섹션의 크기가 0x1000의 배수라고 할 수 있습니다. 섹션의 크기에서 구조체 크기를 제외한 빈 공간은 모두 0으로 채워지며 이 것을 패딩(padding)이라고 합니다.
FileAlignment: 파일에서 섹션의 최소단위를 나타냅니다. 파일에서 섹션의 크기는 반드시 FileAlignment의 배수가 되어야 합니다.
위 그림에서 FileAlignment의 값은 00000200(0x200) 입니다. 이는 파일에서 섹션의 크기가 0x200의 배수라고 할 수 있습니다. SectionAlignment와 마찬가지로 섹션의 크기에서 구조체 크기를 제외한 빈 공간은 모두 0으로 채워집니다.
SizeOfImage: PE 파일이 메모리에 로딩되었을 때의 전체 크기를 담고 있습니다. 이 값은 파일의 크기와 같을 때도 있으며, 다를때도 있으나 다른 경우가 더 많습니다. PE 파일이 메모리에 로딩되고 나서는 SectionAlignment의 영향을 받아 패딩이 따라붙으며, SizeOfImage 역시 SectionAlignment의 영향을 받는다고 할 수 있습니다.
위에서 SizeOfImage의 값은 000C0000(0xC0000)이며, 이는 PE 파일이 메모리에 로딩되었을 때의 전체 크기가 0xC0000 라는 말이 됩니다.
SizeOfHeaders: 이름 그대로 모든 헤더의 크기를 담고 있습니다. 즉, 도스 헤더, 도스 스텁, PE 헤더, 섹션 헤더의 크기를 모두 더한 값이라고 할 수 있으며 파일의 시작점에서 SizeOfHeaders 만큼 떨어진 Offset에 첫번째 섹션이 존재합니다.
위 그림에서 SizeOfHeaders의 값은 00000400(0x400) 이며, 헤더의 총 크기는 0x400이라고 할 수 있습니다.
Subsystem: 이 값을 통해 시스템 드라이버 파일인지, 프로그램이 GUI 혹은 CUI 인지 알아낼 수 있습니다.
1 2 3 | #define IMAGE_SUBSYSTEM_NATIVE 1 // Image doesn't require a subsystem. #define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // Image runs in the Windows GUI subsystem. #define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // Image runs in the Windows character subsystem. |
1인 경우에는 시스템 드라이버 파일, 2인 경우에는 GUI 파일, 3인 경우에는 CUI 파일입니다. 이것 외에도 OS2, POSIX, CE 등과 같은 서브시스템이 존재하지만 GUI와 CUI 둘 중 하나인 것이 많아 위의 3개의 상수만 보여드렸습니다.
위 그림에서는 Subsystem의 값이 0002(0x2)로 GUI 파일이 되겠습니다. 만약 이 값이 0x3이였다면, CUI 파일이라고 할 수 있습니다.
DataDirectory: IMAGE_DATA_DIRECTORY 구조체를 보시면 VirtualAddress와 Size라는 필드가 존재합니다. 우선은 아래의 코드를 먼저 보도록 합시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory #define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory #define IMAGE_DIRECTORY_ENTRY_BASERELOC 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 8 // RVA of GP #define IMAGE_DIRECTORY_ENTRY_TLS 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_OPTIONAL_HEADER의 DataDirectory 필드는 익스포트 디렉터리, 임포트 디렉터리, 리소스 디렉터리, 예외 디렉터리, 보안 디렉터리 영역 등에 접근할 수 있는 주소와 크기를 지니고 있는 배열로, IMAGE_DATA_DIRECTORY 구조체의 VirtualAddress를 통해 가상 주소를 알 수 있으며, Size를 통해 크기를 알 수 있습니다. 여기서 중요한 값은 EXPORT, IMPORT, RESOURCE, TLS, IAT인데 우선은 이것들을 잘 기억해두시기 바랍니다. 이부분에 대해서는 추후에 다시 설명하도록 하겠습니다.
4. IMAGE_SECTION_HEADER
옵셔널 헤더의 다음으로 섹션 헤더에 대해서 알아볼텐데, 섹션 헤더는 섹션 테이블이라고도 하며 IMAGE_SECTION_HEADER 구조체는 섹션에 대한 정보를 관리하는 구조체라고 할 수 있습니다. 이 구조체를 가지고 .text 섹션이나, .data 섹션, .rdata 섹션 등에 대한 정보를 알 수 있다는 것입니다. 우선은 아래의 코드를 먼저 보도록 합시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #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; #define IMAGE_SIZEOF_SECTION_HEADER 40 |
위 코드를 보시면 IMAGE_SECTION_HEADER가 보이는데, 필드는 총 11개로 이 중에서 Name, VirtualSize, VirtualAddress, SizeOfRawData, PointerToRawData, Characteristics 필드만 간략하게 알아보도록 하겠습니다. 먼저 Name 필드부터 알아보도록 합시다. 아래에서는 .text 섹션의 헤더를 가지고 설명을 하도록 하겠습니다.
<.text 섹션 헤더가 차지하는 공간>
Name: 필드명 그대로 섹션의 이름을 나타냅니다. 저기 상수 IMAGE_SIZEOF_SHORT_NAME의 값인 8, 섹션의 이름은 최대 8바이트까지 가능하다는 겁니다. 그리고 이 필드의 값은 NULL로 비어있을 수 있으며, 다 꽉 채울수도 있습니다. 기본적인 섹션의 이름에 대한 용도는 아래에 표로 정리해 두었습니다.
섹션명 | 용도 |
.text | 코드, 실행, 읽기 속성을 지니며 컴파일 후의 결과가 이곳에 저장됩니다. 즉, 이 섹션은 실행되는 코드들이 들어가는 섹션입니다. |
.data | 초기화, 읽기, 쓰기 속성을 지니며 초기화된 전역 변수를 가집니다. |
.rdata | 초기화, 읽기 속성을 지니며 문자열 상수나 const로 선언된 변수처럼 읽기만 가능한 읽기 전용 데이터 섹션입니다. |
.bss | 비초기화, 읽기, 쓰기 속성을 지니며 초기화되지 않은 전역 변수의 섹션입니다. |
.edata | 초기화, 읽기 속성을 지니며 EAT와 관련된 정보가 들어가 있는 섹션입니다. |
.idata | 초기화, 읽기, 쓰기 속성을 지니며 IAT와 관련된 정보가 들어가 있는 섹션입니다. |
.rsrc | 초기화, 읽기 속성을 지니며 리소스가 저장되는 섹션입니다. |
먼저 위 그림에서의 Name 필드를 보도록 합시다. Name 필드는 8바이트를 차지하니, 앞에서부터 8바이트를 그대로 읽으시면 됩니다. 읽었더니 2E 74 65 78 74 (NULL 생략)이며, 2E는 10진수로 64고 이 아스키코드에 해당하는 문자는 '.'이며, 74는 116으로 't', 65는 101으로 'e', 78은 120으로 'x', 74는 116으로 't' 합쳐서 ".text"라고 읽습니다.
VirtualSize: PE 로더를 통해 PE 파일이 메모리에 로드되고 나서의 메모리에서 섹션이 차지하는 크기를 가집니다. 위 그림에서 VirtualSize와 PhysicalAddress를 필드로 갖는 공용체가 존재하지만, 여기서 PhysicalAddress는 현재 사용되지 않는 필드고 VirtualSize 필드만 사용됩니다. Name 필드 다음부터 4바이트를 읽으면 A1 2C 05 00이 되고, 이는 00052CA1 이라는 값이 됩니다.
VirtualAddress: PE 로더를 통해 PE 파일이 메모리에 로드되고 나서의 해당하는 섹션의 RVA 값입니다. 즉, RVA는 이미지 베이스를 기준으로 하는 것이기에 예를 들어서 ImageBase의 값이 0x400000이고, VirtualAddress의 값이 0x1000이라면 로더는 0x401000에 섹션을 올리게 됩니다. 즉 ImageBase + VirtualAddress는 해당 섹션의 실제 주소값이라고 할 수 있습니다. 위 그림에서 VirtualSize 필드 다음부터 4바이트를 읽게 되면 00 10 00 00으로, 이는 00001000(0x1000)의 값이 됩니다.
SizeOfRawData: 파일 상에서의 해당 섹션이 차지하는 크기(옵셔널 헤더 구조체의 FileAlignment 값의 배수가 되도록 올림한 값)를 가집니다. 이는 실제로 사용된 크기이며, 패딩을 제외한 크기라고 할 수 있습니다. 위 그림에서 VirtualAddress 필드 다음부터 4바이트를 읽게 되면 00 2E 05 00으로, 이는 00052E00(0x52E00)의 값이 됩니다.
PointerToRawData: 파일 상에서의 해당 섹션이 시작하는 위치(파일 오프셋)를 담고 있습니다. 이 값 역시도 옵셔널 헤더 구조체의 FileAlignment 값의 배수가 되어야 하며, 위 그림에서 SizeOfRawData 필드 다음부터 4바이트를 읽게 되면 00 04 00 00으로, 이는 00000400(0x400)이 됩니다.
Characteristics: 섹션의 속성 정보를 플래그로 지니며, 여기서는 6가지의 속성만 알아보도록 하겠습니다.
1 2 3 4 5 6 | #define IMAGE_SCN_CNT_CODE 0x00000020 // 코드로 채워진 섹션 #define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // 데이터가 초기화된 섹션 #define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // 데이터가 비초기화된 섹션 #define IMAGE_SCN_MEM_EXECUTE 0x20000000 // 실행 가능한 섹션 #define IMAGE_SCN_MEM_READ 0x40000000 // 읽기가 가능한 섹션 #define IMAGE_SCN_MEM_WRITE 0x80000000 // 쓰기가 가능한 섹션 |
한가지의 속성만 지닐 수 있는게 아닌, 여러가지 속성이 조합된 값을 가지며 만약 이 필드의 값이 0x60000020이라면, 0x40000000(IMAGE_SCN_MEM_READ), 0x20000000(IMAGE_SCN_MEM_EXECUTE), 0x00000020(IMAGE_SCN_CNT_CODE)의 플래그를 지니는 것으로 해당 섹션은 실행 가능한 섹션이며, 읽을 수 있고 코드로 채워진 섹션이라고 할 수 있습니다. 위 그림에서는 영역의 마지막 4바이트를 보시면 20 00 00 60으로, 이는 60000020(0x60000020)이 되는 것이라고 할 수 있습니다.
IAT(Import Address Table)
IAT(Import Address Table)을 알아보기 전에 몇몇 의문점을 풀어나가보려 합니다. 여기서는 DLL(Dynamic Linked Library, 동적 연결 라이브러리)에 대한 기본적인 지식은 갖추고 있다고 가정하겠습니다. 우리가 쓰고 있는 Win32 응용 프로그램은 과연 함수를 호출할 때 어떤 함수가 어디에 있는지는 어떻게 알고있는 걸까요? 프로그램은 혼자 실행되는 것이 아니라 외부 DLL를 로딩하여 함수를 호출합니다. 예를 들면, Win32 API 함수의 경우는 시스템 DLL에서 가져와 사용을 하게 되는데 함수가 어떤 방식으로 호출되는지 알아보기 위하여 FindWindow API를 사용하는 간단한 프로그램을 디버깅 해보도록 하겠습니다.
0x401031 번지를 보시면 FindWindow 함수를 직접 호출하는게 아니라,
042528C 번지에 있는 74FFFB43이라는 값을 가져와 호출을 하는 간접적인 호출 방식입니다. 왜 이러한 호출 방식을 사용하는 것일까요? 그냥 편하게 CALL 74FFFB43과 같이 직접적인 방식으로 호출을 하면 안되는 걸까요? 한번 간접 호출 방식이 아니라 직접 호출 방식으로 바뀌었다고 가정을 해봅시다. 그렇다면 CALL 74FFFB43과 같은 문장을 만나면 74FFFB43이란 주소가 user32.dll에 있는 FindWindowA의 실제 주소여야 합니다. 하지만 항상 user32.dll를 사용하려고 로딩할때마다 FindWindowA 함수가 74FFFB43를 실제 주소값으로 갖는게 아니라 다른 외부 DLL이 미리 자리를 차지하고 있으면 PE 로더가 다른 빈 공간을 찾아 로딩을 하여 재배치가 이루어지거나, 운영체제의 환경에 따라 user32.dll 내의 FindWindowA의 주소값이 바뀌기도 합니다.
그렇기 때문에, 어떠한 환경에서든 FindWindowA 함수의 호출을 보장하기 위해서 컴파일러가 42528C 번지에 미리 공간을 마련하고 파일이 실행된 직후에 PE 로더가 이 공간에 FindWindowA의 실제 주소를 넣어주는 것입니다. 실제로, DLL 내의 함수 주소들을 모아 놓은 테이블을 만들고 코드 섹션에서 만들어 놓은 테이블을 가져다 쓰는 방식으로 관리를 하며 여기서 이 테이블을 IAT라고 합니다. 이 부분에 대해서는 차차 알아가도록 하도록 하고, 우선은 IMAGE_IMPORT_DESCRIPTOR 구조체에 대해 간단히 알아보도록 하겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) } DUMMYUNIONNAME; 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; typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; CHAR Name[1]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME; |
위 구조체가 바로 IMAGE_IMPORT_DESCRIPTOR 구조체 입니다. 이 구조체에서는 6개의 필드중OriginalFirstThunk, Name, FirstThunk 필드만 알아보도록 하겠습니다. 하지만, 그전에 IMAGE_IMPORT_DESCRIPTOR가 PE 파일의 어느 곳에 위치하는지 알아보도록 하겠습니다. 옵셔널 헤더 구조체 내의 구조체 배열 DataDirectory의 2번째 요소가 바로 Import Directory 인것 기억하시죠?
여기서 앞의 4바이트는 VirtualAddress이며, 뒤의 4바이트는 VirtualSize로 VirtualAddress의 값은 00051AFC(RVA)이며, VirtualSize의 값은 00000154입니다. 여기서 RVA의 값을 가지고 RAW를 알 수 있으며 "RAW = RVA - VirtualAddress(메모리 공간에서의 섹션 시작 주소) + PointerToRawData(파일에서의 섹션 시작 위치)"의 식을 통해 RAW의 값을 알아낼 수 있습니다. 51AFC는 .text 섹션에 속해있으며, .text 섹션의 VirtualAddress 값은 1000, PointerToRawData의 값은 400입니다. 이 식을 통해 RAW의 값을 구하면 RAW = 51AFC - 1000 + 400, RAW는 50EFC이 됩니다.
위 영역이 모두 IMAGE_IMPORT_DESCRIPTOR 구조체 배열이며 이 영역 처음에서 20바이트 까지는 구조체 배열의 첫번째 요소라 할 수 있습니다. OriginalFirstThunk에 대해 간단히 알아보기 전, IMAGE_THUNK_DATA라는 구조체를 잠시 보고 넘어가도록 하겠습니다.
1 2 3 4 5 6 7 8 9 | 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; |
IMAGE_THUNK_DATA의 필드는 총 4개로 모두 공용체의 멤버이며, 공용체의 멤버인 만큼 DWORD가 차지하는 크기인 4바이트의 공간을 4개의 필드가 공유하며, 이 구조체는 AddressOfData 필드만 쓰이기도 하며, Ordinal 필드만 쓰이기도 하고, Function 필드만 쓰이기도 합니다.
OriginalFirstThunk: Import Name Table(INT)의 RVA를 지닙니다. 혹은 Import Lookup Table(ILT)의 RVA를 지닌다고 합니다. 여기서 INT는 구조체 IMAGE_THUNK_DATA의 배열로 구성이 되며, 구조체 IMAGE_THUNK_DATA의 AddressOfData 필드는 실제 Import되는 함수의 이름이 포함된 구조체 IMAGE_IMPORT_BY_NAME에 대한 RVA를 지닙니다.
위 그림에서 구조체 배열 첫번째 요소의 OriginalFirstThunk 값을 읽으면 00051D20(RVA)이고, 이는 51D20 - 1000 + 400으로 RAW는 51120 입니다. 51120로 이동하여 한번 보도록 합시다.
위 그림에서 영역 지정된 부분은 INT 영역, 즉 구조체 IMAGE_THUNK_DATA 배열의 영역이며 IMAGE_THUNK_DATA의 크기는 총 4바이트 입니다. (4개의 필드가 있으나 이는 모두 다 공용체의 안의 필드라서 같은 공간을 공유합니다) 그리고 INT의 끝은 NULL로 알 수 있으며, 이는 즉 읽어낸 4바이트가 모두 0일 경우에 그곳을 INT의 끝이라고 할 수 있습니다. 우선 첫번째 값을 읽으면 00052350(RVA)인데, RVA를 RAW로 바꾸면 52350 - 1000 + 400이니까 51750이 됩니다. 51750으로 이동해보도록 합시다.
위 그림에서 드래그 된 영역은 구조체 IMAGE_IMPORT_BY_NAME의 영역으로 여기서 라이브러리 안 함수의 이름이 나왔는데, 앞의 2바이트(WORD)인 00E1은 Ordinal로 라이브러리 내의 함수 고유 번호라고 할 수 있습니다. 문자열의 끝은 \0(NULL)이므로 함수명을 읽으면 SHGetSpecialFolderPathW가 되겠습니다.
Name: 임포트(Import)된 DLL의 이름을 담은 문자열의 주소를 지닙니다.
위 그림에서 구조체 배열 첫번째 요소의 Name의 값을 읽으면 00051D14(RVA)이고, 이는 51D14 - 1000 + 400으로 RAW는 51114가 됩니다. 51114로 이동하여 어떤 DLL이 임포트 되었는지 한번 보도록 합시다.
저기에 SHELL32.dll이 보이시죠? 그리고 Name의 끝은 당연히 문자열이므로 \0(NULL)이 되겠습니다.
FirstThunk: OriginalFirstThunk와 마찬가지로 FirstThunk 필드도 구조체 IMAGE_THUNK_DATA의 RVA 값을 지닙니다. PE 파일이 메모리에 로딩되고 나서는 구조체 IMAGE_THUNK_DATA는 Import한 DLL 내의 함수의 실제 주소값을 지니며, 이렇게 함수의 주소값을 담고있는 구조체 IMAGE_THUNK_DATA 배열을 Import Address Table(IAT)라고 합니다.
위 그림에서 구조체 배열 첫번째 요소의 FirstThunk 값을 읽으면 00001000(RVA)이고, 이는 1000 - 1000 + 400으로 RAW는 400이 됩니다. 우선 400으로 이동해보도록 합시다.
위 영역은 SHELL32.dll의 IAT 배열 영역으로 INT와 같이 구조체 IMAGE_THUNK_DATA 배열이며, 여기서는 Function 필드에 함수의 주소가 들어갑니다. IMAGE_THUNK_DATA는 총 4바이트니, 앞의 4바이트를 읽어보면 73820468 입니다. 위에서 PE 파일이 메모리에 로딩되기 전에는 AddressOfData로 쓰이거나 하지만, 로딩된 후에는 IMAGE_THUNK_DATA는 임포트한 DLL 내의 함수의 실제 주소값을 지닙니다.
IAT에 대한 설명은 여기서 마치고, EAT도 같이 설명하려고 하였으나 내용이 비슷하여 생략하도록 하겠습니다. IAT를 충분히 이해하시고 계시다면 EAT도 별 어려움없이 볼 수 있으실거라 생각합니다.
'리버싱' 카테고리의 다른 글
[조립하면서 배우는 PE] 세번째 이야기 : PE Header(1) Reverse Engineering (0) | 2017.01.10 |
---|---|
[조립하면서 배우는 PE] 두번째 이야기 : DOS header 와 DOS stub Code Reverse Engineering (0) | 2017.01.10 |
[조립하면서 배우는 PE] 첫번째 이야기 : PE의 전체적인 구조 Reverse Engineering (1) | 2017.01.10 |
[개념 이해] PE 파일의 구성 (0) | 2017.01.10 |
PE 파일 포맷의 이해 (0) | 2017.01.09 |