Reversing!/기본 학습

PE File Format - PE 헤더_

ddiyoung 2020. 9. 20. 22:19

* PE란 무엇일까?

PE(Portable Executable)은 Windows 운영체제에서 사용되는 실행 파일 형식입니다.

아래의 표는 PE 파일의 종류입니다.

PE 파일 종류

즉 우리가 흔히 보던 EXE, SYS, DLL 등의 확장자를 가진 것들은 PE 파일이며 모두  PE 파일 포맷을 갖고 있다는 의미입니다!

리버싱에서는 보통 EXE, SYS, DLL 등의 파일들을 분석하기 때문에 PE 파일 포맷 공부는 필수적이라고 생각합니다.

* 기본 구조

PE 헤더 : DOS header -> DOS stub -> NT header -> Section header
PE 바디 : Section

기본 구조

위 그림과 같이 기본적인 구성은 이렇게 되어 있습니다.
보통 Section의 경우는 코드(code)/데이터(data)/리소스(rsrc)로 나누어서 저장됩니다.

code, data, resourse들의 액세스 권한

여담으로 .reloc 라는 Section header가 추가로 있습니다.
보통 같은 주소 공간에 여러 개가 올라가는 DLL을 대상으로만 이루어졌었습니다.
exe 같은 파일은 가상 주소 공간에 올라가기 때문에 재배치를 해줄 이유가 없지만 메모리 보호기 법인 ASLR기능 때문에
.reloc이라는 Section이 필요해진 것입니다. 하지만 기본 구조에 대해서 공부하고 있으므로 일단 대표적인 기본 구조에 대해서 이야기해봅시다.


위처럼 대표적인 구조를 갖고 있는 예시는 바로 notepad.exe입니다.
따라서 notepad.exe를 이용해서 하나씩 살펴보면 이해가 좋을 것입니다.

notepad.exe가 메모리에 로딩되는 그림

메모리에 로딩되면서 변하는 부분을 볼 수 있을 겁니다.
NULL 부분이 커져서 메모리에 로딩이 됩니다. 이것을 NULL padding이라고 부릅니다
이는 파일, 메모리, 네트워크 패킷을 처리할 때 효율을 높이기 위한 방법인 최소 기본 단위 개념을 사용하는 것입니다.

간단하게 <File> 부분에서 offset 부분과 <Memory>의 address부분을 비교해봅시다.

NULL padding이 적용되는 빨간 상자 안의 값들을 잘 살펴봅시다.
<File> : 400 -> 7C00 -> 8400 -> 10800
<Memory> : 1000 -> 9000 -> B000
어떤가요? Memory가 더욱 규칙적이고 딱딱 끊어지는 느낌이죠?
바로 이러한 부분들 때문에 NULL padding을 해줍니다!

* VA와 RVA

PE 헤더 부분에 들어가기 전에 VA와 RVA의 개념을 알고 갑시다.
VA(Virtual Address)는 가상 메모리의 절대 주소를 의미하고,
RVA(Relative Virtual Address)는 어느 기준(ImageBase)으로부터 상대 주소를 말합니다!

RVA + ImageBase = VA

대부분의 PE 헤더는 RVA 형태로 되어 있습니다.
그 이유는 바로 로딩될 위치에 이미 PE 파일이 존재할 수 있기 때문입니다.

* PE 헤더

드디어 PE헤더에 대해 하나씩 살펴봅시다. 각 헤더의 구조체마다 주목해서 봐야 할 부분들이 있습니다.
그 부분들을 주목해서 이해해봅시다.

1. IMAGE_DOS_HEADER

IMAGE_DOS_HEADER는 DOS EXE Header를 확장시킨 구조체입니다

이 헤더에서는 e_magice_lfanew의 값을 잘 알아 둬야 합니다.
WORD        e_magic : DOS Signature으로 항상 4D5A값을 가집니다 아스키코드로 "MZ"입니다. 

LONG        e_lfanew : NT Header의 offset값을 가지고 있습니다.

리틀 에디안 방식이므로 e_lfanew의 값은 0x000000E0의 값을 가집니다.
NT Header의 offset이 진짜 0xE0값인지 뒤에 나올 PEView를 잠깐 이용해서 확인해 봅시다!

PEView

