본문 바로가기

Kitri_NCS3기 보안과정/시스템 해킹

[Reverse Engineering]PE-PE Header

참고 : http://zesrever.tistory.com


※ PE 파일이란 ?


 윈도우즈 환경의 실행파일 포맷을 PE 라고 하며, Portable Executable의 약자. 굳이 EXE 파일만 일컫는것이 아니라 SCR,SYS,DLL,OCX등도 포함됨. PE의 이해는 API후킹, 압축실행파일 등과 같은 고급 리버싱기법의 기본 바탕이된다.


PE파일은 디스크상의 모습과 메모리상의 모습이 거의 같다, 실행시 하드에 저장되어 있던 파일의 형태 그대로 메모리에 올라가는 모습을 보인다.

PE를 파악하기위해서는 PE파일의 구성요소와 그 구성요소들의 시작점을 찾는 방법을 잘 숙지해야함.


실행파일 분석기를 만들어보는것이 PE를 연구하는데 가장 효과적인방법...


PE구성요소 

- DOS header

- DOS stub code

- PE header

- Section table

- Section (1~n)


각 구성요소의 시작위치 계산법 

 시작위치

설명 

DOS header

PE 파일은 DOS header로 시작. 디스크상에서의 위치와 메모리상에서의 위치가 다르다.

디스크 : 파일의 첫부분이 DOS header

메모리 : ImageBase에서 시작

DOS stub code

디스크상이나 메모리 상에서  DOS의 스텁 코드는 시작점(Dos header)으로부터 64byte만큼 떨어진곳.


프로그램을 도스모드에서 실행시켯을때 실행되는 코드이며 DOS header 다음에 위치한다. 필수 구성요소가 아니기 때문에 없어도 실행하는데는 지장이 없으며, 크기는 가변적이다.

PE header

e_lfanew에는 4byte의 크기로 DOS header의 마지막에 위치하며  파일의 시작점에서부터 PE 헤더까지의 오프셋 값이 저장되어있음. e_lfanew의 값을 시작주소로 찾으보면 PE헤더를 찾을 수 있다.

Section table

PE의 헤더 시작주소에서 PE 헤더 사이즈를 더해주면 된다. 

PE 헤더 사이즈 (PE signature + File Header + Optional Header)로 구성되어 있습니다.

Section (1~n) 

각 섹션 테이블에 저장된 섹션 헤더를 통해서 확인 가능합니다.

PointerToRawData는 디스크상의 섹션의 위치

VirtualAddress는 메모리상에서의 section의 위치를 가르키는 offset




PE HEADER(IMAGE_NT_HEADERS)






세개의 영역으로 구분되어 있다. 첫번째는 PE signature 영역으로 50 45 00 00 으로 고정값을 갖는다.

두번째는 20Byte의 고정사이즈를 가지는 IMAGE_FILE_HEADER이다. 7개의 필드로 구성되어 있으며 이중 필요한 필드는 4개이다. 


<IMAGE_FILE_HEADER>

필드

 사이즈

설명 

Machine 

WORD

CPU의 ID를 나타냄 (IA32 = 0x14c, IA64 = 0x200)

NumberOfSections 

WORD

섹션의 개수를 의미, 리버싱중 섹션의 변경사항을 반드시 반영해 주어야 한다. 

TimeDateStamp 

DWORD

파일 제작 일시

PointerToSymbolTable 

DWORD

심볼 테이블 오프셋 

NumberOfSymbols

DWORD

심볼 테이블 엔트리 수 

SizeOfOptionalHeader  

WORD

 헤더의 뒤에 오는 Optional 헤더의 사이즈. 옵셔널 헤더에 포함되어 있는 데이터 디렉토리만이 사이즈에 영향을 미치며 일반적인경우 


(데이터 디렉토리 + 옵셔널 헤더 = 224 byte = 0xE0, 

데이터 디렉토리가 없는 옵셔널 헤더는 96byte  = 0x60 )


Characteristics 

WORD

PE파일의 속성을 의미함. 파일이 EXE인지 DLL인지 재배치가 가능한가 등의 정보가 담겨있다. 

재배치란 옵셔널 헤더에 지정되어 있는 Image Base 에 PE파일을 로드할 수 없는경우 로드 가능한 주소에  PE파일을 로드하고 실행코드 내에서 절대주소값등을 변경하는 작업.

EXE는 재배치가 발생하지 않지만 DLL은 상황에 따라 ImageBase가 로드 될수없는 경우가 발생하기 때문에 재배치가 발생 할 수 있습니다. 


일반파일은 0x10F


