PE 파일 구조
PE 파일 형식(Portable Executable File Format)은 1993년 MS에서 표준안을 만든 것으로, 메모리에서도 디스크에 저장된 파일 형태로 바로 실행될 수 있도록 설계되었다. PE 파일 형식은 Win32의 기본 파일 형식으로 윈도우에서 실행되는 프로그램은 모두가 이에 해당한다. EXE 파일이 PE 파일의 대표 예이며 동적 링크 라이브러리(DLL, Dynamic Linking Library) 파일도 PE 파일 형식을 가지고 있다. 따라서 PE 파일의 구조는 윈도우에서 동작하는 프로그램을
이해하는 데 매우 중요하고 리버스 엔지니어링할 때도 많은 도움이 된다. PE 파일의 기본 구조를 살펴보면 다음과 같다.
PE 파일의 구조를 구체적으로 살펴보기 위해 간단하게 "Hello World!"를 출력하는 REVERSE_1.EXE 실행 파일을 이용한다. 그리고 파일 구조를 효과적으로 살펴보기 위해 STUD_PE 툴을 사용한다. 먼저 STUD_PE 툴로 REVERSE_1.EXE를 열어보도록 하겠다.
PE 파일은 DOS 헤더로 시작한다. DOS 헤더는 DOS와 호환하기 위해 사용되며 항상 크기가 64바이트이다. STUD_PE에서 DOS 헤더를 확인해 보겠다. STUD_PE에서는 구조를 쉽게 확인할 수 있는데 앞의 그림에서 <Basic HEADERS tree view in hexeditor> 버튼을 누르면 창 2개가 뜬다. 왼쪽 창에서 선택한 항목이 오른쪽 창에서 붉은색 박스 안에 표시된다. 예를 들어, DOS 헤더를 선택하면 앞 4줄이 붉은색 박스 안에 표시된다. HEX 에디터에서 1줄이 16바이트이므로 4줄인 DOS 헤더는 64바이트이다.
[그림 4] HEX 에디터에서 열어본 REVERSE_1.EXE 파일
DOS 헤더 항목을 확장한 'Magic Number' 부분은 아래 그림과 같이 HEX 에디터의 오른쪽 블록에서 'MZ' 문자열을 확인할 수 있다. MZ는 DOS 개발자 중 한 명인 마크 지콥스키(Mark Zbikowski)의 이니셜로, DOS 헤더의 시그니처로 사용된다. 운영체제가 MZ 문자열을 통해 해당 파일을 PE 파일로 처음 인지하는 것이다. DOS 헤더에서 중요한 부분은 DOS 헤더의 마지막 'File address of new exe header'이다. e_lfanew 값이라고도 하는데 여기에는 PE 헤더의 주소값이 적혀 있다.
[그림 7] 'File address of new exe header' 값 확인
위의 그림에서 REVERSE_1.EXE의 'File address of new exe header' 값은 'D8 00 00 00'인데
윈도우는 리틀 엔디언 시스템이므로 상위 주소부터 읽어야 한다. 따라서 실제 주소 값은 000000D8이 된다.
코드앞서 보인 그림에서 'File address of new exe header' 값이 기록된 부분(00000040)과 000000D8 주소값 사이에 표시한 부분은 DOS Stub 코드가 된다. DOS Stub 코드에는 'This program cannot be run in DOS mode'와 같이 적절하지 못한 상황에서 PE 파일이 실행될 때 경고문 등에 대한 데이터를 담고 있다. 크기는 파일에 따라 가변적이며 파일 분석 시 중요한 부분은 아니다.
PE 헤더는 PE 파일의 구성 정보를 가지고 있다. PE 파일에서는 데이터 특성별 섹션으로 구분하여 저장한다. 따라서 PE 파일 헤더에는 해당 PE 파일이 가지는 섹션 개수나 파일 속성, 섹션 헤더의 크기를 비롯해 이미지 베이스(Image Base), 엔트리 포인트(Entry Point) 주소, 메모리상의 각 섹션이 차지하는 크기, 디스크 상에서의 섹션 정렬, PE 파일의 총 크기, 디스크 상에서의 섹션 정렬, PE 파일의 총 크기, 디스크 상에서의 헤더의 총 크기 등과 같은 정보를 모두 담고 있다. 앞서 DOS 헤더의 'e_lfanew'에서 확인한 바와 같이 000000D8 주소에서 시작한다.
PE 헤더도 DOS 정보와 유사하게 4바이트의 시그니처로 시작하는데 값은 항상 'PE\0\0'이다. 이외에 PE 헤더에서 살펴볼 만한 필드는 다음과 같다.
3.1.1 Machine(0x014C, 2바이트) CPU ID이다. IA32(Intel Architecture 32)의 경우 0x014C가 되고 IA64일 경우 0x0200이다. 여기서는 0x014C로 IA32 시스템에서 사용되는 파일임을 알 수 있다.
3.1.2 NumberOfSections(0x0005, 2바이트) 파일에 존재하는 섹션의 개수이다. 섹션을 추가/삭제하려면 이 값을 조작한다. 이 파일에서는 섹션의 수가 0x0005이다.
3.1.3 TimeDateStamp(0x4CAD899A, 4바이트) 파일이 생성된 시간과 날짜이다.
3.1.4 PointerToSymbolTable(0x00000000, 4바이트), NumberOfSymbol(0x00000000, 4바이트) 디버깅을 위해 사용한다.
3.1.5 SizeOfOptionalHeader(0x000E, 2바이트) CPE 파일 헤더 뒤에 오는 옵션 헤더(optional header)의 크기를 16으로 나눈 값이 입력된다. 기본적으로 크기가 224 바이트지만 데이터 디렉터리가 생략될 경우 96바이트가 된다. 이 파일은 SizeOfOptionalHeader 값이 0x000E(14(0xE)*16)로 224바이트의 옵션 헤더를 가지고 있다.
3.1.6 Characteristics(0x010E, 2바이트) 파일의 속성을 나타낸다. 일반 실행 파일의 경우 값이 0x010F이다. 이 속성을 통해 파일이 exe인지 dll인지 구분한다.
섹션 헤더는 Optional 헤더라고도 불리며 PE 파일에서 프로그램과 관련한 필수 정보를 담고 있는 가장 중요한 부분이다.
Optional 헤더는 필드 30개와 데이터 디렉터리 1개를 가지고 있다. 중요한 몇 가지 필드만 살펴보도록 하겠다.
3.2.1 Magic(0x010B, 2바이트) Optional 헤더의 시작 위치에 존재하는 필드로 Optional 헤더를 구분하는 시그니처로 사용된다. 값은 0x010B로 고정되어 있다.
3.2.2 AddressOfEntryPoint(0x00001120, 4바이트) 엔트리 포인트는 PE 파일이 메모리에 로드된 후 처음으로 실행되는 코드의 주소를 가지고 있다. 이 부분에 지정된 주소 값은 가상 주소의 절대 값이 아닌 이미지 베이스부터의 오프셋 값인 상대 가상 주소(RVA, Relative Virtual Address)이다. 일반적으로 엔트리 포인트는 .text 섹션(실행 코드를 담고 있는 메모리 영역)의 시작점인 경우가 대부분이기 때문에 이 값은 뒤에 언급할 .text 섹션 헤더의 가상 주소 값과 일치하는 경우가 많다.
3.2.3 Image Base(0x00400000, 4바이트) PE 파일이 로더에 의해서 메모리에 로드되는 위치이다. 로더는 PE 파일을 로드할 때 이미지 베이스 값을 참조하여 가급적이면 이미지 베이스부터 로드하려고 시도한다. EXE 파일의 경우 가상 메모리 공간에 가장 처음 로드되므로 항상 이미지 베이스에 로드된다. 하지만 DLL의 경우 이미지 베이스로 지정된 주소 공간이 다른 모듈에 의해서 이미 사용 중인 상황이 발생할 수 있다. 이러한 경우 로더는 해당 DLL을 다른 곳에 로드하고 재배치 작업을 수행한다. 대부분 링커는 이 값을 0x00400000(EXE의 경우), 0x10000000(DLL의 경우)로 설정한다.
3.2.4 Section Alignment(0x00001000, 4바이트) 각 섹션이 메모리에서 차지하는 최소 단위이다. 각 섹션의 시작 주소는 언제나 여기에서 지정된 값의 배수가 되어야 한다. 예를 들어 Section Alignment 값이 4096(1000h) 바이트라면 각 섹션은 반드시 4096의 배수가 되는 곳에서 시작한다. 만약 첫번째 섹션이 401000h에 위치하고 크기가 10바이트라면 다음 섹션은 402000h부터 시작해야 한다. 그러나 첫번째 섹션의 크기가 5012바이트라면 다음 섹션은 403000h부터 시작한다.
3.2.5 File Alignment(0x00001000, 4바이트) Section Alignment가 메모리상에서 섹션 정렬과 관련 있다면 File Alignment는 디스크의 섹션 정렬과 관련있는 필드이다. 개념은 Section Alignment와 동일하다. 512~65535 중에서 2의 n승 값 중에서 사용하도록 되어 있다. 일부 예외적인 상황을 제외하고 만약 이 값이 Section Alignment와 같으면 디스크의 PE 파일 모습이나 메모리의 PE 파일 모습은 같다.
3.2.6 Size Of Image(0x00020070, 4바이트) 메모리에 로드된 PE 파일의 총 크기로 Section Alignment의 배수가 되어야 한다.
3.2.7 Size Of Header(0x00001000, 4바이트) 디스크에서 헤더의 총 크기로 File Alignment의 배수가 되어야 한다.
3.3 섹션 헤더 - Data Directory다른 DLL에서 함수를 가져오거나 본 파일의 함수를 내보낼 때 해당 정보는 Data Directory에 기록한다.
3.3.1 Export Table(16바이트) Export 함수들에 대한 Export Table의 시작 위치와 크기이다.
3.3.2 Import Table(16바이트) Import 함수들에 대한 Import Table의 시작 위치와 크기이다.
PE 파일이 실행될 때 외부에서 가져와 사용하는 함수(DLL) 목록이 들어있다. DLL 함수를 Export하고 EXE가 해당 DLL로부터 함수를 Import한다. Export된 함수를 가져온다는 것은 결국 해당 DLL과 사용하는 함수에 대한 정보를 어딘가에 저장함을 의미하고 이 정보를 저장하고 있는 곳이 Import Table이다. 일반적으로 PE 파일의 섹션 테이블에는 .idata라는 이름으로 지정된다.
섹션은 성질이 동일한 데이터가 저장되어 있는 영역으로 윈도우에서 사용하는 메모리 보호 메커니즘과 연관이 있다. 윈도우의 경우 메모리 보호를 위한 최소 단위가 페이지이므로, 페이지 단위로 여러 속성을 설정한다. 그리고 속성에 위배되는 행동 시에는 접근 오류를 발생하여 메모리를 보호한다.
하지만 페이지 단위로 메모리의 속성을 정한다는 것은 성질이 다른 데이터들은 하나의 페이지에 담을 수 없다는 것을 의미한다. 따라서 프로그램에 포함된 데이터 중 읽기와 실행이 가능해야 하는 데이터인 실행 코드, 읽고 쓰기가 가능한 데이터, 읽기만 가능한 데이터는 별도의 페이지에 두어야 한다. 하지만 로더 입장에서는 데이터의 속성을 구분할 방법이 없으므로 섹션이라는 개념을 두어 실행 파일 생성 단계에서 이를 구분해 놓는다.
PE 파일에 존재하는 섹션에는 다음과 같은 것이 있다.
실행되는 코드를 담고 있는 섹션이다.
초기화된 전역 변수를 담고 있으며 읽고 쓰기가 가능한 섹션이다.
읽기 전용의 데이터 섹션, 문자열 표현이나 C++/com 가상 함수 테이블을 담고 있다.
초기화되지 않은 전역 변수를 위한 섹션이다.
다른 DLL에서 가져다 쓰는 함수들의 정보를 담고 있는 섹션이다. IMAGE_IMPORT_DESCRIPTOR의 배열로 이루어져 있으며 하나당 DLL 하나의 정보를 담고 있다.
다른 모듈이 이 PE 파일에 정의되어 있는 함수를 사용할 수 있도록 함수 목록을 담고 있는 섹션이다.
섹션 테이블은 파일에 존재하는 이런 섹션을 메모리에 로드하기 위한 정보의 배열이다.
섹션 테이블 중 .text 섹션을 기준으로 살펴보도록 하겠다.
4.1.1 Name(.text, 8바이트) 섹션 이름을 나타낸다.
4.1.2 Virtual Size(0x0001E100, 8바이트) 섹션 크기를 나타낸다.
4.1.3 Virtual Offset(0x00001000, 8바이트) 섹션이 로드될 가상 주소, PE 형식 내에서 모든 가상 주소 값과 마찬가지로 이 필드도 상대 가상 주소(RVA)이다.
4.1.4 Size of Raw Data(0x000100F0, 8바이트) File Alignment의 다음 배수 값까지 반올림한 섹션 데이터의 크기이다.
4.1.5 Pointer To Raw Data(0x0000100, 8바이트) 섹션의 시작을 담고 있는 파일 오프셋이다. PE 로더는 파일 안의 섹션에서 데이터가 있는 위치를 알기 위해 이 영역의 값을 이용한다.
4.1.6 Characteristics(0x60000020, 8바이트) 섹션의 실행 코드이다. 초기화된 데이터, 비초기화된 데이터와 같은 Flag(플래그)를 가지고 있다. 여기에서 메모리에 대한 실행(eXecute), 읽기(Read), 쓰기(Write) 속성을 지정한다. 요약하면 로더는 PointerToRawData가 지정한 곳부터 데이터를 SizeOfRawData만큼 읽어들인다. 그리고 Virtual Offset에 맵핑한 후 Characteristics에 설정된 속성 정보를 이용해 페이지별로 메모리 보호를 적용한다.
다음과 같이 섹션 테이블의 내용을 Stud_PE의 [Section] 탭에서 각 섹션별로 확인할 수 있다.
섹션은 앞서 설명한 바와 같이 동일한 성질의 데이터가 저장되어 있는 영역이다. 각 섹션은 다음과 같이 해당 섹션을 선택한 뒤, 마우스 오른쪽 버튼의 팝업 메뉴에서 [Go To Section Start]로 해당 값을 조회해볼 수 있다.
'IT 정보' 카테고리의 다른 글
소프트웨어 테스터를 위한 구글 벤치마킹_문제풀이 (0) | 2014.07.23 |
---|---|
MS Internet Explorer 원격코드 실행 신규 취약점 (0) | 2014.07.23 |
Fiddler_TextWizard (0) | 2014.04.24 |
Fiddler_피들러로 할 수 있는 일 & 없는 일 (0) | 2014.04.11 |
스미싱 및 파밍 대응책 (0) | 2014.02.27 |