[rev] 리버싱 핵심원리 2부) PE File Format 13장

2023. 3. 7. 17:18리버스 엔지니어링/2부) PE File Format

이번 글은 2부 PE File Format 입니다.

1부에서 파일들을 IDA로 디버깅하여 공부를 해봤는데요, 그럼 이런 생각이 들 수도 있습니다.

 

" 모든 파일을 IDA로 실행 가능할까? "

 

하지만 이것은 불가능합니다. 왜냐하면 IDA는 디버거라 PE파일만 대상으로 하기 때문입니다.

여기서 PE파일이란 Windows에서 사용하는 실행파일을 말합니다.

흔히 사용하는 .exe 확장자를 가진 파일도 PE파일이죠.

 

PE파일의 종류는 exe 외에 여러가지가 있습니다.

종류 주요 확장자 종류 주요 확장자
실행 계열 EXE, SCR 드라이버 계열 SYS, VXD
라이브러리 계열 DLL, OCX, CPL, DRV 오브젝트 파일 계열 OBJ

이제부터 PE파일의 구조를 살펴보겠습니다.

 


PE File Format

교재 <리버싱 핵심원리>는 PE파일 형식을 notepad.exe(메모장)을 통해 보여주고 있습니다.

따라서 이번 글도 notepad.exe를 통해 보여드리나

교재와 실습 간의 환경의 차이, 운영체제의 차이로 인해 주소가 상이 할 수 있습니다.

 

기본 구조

 

PE파일은 헤더와 바디로 나뉘어집니다. DOS header부터 Section header까지를 PE헤더

아래의 Section들을 PE 바디로 부릅니다.

 

모든 PE파일이 3가지의 섹션으로 구성되지는 않습니다.

이외에도 .idata섹션, .rdata섹션 등이 존재합니다.

 

각 섹션의 끝에는 NULL padding이 존재합니다.

NULL padding이 존재하는 이유는 프로세스 처리를 위해 섹션의 기본단위를 설정하는데

이 기본단위의 배수로 섹션의 공간을 설정하기 위해 부족한 공간은 NULL padding으로 채웁니다.

 

VA & RVA

프로세스를 통해 파일 시스템 내의 파일이 로딩을 통해 메모리에 이미지됩니다.

           ※이미지: PE파일이 메모리에 로딩되는 모습을 말합니다.

따라서 로딩되기 이전의 파일 내의 주소값과 로딩된 이후 메모리 내의 주소값은 다릅니다.

 

VA는 프로세스 가상 메모리의 절대주소를 말합니다.

RVA는 프로세스에서 어느 기준점(ImageBase)으로부터의 상대주소를 말합니다.

File Offset(RAW)은 로딩되기 전 파일의 시작지점부터 커서까지의 거리

 

따라서 RVA + ImageBase = VA 라는 식을 가집니다.

 

PE header

 

Dos Header

: 이전 널리 사용되던 DOS파일의 하위호환을 위해 만들어진 헤더

IMAGE_DOS_HEADER 구조체 형식으로 존재한다. 구조체의 크기는 40h

IMAGE_DOS_HEADER 구조체에서 중요한 멤버는 e_magic 과 e_lfanew인데

e_magic: DOS Signature (4D5A == "MZ")

e_lfanew: NT Header의 file offset을 저장

e_magic은 구조체의 첫 맴버이다. 따라서 모든 PE파일은 MZ(4D5A)로 시작한다.

 

PEview를 사용하여 PE파일의 data 및 헤더주소를 볼 수 있다.

http://wjradburn.com/software/

 

WJR Software - PEview (PE/COFF file viewer),...

Utilities (for use with Windows® XP operating system or later) PEview provides a quick and easy way to view the structure and content of 32-bit Portable Executable (PE) and Component Object File Format (COFF) files. This PE/COFF file viewer displays heade

wjradburn.com

 

메모장은 Windows에서 c:\Windows\system32\notepad.exe 로 저장되어 있다. 한 번 실행시켜보자

             ※관리자 권한으로 실행시켜야 notepad.exe를 열 수 있다.

MZ로 시작하는 것을 알 수 있다

e_lfanew 값을 00000100으로 저장되어 있는걸 알 수 있는데

실제로 00000100에서부터 NT_Header의 offset 인 걸 알 수 있다.

 

DOS Stub

: DOS Header 아래 존재하는 헤더로써 해당 PE파일이 DOS모드에서 실행 가능한지 알려준다.

DOS Header은 옵션, 즉 없어도 실행에 문제없는 부분이다.

 

NT_Header

: NT_Header은 IMAGE_NT_HEADERS 구조체로 이루어져 있습니다.

구조체는 3개의 맴버로 되어 있습니다.