<IMAGE OPTIONAL_HEADER>

 PE파일의 논리적 구조에 대한 중요한 정보를 담고 있다. 30개의 필드와 1개의 데이터디렉토리로 구성되어 있다. 

30개의 필드중 필요한 필드는 10여개인데 먼저 필드부터 살펴보자. 


 필드

사이즈 

설명 

 Standard fields

Magic

WORD

 optional header의 시작위치에 존재하는 필드, 

 optional의 시그니쳐(0x10B)

MajorLinkerVersion 

BYTE

 링커 버전 (0 으로 채워도 실행에 지장이없다)

MinorLinkerVersion 

BYTE 

 링커 버전 (0 으로 채워도 실행에 지장이없다)

SizeOfCode 

DWORD 

 코드 섹션들의 사이즈의 합 ( FileAlignment * n )

SizeOfInitializedData 

DWORD 

 코드 섹션 제외 초기화된 사이즈 

SizeOfUninitializedData 

DWORD 

 초기화 안된 데이터 섹션의 크기 

AddressOfEntryPoint 

DWORD 

 PE파일이 메모리에 로드된 후 맨 처음에 실행되야 하는 코드의 주소를 담고 있다. 주의할점은 이 필드에 Virtual Address가 아닌 RVA값 즉 , ImageBase로 부터의 offset이 기록된다는 사실. 일반적으로 엔트리 포인트는 text 섹션 (실행코드를 담고 있는 메모리 영역) 의 시작점인 경우가 대부분이다.

BaseOfCode 

DWORD 

코드 섹션 RVA

BaseOfData 

DWORD 

데이터 섹션 RVA 

NT

Additional

Fields

ImageBase 

DWORD

 로더는 PE 파일을 로드할때 ImageBase값을 참조하여 가급적이면 ImageBase부터 로드 하려한다. 


EXE 파일의 경우 가상 메모리 공간에 가장 처음 로드되므로 항상 ImageBase에 로드된다. (0x00400000)

DLL 파일의 경우 ImageBase로 지정된 주소 공간이 다른 모듈에 의해서 이미 사용중인 상황이 발생할 수 있는데 이 경우 재배치 작업 수행 

(0x10000000)

SectionAlignment 

DWORD 

 각 섹션이 메모리상에서 차지해야 하는 최소의 단위.

예를 들어 

Section Alignment = 4096 .text = 100 → .text= 4096(한 단위 차지)

Section Alignment = 4096 .text = 5000 → .text= 8192(두 단위 차지)


- 메모리 상에서 각 섹션은 Section Alignment x n 번지에서 시작

- 메모리 상에서 하나의 섹션은 Section Alignment x m 사이즈를 가진다

일반적으로 Section Alignment의 값은 페이지 사이즈와 동일한 4096값을 사용하나. 제한적인상황에서는 작아도 실행하는데 제한이 없다.

FileAlignment 

WORD 

 SectionAlignment가 메모리상에서의 섹션 정렬과 관련있엇다면 

FileAlignment는 디스크상에서의 섹션정렬과 관련있는 필드이다.

개념은 SectionAlignment와 동일 

이 값이 Section Alignment와 동일하다면 디스크상의 PE 파일 모습이나 메모리상의 PE 파일 모습은 100%같다.

MajorOperatingSystemVersion 

WORD 

 운영체제 최소 버전(0x4)

MinorOperatingSystemVersion

WORD

 운영체제 최소 버전(0x0)

MajorImageVersion

WORD

 유저 정의 파일 버전 

MinorImageVersion 

WORD

 유저 정의 파일 버전 

MajorSubsystemVersion 

WORD

  Win32 애플리케이션의 경우 버전을 4.0으로 해야함. 

따라서 대부분의 경우 MajorSubsystem 값은 4, MinorSubsystem 값은 0 이됩니다.

MinorSubsystemVersion 

WORD

Win32VersionValue 

DWORD 

 사용 x (0x0000)

SizeOfImage 

DWORD 

 메모리상에 로드된 PE 파일의 총 사이즈 

 =  SectionAlignment x n

SizeOfHeader

DWORD 

디스크 상에서의 헤더의 총 사이즈 

CheckSum 

DWORD 

 체크섬 값 

Subsystem 

WORD 

Console용 애플리케이션인 경우 Windows CUI(0x3)

GUI용 애플리케이션이 경우 Windows GUI(0x2) 

DllCharateristics 

WORD 

 DLL 초기화 함수(현재 사용 x) 

SizeOfStackReserve 

