PE(Portable Executable) File Format (7) - PE Header
EAT (Export Address Table)
Windows 운영체제에서 라이브러리(Library) 란 다른 프로그램에서 불러 쓸 수 있도록
관련 함수들을 모아놓은 파일(DLL/SYS)입니다.
Win32 API 가 대표적인 Library 이며, 그 중에서도 kernel32.dll 파일이 가장 대표적인 Library 파일이라고 할 수 있습니다.
EAT(Export Address Table) 은 라이브러리 파일에서 제공하는 함수를
다른 프로그램에서 가져다 사용할 수 있도록 해주는 매커니즘 입니다.
앞서 설명드린 IAT 와 마찬가지로 PE 파일내에 특정 구조체(IMAGE_EXPORT_DIRECTORY)에 정보를 저장하고 있습니다.
라이브러리의 EAT 를 설명하는 IMAGE_EXPORT_DIRECTORY 구조체는 PE 파일에 하나만 존재합니다.
* 참고로 IAT 를 설명하는 IMAGE_IMPORT_DESCRIPTOR 구조체는 여러개의 멤버를 가진 배열 형태로 존재합니다.
왜냐하면 PE 파일은 여러개의 라이브러리를 동시에 Import 할 수 있기 때문이지요
PE 파일내에서 IMAGE_EXPORT_DIRECTORY 구조체의 위치는 PE Header 에서 찾을 수 있습니다.
IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress 값이
실제 IMAGE_EXPORT_DIRECTORY 구조체 배열의 시작 주소 입니다. (RVA 값입니다.)
아래는 kernel32.dll 파일의 IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress 를 보여주고 있습니다.
(첫번째 4 byte 가 VirtualAddress, 두번째 4 byte 가 Size 멤버입니다.)
----------------------------------------------
...
00000160 00000000 loader flags
00000164 00000010 number of directories
00000168 0000262C RVA of EXPORT Directory
0000016C 00006D19 size of EXPORT Directory
00000170 00081898 RVA of IMPORT Directory
00000174 00000028 size of IMPORT Directory
...
* IMAGE_OPTIONAL_HEADER32 구조체에 대해서 궁금하신 분은
IMAGE_OPTIONAL_HEADER 설명 을 참고하시기 바랍니다.
RVA 값이 262Ch 이므로 File offset 은 1A2Ch 입니다.
(RVA 와 File offset 간의 변환과정이 잘 이해 안가시는 분은 IMAGE_SECTION_HEADER 설명을 참고하시기 바랍니다.)
IMAGE_EXPORT_DIRECTORY
IMAGE_EXPORT_DIRECTORY 구조체는 아래와 같습니다.
DWORD Characteristics;
DWORD TimeDateStamp; // creation time date stamp
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // address of library file name
DWORD NumberOfFunctions; // number of functions
DWORD NumberOfNames; // number of names
DWORD AddressOfFunctions; // address of function start address array
DWORD AddressOfNames; // address of functino name string array
DWORD AddressOfNameOrdinals; // address of ordinal array
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
* 출처 : Microsoft 의 Visual C++ 에서 제공하는 winnt.h
중요 멤버들에 대한 설명입니다. (여기에 나오는 주소는 모두 RVA 입니다.)
NumberOfFunctions : 실제 export 함수 갯수
NumberOfNames : export 함수중에서 이름을 가지는 함수 갯수 (<= NumberOfFunctions)
AddressOfFunctions : export 함수들의 시작 위치 배열의 주소 (배열의 원소개수 = NumberOfFunctions)
AddressOfNames : 함수 이름 배열의 주소 (배열의 원소개수 = NumberOfNames)
AddressOfOrdinals : ordinal 배열의 주소 (배열의 원소개수 = NumberOfNames)
아래 그림은 kernel32.dll 파일의 IMAGE_EXPORT_DIRECTORY 의 구조를 나타내고 있습니다.
<Fig. EAT 구조>
라이브러리에서 함수 주소를 얻는 API 는 GetProcAddress() 입니다.
GetProcAddress() 함수가 함수 이름을 가지고 어떻게 함수 주소를 얻어내는 순서를 설명드리겠습니다.
2. "함수 이름 배열"은 문자열 주소가 저장되어 있습니다. 문자열 비교(strcmp)를 통하여 원하는 함수 이름을 찾습니다.
이 때의 배열 인덱스를 name_index 라고 하겠습니다.
3. AddressOfNameOrdinals 멤버를 이용해 "ordinal 배열" 로 갑니다.
4. "ordinal 배열" 에서 name_index 로 해당 ordinal_index 값을 찾습니다.
5. AddressOfFunctions 멤버를 이용해 "함수 주소 배열 - EAT" 로 갑니다.
6. "함수 주소 배열 - EAT" 에서 아까 구한 ordinal_index 를 배열 인덱스로 하여 원하는 함수의 시작 주소를 얻습니다.
위 <Fig. EAT 구조> 는 kernel32.dll 의 경우를 보여주고 있습니다.
kernel32.dll 은 export 하는 모든 함수에 이름이 존재하며,
AddressOfNameOrdinals 배열의 값이 index = ordinal 형태로 되어있습니다.
하지만 모든 DLL 파일이 이와 같지는 않습니다.
export 하는 함수 중에 이름이 존재하지 않을 수 도 있으며 (ordinal 로만 export 함)
AddressOfNameOrdinals 배열의 값이 index != ordinal 인 경우도 있습니다.
따라서 위 순서를 따라야만 정확한 함수 주소를 얻을 수 있습니다.
* 참고로 함수 이름 없이 ordinal 로만 export 된 함수의 주소를 찾을 수 도 있습니다.
kernel32.dll 을 이용한 실습
실제 kernel32.dll 파일의 EAT 에서 AddAtomW 함수 주소를 찾는 실습을 해보겠습니다.
(<Fig. EAT 구조> 를 참고하세요.)
앞에서 kernel32.dll 의 IMPORT_EXPORT_DIRECTORY 구조체 file offset 은 1A2Ch 라고 하였습니다.
hex editor 로 1A2Ch 주소로 갑니다.
각 구조체 멤버별로 나타내 보겠습니다.
TimeDateStamp = 49C4D12Eh
MajorVersion = 0000h
MinorVersion = 0000h
Name = 00004B98h
Base = 00000001h
NumberOfFunctions = 000003BAh
NumberOfNames = 000003BAh
AddressOfFunctions = 00002654h
AddressOfNames = 0000353Ch
AddressOfNameOrdinals = 00004424h
위에서 알려드린 순서대로 진행하겠습니다.
1. "함수 이름 배열"
AddressOfNames 멤버의 값은 RVA = 353Ch 이므로 file offset = 293Ch 입니다.
4 byte 의 RVA 로 이루어진 배열입니다. 배열 원소의 갯수는 NumberOfNames (3BAh) 입니다.
저 모든 RVA 값을 하나하나 따라가면 함수 이름 문자열이 나타납니다.
2. 원하는 함수 이름 찾기
설명의 편의를 위해 우리가 찾는 "AddAtomW" 함수 이름 문자열은 배열의 세번째 원소의 값(주소)를 따라가면 됩니다.
RVA = 4BBDh 이므로 file offset = 3FBDh 입니다.
이때 배열의 인덱스(index) 는 2 입니다.
3. "Ordinal 배열"
AddressOfNameOrdinals 멤버의 값은 RVA = 4424h 이므로 file offset = 3824h 입니다.
2 byte 의 ordinal 로 이루어진 배열이 나타납니다.
4. ordinal
위에서 구한 index 값 2 를 위의 "ordinal 배열" 에 적용하면 ordinal 2 를 구할 수 있습니다.
AddressOfNameOrdinals[index(2)] = ordinal(2)
5. "함수 주소 배열(EAT)"
AddressOfFunctions 멤버의 값은 RVA = 2654h 이므로 file offset = 1A54h 입니다.
4 byte 함수 주소 RVA 배열이 나타납니다.
6. AddAtomW 함수 주소
위에서 구한 ordinal_index 를 "함수 주소 배열(EAT)" 에 적용하면 해당 함수의 RVA (000326F1h)를 얻을 수 있습니다.
AddressOfFunctions[ordinal(2)] = 326F1
kernel32.dll 의 ImageBase = 7C7D0000h 입니다.
따라서 "AddAtomW" 함수의 실제 주소(VA)는 7C8026F1h 입니다.
OllyDbg 를 이용해서 확인해 보겠습니다.
네, 정확히 7C8026F1h 주소(VA)에 우리가 찾는 "AddAtomW" 함수가 나타납니다.
+---+
이상으로 EAT(Export Address Table) 에 대해서 알아보았습니다.
이제 가장 기본적이면서 중요한 부분은 전부 끝났습니다.
힘드셨나요? 아마 쉽지는 않으셨을 겁니다.
이해 가지 않는 부분은 실습 위주로 차근차근 따라 해보시기 바랍니다.
다음에는 PE Header 마무리 시간을 갖도록 하겠습니다.