2019. 10. 16. 13:44ㆍReversing
PE(Portable Executable)은 Windows에서 사용되는 실행 파일 형식을 가리키는 말이다. UNIX에서 사용되는 파일 형식인 COFF(Common Object File Format)을 기반으로 windows에서 개발했다. 일반적인 PE 파일은 32bit에서 구동되는 PE파일을 말하고 PE32+ 또는 PE+는 PE 파일의 구조가 확장된 64비트 형식을 일컫는 말이다. 64bit에서는 PE64가 아니다.
OBJ(오브젝트) 파일을 제외한 모든 파일은 실행 가능한 파일이다. DLL,SYS,쉘 등은 직접 실행할 수는 없지만 다른 형태의 방법으로는 실행 할 수 있다.
PE 공식 스팩에서는 OBJ파일도 실행 가능한 파일로 보고있다.
NotePad.exe 라는 파일의 시작 부분이며, PE 파일의 헤더(PE Header) 부분이다. PE 헤더 부분에는 notepad.exe 파일이 실행되기 위해 필요한 모든 정보들이 적혀있다. 메모리에 적재되는 방법과, 어디서부터 실행하는 실행 위치 그리고 실행에필요한 DLL들은 어떤 것이 있는지, 필요한 stack과 heap 메모리의 크기는 얼마로 할지 등등 파일이 실행하기 위한 정보가 PE 헤더 구조체에 모두 담겨져 있게 된다. PE에 대한 정보들은 PE 헤더에 구조체 형식으로 저장되어 있는데, 우리가 공부하는 것은 PE 구조체를 분석하고 거기에 담겨져 있는 요소들을 공부하는 것으로 PE 헤더 구조체를 공부하는 것과 같다.
notepad는 일반적인 PE 파일의 기본 구조이다.위의 그림은notepad.exe 파일이 메모리에 적재될때 의 모습을 나타낸
것이다. DOS header부터 Section header까지를 PE 헤더, 그 밑의 Section들을 합쳐서 PE body라고 한다. 파일에서는 Offset, 메모리에서는 VA(Virtaul Address,절대주소)로 위치를 표시한다. 파일이 메모리에 로딩되면 모양이 아예 달라지는데(Section의 크기,위치 등),파일의 내용은 보통 코드(.text), 데이터(.data),리소스(.rsrc)섹션에 섹션별로 나눠져서 저장되게 된다.
개발도구(VB/VC++/Delphi/etc)와 빌드 옵션에 따라 섹션의 이름,크기,개수,저장 내용 등은 달라진다. 중요한 것은 각 용도별로 여러개의 섹션이 나눠서 저장이 된다는 것이다.
섹션 헤더에 각 Section에 대한 파일/메모리에서의 크기,위치,속성 등이 정의 되어 있다.
PE 헤더의 끝부분과 각 세션의 끝에는 NULL padding이라고 불리는 영역이 존재한다. NULL padding은 컴퓨터에서 파일,메모리,네트워크 패킷 등을 처리할 때 효율을 높이기 위해 최소 기본 단위 개념을 사용하는데, PE 파일에도 같은 개념이 적용된다. 파일/메모리에서 섹션의 시작 위치는 각 파일/메모리의 최소 기본 단위의 배수에 해당하는 위치여야 하고, 빈 공간은 NULL로 채워버린다. 위의 그림에서 보면 각 섹션의 시작 주소가 어떤 규칙에 의해 딱딱 들어맞게 끊어지는 걸 볼 수 있다.
VA&RVA
VA는 프로세스 가상 메모리의 절대 주소를 말하며,RVA(Relative Virtual Address)는 어느 기준 위치(ImageBase)에서부터의 상대주소를 말한다. Va와 RVA의 관계는 RVA + 기준위치(ImageBase) = VA(virtual Address)
PE 헤더 내의 정보는 RVA 형태로 된 것이 많다. 이유는 PE 파일(주로 DLL)이 프로세스 가상 메모리의 특정 위치에 로딩되는 순간 이미 그 위치에 다른 PE 파일(DLL)이 로딩되어 있을 수 있다. 그럴 때를 위해서 재배치(Relocation)과정을 통해서 비어 있는 다른 위치에 로딩되어야 하는데, 만약 PE 헤더 정보들이 VA(Virtual Address, 절대주소)로 되어 있다면 정상적인 엑세스가 이루어지지 않을 것이다. 그러므로 정보를 RVA(Relative Virtual Address,상대주소)로 해두면 Relocation이 발생해도 기준위치에 대한 상대주소가 변하지 않기 떄문에 아무런 문제없이 원하는 정보에 엑세스할 수 있는 것이다.
32비트 Windows에서 각 프로세스에게는 4GB 크기의 가상 메모리가 할당된다. 그러므로 프로세스에서 VA 값 범위는
00000000~FFFFFFFF까지 이다.
PE헤더
DOS Header
MS사는 PE 구조를 만들 당시에 많이 사용하던 DOS파일에 대한 하위 호한성을 고려해서 만들었는데, 그 결과로 PE 헤더
의 제일 앞부분에는 기존 DOS EXE Header를 확장시킨 IMAGE_DOS_HEADER 구조체가 존재한다.
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;
위의 구조체는 visual studio에 정의 되어 있는 헤더를 가져온것이다.
IMAGE_DOS_HEADER 구조체의 크기는 64이다. 여기서 꼭 알아둬야 할 중요한 멤버는 e_magic과 e_lfanew이다.
e_magic: DOS signature(4D5A=>ASCII값 "MZ")
e_lfanew : NT header의 옵셋을 표시(파일에 따라 가변적인 값을 가진다.)
모든 PE파일은 시작 부분(e_magic)에 DOS signature ("MZ")가 존재하고, e_lfanew 값이 가라키는 위치에 NT Header 구조체가 존재해야 한다.(NT Header 구조체의 이름은 IMAGE_NT_HEADERS이다.)
MZ는 DOS 실행 파일을 설계한 사람의 영문 이니셜이다.
아래 사진은 notepad.exe를 hex editor로 열어서 IMAGE_DOS_HEADER 구조체를 확인해 본 화면이다.
WORD는 unsigned short를 typedef로 편하게 줄여서 부른것인데, WORD는 2byte의 크기를 가질수 있다.
Intel 계열 CPU는 자료를 역순으로 저장하는 리틀 엔디언 방식을 사용한다. 리틀 엔디언 표기법은 낮은 주소부터 시작해서 하위 바이트를 기록하는 것을 말하는데, 만약 0x12345678이라는 값을 어떤 저장 공간에 기록하고 싶으면, 하위 바이트부터 시작해서 차례대로 78 56 34 12로 저장되게 된다. 바이트의 순서는 이렇지만,실제 값은 0x78563412가 아니라 0x12345678이라는 것이다.
PE 스팩에 맞춰서 파일 시작 2바이트는 4D5A이며,e_lfanew 값은 000000E0이다.
DOS Stub
Dos Header 밑에는 DOS Stub이 존재하는데 DOS Stub의 존재 여부는 옵션이고 크기도 일정하지 않는다.
DOS stub이 없어도 파일 실행에는 문제가 없다. DOS Stub은 코드와 데이터의 혼합으로 이루어져 있는데.notepad
에 DOS Stub이 나타나 있다.
40~4D 영역은 16비트 어셈블리 명령어다. 32비트 Windows OS에서는 이쪽 명령어가 실행되지 않는다(PE 파일로 인식하기 때문에 아예 16비트 어셈블리 명령을 무시하게 된다.). notepad파일을 DOS환경에서 실행하거나,DOS용 디버거를 이용해서 실행하면 저 코드를 실행시킬 수 있다.
NT Header
NT header 구조체는 IMAGE_NT_HEADERS이다.
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE Signature : 50450000 ("PE"00)
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
IMAGE_NT_HEARS 구조체는 3개의 멤버로 되어 있는데, 제일 첫 멤버는 Signature로 50450000h("PE"00)값을 가진다. 그리고 FileHeader와 OptionalHeader 구조체 멤버가 있다.
IMAGE_NT_HEADERS 구조체의 크기는 F8이다.
NT Header 안에 멤버로 있는 _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;
IMAGE_FILE_HEADER 구조체에서 4 가지 멤버가 중요하다.(이 값이 정확히 세팅되어 있지 않으면 파일이 정상적으로 실행되지 않는다.)
Machine 멤버:
Machine 넘버는 CPU별로 고유한 값이며 32비트 Intel x86 호환 칩은 14C의 값을 가지는데, 이 값들은 winnt.h 파일에 정의된 Machine 넘버의 값들이다.
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_TARGET_HOST 0x0001 // Useful for indicating we want to interact with the host and not a WoW guest.
#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_ARM64 0xAA64 // ARM64 Little-Endian
#define IMAGE_FILE_MACHINE_CEE 0xC0EE
NumberOfSections 멤버:
PE 파일은 코드,데이터,리소스 등이 각각의 섹션에 나눠서 저장되는데 NumberOfSections는 그 섹션의 개수를 나타내는 것이다. 그런데 이 값은 반드시 0보다 커야 하고, 정의된 섹션 개수와 실제 섹션이 다르면 실행 에러가 발생한다.
SizeOfOptionalHeader 멤버
IMAGE_NT_HEADER 구조체의 마지막 멤버는 IMAGE_OPTIONAL_HEADER32 구조체이다.SizeofOptionalHeader 멤버는 바로 이 IMAGE_OPTIONAL_HEADER32 구조체의 크기를 나타낸다. IMAGE_OPTIONAL_HEADER32는 C언어 구조체이기 떄문에 그 크기가 결정되어 있다. 그런데 Windows의 PE 로더는 IMAGE_FILE_HEADER의 SizeofOptionalHeader 값을 보고IMAGE_OPTIONAL_HEADER32 구조체의 크기를 인식한다.
PE32+ 형태의 파일인 경우에는 IMAGE_OPTIONAL_HEADER32 구조체 대신 IMAGE_OPTIONAL_HEADER64 구조체를 사용하는데, 두 구조체의 크기가 다르기 떄문에 SizeOfOptionalHeader 멤버에 구조체 크기를 명시하는 것이다.
IMAGE_DOS_HEADER의 e_lfanew 멤버와 IMAGE_FILE_HEADER의 SizeOfOptionalHeader 멤버 때문에 일반적인 PE 파일 형식을 벗어나는 꽈배기 PE파일(PE Patch)를 만들 수 있다.
Characteristics 멤버:
파일의 속성을 나타내는 값으로, 실행이 가능한 형태인지(executable or not)혹은 DLL파일인지 등의 정보들이 bit OR 형식으로 조합된다.
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved external 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 // Aggressively 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.
PE 파일 중에 Characteristics 값에 0002h가 없는 경우(실행 파일이 아닌경우)가 있는데, *.obj와 같은 object 파일 및 resource DLL같은 파일이 있다.
TimeDateStamp 멤버의 값은 파일의 실행에 영향을 미치지 않는 값으로 해당 파일의 빌드 시간을 나타낸다. 그래서 개발도구에 따라서 이 값을 세팅해주는 도구가 있고, 그렇지 않은 도구가 있고,지원이 되는 도구더라도 개발 도구의 옵션에 따라서 달라질 수 있다.
IMAGE_FILE_HEADER
NT Header-OptionalHeader
PE 헤더 구조체중에서 가장 크기가 큰 IMAGE_OPTIONAL_HEADER32이다.
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
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;
IMAGE_OPTIONAL_HEADER32 구조체에서 자세히 봐야할 멤버들은 9가지 정도가 된다. 이 값들 또한 파일 실행에 있어서 필수적이라서 잘못 세팅되면 파일이 정상 실행되지 않는다.
Magic 멤버:
Magic 넘버는 IMAGE_OPTIONAL_HEADER32 구조체인 경우 10B,IMAGE_OPTIONAL_HEADER64 구조체인 경우 20바이트 값을 가진다.
AddressOfEntryPoint 멤버:
이 멤버는 EP(Entry Point,시작점)의 RVA(Relative Virtual Address,상대주소)값을 가지고 있다. 이 값은 프로그램에서 최초로 실행되는 코드의 시작 주소를 가지고 있어 매우 중요한 값이다.
ImageBase 멤버:
프로세스의 가상 메모리는 0~FFFFFFFF 범위이다.(32bit 기준) ImageBase는 이렇게 범위가 큰 메모리에서 PE 파일이 로딩되는 시작 주소를 나타낸다. EXE,DLL 파일은 user memory 영역인 0~7FFFFFFF 범위에 로딩되고,SYS 파일은 kernel memory 영역인 80000000~FFFFFFFF 범위에 로딩된다. 일반적으로 개발 도구(VB/VC++/Delphi)들이 만들어내는 EXE 파일의 Image Base 값은 00400000이고, DLL 파일의 ImageBase 값은 10000000이다.(물론 다른 값도 지정할 수 있다.)PE 로더는 PE 파일을 실행시키기 위해 프로세스를 생성하고 파일을 메모리에 로딩한 후 EIP 레지스터 값을 ImageBase + AddressOfEntryPoint 값으로 세팅한다.
SectionAlignment,FIleAlignment 멤버:
PE 파일의 Body 부분은 섹션(Section)으로 나뉘어져 있다. 파일에서 섹션의 최소단위를 나타내는 것이 FileAlignment이고 메모리에서 섹션의 최소단위를 나타내는 것이 SectionAlignment이다(하나의 파일에서 FileAlignment와 SectionAlignment의 값이 같을 수도 있지만 다를 수도 있다.)파일/메모리의 섹션 크기는 반드시 각각 FileAlignment/SectionAlignment의 배수가 되어야 한다.
SizeOfImage 멤버:
이 멤버는 PE 파일이 메모리에 로딩되었을 때 가상 메모리에서 PE Image가 차지하는 크기를 나타낸다. 일반적으로 파일의 크기와 메모리에 로딩된 크기는 다르다.(각 섹션의 로딩 위치와 메모리 점유 크기는 섹션 헤더에 정의되어 있다.)
SizeOfHeader 멤버:
SizeOfHeader는 PE 헤더의 전체 크기를 나타낸다. 이 값 역시 FileAlignment의 배수여야 한다.파일 시작에서 SizeOfHeader 옵셋만큼 떨어진 위치에 첫 번째 섹션이 위치한다.
Subsystem 멤버:
Subsystem의 값을 보고 시스템 드라이버 파일(*.sys)인지 일반 실행 파일(*.exe,*.dll)인지 구분할 수 있다. Subsystem 멤버는 아래와 같은 값을 가질 수 있다.
#define IMAGE_SUBSYSTEM_UNKNOWN 0 // Unknown subsystem.
#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.
#define IMAGE_SUBSYSTEM_OS2_CUI 5 // image runs in the OS/2 character subsystem.
#define IMAGE_SUBSYSTEM_POSIX_CUI 7 // image runs in the Posix character subsystem.
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // image is a native Win9x driver.
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // Image runs in the Windows CE subsystem.
#define IMAGE_SUBSYSTEM_EFI_APPLICATION 10 //
#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER 11 //
#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER 12 //
#define IMAGE_SUBSYSTEM_EFI_ROM 13
#define IMAGE_SUBSYSTEM_XBOX 14
#define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16
#define IMAGE_SUBSYSTEM_XBOX_CODE_CATALOG 17
visual studio에서 찾았을떄는 이렇게 많이 있었지만 사용하는 건 아래처럼 3가지 경우를 많이 사용하는 것 같다.
NumberOfRvaAndSizes 멤버:
이 멤버는 IMAGE_OPTIONAL_HEADER32 구조체의 마지막 멤버인 DataDirectory 배열의 개수를 나타낸다. 구조체 정의에 분명히 배열 개수가 IMAGE_NUMBEROF_DIRECTORY_ENTRIES(16)이라고 명시되어 있지만, PE 로더는 NumberOfRvaAndSizes의 값을 보고 배열의 크기를 인식한다. #define으로 16이라고 치환되어져있지만 16이 아닌 수도 있다는 뜻이다.
DataDirectory 멤버:
DataDirectory는 IMAGE_DATA_DIRECTORY 구조체의 배열로,배열의 각 항목마다 정의된 값을 가진다. 아래에 값들이 나와 있다.
DataDirectory[0] = EXPORT Directory
DataDirectory[l] = IMPORT Directory
DataDirectory[2] = RESOURCE Directory
DataDirectory[3] = EXCEPTION Directory
DataDirectory[4] = SECURITY Directory
DataDirectory[5] = BASERELOC Directory
DataDirectory[6] = DEBUG Directory
DataDirectory[7] = COPYRIGHT Directory
DataDirectory[8] = GLOBALPTR Directory
DataDirectory[9] = TLS Directory
DataDirectory[A] = LOAD_CONFIG Directory
DataDirectory[B] = BOUND_IMPORT Directory
DataDirectory[C] = IAT Directory
DataDirectory[D] = DELAY_IMPORT Directory
DataDirectory[E] = COM_DESCRIPTOR Directory
DataDirectory[F] = Reserved Directory
여기서 말하는 Directory는 그냥 어떤 구조체의 배열이라고 생각하면 된다. EXPORT,IMPORT,RESOURCE,TLS Directory가 중요하다. 특히 IMPORT와 EXPORT Directory 구조는 PE 헤더에서 매우 중요하다. 나머지는 지금은 크게 중요하지는 않다.
IMAGE_OPTIONAL_HEADER
섹션 헤더
각 섹션의 속성을 정의한 것이 섹션 헤더이다.PE 파일은 code,data,resource 등을 각각의 섹션으로 나눠서 저장한다. 그렇다면 분명 PE File Format을 설계한 사람들은 어떤 장점이 있기 떄문에 그랬을 것이다.PE 파일을 여러 개의 섹션 구조로만들었을 때 장점은 바로 프로그램의 안정성이다. code와 data가 하나의 섹션으로 되어 서로 엉켜있으면 복잡함은 물론이거니와 안정성에 문제가 생길 수 있다. 예를 들면 문자열 data에 값을 쓰다가 overflow가 발생(버퍼 크기를 초과해서 입력)했을 때 바로 다음의 code(명령어)를 그대로 덮어써버려서 프로그램은 그대로 뻗어 버리게된다. 그래서 PE File Format 설계자들은 비슷한 성격의 자료를 섹션이라고 이름 붙인 곳에 모아두기로 결정했고, 각각의 섹션의 속성을 기술할 헤더가 필요하게 된 것이다.(섹션의 속성에는 file/memory에서의 시작 위치,크기,엑세스 권한 등이 있어야한다.)
code와data,resource 마다 각각의 특성,접근 권한등을 다르게 설정할 필요가 있는 것이다.
IMAGE_SECTION_HEADER
섹션 헤더는 각 섹션별 IMAGE_SECTION_HEADER 구조체의 배열로 구성되어 있다.
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;
IMAGE_SECTION_HEADER 구조체에서 알아야 할 중요 멤버는 아래 설명되어 있다.
VirtualSize -> 메모리에서 섹션이 차지하는 크기
VirtualAddress -> 메모리에서 섹션의 시작 주소(RVA)
SizeOfRawData -> 파일에서 섹션이 차지하는 크기
PointerToRawData -> 파일에서 섹션의 시작 위치
Characteristics -> 섹션의 속성(bit OR)
VirtualAddress와 PointerToRawData는 아무 값이나 가질 수 없고, 각각 MAGE_OPTIONAL_HEADER32에 정의된
SectionAlignment와 FileAlignment에 맞게 결정된다.
VirtualSize와 SizeOfRawData는 일반적으로 서로 다른 값을 가진다. 파일에서의 섹션 크기아 메모리에 로딩된 섹션의 크기는 다르다는 애기가 된다. Characteristics는 코드에 표시된 값들의 조합(bit OR)으로 이루어진다.
//
// Section characteristics.
//
// 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 0x00000040 // Section contains initialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // Section contains uninitialized data.
#define IMAGE_SCN_LNK_OTHER 0x00000100 // Reserved.
#define IMAGE_SCN_LNK_INFO 0x00000200 // Section contains comments or some other type of information.
// IMAGE_SCN_TYPE_OVER 0x00000400 // Reserved.
#define IMAGE_SCN_LNK_REMOVE 0x00000800 // Section contents will not become part of image.
#define IMAGE_SCN_LNK_COMDAT 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 0x00008000 // Section content can be accessed relative to GP
#define IMAGE_SCN_MEM_FARDATA 0x00008000
// IMAGE_SCN_MEM_SYSHEAP - Obsolete 0x00010000
#define IMAGE_SCN_MEM_PURGEABLE 0x00020000
#define IMAGE_SCN_MEM_16BIT 0x00020000
#define IMAGE_SCN_MEM_LOCKED 0x00040000
#define IMAGE_SCN_MEM_PRELOAD 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 0x01000000 // Section contains extended relocations.
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // Section can be discarded.
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // Section is not cachable.
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 // Section is not pageable.
#define IMAGE_SCN_MEM_SHARED 0x10000000 // Section is shareable.
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // Section is executable.
#define IMAGE_SCN_MEM_READ 0x40000000 // Section is readable.
#define IMAGE_SCN_MEM_WRITE 0x80000000 // Section is writeable.
#define IMAGE_SCN_CNT_CODE 0x00000020 // Section contains code.
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // Section contains initialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // Section contains uninitialized data.
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // Section is executable.
#define IMAGE_SCN_MEM_READ 0x40000000 // Section is readable.
#define IMAGE_SCN_MEM_WRITE 0x80000000 // Section is writeable.
대부분 6개의 위에 있는 항목을 사용한다.
Name 멤버:
이 멤버는 C 언어의 문자열처럼 NULL로 끝나지 않고 ASCII 값만 와야한다는 제한도 없다.PE 스팩에서는 섹션 Name에 대한 어떠한 명시적인 규칙이 없기 떄문에 어떠한 값을 넣어도 되고 심지어 NULL로 채워도 된다. 따라서 섹션의 Name은 그냥 참고용일 뿐 어떤 정보로 활용하기에는 100% 장담할 수 없다.
PE 파일이 메모리에 로딩될 때 파일이 그대로 올라가는 것이 아니라,섹션 헤더에 정의된 대로 섹션 시작 주소, 섹션 크기 등에 맞춰서 올라간다. 따라서 파일에서의 PE와 메모리에서의 PE는 서로 다른 모양을 가진다. 이를 구별하기 위해서 메모리에 로딩된 상태를 이미지라는 용어를 사용해서 구별한다.
RVA to RAW
PE 파일이 메모리에 로딩되었을 때 각 섹션에서 메모리의 주소(RVA)와 파일 옵셋을 잘 매핑할 수 있어야 한다. 이러한 매핑을 일반적으로 'RVA to RAW'라고 부른다. 방법은 아래에 있다.
1.RVA가 속해 있는 섹션을 찾는다.
2. 간단한 비례식을 사용해서 파일 옵셋(RAW)을 계산한다.
IMAGE_SECTION_HEADER 구조체에 의하면 비례식은 아래와 같다.
RAW = 파일에서의 주소
RVA = 메모리에서의 주소
VirtualAddress = 메모리에서의 섹션 시작 위치
PointerToRawData = 파일에서의 섹션 시작 위치
RAW - PointerToRawData = RVA - VirtualAddress
RAW = RVA - VirtualAddress + PointerToRawData