DWORD 

 이 값은 stack 영역으로 예약된 메모리 사이즈와 할당된 메모리 사이즈 값을 가진다.

보통 스택영역으로는 1page를 할당하며, 16page를 예약해둔다. 따라서 대부분의 경우 SizeOfStackReserve 값은 0x10000, SizeOfStackCommit값은 0x1000이 된다

SizeOfStackCommit

DWORD 

SizeOfHeapReserve 

DWORD 

 위와 동일 , Heap 영역 사이즈 정보

SizeOfHeapCommit 

DWORD 

LoaderFlags 

DWORD 

 사용 x 

NumberOfRvaAndSizes

DWORD 

 Data Directory 배열의 원소 개수 


<Standard Field> 부분 부터 살펴보자 . 

Magic "OB 01" Optional header의 시작위치에 존재하는 필드이다 0x10B를 가진다. 

다음으로 오는 MajorLinkerVersion과 MinorLinkerVersion은 0을 채워도 실행에 지장이 없다. 

Size of Code는 0을 채워도 무방하지만 이 필드로부터 Code size 등을 읽어 보내는 프로그램이 있다거나 이부분을 참조해서 사용하는 경우에는 작성을 해줘야 한다. FileAlignment * n 의 결과를 가진다.  

Address EntryPoint는 코드섹션의 시작점을 가리킨다. 시작점의 계산은 밑의 그림과 함께 살펴보자.

 ImageBase

(0x00400000)↘ 

 <AddressOfEntryPoint>

 

 

 DOS Header

 64byte

 

 PE Header

(Data Directory 포함) 

248byte 

 

Section Table 

40byte * 3 = 120 byte

(Section Table 의 크기는 40byte * 섹션의 개수)

 AddressOfEntryPoint↘ 

(Padding) 

 

 

Section #1(코드)














.text

★SectionAlignment = 4096, ImageBase = 0x00400000 이라고 가정 


 4096*(n-1) <= 64+248+40 <= 4096*n


n=1 → 4096 → 0x1000

 계산은 이런식지이만 결과적으로 4096단위로 할당되는 범위안에 

DOS header ~ 코드부분의 시작점까지의 크기가 들어가느냐 아니냐이다. 

크기가 4096넘은 5000이라면 8192가 AdderssOfEntryPoint가 될것이다. 


0x1000의 결과가 나왔다. 

.text의 시작주소는 ImageBase + 0x1000 = 0x00401000이며 

EntryPoint는 RVA 값이므로 0x1000이다. 


AddressEntryPoint 의 값은 0x1000이다.

Base of Code와 Base of Data는 섹션에서 코드와 데이터부분의 시작점을 알려준다.  ntdll.dll에 구현되있는 로더는 이 필드를 사용하지 않는것 같다고 한다. 결국 0으로 채워도 실행에는 지장이 없다는말. 


  0x00400000↘ 

 <Size of Image>

  

 DOS header

┐ 



 4
 0
 9
 6



 

 

PE header 

 ← Size of Header

  = FileAlignment * n =512 = 0x200


실제 PE 파일이 메모리에 로드되면 SectionAlignment * n이지만 

이 값은 로더에의해서만 사용되는 값이므로 변하지 않고 유지됨

 

Section Table 

 

Base of Code 0x00401000↘ 

 (Padding) 

 

 

Section #1(code)


.text 

┐ 

 4
 0
 9
 6

 

Base of Data

  0x00402000↘ 

 (Padding) 

 

 

Section #2(Data)


 .rdata

┐ 

 4
 0
 9
 6

 


 0x00403000↘ 

 (Padding) 

 

 

Section #3(API)


.idata 

┐ 

 4
 0
 9
 6

  ▶ Size of Image = 4096 * 4

 

 (Padding) 

 

Base of Code 는 0x1000. Base of Data는 0x2000


<NT Additional Field>



ImageBase값은 일반적으로 exe 파일은 0x00400000 으로  DLL 파일은 0x10000000 으로 설정되는 경향이 있는데 이것은 정해진것이 아닌 컴파일러가 일반적으로 만들어주는 모양이라고 한다. 


<Data Directory>


 : Data Directory는 PE 헤더의 마지막에 위치한 128 byte 사이즈의 배열이다. 



 ......


NumberOfRvaAndSize = 0x10

 IMAGE_DIRECTORY_ENTRY_EXPORT

- size    - virtualAddress 

 IMAGE_DIRECTORY_ENTRY_IMPORT

