본문 바로가기

Reversing/리버싱 공부

PE 파일 포맷 ( PE File Format ) 요약


PE File Format

 

PE(Portable Executable)파일 - Windows 운영체제에서 사용되는 실행 파일 형식

 

종류

- 실행 계열 : exe, scr

- 라이브러리 계열 : dll, ocx, cpl, drv

- 드라이버 계열 : sys, vxd

- 오브젝트 파일 계열 : obj

 

구조

PE Header : DOS Header ~ Section Header

PE Body : PE헤더 이후 Section들

 

파일을 실행하기 위한 모든 정보는 구조체 형식으로 PE 헤더에 저장되어 있다.

섹션 헤더에 각 섹션에 대한 크기/위치/속성이 정의되어 있고,

파일의 내용은 코드(.text), 데이터(.data), 리소스(.rsrc) 섹션에 나누어 저장한다.

 

파일에서는 offset으로 메모리에서는 VA로 위치 표현

VA(Virtual Address)는 프로세스 가상 메모리의 절대주소를 말하고

RVA(Relative Virtual Address)는 어느 기준 위치(ImageBase)에서부터의 상대주소를 말합니다.

(DLL Relocation이 일어날 경우를 대비해, 절대주소 대신 상대주소를 사용한다.)

 

RVA + ImageBase = VA


 

------------------------------------------------------------------------------------------------------

 

 

1. PE Header

 

1) DOS Header - DOS 파일에 대한 하위 호환성을 고려해서 만듬.

IMAGE_DOS_HEADER 구조체의 크기는 40

구조체 멤버 중.. 

e_magic : DOS Signatue (4D5A -> ASCII값 “MZ”)

e_lfanew : NT 헤더의 오프셋을 표시

 

2) DOS Stub - DOS 환경에서 실행되는 코드를 가진 영역, 일종의 옵션임(없어도 실행에 문제가 없으므로)

코드와 데이터의혼합으로 이루어짐

3) NT Header

IMAGE_NT_HEADERS 구조체 - 3개의 멤버로 이루어짐

  Signature : 표식, 일반적으로 50450000h(“PE”00)을 가짐

FileHeader 구조체

Optional Header 구조체

 

FileHeader 구조체 ( IMAGE_FILE_HEADER )

구조체 멤버 중..

Machine : CPU별로 고유한 값 

NumberOfSections : 섹션의 개수

SizeOfOptionalHeader : IMAGE_OPTIONAL_HEADER32 구조체의 크기

Characteristics : 파일의 속성(실행가능?, 시스템 파일? dll파일? 등)

 

Optional Header 구조체( IMAGE_OPTIONAL_HEADER32 )

구조체 멤버 중..

Magic : 32bit 형태는 10B, 64bit 형태는 20B 값을 가짐

AddressOfEntryPoint : EntryPoint의 RVA 값을 가짐

ImageBase : PE파일이 로딩되는 시작 주소

SectionAlignment : 섹션의 최소 단위

FileAlignment : 파일의 최소 단위

SizeOfImage : PE파일이 메모리에 로딩되었을 때 가상 메모리에서 PE 이미지 크기

SizeOfHeader : PE헤더의 전체 크기

Subsystem : 파일의 종류 구분(드라이버 파일? or CUI 파일 or GUI 파일)

NumberOfRvaAndSizes : DataDirectory 배열의 개수

DataDirectory : 구조체의 배열로 배열의 각 항목마다 정의된 값을 가짐

 

4) Section Header - 각 섹션의 속성을 정의함

파일/메모리 에서의 시작 위치, 크기, 접근 권한 등

 

code 섹션 - 실행, 읽기 권한

data 섹션 - 비실행, 읽기, 쓰기 권한

resource 섹션 - 비실행, 읽기 권한

 

IMAGE_SECTION_HEADER 구조체

구조체 멤버 중..

VirtualSize : 메모리에서 섹션 크기

VirtualAddress : 메모리에서 섹션의 시작 주소(RVA)

SizeOfRawData : 파일에서 섹션 크기

PointerToRawData : 파일에서 섹션 시작 위치

Characteristics : 섹션의 속성

Name : 섹션 이름

 

 

------------------------------------------------------------------------------------------------------ 

 

 

 

RVA to RAW 계산


PE 파일이 메모리에 로딩되었을 때 각 섹션에서 메모리의 주소(RVA)와 파일 옵셋을 매핑하는 방법

 

