PE File Format
PE파일이란?
PE(Portable Executable)파일은 Windows 운영체제에서 사용되는 실행 파일 형식이다.
PE 파일은 32비트 형태의 실행 파일을 의미하고, PE32라고도 한다.
64비트 형태의 실행 파일은 PE+ 또는 PE32+라고 한다.
PE파일의 종류
종류 | 주요 확장자 | 종류 | 주요 확장자 |
실행 계열 | EXE, SCR | 드라이버 계열 | SYS, VXD |
라이브러리 계열 | DLL, OCX, CPL, DRV | 오브젝트 파일 계열 | OBJ |
OBJ 파일을 제외한 모든 종류의 파일은 실행이 가능하나, DLL과 SYS 파일은 셸에서 직접 실행하는 것이 불가능하고 디버거 등을 통해 실행이 가능하다.
기본 구조
DOS header부터 Section Header까지를 PE헤더, 그 밑의 Section들을 PE 바디라고 한다.
파일에서는 Offset으로, 메모리에서는 VA(Virtual Address, 절대주소)로 위치를 표현한다.
위 그림과 같이 파일이 메모리에 로딩이 되면 Section의 크기나 위치 등이 달라진다.
파일의 내용은 코드(.text), 데이터(.data), 리소스(.rsrc) 섹션에 나뉘어서 저장된다.
섹션 헤더에 각 섹션에 대한 파일/메모리에서의 크기, 위치, 속성 등이 정의되어 있다.
PE 헤더의 끝 부분과 각 섹션의 끝에는 NULL padding이라고 불리우는 영역이 존재한다.
이 영역은 컴퓨터에서 파일이나 메모리 등을 처리할때 효율을 높이기 위한 최소 기본 단위 개념이 적용된 것이다.
파일/메모리에서 섹션의 시작 위치는 각 파일/메모리의 최소 기본 단위의 배수에 해당하는 위치여야 하고, 빈 공간은 NULL로 채운다.
VA & RVA
VA는 프로세스 가상 메모리의 절대 주소이다.
RVA(Rela-tive Virtual Address)는 ImageBase에서부터의 상대주소를 말한다.
둘의 관계는 아래와 같다.
RVA+ImageBase = VA
PE 헤더 내의 정보는 RVA 형태로 된 것이 많은데, 이는 DLL이 프로세스 가상 메모리의 특정 위치에 로딩되는 순간 이미 그 위치에 다른 DLL이 로딩되어 있을 수 있다. 이런 상황에서 DLL Relocation(DLL 재배치) 과정을 통해 비어있는 위치에 로딩시켜야 한다. 만약 VA(절대주소)로 되어있었다면 문제가 발생할수 있다. RVA는 상대적인 위치이므로 재배치가 발생해도 기준 위치에서의 상대적인 주소가 변하지는 않으므로 정상적으로 로딩이 될 것이다.
PE 헤더
⦁ DOS Header
IMAGE_DOS_HEADER 구조체의 크기는 40이고, 알아두어야 할 멤버는 e_magic과 e_lfanew이다.
e_magic : DOS signature (4A5D -> ASCII 값으로 "MZ"
e_lfanew : NT header의 오프셋을 표시(파일에 따라 가변적인 값을 가진다)
모든 PE 파일은 시작 부분에 MZ가 존재하고 e_lfanew 값이 가리키는 위치에 NT header 구조체가 존재해야 한다.
Intel 계열 CPU는 리틀 엔디언 방식으로 값을 저장한다.
⦁ DOS Stub
DOS Stub의 존재 여부는 필수가 아니고, 크기도 일정하지 않다.
40~4D는 16비트 어셈블리 명령어이다. 32비트 OS에서는 이 부분이 실행되지 않는다.
notpad.exe를 DOS 환경에서 실행하면 해당 코드를 실행시킬 수 있다.
화면에 This program cannot be run in DOS mode 라는 문자열을 출력시키고 프로그램이 종료된다.
즉, notepad.exe는 32비트용 PE파일이지만 MS-DOS 호환 모드가 있어 DOS 환경에서도 해당 문자열을 확인할 수 있다.
이 특성을 이용해서 하나의 실행 파일에서 DOS와 Windows 환경에서 모두 실행 가능한 파일을 만들 수 있다.
어디까지나 이는 옵션이므로 개발 도구에서 지원해줘야 가능하다.
⦁ NT Header
첫 번째 멤버 Signature는 50450000h("PE"00) 값을 가진다.
두 번째, 세 번째 멤버는 구조체이다.
IMAGE_NT_HEADERS의 크기는 F8이다.
NT Header - File Header
NT 헤더의 두 번째 멤버인 File Header는 아래와 같이 정의되어 있다.
IMAGE_FILE_HEADER 구조체에서는 4가지 멤버가 중요한데, 해당 멤버들은 정확히 세팅되어있지 않으면 파일이 정상적으로 실행되지 않는다.
- Machine - CPU별로 고유한 값이며 아래와 같이 정의되어 있다.
정의된 Machine의 값 - NumberOfSections - PE 파일의 섹션의 개수로, 반드시 0보다 커야한다.
- SizeOfOptionalHeader - 해당 멤버는 IMAGE_OPTIONAL_HEADER32 구조체의 크기를 나타낸다. 그런데 IMAGE_OPTIONAL_HEADER32는 이미 크기가 정해져있으나 Windows의 PE 로더는 IMAGE_FILE_HEADER의 SizeOfOptionalHeader 값을 보고 IMAGE_OPTIONAL_HEADER32의 크기를 인식한다.
왜냐하면 PE32+의 IMAGE_OPTIONAL_HEADER64 구조체 크기가 다르기 때문이다. 이러한 특성을 이용하여 일반적인 PE 형식을 벗어나는 PE 파일을 만들 수 있다. - Characteristics - 파일의 속성을 나타내는 값으로 bit OR 형식으로 조합된다. 아래와 같이 정의되어 있다.
Charateristics
4C01 - machine
0004 - number of sections
559EA6FF - time date stamp
00000000 - offset to symbol table
00000000 - number of symbols
00E0 - size of optional header
0102 - charateristics (IMAGE_FILE_EXECUTABLE_IMAGE, IMAGE_FILE_32BIT_MACHINE)
NT Header - Optional Header
IMAGE_OPTIONAL_HEADER32는 PE 헤더 구조체 중에서 가장 크기가 크다.
주목해야 할 멤버들은 아래와 같다.
- Magic
IMAGE_OPTIONAL_HEADER32 구조체인 경우 10B, 64비트인 경우 20B의 값을 갖는다. - AddressOfEntryPoint
EP(Entry Point)의 RVA 값을 갖는다. 프로그램에서 최초 실행되는 코드의 시작 주소이다. - ImageBase
가상 메모리에서 PE 파일이 로딩되는 시작 주소를 나타낸다. EXE, DLL 파일은 user memory 영역인 0~7FFFFFFF 범위에 로딩되고(32비트 기준), SYS 파일은 kernel memory 영역인 80000000~FFFFFFFF 범위에 로딩된다. 일반적으로 ImageBase는 00400000이고 DLL의 ImageBase는 10000000이다.
PE로더는 PE 파일을 실행시키기 위해서 프로세스를 생성하고 파일을 메모리에 로딩한 후 EIP 레지스터 값을 ImageBase + AddressOfEntryPoint 값으로 세팅한다. - SectionAlignment, FileAlignment
파일에서의 섹션의 최소 단위를 나타내는 것이 FileAlignment이고 메모리에서 섹션의 최소단위를 나타내는 것이 SectionAlignment이다. 파일과 메모리의 섹션 크기는 각각 FilAlignment, SectionAlignment와 같아야 한다. - SizeOfImage
PE 파일이 메모리에 로딩되었을 때 가상 메모리에서 PE Image가 차지하는 크기를 나타낸다. - SizeOfHeader
PE 헤더의 전체 크기를 나타낸다. 이 값도 FileAlignment의 배수여야 한다. 파일 시작에서 SizeOfHeader offset만큼 떨어진 위치에 첫 번째 섹션이 위치한다. - SubSystem
값 의미 비고 1 Driver File 시스템 드라이버 (예 : ntfs.sys) 2 GUI(Graphic User Interface) 파일 창 기반 애플리케이션 (예 : notepad.exe) 3 CUI(Console User Interface) 파일 콘솔 기반 애플리케이션 (예 : cmd.exe) - NumberOfRvaAndSizes
DataDirectory 배열의 개수를 나타낸다. 해당 배열은 분명 16개라고 명시되어 있지만, PE로더는 이 값을 보고 배열 크기를 인식한다. - DataDirectory
메모리 속성별 액세스 권한
종류 | 액세스 권한 |
code | 실행, 읽기 권한 |
data | 비실행, 읽기, 쓰기 권한 |
resource | 비실행, 읽기 권한 |
⦁ IMAGE_SECTION_HEADER
이 구조체에서 중요한 멤버는 아래와 같고, 해당 멤버들 외에는 사용되지 않는다.
항목 | 의미 |
VirtualSize | 메모리에서 섹션이 차지하는 크기 |
VirtualAddress | 메모리에서 섹션의 시작 주소(RVA) |
SizeOfRawData | 파일에서 섹션이 차지하는 크기 |
PointerToRawData | 파일에서 섹션의 시작 위치 |
Characteristics | 섹션의 속성(bit OR로 조합) |
- VirtualAddress, PointerToRawData
SectionAlignment와 FileAlignment에 맞게 결정된다. - VirtualSize와 SizeOfRawData는 일반적으로 서로 값이 다르다. 파일에서의 섹션 크기와 메모리에 로딩되고 난 후의 섹션의 크기가 다르다는 뜻이다.
- Characteristics는 아래 값들을 bit OR 연산으로 조합한 값을 갖는다. 아래 값들 외에도 많은 값들이 정의되어 있다.
- NAME의 멤버는 일반적인 C언어 문자열처럼 NULL로 끝나지 않고, 어떠한 값이 와야한다는 제약이 없다.
따라서 참고용일 뿐 정보로써의 효용성이 100%라고 장담할 수 없다.
※ 이미지(Image) - PE파일이 메모리에 로딩된 상태를 이미지라고 한다.
⦁ RVA to RAW
PE 파일이 메모리에 로딩되었을 때 각 섹션에서 메모리의 주소(RVA)와 파일 offset을 매핑할 수 있어야 한다.
과정은 아래와 같다.
1. RVA가 속해 있는 섹션을 찾는다.
2. 간단한 비례식을 사용해서 파일 offset(RAW)를 계산한다.
RAW - PointerToRawData = RVA - VirtualAddress
RAW = RVA - VirtualAddress + PointerToRawData
위 상황에서 만약 RVA가 5000이라면 File Offset은 아래와 같이 구할 수 있다.
1. RVA 5000이 속한 섹션은 .text 섹션이다.
2. RAW = 5000(RVA) - 1000(VirtualAddress) + 400(PointerToRawData) = 4400
VirtualSize와 SizeOfRawData 값이 서로 다르면 File Offset을 정의할 수 없는 경우도 존재한다.