- size    - virtualAddress 

 IMAGE_DIRECTORY_ENTRY_RESOURCE

 IMAGE_DIRECTORY_ENTRY_EXCEPTION

 IMAGE_DIRECTORY_ENTRY_SECURITY

 IMAGE_DIRECTORY_ENTRY_BASERELOC

 IMAGE_DIRECTORY_ENTRY_DEBUG

 IMAGE_DIRECTORY_ENTRY_COPYRIGHT

 IMAGE_DIRECTORY_ENTRY_GLOBALPTR

 IMAGE_DIRECTORY_ENTRY_ TLS

 IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG

 IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT

 IMAGE_DIRECTORY_ENTRY_IAT

 IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT

 IMAGE_DIRECTORY_ENTRY_DESCRIPTOR

NULL 



IMAGE_DIRECTORY_ENTRY_EXPORT 

 EXPORT TABLE 메모리상에서 시작점과 크기에 대한 정보를 가지고 있다. EXPORT TABLE은 대부분 DLL에 존재.

 IMAGE_DIRECTORY_ENTRY_IMPORT

 IMPORT TABLE 메모리상에서 시작점과 크기에 대한 정보를 가지고 있다. 메뉴얼 언패킹을 하는경우 이부분을 확인한다.

IMAGE_DIRECTORY_ENTRY_BASERELOC

 재배치와 관련된 데이터 구조에 대한 시작점과 크기정보를 가지고 있다. 재배치는 일반 EXE 파일과는 관련이 없고 DLL에서 일어남  

IMAGE_DIRECTORY_ENTRY_TLS

 TLS는 TLS Callback 함수를 이용한 안티 리버싱 테크닉에 사용함. 


두번째 엔트리인 IMAGE_DIRECTORY_ENTRY_IMPORT에서 임포트 테이블의 시작 위치와 크기를 가리키고 있다.

알려주는 위치는 RVA 값이므로 계산해야 한다. SectionAlignment = 0x1000 이고 FileAlignment= 0x200이기 때문에 

0x3000 은 디스크상에서 0x600이다. 


0x600의 위치에서 OriginalFirstThunk로 시작하는 Import Table 확인



IMPORT Table (IMPORT DIRECTORY)


1. PE 파일을 메모리에 로드한 후 데이터 디렉토리의 두번째 엔트리인 IMAGE_DIRECTORY_ENTRY_IMPORT로 부터 임포트 테이블의 주소를 구한다.


2. 임포트 테이블을 구성하는 각각의 IMAGE_IMPORT_DESCRIPTOR로 부터 임포트 할 DLL의 이름을 알아낸다.


3. 해당 DLL을 위한 공간을 확보하고 DLL을 메모리에 맵핑 시킨다.


4. ILT(Import Lookup Table)로 부터 임포트할 함수의 이름 또는 ordinal 값을 을아낸다.


5. 위의 정보를 이용하여 임포트할 DLL의 익스포트 테이블로부터 실제 함수의 주소를 알아낸다.


6. 알아낸 함수의 주소를 IAT에 기록한다.



 OriginalFirstThunk 

 ILT를 가르키는 RVA값.

 TimaDataStamp 

 바인딩 전에는 0 바인딩 후에는 -1

 ForwarderChain 

 바인딩 전에는 0 바인딩 후에는 -1

 Name 

 임포트한 DLL 의 이름을 가르키는 포인터값(RVA)

 FirstThunk 

 IAT의 주소를 가지고 있다. IAT는 바인딩전에는 ILT와 같은 모습을 가진다. PE 파일이 메모리에 로드 된 후에는 로더가 임포트 테이블의 각 엔트리의 이름을 확인한 후 해당 DLL의 Export Table을 참조하여 함수의 실제 주소를 알아낸 후 IAT를 실제 함수 주소로 업데이트 함.


바인딩 전 : IAT와 ILT 둘다 IMAGE_IMPORT_NAME을 가리킴 

바인딩 후 : IAT는 실제 함수의 주소를 가리킨다. 


디스크상에서 IAT와 ILT는 함수의 이름을 가리킨다. 디스크에있던 내용이 로더에 의해 메모리에 로드 되었을때 IAT는 함수의 이름이 아닌 함수의 주소를 가리키게 바뀐다. 


임포트 테이블은 반드시 임포트 센션에 위치하는 것이 아니다. 데이터 디렉토리에서 찾을 수 있는데 두번재 엔트리인 IMAGE_DIRECTORY_ENTRY_IMPORT에는 임포트 테이블이 시작되는 가상 주소의 RVA 값과 사이즈가 들어 있다. 


임포트 섹션을 생성하지않고 데이터 섹션등에 임포트 테이블을 두는 것이 가능하다.