정말 NT_Header의 offset이 0xe0의 값을 가진 것을 확인할 수 있습니다.

2. DOS stub

DOS header 밑에 DOS stub가 존재합니다. DOS 환경을 위해서 만들어진 부분이기 때문에 존재하지 않아도 파일이 잘 실행이 됩니다 특별히 중요하게 볼 정보가 없다고 생각합니다.

3. IMAGE_NT_HEADER

NT header 구조체에는 3가지가 있습니다.

잠깐 위에서 봤듯 NT header의 시작하는 부분은 Signature 부분으로 0x00004550의 값을 가지고 있습니다.

바로 NT header의 시작점이고 이름 그래도 시그니쳐 부분을 담당하고 있습니다.
이제 File헤더부터 Optional 헤더를 차례로 살펴봅시다!

3.1 IMAGE_FILE_HEADER

아래 그림은 FILE_HEADER의 구조체를 표로 표현한 것입니다.

IMAGE_FILE_HEADER 구조체

빨간색으로 표시된 4가지의 값들이 IMAGE_FILE_HEADER에서 주의 깊게 봐야 할 값들입니다.

3.1.1 Machine
Machine의 값은 CPU별 고유한 값들입니다.

각 CPU 별로 위의 값들을 가지게 됩니다.
저의 경우는 Intel CPU를 사용하기 때문에 0x014C의 값을 가지고 있습니다.

IMAGE_FILE_HEADER의 Machine 값

 

3.1.2 NumberOfSections

Section의 개수가 몇 개인지 표시가 되어 있는 부분입니다

IMAGE_FILE_HEADER의 NumberOfSections 값

0x0004의 값을 가지고 있습니다. 즉 4개의 Section을 가지고 있습니다.
앞에서 Section부분에 대해서 code / data / resource로 3가지가 있다고 조금 이야기를 했습니다.
이 3가지의 Section에서 .reloc 의 섹션이 추가되어 총 4가지의 Section이 존재합니다.

3.1.3 SizeOfOptionalHeader

IMAGE_OPTIONAL_HEADER의 크기를 명시해주는 값입니다.

IMAGE_FILE_HEADER의 SizeOfOptionalHeader

저의 OPTIONAL_HEADER의 크기는 0x000E입니다.


※ SizeOfOptionalHeader의 경우 NumberOfSections 바로 다음의 값이 아니므로 따라 하시면서 착각하지 않았으면 좋겠습니다.

3.1.4 Characteristics

파일의 속성에 관한 값을 담고 있습니다.  bit OR 형식으로 조합되어 있습니다.

winnt.h 에 정의된 Characteristics의 값들

docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_file_header

 

IMAGE_FILE_HEADER (winnt.h) - Win32 apps

Represents the COFF header format.

docs.microsoft.com

Microsoft 공식 문서에서 값과 각 값에 관한 의미를 찾을 수 있습니다.

저의 경우 0x0102의 값을 가지고 있습니다. 102의 값이므로 실행 가능하고 32bit라는 의미입니다
(환경 실습이 Win7 32bit인 점을 생각하면 맞아떨어집니다)

3.2 IMAGE_OPTIONAL_HEADER

아래 사진은 Microsoft 공식 문서의 winnt.h에서 참고했습니다.

IMAGE_OPTIONAL_HEADER의 구조체

빨간 상자로 표시된 부분들이 주의 깊게 봐야 할 값들입니다.

3.2.1 Magic

Magic의 값은 OPTIONAL_HEADER에서 32 구조체인지 64 구조체인지 구별을 해줍니다. 
IMAGE_OPTIONAL_HEADER32의 경우 0x010B의 값을, IMAGE_OPTIONAL_HEADER64의 경우 0x020B의 값을 가집니다.

IMAGE_OPTIONAL_HEADER32 의 Magic 값

32 구조체를 갖고 있기 때문에 저의 경우는 0x010B의 값을 갖고 있습니다.

3.2.2 AddressOfEntryPoint

Entry Point의 RVA값을 갖고 있습니다. Entry Point는 프로그램이 시작되는 주소로 매우 중요한 값입니다.
리버싱 문제를 풀다 보면 이 Entry Point를 찾아야 할 때 가 많습니다.
메모리에 로딩된 후에 EIP 레지스터 값이 ImageBase + EP의 주소로 설정되기 때문에
다음에 나올 ImageBase와 이번에 하는 EP는 정말 중요합니다

