PE, Portable Executable
Windows 운영체제에서 실행파일(exe)은 PE, Portable Executable 파일이라고 한다. 이런 PE 파일에는 PE 헤더에 이 프로그램을 실행하는 데 필요한 기본 정보와 파일의 데이터를 메모리 어디에 배치시킬 지 알려주는 파일 구성 정보가 담겨있다. PE 바디에는 프로그램이 사용할 코드, 데이터 등이 포함되어 있다.
이런 PE 형식 실행파일(exe, dll 등)을 실행시키면 운영체제의 로더(Loader)에서 이 PE 헤더의 정보를 분석, PE 바디의 코드와 데이터를 메모리의 코드 영역, 데이터 영역, 스택 영역, 힙 영역에 특성에 맞게 배치한다. 당연하지만 PE 파일은 PE 포맷에 맞게 만들어야 하며 이는 윈도우 기반 프로그램들이 지켜야 할 일종의 규칙이다. 윈도우에서 프로그램을 개발, 컴파일하면 이 포맷으로 파일이 만들어진다.
PE 파일은 크게 PE 헤더, 섹션 헤더, 섹션 데이터 세 가지로 나뉘어진다. 모든 PE 파일들은 IMAGE_DOS_HEADER, MS-DOS Stub Program, IMAGE_NT_HEADERS 헤더를 가지고 있다. 그 외의 섹션 헤더, 데이터 영역들은 PE 파일마다 가지는 내용과 형태가 다르지만 최소 1개 이상의 섹션 헤더, 데이터를 가지고 있다.
IMAGE_DOS_HEADER
DOS에서 PE 파일을 실행했을 때 하위 호환성을 위해 에러 메시지를 출력하고 실제 윈도우용 PE 헤더 위치를 표시하는 역할이다.
MS-DOS Stub Program
DOS에서 PE 파일을 실행했을 때 보일 오류 메시지가 저장되어 있다. 위의 IMAGE_DOS_HEADER와 더불어 오류 처리를 위한 작은 DOS 프로그램이 내장되어 있는 것이다.
IMAGE_NT_HEADERS
이 헤더에는 4바이트짜리 시그니처(PE)와 2개의 IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER 구조체가 포함되어 있다.
IMAGE_FILE_HEADER 구조체에는 다음과 같은 필드들이 존재한다.
-
Machine: CPU 고유값
-
Number of Sections: PE 파일 섹션의 갯수. 코드, 데이터, 리소스 등이 담기는 SECTION이 code, data, const, .rsrc, .idata 총 5개가 있으므로 '5' 값이 해당된다.
-
Time Date Stamp: 파일이 생성된 날짜(1970/01/01 기준)
-
Pointer to Symbol Table: COFF 심볼 테이블에 대한 오프셋. 테이블이 존재하지 않는다면 0 값을 가진다.
-
Number of Symbols: 심볼 테이블의 엔트리 갯수. 관련 기능이 deprecated 되었기 때문에 0 값을 가져야 한다.
-
Size of Optional Header: IMAGE_OPTIONAL_HEADER의 크기. 실행 파일에 필요하며 Object 파일에서는 0 값을 가져야 한다.
-
Characteristics: 해당 파일의 특징을 나타낸 플래그. 각각의 특징은 링크과 같다. 주요 특징은 다음과 같다.
-
0x0002: IMAGE_FILE_EXECUTABLE_IMAGE. 해당 파일이 실행 가능한 파일임을 뜻한다.
-
0x0100: IMAGE_FILE_32BIT_MACHINE. 해당 파일이 32비트 환경 기반임을 뜻한다.
-
0x0200: IMAGE_FILE_DLL: 해당 파일이 dll 파일임을 뜻한다.
-
IMAGE_OPTIONAL_HEADER 구조체에는 다음과 같은 필드들이 존재한다.
수많은 필드가 존재하는 거대한 구조체이므로 몇가지 중요한 필드만 추려내보자면 다음과 같다.
-
Magic: 0x010B. CPU 버전(32비트, 64비트)에 따라 0x010B나 0x020B 값을 가진다. 운영체제는 64비트지만 프로그램이 32비트 기반이기 때문이 0x010B 값을 가지는듯.
-
Address of Entry Point: PE 파일을 실행시킬때는 엔트리 포인트라는 곳에서 처음 시작되는데 이곳을 찾기 위해 이 필드를 Image Base와 합하여 참조하게 된다.
-
Image Base: 가상 메모리상에서 PE 파일이 로드(매핑)되는 시작주소를 나타낸다. 대부분의 개발 도구들이 빌드한 실행 파일은 0x00400000 값을 가진다.
- PE Loader가 Address of Entry Point + Image Base 값을 EIP 레지스터 값으로 세팅한다고 한다.
-
Section Alignment / File Alignment: PE 파일은 섹션으로 나뉘어져 있으며 파일, 메모리에서 사용하는 섹션의 최소 단위를 지정해준다.
-
Size of Image: PE 파일이 메모리에 로딩되었을 때 가상 메모리에서 PE Image, 즉 해당 실행파일이 차지하는 크기를 의미한다. Section Alignment의 배수 크기가 되어야 한다.
-
Size of Headers: MS-DOS Stub, PE 헤더, 섹션 헤더들의 크기의 합을 File Alignment 배수 크기로 올림(round-up)한 크기.
-
Subsystem: 해당 실행파일(Image)을 실행하기 위해 필요한 서브시스템. 드라이버 파일, GUI 파일, CUI 파일로 구분된다.
-
Number of Data Directories: Data Directory, 이 IMAGE_OPTINAL_HEADER 뒤에 따라오는 구조체들의 갯수를 의미한다. 16진수이므로 16개가 있음을 의미한다.
나머지 필드에 대한 정보는 이 링크에서 확인할 수 있다.
이 뒤에는 8바이트짜리 구조체 배열이 따라오는데 각 구조체는 데이터가 저장되는 테이블의 시작 주소, 크기 등이 저장되어 있다. 16개의 IMAGE_DATA_DIRECTORY 구조체가 포함되어 있으며 맨 마지막 구조체는 아무런 값을 가지지 않음으로써 구조체 목록의 끝을 표시한다.
이 중 자주 언급되는 6개의 구조체를 추려해보면 다음과 같다.
-
EXPORT Table: PE 파일에서 외부에 공개하는, 즉 다른 PE 파일에서 사용할 수 있는 함수 정보를 저장한다. IMAGE_EXPORT_DIRECTORY 구조체 형태의 데이터를 저장하고 있다.
-
IMPORT Table: EXPORT Table과는 반대로 PE 파일이 다른 라이브러리로부터 사용하는 공개된 함수 정보를 저장하고 있다. IMAGE_IMPORT_DIRECTORY 구조체 형태의 데이터를 저장하고 있다.
-
IMPORT Address Table: 프로그램이 Import하는 함수들의 주소를 저장하는 테이블이다. PE 파일에서 이 테이블은 현재 IMAGE_OPTIONAL_HEADER 내부에 있는 이 IMAGE_DATA_DIRECTORY 구조체와 IMPORT Table에서 가리키고 있는 IMAGE_IMPORT_DIRECTORY 구조체로 두 번 발견할 수 있는데 전자의 경우 후자에 대한 주소와 크기를 저장하고 있다.
-
특이한 것은 전자의 경우 값이 0으로 채워져 있는데 이는 프로그램에서 여러 DLL을 임포트한다는 것을 의미한다. 오직 하나의 DLL을 임포트한다면 위에서 말한것처럼 IMPORT Table에서 가리키는 IMAGE_IMPORT_DIRECTORY 구조체 데이터의 Import Address Table 구조체의 시작 주소와 크기가 저장된다.
-
-
RESOURCE Table: PE 파일에서 사용하는 아이콘, 이미지, 레이아웃 등 리소스가 저장된 영역으로 IMAGE_RESOURCE_DIRECTORY 구조체 형태의 데이터를 저장하고 있다.
-
BASE RELOCATION Table: PE 헤더의 Image Base 값을 사용하지 못할 때 PE 파일 내부의 절대 주소를 변경하는 데 사용된다. 운영체제는 여러 프로그램을 동시에 메모리에 올리기 때문에 한 프로그램의 PE 헤더에 기록된 Image Base가 다른 프로그램에 의해 선점되어 사용하지 못하는 경우가 발생한다. 프로그램 내부에서는 Image Base + RVA로 절대 주소를 사용하는 값들이 존재하며 이 경우 이 테이블을 이용하여 절대 주소들을 새로운 Image Base 기반으로 수정한다.
-
TLS Table: 엔트리 포인트 이전에 실행되는 함수를 지정한다. 디버거에서 F9로 Entry Point에 진입했을 때 그 시점 이전에 미리 호출되는 함수로 디버거 탐지 루틴등이 포함된다.
PE 파일에서 주소를 다룰 때는 크게 다음과 같이 3가지 방식이 있다.
-
RAW: PE 파일 내부에서의 오프셋. 물리적으로 하드디스크에 저장됐을때나 사용.
-
RVA: Relative Virtual Address. PE 파일이 메모리로 로드됐을 때 저장되는 상대 주소
-
VA: Virtual Address. 가상 메모리상에서 저장되는 실제 주소
위의 RVA, VA 주소는 실제로 PE 파일을 실행, 즉 메모리에 불러들일 때 사용하게 된다. RVA 주소는 상대 주소라고 하였는데 이는 PE 파일이 메모리에 저장되는 시작 주소인 Image Base로부터의 거리, 오프셋(Offset)을 나타낸다. 위의 Address of Entry Point는 이런 RVA로 PE 파일이 메모리에 로드되면 해당 필드의 값과 Image Base의 값이 합산되어 실제 Entry Point의 주소를 계산하게 된다. 이렇게 합산된 주소가 가상 메모리상에서 실제로 저장되는 VA(Virtual Address)가 된다.
IMAGE_SECTION_HEADER, SECTION
IMAGE_SECTION_HEADER에는 각 섹션에 대한 헤더가, SECTION 자체에는 섹션의 데이터들이 담겨있다. 섹션 헤더에는 PE 로더가 섹션 데이터를 메모리로 로딩하고 속성을 설정하는 데 필요한 정보가 담겨있으며 IMAGE_SECTION_HEADER 구조체로 구성되어 있다. PE 헤더에서 데이터에 접근하기 위한 정보들이 들어있다면 섹션에는 실제로 프로그램을 실행하기 위한 데이터가 들어있다. 섹션 데이터는 구조체를 사용하지 않고 바이너리가 자유롭게 분포되어 있는데 특정 영역을 지정하여 구조화된 데이터가 저장될 수도 있다.
대표적인 예로 CODE 섹션의 경우 데이터가 일정한 형식을 가지지 않고 섹션 헤더에서 지정한 위치(RVA 1000)부터 데이터가 저장되어 있다.
그러나 idata 섹션의 데이터를 보면 IMPORT Directory Table, IMPORT Name Table 처럼 구조화된 데이터가 저장된 것을 확인할 수 있다. 이 영역에 저장되는 데이터는 위에서 본 IMAGE_OPTIONAL_HEADER의 IMPORT Table에서 지정하고 있으며 이는 즉 PE 헤더에 따라 섹션 데이터가 달라질 수 있다는 것을 의미한다.
섹션 헤더는 여러 속성을 가지고있는데 중요한 속성을 몇가지 골라보자면 다음과 같다.
-
Virtual Size: 메모리에 파일이 로딩되었을 때 해당 섹션이 차지하는 총 크기. Size of Raw Data보다 클 경우 0으로 패딩된다. 실행파일이 아닌 경우 0으로 채워진다.
-
Virtual Address: 실행파일에서 섹션이 메모리에 로딩되었을 때 Image Base를 기준으로 해당 섹션의 첫번째 바이트의 위치. 위의 PEView에서는 RVA 필드에 해당한다(위치 상).
-
Size of Raw Data: Object 파일의 경우 섹션의 크기, 실행 파일의 경우 디스크에 저장된 초기화된 데이터(initialized data)의 크기를 의미한다. 이는 IMAGE_OPTIONAL_HEADER의 File Alignment의 배수 크기가 되어야 한다. Virtual Size보다 작을 경우 섹션의 나머지 영역은 0으로 채워진다.
-
Pointer to Raw Data: COFF 파일 내에서 섹션의 첫번째 페이지(page)를 가리키는 파일 포인터. 실행 파일의 경우 역시 File Alignment의 배수 크기가 되어야 한다. 섹션이 초기화되지 않은 데이터만 갖고 있다면 0 값을 가진다.
-
Characteristics: 해당 섹션의 특성.
대개 Virtual Size와 Size of Raw Data는 다른 값을 가지는데 이는 실제 섹션의 크기와 메모리에 로딩되는 섹션의 크기가 다르다는 것을 의미한다. 또한 PE 파일에는 여러개의 섹션에 PE 파일의 코드, 데이터, 리소스 등이 분리되어 담겨 있는데 각 섹션의 특징에 따라 부여된 읽기, 쓰기, 실행(즉 RWX) 속성이 다르다. 예를 들어 코드 섹션에는 프로그램의 코드, 즉 메모리로 불러들여 처리해야 하는 데이터가 담겨있기 때문에 이 섹션의 데이터에 대해서는 읽고 실행할 수 있어야 하므로 Characteristics에 다음처럼 특성이 지정된다.
이를 이용하여 PE 파일이 메모리에 로딩되었을 때 다음과 같은 공식으로 각 섹션에서 메모리의 주소(RVA)와 파일에서의 오프셋(RAW)을 비교할 수 있다.
RAW - Pointer to Raw Data = RVA - Virtual Address
RAW = RVA - Virtual Address + Pointer to Raw Data
여기서 Virtual Address는 VA = Image Base + RVA 에서 사용하던 VA가 아닌 섹션 헤더의 멤버(Virtual Address)에 해당한다. 이는 위의 PEView에서 RVA로 표시된 필드이며 찾고자 하는 값이 위 공식의 RVA에 해당할 것이다. 다음과 같은 예제(notepad.exe)를 보자.
먼저 찾고자 하는 주소, 즉 섹션에서 메모리의 주소를 0x00005000이라 가정할 때 RVA와 Image Base (0x01000000이라 가정)를 더하면 메모리에서 0x01005000에 위치하게 된다. 해당 도표에서 볼 때 이는 Section(".text")에 있으므로 해당 섹션에서 Virtual Address 필드를 찾아본 결과 0x00001000이며 Pointer to Raw Data 필드가 0x00000400이라 가정할 때 파일에서의 오프셋(RAW)은 다음과 같이 계산될 수 있다.
RAW = 0x00005000 - 0x00001000 + 0x00000400 = 0x00004400
나머지 섹션 헤더들도 동일한 포맷이며 헤더들 뒤에는 각 섹션들의 바디가 따라온다. 이렇게 섹션 헤더에 섹션 데이터를 사용하기 위한 기본 정보가 들어가 있기 때문에 물리적으로 어디 저장되어 있는지, 메모리 어리에 로드되는지, 특징이 무엇인지 파악할 수 있게 된다.
[출처| https://reversecore.com/]
[출처| https://docs.microsoft.com/en-us/windows/win32/debug/pe-format]
'리버싱 > 기초 지식' 카테고리의 다른 글
Study04 - 기초 지식(패킹, 언패킹) (0) | 2020.11.23 |
---|---|
Study03 - 기초 지식(IAT) (0) | 2020.11.15 |
Study 02 - 기초 지식(Register, Call Stack, Prologue/Epilogue) (0) | 2020.08.29 |