1) RVA가 속한 섹션을 찾는다.

2) 간단한 비례식을 통해 파일 옵셋(RAW) 계산

 

RAW-PointerToRawData = RVA - VirtualAddress 이므로

 

RAW = RVA - VirtualAddress + PointerToRawData 가 됩니다.

 

파일의 오프셋 = 메모리 상대주소 - 메모리의 해당 섹션 시작 위치 + 파일의 해당 섹션 시작 위치


 

 

------------------------------------------------------------------------------------------------------ 

 

 

 

DLL

 

메모리 낭비를 피하기 위하여,

프로그램에 라이브러리를 포함시키지 말고, 별도의 파일(DLL)을 구성하여 필요할 때마다 불러쓴다.

한 번 로딩된 DDLL의 코드, 리소스는 Memory Mapping 기술로 여러 프로세스에서 공유해 사용

 

로딩방식

Explicit Linking : 프로그램에서 사용되는 순간에 로딩, 사용 후 해제

Implicit Linking : 프로그램 시작 시 로딩, 종료 시 해제

 

 

------------------------------------------------------------------------------------------------------ 

 

 

 

IAT ( Import Address Table )


프로그램이 어떤 라이브러리에서 어떤 함수를 사용하고 있는지 나타낸 테이블

DLL의 Implicit Linking 방식에 대한 메커니즘을 제공한다.

 

 

1) IMAGE_IMPORT_DESCRIPTOR (IID)

구조체 배열 중..

OriginalFirstThunk : INT(Import Name Table)의 주소(RVA)

Name : 라이브러리 이름 문자열의 주소(RVA)

FirstThunk : IAT의 주소(RVA)

INT와 IAT는 long 타입의 4바이트 자료형 배열

INT의 각 원소 값은 IMAGE_IMPORT_BY_NAME 구조체 포인터

 

2) IAT 입력 순서

1. IID의 Name 멤버를 읽어 라이브러리 이름 문자열을 찾는다.

2. 해당 라이브러리 로딩 -> LoadLibrary(“이름”)

3. IID의 OriginalFirstThunk 멤버를 읽어 INT의 주소를 찾는다.

4. INT의 배열에서 하나씩 값을 읽어, IMAGE_IMPORT_BY_NAME의 주소를 찾고

5. IMAGE_IMPORT_BY_NAME의 ordinal, Name 항목을 통해 해당 함수의 주소를 찾는다.

6. IID의 FirstThunk를 통해 IAT 주소를 찾고

7. 해당 IAT 배열에 찾은 함수의 주소 입력

8. 4~7 과정 반복

 

Ordinal 이란, 함수의 고유 번호를 나타내는 2바이트 값

 

 

------------------------------------------------------------------------------------------------------ 

 

 

EAT( Export Address Table )


라이브러리 파일에서 제공하는 함수를 다른 프로그램에서 가져다 사용할 수 있도록 하는 메커니즘

 

1) IMAGE_EXPORT_DESCRIPTOR

구조체 배열 중.. 

NumberOfFunctions : 실제 Export 함수 개수

NumberOfNames : Export 함수 중 이름을 가진 함수 개수

AddressOfFunctions Export : 함수 주소 배열

AddressOfNames : 함수 이름 주소 배열

AddressOfNameOrdinals : Ordinal 주소 배열

 

2) GetProcAddress()

라이브러리에서 함수 주소를 얻는 API

 

동작 원리

1. AddressOfNames 멤버를 이용해 ‘함수 이름 배열’로 감

2. ‘함수 이름 배열’에 저장된 문자열 주소들을 문자열 비교하여 원하는 함수를 찾는다. -> index

3. AddressOfNameOrdinals 멤버를 이용해 ‘Ordinal 배열’로 감

4. ‘Ordinal 배열’에서 index를 이용해 해당 ordinal 값을 찾음

5. AddressOfFunctions 멤버를 이용해 ‘함수 주소 배열’ (EAT)로 감

6. EAT에서 아까 구한 ordinal을 배열 인덱스로 원하는 함수의 시작 주소를 얻는다.

 

이름 없이 ordinal만으로 함수의 주소를 찾을 땐

(Ordinal - IMAGE_EXPORT_DIRECTOR.BASE 멤버 )를 EAT의 인덱스로 함