IMAGE_OPTIONAL_HEADERS32 의 AddressOfEntryPoint 값

3.2.3 ImageBase

ImageBase는 PE 파일이 로딩되는 시작 주소를 나타냅니다!
EXE와 DLL 파일은 user memory 영역인 0~7FFFFFFF 범위에,
SYS 파일은 kernel memory 영역인 80000000~FFFFFFFF 범위에 로딩이 됩니다.

IMAGE_OPTIONAL_HEADER32 의 ImageBase 값

아래 사진은 x32dbg를 이용해서 EP에 도달했을 때 주소를 캡처해봤습니다.

3.2.2에서 본 AddressOfEntryPoint의 값이 ImageBase의 값인 0x2C0000에 더해져서 EP의 주소가 0x2C3689가 된 것을
확인할 수 있습니다.


※ ImageBase의 값은 0x1000000인데 왜 0x2C0000 일까요? 바로 ASLR 보호 기법 때문에 그렇습니다. 디버거로 새로 킬 때마다 ImageBase의 값이 변하는 것을 확인할 수 있습니다.

3.2.4 SectionAlignment, FileAlignment

처음 PE 파일 소개를 할 때 NULL padding이란 개념을 조금 설명을 했습니다.
그 부분에서 최소 기본 단위 개념을 사용한다고 했습니다. 그렇다면 최소 단위를 나타내는 값은 무엇일까요?
바로 SectionAlignment와 FileAlignment를 사용해서 만듭니다.
파일/메모리의 섹션 크기는 이 FileAlignment/SectionAlignment의 배수가 되어야 합니다.

IMAGE_OPTIONAL_HEADER32의 SectionAlignment 와 FileAlignment

왼쪽의 빨간 박스의 값부터 각각 SectionAlignment(0x1000) , FileAlignment(0x200) 값들입니다.

3.2.5 SizeOfImage

PE 파일이 메모리에 로딩됐을 때 가상 메모리에서 PE Image가 차지하는 크기를 의미합니다.
파일의 크기랑 메모리에 로딩된 크기는 다르기 때문에 메모리에 로딩 됐을 때의 크기라는 것을 알았으면 좋겠습니다.

IMAGE_OPTIONAL_HEADER32의 SizeOfImage

3.2.6 SizeOfHeader

PE 헤더의 전체 크기를 의미합니다. 파일 시작에서 SizeOfHeader 옵셋만큼 떨어진 위치에 첫 번째 섹션이 위치합니다.
섹션의 위치를 찾기 위해서는 SizeOfHeader의 값이 중요할 것이고, PE 헤더의 크기 이므로 FileAlignment의 배수일 것입니다.

IMAGE_OPTIONAL_HEADERS32의 SizeOfHeader

0x400의 값으로 FileAlignment의 값이었던 0x200의 배수인 것을 확인할 수 있습니다!!

3.2.7 Subsystem

Driverfile인지 GUI 파일인지 CUI 파일인지 구분할 수 있는 값이 적혀 있습니다. 아래 표를 보면서 구분을 해보면 좋을 거 같습니다!!

IMAGE_OPTIONAL_HEADERS32의 Subsystem

notepad.exe의 경우 GUI파일이므로 0x2의 값을 갖고 있습니다.

4. IMAGE_Section_Header

Section 헤더의 구조체입니다

빨간 박스 안의 값들을 하나씩 차례로 살펴보도록 합시다!

4.1 VirtualSize

메모리에서 섹션이 차지하는 크기입니다.

4.2 VirtualAddress

메모리에서 섹션의 시작 주소(RVA)입니다.

4.3 SizeOfRawData

파일에서 섹션이 차지하는 크기입니다.

4.4 PointerToRawData

파일에서 섹션의 시작 위치입니다.

4.5 Characteristics

섹션의 속성입니다. bit OR으로 표시됩니다.

너무 많은 양이고 각 섹션 헤더마다 표시되는 것이 보기 편할 거 같아서
PEView를 이용해서 캡처를 했습니다

다음은 IAT와 EAT에 대해서 다루어 보도록 하겠습니다!