참고 : 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 | |
SizeOfHeaders | 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 | ┐ |
| |
| PE header | ← Size of Header = FileAlignment * n =512 = 0x200 실제 PE 파일이 메모리에 로드되면 SectionAlignment * n이지만 이 값은 로더에의해서만 사용되는 값이므로 변하지 않고 유지됨 | |
| Section Table |
| |
Base of Code 0x00401000↘ | (Padding) |
| |
| Section #1(code) .text | ┐ 4 |
|
Base of Data 0x00402000↘ | (Padding) |
| |
| Section #2(Data) .rdata | ┐ 4 |
|
0x00403000↘ | (Padding) |
| |
| Section #3(API) .idata | ┐ 4 | ▶ 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 값과 사이즈가 들어 있다.
임포트 섹션을 생성하지않고 데이터 섹션등에 임포트 테이블을 두는 것이 가능하다.
'Kitri_NCS3기 보안과정 > 시스템 해킹' 카테고리의 다른 글
[Reverse Engineering]PE-Section Table (0) | 2017.05.24 |
---|---|
[Reverse Engineering]PE-DOS Header (0) | 2017.05.21 |
170519 힙영역 오버플로우 (0) | 2017.05.19 |
170518 환경변수 변조 (export LD_PRELOAD) (0) | 2017.05.18 |
170516 오버플로우를 이용한 RET 변조 (0) | 2017.05.16 |