typedef struct _IMAGE_NT_HEADERS {
	DWORD Signature;
    	IMAGE_FILE_HEADER FileHeader;
    	IMAGE_OPITIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32

 

Signature: 50450000h("PE"00)을 가진다

FileHeader: 파일의 개략적인 속성을 나타낸 구조체

OptionalHeader: ImageBase, 섹션의 기본단위, 헤더의 크기 등을 저장

 

FileHeader

FileHeader에서는 4가지 맴버에 주목한다.

  • Machine
  • NumberOfSections
  • SizeOfOptionalHeader
  • Characteristics

 

#1. Machine

: CPU별로 고유한 값을 가진다. x86 호환 CPU는 14C의 값을 가진다.

Machine의 값이 014C

 

#2. NumberOfSections

: 섹션의 개수, 이 값은 반드시 0보다 커야한다.

윗 사진을 보면 섹션의 갯수가 6개인 것을 알 수 있다.

 

#3. SizeOfOptionalHeader

: NT_Header의 마지막 맴버였던 OptionalHeader의 크기를 저장한다.

윗 사진을 보면 OptionalHeader의 크기가 0E0임을 알 수 있다.

 

#4. Characteristic

: 실행이 가능한 형태인지, DLL파일인지 속성을 나타낸다.

사진에서 0002h는 실행 가능한 파일임을 의미하고, 0100h는 32bit machine임을 의미한다.

 

OptionalHeader

OptionalHeader에서 주목할 맴버는 다음과 같다.

  • Magic
  • AddressOfEntryPoint
  • ImageBase
  • SectionAlignment, FileAlignment
  • SizeOfImage
  • SizeOfHeader
  • Subsystem
  • NumberOfRvaAndSizes
  • DataDirectory

#1. Magic

: IMAGE_OPTIONAL_HEADER32 면 10B, HEADER64 이면 20B

 

#2. AddressOfEntryPoint

: EP(Entry Point)의 RVA값

 

#3. ImageBase

: 가상메모리에서 주소들의 기준점

 

#4. SectionAlignment, FileAlignment

: 섹션의 최소단위, SectionAlignment는 메모리에서 섹션의 최소단위

FileAlignment는 파일에서 섹션의 최소단위

 

#5. SizeOfImage

: 파일이 메모리에 로딩됐을 때 PE 파일의 크기

 

#6. SizeOfHeader

: PE헤더 전체의 크기, FileAlignment의 배수이다.

 

PE 헤더와 PE바디는 FileAlignment의 배수이다.

따라서 PE파일 전체 크기 역시 FileAlignment의 배수인 것을 알 수 있다.

 

#7. Subsystem

: 이 파일이 시스템 드라이버 파일(.sys)인지 일반 실행 파일인지 알 수 있다.

 

#8. NumberOfRvaAndSizes

: #9. DataDirectory의 구조체 배열의 갯수를 나타낸다.

 

#9. DataDirectory

: IMAGE_DATA_DiRECTORY 구조체의 배열이다. 0번째와 1번째 배열인

EXPORT Directory, IMPORT Directory는 차후에 설명할 IDT, INT, EAT에 필요하다.

 

섹션 헤더

각 섹션의 정보를 가지고 있는 헤더이다.

섹션의 크기, 시작주소(RVA and RAW), 크기, 권한 등을 저장하고 있다.

 


RVA to RAW

PE파일이 메모리에 로딩되는 과정에서 File Offset(이하 RAW)이 RVA로 변경되기 때문에 매핑하는 과정이 필요하다.

RVA를 RAW로 변환하는 공식은 다음과 같다.

RAW - PointerToRawData = RVA - VirtualAddress
  <=>  RAW = RVA - VirtualAddress + PointerToRawData

VirtualAddress: 메모리에서 해당 주소가 속해있는 섹션의 시작주소

PointerToRawData: 파일에서 해당 주소가 속해있는 섹션의 시작주소

 

따라서 공식은 RVA에서 해당하는 섹션의 시작주소만큼 빼고,

뺀 값을 파일에서 해당하는 섹션의 시작주소에 더함으로써 RAW를 알아낸다.

 

Ex) ImageBase = 01000000, RVA = 6245이면 RAW값은?

RAW = 6245(RVA) - 1000(VirtualAddress) + 400(PointerToRawData) = 5645

 


IAT / EAT

IAT를 설명하기 이전에 DLL파일을 먼저 설명하겠습니다.

 

DLL(Dynamic Linked Library)

: 동적 연결 라이브러리라고 하며, 코드에서 함수를 사용할 때 불러오는 라이브러리 함수 파일

 

코드에 필요한 DLL파일만 로딩하기 때문에 동적으로 로딩하며, 로딩하는 방식은 2가지이다.

  • 프로그램 사용 시에 로딩하고, 끝나면 해제
  • 프로그램 시작할 때 로딩되어 종료할 때 해제

주소를 호출할 때 함수가 저장되어있는 주소를 입력하여 호출하는 방식을 하드코딩이라고 합니다.

Ex) CALL 7C8107F0

 

하지만 API호출은 하드코딩으로 이루어지지 않습니다.

