참조:
http://www.reversecore.com
PE(Portable Executable) 파일이란?
Win32 운영체제라면 어디에서라도 잘 돌아가는 파일 포맷을 말한다.
PE라는 개념을 도입한 이유
PE 헤더에는 실행 파일을 실행하기 위한 각종 정보(메모리 적재 방법, 실행 위치, 필요한 DLL 목록 등)가 기록되어 있다.
즉, PE구조를 통해 파일이 실행되기 위해 필요한 정보들을 볼 수 있으며, 파일 실행 시, 코드가 실행되기 전에 PE의 정보부터 읽어 들여 바이너리를 메모리에 올리기 위한 데이터 설정 작업을 하게 된다.
PE 파일의 종류
- 실행 파일 계열 : exe, scr
- 라이브러리 계열 : dll, ocx
- 드라이버 계열 : sys
- 오브젝트 파일 계열 : obj
※ obj 파일을 제외한 모든 파일들은 실행 가능한 파일이다.
dll, sys 파일등은 쉘(Explorer.exe)에서 직접 실행 할 수는 없지만,
다른 형태의 방법(디버거, 서비스, 기타)을 이용하여 실행이 가능한 파일들이다.
PE파일 구조
- DOS header부터 Section header까지를 PE Header, 그 밑의 Section들을 합쳐서 PE Body라고 한다.
- 파일에서는 offset, 메모리에서는 VA(Virtual Address)로 위치를 표현한다.
- 파일이 메모리에 로딩되면 모양이 달라진다. (Sectioon의 크기, 위치 등)
- 파일의 내용은 보통 코드, 데이터, 리소스 섹션에 나위어서 저장된다.
- Section Header에 각 Section에 대한 파일/메모리에서의 크기, 위치, 속성등이 정의 되어 있다.
- PE Header의 끝부분과 각 Section들의 끝에는 NULL padding이라고 불리우는 영역이 존재한다.
컴퓨터에서 효율을 높이기 위해 최소 기본 단위 개념을 사용하는데, PE파일에도 같은 개념이 적용된 것이다.
- 파일/메모리에서 섹션의 시작위치는 각 파일/메모리의 최소 기본 단위의 배수에 해당하는 위치여야 하고, 빈 공간은 NULL로 채워버린다.
VA & RVA
VA(Virtual Address) : 프로세스 가상 메모리의 절대 주소를 말한다.
RVA(Relative Virtual Address) : 어느 기준위치(ImageBase)에서부터의 상대 주소를 말한다.
RVA + ImageBase = VA
PE header내의 많은 정보는 RVA형태로 된 것들이 많다.
그 이유는 PE파일(주로 DLL)이 프로세스 가상 메모리의 특정 위치에 로딩되는 순간,
이미 그 위치에 다른 PE파일(DLL)이 로딩 되어 있을 수 있다.
그럴때는 재배치(Relocation)과정을 통해서 비어 있는 다른 위치에 로딩되어야 하는데,
만약 PE header정보들이 VA로 되어 있다면 정상적인 엑세스가 이루어지지 않을 것이다.
PE header Structure
DOS Header
Microsoft는 PE 파일 포맷을 만들 때 당시에 널리 사용되던 DOS파일에 대한 하위 호환성을 고려해서 만들었다.
그 결과 PE header의 제일 앞부분에는 기존 DOS EXE header를 확장시킨 IMAGE_DOS_HEADER 구조체가 존재한다.
IMAGE_DOS_HEADER 구조체의 크기는 40h(64byte)이다.
- e_magic : DOS signature(4D5A => ASCII 값 "MZ")
※ MZ : Mark Zbikowski이라는 사람의 이니셜
- e_lfanew : NT header의 offset을 표시 (가변적인 값을 가짐)
DOS Stub
DOS Header 밑에는 DOS Stub이 존재한다.
DOS Stub의 존재여부는 옵션이며 크기도 일정하지 않다. (DOS Stub은 없어도 파일 실행에는 문제가 없다.)
MS-DOS 호환모드를 가지고 있어서 DOS환경에서 윈도우 프로그램을 실행시켰을 때 "This program cannot be run in DOS mode" 문자열을 출력하고 종료한다.
이 특성을 잘 이용하면 하나의 실행(exe) 파일에 DOS와 windows에서 모두 실행 가능한 파일을 만들 수 있도 있다.
DOS Stub은 옵션이기 때문에 개발 도구에서 지원해 줘야 한다.
NT header
NT header의 구조체는 IMAGE_NT_HEADER 이다.
IMAGE_NT_HEADER 구조체는 3개의 멤버로 되어 있다.
제일 첫 멤버는 Signiture로서 50450000h("PE"00)값을 가진다.(변경 불가)
그리고 FileHeader와 OptionalHeader 구조체 멤버가 있다.
IMAGE_NT_HEADERS 구조체의 크기는 F8h이다. (매우 큼)
파일의 개략적인 속성을 나타내는 IMAGE_FILE_HEADER 구조체
#1. Machine
Machine 넙너는 CPU별로 고유한 값이며, 32bit Intel 호환 칩은 14Ch의 값을 가진다.
아래는 WinNT.H 파일에 정의된 Machine 넘버의 값들이다. (일반적인 14Ch의 값을 기억하면 된다.)
#2. NumberOfSections
PE 파일은 코드, 데이터, 리소스 등이 각각의 섹션에 나뉘어서 저장된다.
NumberOfSections는 바로 그 섹션의 갯수를 나타낸다.
이 값은 반드시 0보다 커야 한다.
정의된 섹션 갯수보다 실제 섹션이 적다면 실행 에러가 발생하며, 정의된 섹션 갯수보다 실제 섹션이 많다면 정의된 갯수만큼만 인식된다.
#3. SizeOfOptionalHeader
IMAGE_NT_HEADERS 구조체의 마지막 멤버는 IMAGE_OPTIONAL_HEADER 구조체이다.
SizeOfOptionalHeader 멤버는 바로 이 IMAGE_OPTIONAL_HEADER 구조체의 크기를 나타낸다.
※ IMAGE_OPTIONAL_HEADER는 C언어의 구조체이기 때문에 이미 그 크기가 결정되어 있다.
그런데 Windows의 PE Loader는 IMAGE_FILE_HEADER의 SizeOfOptionalHeader 값을 보고 IMAGE_OPTIONAL_HEADER 구조체의 크기를 인식한다.
#4. Characteristics
파일의 속성을 나타내는 값으로써, 실행이 가능한 형태인지(executable or not) 혹은 DLL파일인지 등의 정보들이 bit OR 형식으로 조합된다.
아래는 WinNT.H 파일에 정의된 Characteristics 값들이다. (0002h와 2000h의 값을 기억해두자.)
*.obj와 같은 object파일, resource DLL 같은 파일은 0002h가 없다. (not executable)
PE header 구조체 중에서 가장 크기가 큰 IMAGE_OPTIONAL_HEADER
(32 bit PE 파일의 경우 IMAGE_OPTIONAL_HEADER32)
(64 bit PE 파일의 경우 IMAGE_OPTIONAL_HEADER64)
#1. Magic
IMAGE_OPTIONAL_HEADER32인 경우 10Bh, IMAGE_OPTIONAL_HEADER64인 경우 20Bh값을 가지게 된다.
#2. AddressOfEntryPoint
EP의 RVA값을 가지고 있다.
#3. ImageBase
프로세스의 가상 메모리는 0 ~ FFFFFFFFh 범위입니다.(32bit의 경우)
ImageBase는 이렇게 광활한 메모리 내에서 PE파일이 로딩(매핑)되는 시작 주소를 나타냅니다.
exe, dll 파일은 user memory 영역인 0 ~ 7FFFFFFFh 범위에 위치하고,
sys파일은 kernel memory 영역인 80000000h ~ FFFFFFFFh 범위에 위치한다.
PE loader는 PE파일을 실행시키기 위해 프로세스를 생성하고 파일을 메모리에 로딩(매핑) 시킨 후 EIP 레지스터 값을 ImageBase + AddressOfEntryPoint 값으로 세팅한다.
#4. SectionAlignment, FileAlignment
PE 파일은 섹션으로 나뉘어져 있는데 파일에서 섹션의 최소단위를 나타내는 것이 FileAlignment이고, 메모리에서 섹션의 최소단위를 나타내는 것이 SectionAlignment 입니다.
(하나의 파일에서 FileAlignment와 SectionAlignment의 값은 같을 수도 있고 틀릴 수도 있다.)
따라서 파일/메모리의 섹션 크기는 반드시 각각 FileAlignment/SectionAlignment의 배수가 되어야 한다.
#5. SizeOfImage
PE 파일이 메모리에 로딩되었을 때 가상 메모리에서 PE Image가 차지하는 크기를 나타낸다.
#6. SizeOfHeader
PE header의 전체 크기를 나타냅니다.
이 값 역시 FileAlignment의 배수이어야 합니다.
파일 시작에서 SizeOfHeader 옵셋만큼 떨어진 위치에 첫번째 섹션이 위치한다.
#7. Subsystem
1 : Driver file(*.sys)
2 : GUI (Graphic User Interface) 파일 --> 윈도우 기반 어플리케이션
3 : CUI(Console User) 파일 --> 콘솔 기반 어플리케이션
#8. NumberOfRvaAndSizes
마지막 멤버인 DataDirectory 배열의 갯수
구조체 정의에 분명히 배열 갯수가 IMAGE_NUMBEROF_DIRECTORY_ENTRIES라고 명시 되어 있지만, PE loader는 NumberOfRvaAndSizes의 값을 보고 배열의 크기를 인식한다.
#9. DataDirectory
IMAGE_DATA_DIRECTORY 구조체의 배열로써, 배열의 각 항목마다 정의된 값을 가지게 된다.
각 배열 항목
여기서 말하는 Directory란 그냥 어떤 구조체의 배열이라고 생각하면 된다.
Section Header
PE파일을 여러개의 section구조로 만들었을 때 장점은 프로그램의 안정성이다.
안정성을 위해 각각의 section으로 나눠서 저장한다.
각 Section의 속성(property)을 정의한 것이 Section Header 이다.
code/data/resource 마다 각각의 성격(특징, 엑세스 권한)이 틀리다는 것을 알게 된 것이다.
- code : 실행, 읽기 권한
- data : 비실행, 읽기, 쓰기 권한
- resource : 비실행, 읽기 권한
IMAGE_SECTION_HEADER
section header는 각 section별 IMAGE_SECTION_HEADER 구조체의 배열로 되어있다.
- VirtualSize : 메모리에서 섹션이 차지하는 크기
- VirtualAddress : 메모리에서 섹션의 시작 주소 (RVA)
- SizeOfRawData : 파일에서 섹션이 차지하는 크기
- PointerToRawData : 파일에서 섹션의 시작 위치
- Characteristics : 섹션의 특징(bit OR)
VirtualAddress와 PointerToRawData의 값은 아무 값이나 가질 수 없고, 각각(IMAGE_OPTIONAL_HEADER에 정의된) SectionAlignment와 FileAlignment에 맞게 결정된다.
VirtualSize와 SizeOfRawData는 일반적으로 서로 틀린값을 가진다.
즉, 파일에서의 섹션 크기와 메모리에 로딩된 섹션의 크기는 틀리다는 얘기가 된다.
Characteristics는 아래 값들의 조합(bit OR)으로 이루어진다.
Name 멤버는 C언어의 문자열처럼 NULL로 끝나지 않습니다.
또한 ASCII 값만 와야한다는 제한도 없습니다.
PE 스펙에는 섹션 Name에 대한 어떠한 명시적인 규칙이 없기 때문에 어떠한 값을 넣어도 되고 심지어 NULL로 채워도 된다.
또한 개발 도구에 따라서 섹션 이름/갯수 등이 달라진다.
따라서 섹션의 Name은 그냥 참고용 일 뿐 어떤 정보로써 활용하기에는 100% 장담할 수 없다.
RVA to RAW
PE파일이 메모리에 로딩되었을 때 각 섹션에서 메모리의 주소(RVA)와 파일 옵셋을 잘 매핑할 수 있어야 한다.
이러한 매핑을 일반적으로 "RVA to RAW"라고 부른다.
방법은
1) RVA가 속해 있는 섹션을 찾는다.
2) 간단한 비례식을 사용해서 파일 옵셋(RAW)을 계산한다.
IMAGE_SECTION_HEADER 구조체에 의하면 비례식은 이렇다.
RAW - PointerToRawData = RVA - VirtualAddress
RVA = RVA - VirtualAddress + PointerToRawData
'Study > Reversing' 카테고리의 다른 글
[리버싱] IAT&EAT (0) | 2017.09.17 |
---|---|
[리버싱] 정적&동적 라이브러리 (0) | 2017.09.16 |
[리버싱] CALL, JMP, RET(RETN) 명령어에 대해 알아보자 (0) | 2017.09.03 |
[리버싱] 인터럽트에 대해 알아보자 (0) | 2017.09.03 |
[리버싱] 함수: 프롤로그&에필로그와 호출 규약 (0) | 2017.09.01 |
댓글