pe 날개달기 ( flying pe ) · web view현재 pe 파일포맷의 세부스펙을 명시해 놓은...
TRANSCRIPT
PE 날개달기 ( Flying PE )
http://kernelx.pe.kr
2006 년 12 월
[목차]
1. 여는 글....................................................................................2
2. PE 파일포맷 기본지식.............................................................2
2.1 PE 요약....................................................................2
2.2 PE 구조....................................................................3
3. VC++ PE 구조체 살펴보기....................................................8
3.1 IMPORT 해부..........................................................8
3.2 EXPORT 해부..........................................................11
4. 파서코드 작성 (PEKits.cpp)...................................................14
4.1 print_pe_import() - IMPORT 정보 출력...............14
4.2 fix_entrypoint() - EntryPoint 패칭.......................17
4.3 fix_perm_sections() - Section 퍼미션 패칭..........19
5. 맺음말
5.1 참고자료 ( References ).........................................21
5.2 마치며 ( Conclusion ).............................................22
1. 여는 글
역어셈블리를 공부하고 활용하는데 PE 파일포맷을 분명하게 이해하지 못하거나 또는
활용하지 못한다는 것은 한걸음 앞으로 나아가는데 있어서 커다란 걸림돌이 됨을 의미합니다.
현재 PE 파일포맷의 세부스펙을 명시해 놓은 MS 사의 영문 명세서(Specification) 가 나와
있으며, 관심을 가진 여러 리버스엔지니어, 개발자, 해커들에 의해서 그 파일포맷의 구성이
설명되어지고, 또 정리되어져 있습니다. 그러나 아직까지 그 구조를 쉽고 명쾌하게 설명함과
동시에 PE 구조체를 이용해 코딩하는 방법에 대해서도 함께 다룬 한글자료는 보질 못했습니다.
본 문서는 그러한 점을 염두하고 한글문서로 PE 를 활용할 수 있는 방법을 쉽게 표현하고자
작성되었습니다. 문서내에서는 간결한 설명을 위해 경어체를 사용하지 않습니다. 양해
바랍니다.
학습을 위해 필요한 도구 :
- MS Windows XP SP2
- PEView.exe ( 공개 PE 뷰어 )
- Visual C++ 6.0 컴파일러
- putty.exe ( SSH 클라이언트 )
2. PE 파일포맷 기본지식
2.1 PE(Portable Executable) 요약
[1] 윈도우즈 NT, XP, 2000 등에서 사용하는 실행 가능한 코드를 포함할 수
있는 파일의 포맷이다.
[2] PE 포맷을 사용하는 파일은 .EXE .DLL .OCX 가 있다.
[3] 윈도우즈 98 에서는 NE 라는 PE 와는 다른 파일포맷이 사용되었다.
[4] 64 비트 윈도우즈에서는 PE+ 라는 파일포맷을 사용한다.
( 본 문서는 32 비트 윈도우즈의 PE 포맷을 다룬다 )
[5] PE 파일은 헤더정보들과 실제 데이터(코드, 데이터, IMPORT 테이블들,
EXPORT 테이블들, 리소스 데이터 등이 포함된다)로 구성된다.
[6] RVA(Relative Virtual Address) 라고 해서 베이스주소에서 오프셋
을 더한 값으로 주소를 표현하기도 한다. 예를 들어 한 PE 파일의
ImageBase 가 0x00400000 일 때, .text 섹션(코드)의 RVA 값이
0x1000 이라면 0x00401000 이 실제 주소가 되는 것이다. 이처럼
오프셋과 같은 개념으로 쓰이는 것이 RVA 주소 값이다.
<PEView.exe 로 본 putty.exe 구조>
2.2 PE 구조
PE 는 위 그림에서 보는 것과 같은 기본적인 골격을 가진다. 이번 장에서는 PE 코딩을 위해서
숙지하고 있어야 할 사항들을 설명한다.
--
+IMAGE_DOS_HEADER
+MS-DOS Stub Program
( 도스모드에서 윈도우프로그램 수행시, 오류메시지 표시하는 코드 )
( “This program cannot to be run in DOS mode" )
+IMAGE_NT_HEADERS
- Signature
- IMAGE_FILE_HEADER
- IMAGE_OPTIONAL_HEADER
+IMAGE_SECTION_HEADER[4]
+.text 섹션 ( 코드포함 )
+.rdata 섹션 ( IMPORT, EXPORT 테이블들 포함 )
+.data 섹션 ( 전역, 정적 변수 포함 )
+.rsrc 섹션 ( 리소스 데이터 포함 - 아이콘, 문자열, 다이얼로그정보 등 )
--
(1) NTHeader 위치를 구하는 방법
NTHeader 가 시작하는 지점을 파일의 시작지점에서 알아보려면, 먼저 DOS 시절과 호환성을
유지하기위해서 남겨진 PE 의 맨 첫 부분인 DOS 헤더의 한 필드를 살펴보면 된다.
DWORD file_base = (메모리상에 PE 파일을 맵핑한 후 그 베이스주소를 담는다);
PIMAGE_NT_HEADERS pinh;
PIMAGE_DOS_HEADER pidh;
// e_lfanew 필드는 NT 헤더의 시작점을 지정하고 있다.
pinh = file_base + pidh->e_lfanew; // 파일베이스 + pidh->e_lfnew = NT
헤더
(2) NT 헤더의 3 가지 구성
NT 헤더(IMAGE_NT_HEADERS 구조체)는 3 가지 필드로 구성되어 있다.
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
..
typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
- Signature : NT 의 시그너처 4 바이트를 담는 필드이다. ( "PE\0\0" )
- FileHeader : 파일헤더 구조체 ( ImageBase, 코드영역주소, 데이터영역주소 등 포함 )
- OptionalHeader : 옵셔날 헤더
( OptionalHeader 는 맨 아래에 IMAGE_DATA_DIRECTORY 구조체 배열을 내포하고 있다. 이
구조체 필드배열은 각각 각 섹션들의 RVA 와 크기를 담고 있다. )
(3) PE 파일의 섹션 개수 알아보기
PE 파일은 여러 가지 섹션들을 포함한다. 코드를 포함하는 .text 섹션, 초기화되지 않은
데이터와 전역변수 정보를 담는 .bss / .data, 리소스데이터를 담는 .rsrc 섹션, 읽기전용
데이터들을 담고 있는 .rdata 섹션 등이 있다. 이런 섹션의 개수가 파일마다 다를 수 있기
때문에 개수를 담고 있는 필드가 있다.
WORD NumSections;
DWORD file_base = (파일베이스 주소);
PIMAGE_DOS_HEADER pidh;
PIMAGE_NT_HEADER pinh;
pinh = file_base + pidh->e_lfanew; // NT 헤더 구하기
NumSections = pinh->FileHeader.NumberOfSections; // 섹션 개수
(4) 섹션 헤더 ( IMAGE_SECTION_HEADER )
섹션헤더들은 NT 헤더를 뒤 따른다. 이 섹션헤더들은 각 섹션에 대한 정보를 담는 필드들로
이루어져 있는데, 이 자체가 섹션은 아니다. 이를테면, .text(코드)는 실제코드가 포함된 영역이
섹션데이터이며, 이 섹션데이터가 존재하는 위치(RVA)와 그 밖의 정보들을 포함하는 것이 .text
섹션헤더이다. 섹션헤더의 주요 필드를 살펴보자.
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
- Name[8] : ".text", ".rdata" 와 같은 섹션 명을 담는다. 8 바이트가 최대 길이이고, 보통
dot(.) 으로 시작하지만, 꼭 그렇게 정해야 하는 것은 아니다. 섹션명은 바꿀 수도 있다. VC++
에서 이 섹션 명을 바꾸려면 아래와 같은 키워드를 사용할 수 있다.
<---------- ReDefine_SegmentName -------->
#pragma data_seg(".mydata"); // .data 를 .mydata 로 선언한다.
#pragma code_seg(".mycode"); // .text 를 .mycode 로 선언한다.
<---------- EOF ------------------------>
- VirtualAddress : 섹션데이터 RVA
(ImageBase + 섹션데이터 RVA = 섹션주소)
- Characteristics : 섹션의 퍼미션 ( 여러 가지 퍼미션을 OR 로 적용함 )
0x00000020 : 코드를 포함
0x00000040 : 초기화된 데이터를 포함 ( .rdata, .data )
0x00000080 : 초기화되지 않은 데이터를 포함 ( .bss )
0x00000200 : Comment ( 코멘트 )
0x10000000 : Shareable ( 공유가능; 보통 DLL 에서만 사용 )
0x20000000 : 실행가능
0x40000000 : 읽기가능
0x80000000 : 쓰기가능
* 여기서 잠깐, putty.exe(putty ssh 클라이언트의 각 섹션의 퍼미션을 알아보자)
-- putty.exe --
.text = 0x60000020 ( 코드포함 | 실행가능 | 읽기가능 )
.rdata = 0x40000040 ( 초기화된 데이터포함 | 읽기가능 )
.data = 0xC0000040 ( 초기화된 데이터포함 | 읽기가능 | 쓰기가능 )
.rsrc = 0x40000040 ( 초기화된 데이터포함 | 읽기가능 )
---------------------
이 퍼미션들 역시 VC++에서 키워드로 재지정 할 수 있다.
#pragma comment(linker, "/section:.text, RWE");
.text 섹션은 보통 읽기(R)와 실행퍼미션(E)의 조합이 기본이다. 그러나 코드섹션의 데이터를
메모리상에서 수정하려면 쓰기(W)이 필요하다. 자신이 만드는 프로그램이 코드섹션의 수정을
허용하도록 하려면, 컴파일 시에 위 키워드를 소스코드 상단에 선언해주면 된다.
(5) 다른 모듈의 함수호출하는 과정 ( IMPORT )
- 위 그림에서 보이는 것처럼 IMPORT 된 API 함수들은 두 단계의 과정을 거쳐 실제 DLL 의 API
함수가 호출되게 된다.
(1) CALL 0x000144408 ; IAT 엔트리로 가는 점프루틴 주소 호출
(2) JMP 0x00040042 ; 호출된 점프루틴은 실제 메모리에 맵핑된 DLL
API 함수주소를 담고 있는 주소를 포함하는 곳으로 점프한다.
(3) 메모리상에 구성된 IAT(Import Address Table) 테이블의 엔트리에 있는
실제 API 함수주소를 통해 DLL 의 API 함수를 호출한다.
참고. IMPORT
* IMPORT 정보를 담는 주요 테이블은 3 가지 종류이다. 나중에 자세히 다루겠지만, 알아두고
넘어가자.
- IMPORT Directory Table
- Import Address Table
- Import Hints/Names & DLL Names
주의: PE 파일을 수정하여 IAT 테이블의 API 함수후킹을 시도하는 것은 때때로 무의미하다.
왜냐하면 로더가 프로그램을 메모리에 맵핑시킨 다음, 이 IAT 테이블을 메모리상에서
재구성하기 때문이다. 후킹을 제대로 하기위해서는 프로세스를 실행시킨 다음, EIP
레지스터가 엔트리 포인트로 진입한 후(즉 IAT 가 재구성된 다음)에 스레드를 멈추고,
메모리상의 IAT 를 수정하는 것이 바람직하다.
(6) 작성한 함수를 외부에서 호출할 수 있게 만들기 ( EXPORT )
IMPORT 가 외부 DLL 의 함수들을 끌어와서 사용하고자 하는 목적이라면, 작성한 함수를 외부
프로그램에서 끌어가서 사용할 수 있도록 만들고자 하는 목적으로 사용되는 것이 EXPORT
이다. IMPORT 테이블과 정보들은 .idata 또는 .rdata 섹션에 포함되어 있었다. EXPORT 에
대한 테이블정보 역시 같은 섹션에 포함되어 있다. 자세한 설명은 본 문서의 뒤편에서 구조체를
다루면서 설명하고 있다.
참고. EXPORT
* EXPORT 정보를 담는 주요 테이블은 4 가지가 있다.
- EXPORT Address Table
- EXPORT Name Pointer Table
- EXPORT Ordinal Table
- EXPORT Names
IMPORT 와는 다르게 EXPORT 된 함수들은 오디날(Ordinal) 번호를 가질 수 있다. 따라서
함수 명으로 EXPORT 된 함수들은 나중에 IMPORT 될 때, 함수 명을 통해서 실제 주소를 구할
수 있고, 오디날번호로 EXPORT 된 함수들은 그 번호를 통해서 실제함수 주소를 구할 수 있다.
(7) 스레드 지역 저장고 ( .TLS 섹션 ; Thread Local Storage )
한 프로세스는 여러 개의 스레드를 만들어서 동작할 수 있다. 따라서 각 스레드별 개별적인
전역변수 저장창고가 필요하다. 이런 이유로 .TLS 섹션은 이것을 가능하게 한다. 프로그램 작성
시에 __declspec(thread) int tva; 와 같이 TLS 키워드를 붙여서 선언하게 되면, .TLS 섹션이
링크될 때 만들어지게 되고, 공간이 할당되어 실행될 때 사용되어 진다. 이 TLS 영역의 변수들을
할당하고 다루는 함수들이 개별적으로 존재한다. (TlsAllocXxX())
참고. TLS 섹션
한계: TLS 는 명시적으로 불러들여진 DLL 에서는 사용할 수 있지만, LoadLibrary() API 를
통해서 동적으로 불러들여진 DLL 에서는 사용할 수 없다.
* tls 섹션이 할당된 것을 확인하려면, “winhex.exe" 를 PEView 로 살펴보길 바란다.
(8) 디버그 정보 ( .debug 섹션 )
디버그섹션은 VC++ 컴파일 시에 Release 가 아닌, Debug 옵션으로 컴파일 할 때
생성되어진다. 이 디버그 섹션은 NULL 로 종결되는 파일경로(FilePath) 를 하나 담고 있다.
이것은 디버깅 시에 사용될 수 있는 심볼정보들(CodeView)을 담고 있는 파일인 .pdb
파일명이다.
3. VC++ PE 구조체 살펴보기
이제 본격적으로 본 문서의 주제인 PE 파서코드 작성에 대해서 알아보자. PE 구조를 이해하고
있지 않으면, PE 구조를 변경하거나 또는 구성할 수 없다. 그러므로 충분히 PE 에 대해서 공부한
다음, 이 “4. 파서코드 작성 ( PEKits )” 장을 통해 실습할 수 있다.
PE 를 다루기 위한 구조체들은 winnt.h 에 모두 선언되어 있다. 따라서 이 헤더파일을 파서의
상단에 선언해주어야 사용할 수 있다.
#include "winnt.h"
PE 를 구성하는 구조체들은 두 가지 타입으로 정의되어 있다. 첫째는 구조체이고, 또 하나는
구조체포인터이다. 구조체포인터는 자료형의 맨 앞에 대문자 ‘P’ 가 붙는다.
아래는 구조체로 살펴본 PE 구성이다. ‘-’ 는 포함되어 있다는 의미이다.
|+IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
|+MS-DOS STUB_PROGRAM (명시적 선언; 자료형 없음)
|+IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
| -IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
| -IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
| -IMAGE_DATA_DIRECTORY DataDirectory[16];
|+IMAGE_SECTION_HEADER[4], *PIMAGE_SECTION_HEADER;
|
|+.text 섹션 데이터
|+.rdata 섹션 데이터
| -IMAGE_THUNK_DATA; // IAT 로 사용됨.
| -IMAGE_IMPORT_DESCRIPTOR; // IMPORT DIRECTORY Table
| -IMAGE_THUNK_DATA; // IMPORT Name Table
| -IMAGE_IMPORT_HINTS; // 직접 선언함 ( 아래 )
| -IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
| -IMAGE_THUNK_DATA; // EXPORT Address Table
| -IMAGE_THUNK_DATA; // EXPORT Name Pointer Table
| -Ordinals[]; // EXPORT Ordinal Table 직접 선언함 ( 아래 )
| -EXPORT_NAMES // 직접 선언함 ( 아래 )
|+.data 섹션 데이터
|+.rsrc 섹션 데이터
| -IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
| -IMAGE_RESOURCE_DIRECTORY_ENTRY,
| *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
| .. (리소스를 구성하는 것은 뒷장에서 다룬다)
<VC++ 구조체로 본 PE 구조>
위 구조는 대표적인 자료 형들을 순서에 맞게 표현한 것이다. 여기에 빠져 있지만 PE 파서를
다룰 때 사용될 수도 있는 구조체들도 winnt.h 에 선언되어 있으므로 참고하길 바란다. 위
구조체들과 PE 구조편의 그림을 비교해보면 특별히 복잡한 것은 없지만, .rdata 섹션에 포함된
IMPORT 구조체가 분명하게 이해가 되지 않을 것이다. 이 부분에 대해서 지금 분명하게
설명하겠다.
3.1 IMPORT 해부
먼저 IMAGE_NT_HEADER->OptionalHeader.DataDirectory[] 이 배열로부터 IMPORT 는
시작된다. 이 배열의 두 세 개의 항목들은 IAT(Import Address Table)과 INT(Import Name
Table), 그리고 IMPORT Table 의 주소와 크기를 표현하고 있다.
IAT 와 INT 는 위의 구조에서 보아서 알겠지만, IMAGE_THUNK_DATA 구조체로 표현되고,
IMPORT Table 은 IMAGE_IMPORT_DESCRIPTOR 구조체로 표현된다.
IMAGE_IMPORT_DESCRIPTOR 구조체는 PE 구조 편에서 PEView 로 살펴본 그림에서의
"IMPORT Directory Table"를 표현하는 엔트리의 구조체이다.
그렇다면, IMPORT 된 “USER32.dll" 파일의 IAT 함수주소를 구하는 방법을 알아보자.
아래 코드는 이해를 돕기 위해서 작성된 것으로, 실제로 컴파일하기 위해서는 수정이 많이
필요한 코드이다.
/*
Import Table = IMPORT Directory Table
= IMAGE_IMPORT_DESCRIPTOR(엔트리모음; 마지막은 NULL 엔트리)
*/
/* 1. Import Table(IMPORT Directory Table 과 같은 것) 의 RVA 와 크기를 구한다 */
PIMAGE_IMPORT_DESCRIPTOR pIID = (IMAGE_NT_HEADER)-
>OptionalHeader.DataDirectory[1].VirtualAddress;
DWORD pIID_size = (IMAGE_NT_HEADER)-
>OptionalHeader.DataDirectory[1].Size;
PIMAGE_THUNK_DATA IAT;
DWORD DllName;
/*
IMAGE_IMPORT_DESCRIPTOR 의 필드인 FirstThunk 와 OriginalFirstThunk
는
IAT 테이블의 시작 RVA 를 가지고 있다. 그리고 Name 필드는 해당 IMPORT 된
DLL 파일명(“ADVAPI32.dll", "USER32.dll", "KERNEL32.dll" ..) 이 저장된
위치의 RVA 를 가지고 있다. 이 구조체는 말 그대로 디렉터리다. 하나의 DLL
파일별로 하나의 IMAGE_IMPORT_DESCRIPTOR 구조체 엔트리를 가지는 것이다.
*/
pIID = pIID + ImageBase; // 메모리상의 실 주소 구함
while(pIID->Name){ // PIMAGE_IMPORT_DESCRIPTOR 가 없을 때까지 반복
if(strcmp(pIID->Name, "USER32.dll") == 0){ // 원하는 DLL 엔트리 발견!
// IAT 테이블 안의 USER32.dll 가 선언된 시작 RVA 은 구함.
HAT = Imageable + pipit->FirstThunk;
break;
}
pIID++; // 다음 엔트리로 이동
}
아래는 winnt.h 에 선언되어 있는 IMAGE_IMPORT_DESCRIPTOR 구조체이다.
주석이 달려 있는 부분을 주의 깊게 기억해두길 바란다. 주석이 달리지 않은 부분은 0 으로
초기화하면 된다.
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // 아래 FirstThunk 와 같이 IAT RVA
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // DLL 파일명을 가리키는 포인터
DWORD FirstThunk; // IAT 의 RVA ( 각 DLL 별 )
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED
*PIMAGE_IMPORT_DESCRIPTOR;
마지막으로 IMPORT 에 대해서 한 가지 사실을 더 알아보고 EXPORT 구조체를 설명하도록
하겠다.
PEView.exe 로 살펴본 PE 구조 그림에서는 아래 4 가지의 주요 IMPORT 항목이 있었다.
- Import Address Table ( IAT )
- IMPORT Directory Table
- IMPORT Name Table ( INT )
- Import Hints/Names & DLL Names ( Import Hints )
IMPORT Directory Table 은 바로 위에서 설명한 IMAGE_IMPORT_DESCRIPTOR 이다.
그리고 이것은 IAT 에서의 해당 DLL 에서 IMPORT 된 API 함수 명에 대한 포인터를 가지고
있다고 설명했다. 그리고 IAT 와 INT 는 완전하게 동일한 구조를 가지고 있다. 이 IAT, INT 에서
가리켜진 각 API 함수 엔트리별 주소 값은 Import Hints 영역에 있는 아래와 같은 문자열을
가리키고 있다.
“[두바이트 오디날번호][API 함수명][널 바이트]”
“[0x21F0][MessageBoxA][\0][0x23C0][CreateWindow][\0]" .....
3.2 EXPORT 해부
putty.exe 는 DLL 이 아니기 때문에, 외부로 EXPORT 한 함수가 존재하지 않아. EXPORT 에
관련된 정보가 나오지 않았다. PEView 로 DLL(동적 라이브러리)파일을 열어보면 아래와 같은
4 가지 EXPORT 관련 테이블이 나오는 것을 확인할 수 있다.
- IMAGE_EXPORT_DIRECTORY
- EXPORT Address Table
- EXPORT Name Pointer Table
- EXPORT Ordinal Table
- EXPORT Names
<PEView.exe 로 열어본 DLL 파일>
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // 익스포트된 DLL 파일명이 있는 RVA
DWORD Base; // 시작 오디날번호
DWORD NumberOfFunctions; // EXPORT 된 함수 개수
DWORD NumberOfNames; // NumberOfFunctions 와 같은 값을 사용
DWORD AddressOfFunctions; // EXPORT Address Table 의 RVA
DWORD AddressOfNames; // EXPORT Name Pointer Table 의 RVA
DWORD AddressOfNameOrdinals; // EXPORT Ordinal Table 의 RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
EXPORT Address Table, EXPORT Name Pointer Table 이 두 개의 테이블은 IMPORT
에서와 마찬가지로 연속된 4 바이트 주소를 담고 있는 테이블이다. 따라서 IMPORT 를 다룰
때에 사용했던 IMAGE_THUNK_DATA 구조체를 여기서도 사용한다. 그리고 EXPORT Ordinal
Table 은 2 바이트 오디날 번호의 연속이다. 오디날 번호는 0x0000 번부터 시작해서 중복되지
않게 할당되어야 한다. 이 테이블은 달리 선언되어 있지 않고 아래처럼 선언한 다음 사용할 수
있다.
PIMAGE_EXPORT_DIRECTORY ped = (이미지 익스포트 디렉토리의 주소값);
WORD Ordinals[ped->NumberOfFunctions];
Ordinals[0] = 0x0000;
Ordinals[1] = 0x0001;
Ordinals[2] = 0x0002;
EXPORT 는 IMPORT 에 비해서 이렇게 단순하다. 하지만 EXPORT Address Table 과
EXPORT Name Pointer Table 을 구성하고 있는 엔트리 주소들이 어디를 가리키는지는 모를
것이다. 위 “PEView 로 열어본 DLL 파일” 그림에서 하나 설명하지 않은 EXPORT Names 가
바로 그 열쇠이다. 여기에는 IMPORT 에서의 IMPORT Hints 영역과 마찬가지로 문자열정보를
담고 있고, 이곳을 앞서 설명한 두 엔트리 테이블에서 가리키게 된다.
자 그럼, EXPORT Names 의 예제를 살펴보자.
설명이 더 필요 없을 듯하지만, 혹 모르실 분을 위해 자세히 정리하고 넘어가자.
(CIAT_hook.. 으로 시작되는 함수는 실제 개발자가 EXPORT 한 함수가 아니다)
처음으로 나오는 문자열은 EXPORT 한 DLL 파일명이다. 그리고 NULL 한바이트로 끝난다.
뒤이어 Mine_recv 함수와 Mine_Send 함수명이 나온다. 역시 NULL 한 바이트로 끝난다. 즉
여기에는 어떠한 오디날번호도 포함되지 않은 순수한 문자열로만 이루어져 있다.
“[DLL 파일명][EXPORT 된 함수명 1][EXPORT 된 함수명 2]”
TCHAR EXPORT_NAMES[]={"IAT_hook.dll\0...생략.\0Mine_recv\0Mine_send\
0"};
자 그럼 첫 번째로 EXPORT 된 함수는 Mine_recv 이며, 오디날번호는 0x0001 이다.
오디날번호는 0x0000 부터 시작했지만, CIAT_hook 과 같은 컴파일러에 의해서 만들어진
문자열값은 제외하면 0x0001 이 맞다. 이 오디날의 시작번호는
(PIMAGE_EXPORT_DIRECTORY)->Base 에 넣어야 한다.
PIMAGE_EXPORT_DIRECTORY ped = (이미지 익스포트 디렉토리의 주소값);
ped->Base = 0x0001; // Mine_recv()
여기까지 유심히 읽어 내려온 독자라면 분명히 PE 의 구조가 머릿속에 그려지고, PE 를 알 것만
같은 느낌을 받을 것이다. 그러나 어떻게 코딩을 시작해야할지, 코딩을 할 때 유의해야 할
사항들은 무엇인지.. 궁금할 것이다. 그것은 다음 장에 첨부된 몇 가지 예제 PE 파서
소스코드들을 통해서 유추, 분석해보길 바란다.
4. 파서코드 작성 ( PEKits )
아래 소스들은 VC++ WIN32 콘솔어플리케이션 프로젝트로 생성한 다음, 사용할 수 있는
함수들이다. 그리고 이 함수들을 사용하기 위해서는 아래 두 세가지 헤더를 반드시 선언해야
한다.
#include "windows.h"
#include "winnt.h"
#include "stdio.h"
4.1 print_pe_import() - IMPORT 정보 출력
/*
- 호출방법 : print_pe_import("c:\\putty.exe");
IMPORT_DESCRIPTOR : 003F0DD8
+DLL: ADVAPI32.dll
- RegCloseKey
- RegCreateKeyA
- RegSetValueExA
- RegOpenKeyA
- RegQueryValueExA
- RegDeleteKeyA
- RegEnumKeyA
+DLL: COMCTL32.dll
- ORD# 14
- ORD# 15
...
*/
int print_pe_import(char *filename)
{
PIMAGE_DOS_HEADER pIDH;
PIMAGE_NT_HEADERS pINH;
PIMAGE_IMPORT_DESCRIPTOR pIID;
PIMAGE_THUNK_DATA pITD;
PIMAGE_IMPORT_BY_NAME pIIBName;
HANDLE hFile, hFileMap;
DWORD file_size;
PCHAR file_base;
DWORD idei;
DWORD idei_size;
DWORD p;
int ret = 0;
hFile = CreateFile(filename, GENERIC_READ, 0, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE){
printf("file open failed %s\n", filename);
ret = -1;
goto RET;
}
file_size = GetFileSize(hFile, NULL);
hFileMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if(hFileMap == NULL){
printf("file mapping failed %s\n", filename);
ret = -2;
goto RET;
}
file_base = (PCHAR) MapViewOfFile(hFileMap, FILE_MAP_READ, 0, 0, file_size);
if(file_base == NULL){
printf("file MapView Failed %s\n", filename);
ret = -3;
goto RET;
}
pIDH = (PIMAGE_DOS_HEADER) file_base;
pINH = (PIMAGE_NT_HEADERS) ((DWORD)pIDH + pIDH->e_lfanew);
if(pIDH->e_magic != IMAGE_DOS_SIGNATURE){
printf("not found MZ DOS Magic [ %s ]\n", filename);
ret = -4;
goto RET;
}
if(pINH->Signature != IMAGE_NT_SIGNATURE){
printf("not found PE NT Signature [ %s ]\n", filename);
ret = -5;
goto RET;
}
/*
IMPORT_DIRECTORY_TABLE =
file_base +
IMAGE_LOAD_CONFIG_DIRECTORY +
72(IMAGE_LOAD_CONFIG_DIRECTORY 크기)
winnt.h:
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
*/
idei = pINH-
>OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddres
s;
idei_size = pINH-
>OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size;
pIID = (PIMAGE_IMPORT_DESCRIPTOR) ((DWORD) file_base + idei);
// PEView.exe 에서의 IMPORT Directory Table 과 IMPORT_DESCRIPTOR 는 같다.
printf("IMPORT_DESCRIPTOR : %p\n", pIID);
while(pIID->Name){
if(pIID->Name != NULL){
/*
pIID->Name 은 RVA 값이다. 이것은 DLL 파일명이 있는 위치의 RVA
를 가지고 있다. 참조하기위해서는 이미지베이스주소(file_base) 를 더해야
유효한 메모리상의 주소가 된다.
*/
printf("+DLL: %s\n", ((DWORD) file_base + pIID->Name));
// pITD = IAT 안의 해당 DLL 첫 API 시작엔트리;
pITD = (PIMAGE_THUNK_DATA) (file_base + pIID->FirstThunk);
while(1){
if(pITD->u1.AddressOfData == 0) // 엔트리 끝났음!
break;
if(pITD->u1.Ordinal & IMAGE_ORDINAL_FLAG){ // 오디날!!
printf(" - ORD#%4u\n", IMAGE_ORDINAL(pITD->u1.Ordinal));
} else{ // 아니면 함수명으로 IMPORT 된것!!
p = (DWORD) pITD->u1.AddressOfData;
pIIBName = (PIMAGE_IMPORT_BY_NAME) ((DWORD) file_base +
p);
printf(" - %s\n", pIIBName->Name);
}
pITD++;
}
}
pIID++;
}
RET:
UnmapViewOfFile(file_base);
CloseHandle(hFile);
return(ret);
}
4.2 fix_entrypoint() - EntryPoint 수정
/*
- 호출방법 : fix_entrypoint("c:\\putty.exe", 0x40000000);
Entry Point changed(belows are not RVAs)!!
Fixed Filename: c:\putty.exe_new_ep.exe
EntryPoint Address: 0044265F => 40000000
*/
/*
putty.exe 를 이 함수로 엔트리포인트를 바꾸면..
엔트리포인트가 바뀐 PE 파일 putty.exe_new_ep.exe 가 생성된다
*/
int fix_entrypoint(char *filename, DWORD new_entrypoint)
{
PIMAGE_DOS_HEADER pIDH;
PIMAGE_NT_HEADERS pINH;
HANDLE hFile, hFileMap;
DWORD file_size;
PCHAR file_base;
DWORD org_entrypoint;
DWORD new_ep_rva;
char out_filename[128] = { 0 };
int ret = 0;
/*
파일출력을 위해서 복사본 생성
(별도로 파일 출력을 하는게 아니라, 사본을 만들어서 맵핑함으로써
두개의 파일을 열 필요가 없게 된다)
*/
sprintf(out_filename, "%s_new_ep.exe", filename);
CopyFile(filename, out_filename, false);
hFile = CreateFile(out_filename, GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE){
printf("file open failed %s\n", out_filename);
ret = -1;
goto RET;
}
file_size = GetFileSize(hFile, NULL);
hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
if(hFileMap == NULL){
printf("file mapping failed %s\n", out_filename);
ret = -2;
goto RET;
}
file_base = (PCHAR) MapViewOfFile(hFileMap, FILE_MAP_READ | FILE_MAP_WRITE,
0, 0, file_size);
if(file_base == NULL){
printf("file MapView Failed %s\n", out_filename);
ret = -3;
goto RET;
}
pIDH = (PIMAGE_DOS_HEADER) file_base;
pINH = (PIMAGE_NT_HEADERS) ((DWORD)pIDH + pIDH->e_lfanew);
if(pIDH->e_magic != IMAGE_DOS_SIGNATURE){
printf("not found MZ DOS Magic [ %s ]\n", out_filename);
ret = -4;
goto RET;
}
if(pINH->Signature != IMAGE_NT_SIGNATURE){
printf("not found PE NT Signature [ %s ]\n", out_filename);
ret = -5;
goto RET;
}
/*
메모리상의 실제 엔트리포인트 =
pINH->OptionalHeader.ImageBase +
pINH->OptionalHeader.AddressOfEntryPoint
이미지베이스에 엔트리포인트 RVA 를 더한 값이 실제 엔트리포인트인 것은
쉽게 납득이 갈것이다. 이 함수는 사용자에게 메모리상의 새로운 엔트리포인트
를 입력받을 것이므로, 입력받은 엔트리포인트 주소에서 이미지베이스를 뺀 다음
AddressOfEntryPoint 에 그 값을 채우고, 메모리상의 이미지 새 파일로 저장시켜야 한다.
*/
new_ep_rva = new_entrypoint - pINH->OptionalHeader.ImageBase;
org_entrypoint = (DWORD)pINH->OptionalHeader.AddressOfEntryPoint +
(DWORD)pINH->OptionalHeader.ImageBase;
// file_base + 0x120 = &OptionalHeader.AddressOfEntryPoint
printf("\nEntry Point changed(belows are not RVAs)!!\n");
printf("Fixed Filename: %s\n", out_filename);
printf("EntryPoint Address: %p => %p\n", org_entrypoint, new_entrypoint);
pINH->OptionalHeader.AddressOfEntryPoint = new_ep_rva; // 새 엔트리포인트 RVA
저장
RET:
UnmapViewOfFile(file_base);
CloseHandle(hFile);
return(ret);
}
4.3 fix_perm_sections() - Section 퍼미션 수정
/*
// READ | WRITE 만 허용된 .text 섹션에 WRITE 권한을 적용
호출방법: fix_perm_sections("c:\\putty.exe", ".text",
IMAGE_SCN_MEM_EXECUTE |
IMAGE_SCN_MEM_READ |
IMAGE_SCN_MEM_WRITE);
.text Section Permission fixed by E0000000
이제 이 .text 섹션은 수정이 가능한 퍼미션을 가지게 되었다.
*/
int fix_perm_sections(char *filename, char *section, DWORD new_permission)
{
PIMAGE_DOS_HEADER pIDH;
PIMAGE_NT_HEADERS pINH;
PIMAGE_SECTION_HEADER pISH;
HANDLE hFile, hFileMap;
DWORD file_size;
PCHAR file_base;
DWORD first_sh;
char out_filename[128] = { 0 };
int ret = 0;
int i = 0;
bool is_found = false;
/*
파일출력을 위해서 복사본 생성
(별도로 파일 출력을 하는게 아니라, 사본을 만들어서 맵핑함으로써
두개의 파일을 열 필요가 없게 된다)
*/
sprintf(out_filename, "%s_new_%s_perm.exe", filename, section);
CopyFile(filename, out_filename, false);
hFile = CreateFile(out_filename, GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE){
printf("file open failed %s\n", out_filename);
ret = -1;
goto RET;
}
file_size = GetFileSize(hFile, NULL);
hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
if(hFileMap == NULL){
printf("file mapping failed %s\n", out_filename);
ret = -2;
goto RET;
}
file_base = (PCHAR) MapViewOfFile(hFileMap, FILE_MAP_READ | FILE_MAP_WRITE,
0, 0, file_size);
if(file_base == NULL){
printf("file MapView Failed %s\n", out_filename);
ret = -3;
goto RET;
}
pIDH = (PIMAGE_DOS_HEADER) file_base;
pINH = (PIMAGE_NT_HEADERS) ((DWORD)pIDH + pIDH->e_lfanew);
if(pIDH->e_magic != IMAGE_DOS_SIGNATURE){
printf("not found MZ DOS Magic [ %s ]\n", out_filename);
ret = -4;
goto RET;
}
if(pINH->Signature != IMAGE_NT_SIGNATURE){
printf("not found PE NT Signature [ %s ]\n", out_filename);
ret = -5;
goto RET;
}
// section 인자와 같은 섹션명을 가진 섹션헤더를 탐색후 퍼미션을 변경
// 첫 섹션헤더 = file_base + 0x1F0
first_sh = (DWORD) file_base + 0x1F0;
pISH = (PIMAGE_SECTION_HEADER) first_sh;
// 사용자가 원하는 섹션헤더를 탐색
for(i = 0; i < pINH->FileHeader.NumberOfSections; i++){
if(strcmp((char *)pISH->Name, section) == 0){ // 발견!!
pISH->Characteristics = new_permission; // 새 퍼미션 저장!!
is_found = true;
}
pISH++;
}
if(is_found == true)
printf("\n%s Section Permission fixed by %p\n", section, new_permission);
else
printf("\nnot found %s Section\n", section);
RET:
UnmapViewOfFile(file_base);
CloseHandle(hFile);
return(ret);
}
5. 맺음말
5.1 참고자료 ( References )
주제에서 벗어나 미처 자세히 다루지 못한 PE 기본에 대해서는 아래 자료들을 참고하십시오.
[1] Matt Pietrek, 1994
Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
( http://msdn2.microsoft.com/en-us/library/ms809762.aspx )
[2] Matt Pietrek, 2002
An In-Depth Look into the Win32 Portable Executable File Format
( http://msdn.microsoft.com/msdnmag/issues/02/02/PE/default.aspx )
[3] Ashkbiz Danehkar, 2005
Make your owner PE Protector Part 1: Your first EXE Protector
( http://www.codeproject.com/cpp/peprotector1.asp )
5.2 마치며 ( Conclusion )
여기까지 새로울 것 없는 PE 구조와 코딩 법을 알아보았습니다. 문서작성 초기에는 PE Code
Injector 를 작성하여 첨부 할 예정이었으나, 조금 더 문서작성 기간을 앞당기고자 포함하지
못했습니다. 아쉬운 감이 없진 않지만, 잘 보셨길 바라며 PE 를 이해하는 것을 넘어서 다루는
방법을 익히는데 작은 보탬이 되셨으면 합니다.