File:Portable Executable 32 bit Structure in SVG fixed.svg - Wikimedia Commons
commons.wikimedia.org
PE FILE FORMAT
PE파일은 섹션 헤더에 각 섹션에 대한 파일/메모리에서의 크기, 위치, 속성 등이 정의 되어있다
헤더의 끝과 각 섹션의 끝에는 NULL padding이라고 불리우는 영역이 존재하는데, 이는 컴퓨터에서 파일, 메모리, 네트워크 패킷 등을 효율적으로 처리하기 위한 기본 단위 개념을 사용하기위해 각 섹션의 시작 위치를 각 파일/메모리의 최소 기본 단위의 배수에 해당하는 위치로 옮기고 빈 공간을 NULL로 채우기 때문이다
VA & RVA
- VA: 프로세스 가상 메모리의 절대 주소
- RVA (Relative Virtual Address): ImageBase에서 부터의 상대 주소
$$ VA = RVA + ImageBase $$
왜 RVA가 필요하고 대부분 PE 헤더 내의 정보가 RVA 형태를 띌까?
그 이유는 메모리를 동적으로 할당하는 부분에 있어, 언제든 다른 파일들이 메모리에 어디든 로드될 수 있기에 절대 경로는 안전하지 않다
그리고 DLL의 특성상 언제든 로드되고 언로드 될 수 있기 때문에 각 PE 파일마다 상대경로를 만들어 두는 것이 훨씬 안전하다
위와 같이 가상 메모리의 특정 위치에 로딩될 때 이미 그 위치에 다른 파일이 로딩되어 있는 경우 다른 위치로 로딩하는 기법을 재배치(Relocation) 과정 이라고 한다
PE FILE HEADERS
DOS Header
PE File Format을 만들 때 당시에 사용하던 DOS 파일에 대한 하위 호환성을 고려해서 DOS EXE Header를 확장시킨 IMAGE_DOS_HEADER 구조체가 존재한다
typedef struct _IMAGE_DOS_HEADER
{
WORD e_magic; // DOS signature : 4D5A ("MZ")
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; // offset to NT header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
lMAGE_DOS_HEADER 구조체의 크기는 40바이트 이다
여기서 가장 중요한 멤버는 주석 처리한 e_magic, e_lfanew로 모든 PE 파일의 시작에는 DOS signature ("MZ")가 존재하고 e_lfanew 값이 가리키는 위치에 NT Header 구조체가 존재해야 한다
NT Header
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;
아래 두개 FileHeader, OptionalHeader에 대해서 알아보자!
IMAGE_FILE_HEADER
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // CPU별로 고유한 값 (intel x86호환 칩은 14C의 값)
WORD NumberOfSections; // 섹션의 개수 (0보다 커야하며 실제 개수와 다르면 에러)
DWORD TimeDateStamp; // 파일의 빌드 시간 (실행에 영향 x)
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER[32, 64] 구조체의 크기
WORD Characteristics; // 파일의 속성을 나타내는 값 (0002: 실행, 2000: DLL)
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
IMAGE_FILE_HEADER 구조체 멤버로 표현하면 다음과 같다
멤버 이름 | 오프셋 | 값 | 설명 |
Machine | 0x104 | 0x8664 | x64 Architecture |
NumberOfSections | 0x106 | 0x0007 | 섹션 7개 |
TimeDateStamp | 0x108 | 0x61FC30C0 | 2022-01-03 06:48:00 UTC |
PointerToSymbolTable | 0x10C | 0x00000000 | 심볼 테이블 없음 |
NumberOfSymbols | 0x110 | 0x000000F0 | 심볼 240개 |
SizeOfOptionalHeader | 0x114 | 0x0022 | Optional Header: 34바이트 |
Characteristics | 0x116 | 0x020B | 실행 파일 (EXE) |
OPTIONAL HEADER
PE 헤더 구조체 중에서 가장 크기가 크다
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;
굉장히 많은 내용들이 보인다
여기서 주목해야할 멤버들은 굉장히 많다
- MAGIC
32의 경우에는 10B, 64의 경우에는 20B의 값을 가진다 - AddressOfEntryPoint
EP의 RVA 값을 가지고 있다
프로그램에서 최초로 실행되는 코드의 시작 주소로, 매우 중요하다 - ImageBase
프로세스의 가상 메모리는 0~FFFFFFFF(32) 범위로 여기서 PE 파일이 로딩되는 시작 주소를 나타낸다
EXE, DLL 파일은 user memory 영역인 0~7FFFFFFF 범윙 로딩되고,
SYS 파일은 kernel memory 영역인 80000000~FFFFFFFF 범위에 로딩된다
PE 로더는 PE 파일을 실행시키기 위해 프로세스를 생성하고 파일을 메모리에 로딩한 후 EIP 레지스터값을 ImageBAse + AddressOfEntryPoint 값으로 세팅한다 - SectionAlignment, FileAlignment
메모리에서 섹션의 최소단위, 파일에서 섹션 최소 단위를 나타낸다
위에서 말했듯 윈도우프로그램들은 해당 단위의 배수에 파일/프로그램을 위치시킴으로서 최적화를한다 - SizeOfImage
PE 파일이 메모리에 로딩되었을 때 갓아 메모리에서 PE Image가 차지하는 크기
일반적으로 파일의 크기와 메모리에 로딩된 크기는 다르다 - SizeOfHeader
PE 헤더의 전체 크기
FileAlignment의 배수여야 한다
파일에서 해당 offset만큼 떨어진 위치에 첫 번째 섹션이 위치한다 - Subsystem
시스템 드라이버 파일(*.sys)인지, 일반 실행 파일(*.exe, *.dll)인지 구분할 수 있다 - NumberOfRvaAndSizes
마지막 멤버인 DataDirectory 배열의 개수
구조체 정의에 #define IMAGE_NUMBEROF_DIRECTORY_ENRIES 16 으로 정의되어 있으나, PE로더는 이 멤버를 보고 배열의 크기를 인식한다 - DataDirectory
IMAGE_DATA_DIRECTORY 구조체의 배열
배열의 각 항목 마다 정의된 값을 가진다
이제 notepad.exe의 IMAGE OPTIONAL HEADER를 살펴보자
멤버 이름 | 데이터 | 값 | 설명 |
Magic | 0B 02 | 0x020B | PE32+ (x64) |
AddressOfEntryPoint | 00 C0 3B 02 | 0x023BC000 | 실행 시작 지점 (RVA) |
ImageBase | 40 01 00 00 00 00 10 00 | 0x0000000100004000 | 이미지의 기본 로드 주소 |
SectionAlignment | 00 02 00 00 | 0x00000200 | 섹션 정렬 크기 |
FileAlignment | 0A 00 00 00 | 0x0000000A | 파일 정렬 크기 |
SizeOfImage | 60 C1 00 00 | 0x0000C160 | 전체 이미지 크기 (섹션 정렬 기준) |
SizeOfHeader | 08 00 00 00 | 0x00000008 | 헤더 크기 |
Subsystem | 00 10 | 0x0010 | 실행 환경 |
NumberOfRvaAndSizes | 10 00 00 00 | 16 | 데이터 디렉터리 개수 |