왜냐하면, 환경에 따라 함수의 주소가 달라지기도 하고,

DLL Relocation을 통해 기존과 다른 위치에 DLL파일이 저장되기도 하기 때문입니다.

     DLL Relocation: dll파일을 로딩 시 이미 로딩할 주소에 다른dll 파일이 로딩되어 있으면 다른 메모리 공간을 찾아서 로딩

 

API를 호출 할 때는 IAT 메모리 영역 내 해당API의 주소를 호출합니다.

 

IMAGE_IMPORT_DESCRIPTOR

: PE파일에 어떤 DLL파일을 로딩했는지 알려주는 구조체

 

※ IMAGE_IMPORT_DESCRIPTOR(IID)을 IMPORT_DIRECTORY_TABLE이라고도 합니다.

(앞으로는 IDT라고 부르겠습니다)

 

IDT의 중요한 맴버로는 

OriginalFirstThunk: INT의 주소

Name: dll파일 이름 문자열의 주소

FirstThunk: IAT의 주소

INT: 임포트하는 함수의 정보(Ordinal, Name)가 담긴 구조체 포인터 배열

IMAGE_IMPORT_BY_NAME: INT가 가리키는 구조체

IAT: 임포트하는 함수의 주소가 담긴 포인터 배열

 

IAT에 함수의 주소가 입력되는 순서는 다음과 같다.

  1. IDT 맴버 Name을 통해 로딩되는 dll의 이름을 얻음
  2. Name을 통해 해당 라이브러리를 로딩 => LoadLibrary("USER32.dll")
  3. IDT 맴버 OriginalFirstThunk을 통해 INT주소를 얻음
  4. INT를 통해 IMAGE_IMPORT_BY_NAME에 저장된 이름을 읽음
  5. 읽은 이름을 이용해 함수의 주소를 얻음 => GetProcAddress("MessageBoxW")
  6. IDT 맴버 FirstThunk를 통해 IAT 주소를 얻음
  7. 얻은 IAT에 5에서 구한 주소를 입력
  8. 4~7번까지의 과정을 NULL을 만날 때까지 시행

실습

(notepad.exe로 실습합니다.)

 

IDT를 통해 KERNEL32.dll의 INT주소, 이름 문자열 주소, IAT주소를 알 수 있다.

 

KERNEL32.dll의 맴버 INT의 시작주소 0000A2B8과 같음

0000A2B8의 값 0000A742로 가면 Ordinal값, 이름을 볼 수 있음

 

KERNEL32.dll의 맴버 IAT의 시작주소 0000102C와 같음

 

RVA = 00001238, ImageBase = 00240000 => VA = 00241238

※ 프로세스를 실행할 때마다 ImageBase값은 변경된다.

그 이유는 ASLR 때문에 ImageBase값을 무작위로 잡아주기 때문

예상대로 주소 00241238에 SendMessage API가 있다. 클릭하여 로딩된 API의 주소를 보자

여기서는 PEview에서 주소값과 IDA의 주소값이 다른 것을 알 수 있다. ????

이렇게 되는 이유는 아직 못찾았네요....이유를 찾아서 수정하겠습니다.

 

IAT에 주소를 입력하는 과정에서 GetProcAddress()를 이용하여 함수의 주소를 구합니다.

이번에는 GetProcAddress()의 주소를 구하는 과정을 알아보겠습니다.

 

함수의 주소는 EAT에 저장합니다. 즉 EAT는 라이브러리 함수의 주소를 저장하고 있습니다.

EAT는 IMAGE_EXPORT_DIRECTORY 구조체의 맴버 AddressOfFunctions입니다.

 

GetProcAddress()가 주소를 찾는 과정은 다음과 같다.

  1. AddressOfNames에서 Strcmp로 원하는 함수이름의 인덱스값을 찾는다.
  2. AddressOfNameOrdinals에서 찾은 인덱스값으로 맞는 Ordinal 값을 찾는다.
  3. AddressOfFunctions을 통해 EAT로 가 Ordinal값으로 맞는 함수의 주소를 찾는다.

IAT와 EAT는 DLL파일을 로딩할 때 받아오는 함수들의 주소를 저장하는 테이블입니다.

 

하지만 모든 API가 IAT에 저장되는 것은 아닙니다.

즉, 프로그램 코드에서 동적으로 DLL을 로딩하면 IAT에 저장되지 않습니다.

IAT에 대한 내용은 4부 API후킹에서 다시 다루어집니다.

 


참고서적

이승원, 「리버싱 핵심원리」, 인사이트, 2012, 141p ~ 191p

http://www.yes24.com/Product/Goods/7529742

 

리버싱 핵심 원리 - YES24

리버서라면 꼭 알아야 할 핵심 원리를 모두 담았다리버싱이란 프로그램의 내부를 깊이 들여다보고 조작할 수 있는 기법이다. 이는 우리가 흔히 사용하는 상용 프로그램 등에도 가능하기 때문에

www.yes24.com