본문 바로가기

ReverseEngineering

그림으로 살펴 보는 PE File Format(IAT, EAT)

 

실습환경 : Windows 7 SP 1

실습대상 : notepad.exe

 

PE File Format 중 PE Header와 IAT, EAT를 그림과 구조체로 설명하겠습니다. 구조체에 대한 설명은 중요한 구조체의 멤버 위주로 설명하겠습니다. (다르게 이야기하면 OS의 PE 로더에 따라 다르지만 일반적인 PE 로더에서는 필요가 없는 구조체 멤버입니다. 즉 해당 공간에 다른 Code 등이 존재해도 정상적으로 실행이될 수 있습니다. 이 부분은 PE 구조를 좀 더 공부하면 자세히 알 수 있습니다.)

PE 관련 Tool이 많지만 공부하는 입장으로써 본 글에서는 Hex Editor(HxD)를 사용하여 Virtual Address가 아닌 File offset 위주로 살펴보겠습니다.

(기본적인 PE File Format에 대한 설명은 생략합니다.)


 


 

1. PE Header

 

아래 그림은 Hex Editor를 이용하여 notepad.exe를 Open한 그림입니다.

PE Header를 색깔로 구분하였고 괄호안에 숫자는 해당 Header(구조체)의 16진수로 나타낸 크기입니다.

(현재 분석 대상은 섹션이 .reloc 섹션까지 총 4개이지만 편의상 .rsrc 섹션까지만 나타냈습니다.)

 

 

 



 

각 Header의 구조체를 하나씩 살펴보겠습니다.

 

DOS Header (40 / 64 Bytes)

 

 

IMAGE_DOS_HEADER 구조체

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {      
    WORD   e_magic;                     // DOS signature : 4D5A ("MZ")
    WORD   e_cblp;                      
    WORD   e_cp;                        
    WORD   e_crlc;                      
    WORD   e_cparhdr;                   
    WORD   e_minalloc;                  
    WORD   e_maxalloc;                  
    WORD   e_ss;                        
    WORD   e_sp;                        
    WORD   e_csum;                      
    WORD   e_ip;                        
    WORD   e_cs;                        
    WORD   e_lfarlc;                    
    WORD   e_ovno;                      
    WORD   e_res[4];                    
    WORD   e_oemid;                     
    WORD   e_oeminfo;                   
    WORD   e_res2[10];                  
    LONG   e_lfanew;                    // offset to NT header (NT header의 옵셋)
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
cs

 

 


 

 

NT Header (F8 / 248 Bytes)

 

DOS stub의 존재 여부는 옵션이며 개발 도구에 따라 크기도 달라집니다. 따라서 IMAGE_DOS_HEADER의 e_lfanew 멤버의 값(Dos header의 마지막 4 Bytes)을 보고 NT header의 시작 위치를 알 수 있습니다. e_lfanew의 값이 0x000000D8이므로 아래 그림과 같이 해당 위치가 NT header의 시작 위치입니다.

 

NT Header의 IMAGE_NT_HEADER 구조체는 File Header와 Optional Header의 구조체를 포함하고 있습니다. 초록색 Box 부분이 File Header이고 노랑색 Box 부분이 Optional Header입니다.

 

 

IMAGE_NT_HEADERS

 

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
  DWORD                 Signature;                  // PE Signature : 50450000 ("PE"00)
  IMAGE_FILE_HEADER     FileHeader;                 // File Header
  IMAGE_OPTIONAL_HEADER OptionalHeader;             // Optional Header
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
cs

 

 



 

NT Header - File Header (14 / 20 Bytes)

 

 

IMAGE_FILE_HEADER 구조체

 

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_FILE_HEADER {
  WORD  Machine;                                 // CPU Type (Intel x86 : 0x14C)
  WORD  NumberOfSections;                        // 섹션의 개수 (실제 섹션 개수와 동일해야함)
  DWORD TimeDateStamp;
  DWORD PointerToSymbolTable;
  DWORD NumberOfSymbols;
  WORD  SizeOfOptionalHeader;                    // IMAGE_OPTIONAL_HEADER32 구조체의 크기 (IMAGE_OPTIONAL_HEADER64와 크기가 다름)
  WORD  Characteristics;                         // 파일의 속성 (실행 가능한 파일인지 혹은 DLL 파일인지 등을 구분) (bit OR 형식)
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
 
cs

 

 


 

 

NT Header - Optional Header (E0 // 224 Bytes)

 

빨간색으로 표시한 Box는 중요한 구조체 멤버이고 노란색으로 표시한 Box는 DataDirectory(IMAGE_DATA_DIRECTORY 구조체의 배열)입니다. DataDirectory는 IAT와 EAT에서 살펴보겠습니다.

 

 

IMAGE_OPTIONAL_HEADER32 구조체

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
typedef struct _IMAGE_DATA_DIRECTORY {
  DWORD                VirtualAddress;
  DOWRD                Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
 
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16
 
typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;                                    // IMAGE_OPTIONAL_HEADER32 : 0x010B, IMAGE_OPTIONAL_HEADER64 : 0x020B
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;                      // EP의 RVA
  DWORD                BaseOfCode;
  DWORD                BaseOfData;
  DWORD                ImageBase;                                // ImageBase (일반적으로 EXE,DLL 파일은 user memory 영역인 0~7FFFFFFF 범위에 로딩, SYS 파일은 Kernel memory 영역인 80000000~FFFFFFFF 범위에 로딩)
  DWORD                SectionAlignment;                         // 메모리에서 섹션의 최소단위 (파일/메모리의 섹션 크기는 반드시 FileAlignment/SectionAlignment의 배수)
  DWORD                FileAlignment;                            // 파일에서 섹션의 최소단위
  WORD                 MajorOperatingSystemVersion;
  WORD                 MinorOperatingSystemVersion;
  WORD                 MajorImageVersion;
  WORD                 MinorImageVersion;
  WORD                 MajorSubsystemVersion;
  WORD                 MinorSubsystemVersion;
  DWORD                Win32VersionValue;
  DWORD                SizeOfImage;                              // 가상 메모리에서 PE Image가 차지하는 크기
  DWORD                SizeOfHeaders;                            // PE 헤더의 전체 크기 (NULL Padding 포함) (파일 시작에서 SizeOfHeaders 옵셋만큼 떨어진 위치에 첫 번째 섹션이 위치)
  DWORD                CheckSum;
  WORD                 Subsystem;                                // 1 : Driver file(시스템 드라이버(*.sys)), 2 : GUI 파일, 3 : CUI 파일
  WORD                 DllCharacteristics;
  DWORD                SizeOfStackReserve;
  DWORD                SizeOfStackCommit;
  DWORD                SizeOfHeapReserve;
  DWORD                SizeOfHeapCommit;
  DWORD                LoaderFlags;
  DWORD                NumberOfRvaAndSizes;                      // DataDirectory 배열의 개수 (구조체 정의에 배열 개수가 IMAGE_NUMBEROF_DIRECTORY_ENTRIES (16)이라고 명시되어 있지만, PE 로더는 NumberOfRvaAndSizes 값을 보고 인식)
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
cs

 

 


 

 

Section Header (28 / 40 Bytes)

 

IMAGE_FILE_HEADER 구조체의 NumberOfSections에서 확인했던 섹션의 개수만큼 Section Header가 존재합니다. 블록지정한 부분이 .text 섹션 헤더 영역이며 빨간색 Box 부분이 중요 구조체 멤버입니다.

 

 

IMAGE_SECTION_HEADER 구조체

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define IMAGE_SIZEOF_SHORT_NAME    8
 
typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;                   // 메모리에서 해당 섹션이 차지하는 크기
  } Misc;
  DWORD VirtualAddress;                  // 메모리에서 해당 섹션의 시작 주소 (RVA)
  DWORD SizeOfRawData;                   // 파일에서 해당 섹션이 차지하는 크기
  DWORD PointerToRawData;                // 파일에서 해당 섹션의 시작 위치
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;                 // 섹션의 속성 (bit OR 형식)
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
cs

 

 


 

 

2. IAT (Import Address Table)

 

PE 파일은 자신이 어떤 라이브러리를 Import하고 있는지 IMAGE_IMPORT_DESCRIPTOR 구조체 배열에 명시합니다. 해당 IMAGE_IMPORT_DESCRIPTOR 구조체 배열은 PE Body에 위치하며 해당 위치는 PE Header의 IMAGE_OPTIONAL_HEADER32에 존재합니다.

 

아래 그림은 NT Header의 Optional Header에서 언급했던 DataDirectory입니다. DataDirectory는 IMAGE_OPTIONAL_HEADER 구조체의 NumberOfRvaAndSizes의 값만큼의 배열 개수를 갖습니다. 그리고 각 배열은 해당 DataDirectory의 VirtualAddress(4Byte), Size(4Byte)로 총 8Byte로 구성되어 있습니다.

해당 환경에서는 NumberOfRvaAndSizes의 값이 0x10(16)임으로 총 16개의 DataDirectory가 존재하고 총 크기는 0x80(0x10 * 8Byte)입니다. 그리고 배열의 각 항목마다 아래와 같은 값을 가집니다.

 

DataDirectory[0] = EXPORT Directory

DataDirectory[1] = IMPORT Directory

DataDirectory[2] = RESOURCE Directory

...

DataDirectory[9] = TLS Directory

...

 

따라서 아래 그림에서 블록지정한 부분이 IMAGE_OPTIONAL_HEADER32.DataDirectory[1]이고 해당 부분에서 0000A0A0이 IMAGE_IMPORT_DESCRIPTOR 구조체 배열의 시작 위치(IMPORT Directory의 RVA)입니다. 0000012C는 IMAGE_IMPORT_DESCRIPTOR(해당 IMAGE_DATA_DIRECTORY)의 크기입니다.

 

 

0000A0A0은 RVA임으로 RAW(file offset)으로 나타내면 94A0(RVA - VirtualAddress + PointerToRawData)입니다.

 

 

위 그림에서 블록지정한 부분이 IMAGE_IMPORT_DESCRIPTOR 구조체 배열이고 빨간색 Box 부분이 구조체 배열의 첫 번째 원소입니다. 구조체 배열의 마지막은 NULL 구조체로 되어 있습니다.

 

IMAGE_IMPORT_DESCRIPTOR 구조체는 다음과 같습니다. (크기 : 14 // 20 Bytes)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
  union {
    DWORD    Characteristics;            
    DWORD    OriginalFirstThunk;        // INT(Import Name Table) address (RVA)
  };
  DWORD    TimeDataStamp;
  DWORD    ForwarderChain;
  DWORD    Name;                        // library name string address (RVA)
  DWORD    FirstThunk;                  // IAT(Import Address Table) address (RVA)
} IMAGE_IMPORT_DESCRIPTOR;
 
typedef struct _IMAGE_IMPORT_BY_NAME {
  WORD    Hint;
  BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
cs

 

 

IMAGE_IMPORT_DESCRIPTOR의 첫 번째 원소의 중요 멤버를 살펴보면 아래 그림과 같습니다.

 

 

각 멤버의 값을 RAW로 나타내면 다음과 같습니다.

 

OriginalFirstThunk(INT) : 0000A28C => 0000968C

Name                    : 0000A27C => 0000967C

FirstThunk(IAT)         : 00001000 => 00000400

 

 

2.1 라이브러리 이름(Name)

 

먼저 Name 항목의 offset을 따라가 보겠습니다.

Name 항목은 Import 함수가 소속된 라이브러리 파일의 이름 문자열 포인터입니다.

 

아래 그림과 같이 "ADVAPI32.dll" 문자열을 확인할 수 있습니다.

 

 

2.2 INT(Import Name Table)

 

이번에는 OriginalFirstThunk(INT) 항목의 offset을 따라가 보겠습니다.

INT는 Import하는 함수의 정보(Ordinal, Name)가 담긴 구조체 포인터 배열로 IMAGE_IMPORT_BY_NAME 구조체를 가리키고 있습니다.

 

아래 그림의 블록지정한 부분이 INT이며 주소 배열 형태로 되어 있습니다. (배열의 끝은 NULL) 주소 값 하나하나가 각각의 IMAGE_IMPORT_BY_NAME 구조체를 가리키고 있습니다.

 

 

위 그림에서 확인한 0000A690(file offset:9A90)을 따라가보면 아래 그림과 같이 첫 번째 IMAGE_IMPORT_BY_NAME 구조체를 확인할 수 있습니다.

 

 

첫 번째 2Bytes가 Ordinal로, 라이브러리에서 함수의 고유번호이고 그 뒤에 문자열("RegSetValueExW")이 함수 이름입니다.

 

 

2.3 IAT(Import Address Table)

 

마지막으로 FirstThunk(IAT)의 offset을 따라가 보겠습니다.

아래 그림에서 블록지정한 부분이 "ADVAPI32.dll" 라이브러리에 해당하는 IAT 배열 영역입니다. INT와 마찬가지로 구조체 포인터 배열 형태로 되어 있으며 배열은 NULL로 끝납니다.

 

빨간색 Box 부분의 주소가 ADVAPI32.dll!RegSetValueExW 함수의 주소입니다.

 

 

 

실제 ADVAPI32.dll!RegSetValueExW 함수의 주소가 맞는지 확인하기 위해 OllyDbg를 이용하여 해당 함수의 Address를 살펴보면 저와 분석 환경이 동일한 경우 77C71436이 아닌 다른 주소일 것입니다. 이것은 Windows Vista, 7에서 추가된 ASLR(Address Space Layout Randomization) 기법 때문입니다.

 

사실 77C71436이라는 값은 의미 없는 값으로, notepad.exe 파일이 메모리에 로딩될 때 해당 주소 부분이 정확한 함수의 주소 값으로 대체됩니다.

 

 


 

 

3. EAT(Export Address Table)

 

라이브러리의 EAT를 설명하는 IMAGE_EXPORT_DIRECTORY 구조체는 PE 파일에 하나만 존재합니다.

IAT와 마찬가지로 IMAGE_EXPORT_DIRECTORY 구조체는 PE Body에 위치하며 그 위치는 PE Header의 IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress값 입니다.

 

분석 대상은 kernel32.dll 입니다.

 

아래 그림은 kernel32.dll의 IMAGE_OPTIONAL_HEADER32.DataDirectory[0] 입니다. 빨간색 Box 영역은 DataDirectory이고 블록지정한 부분인 IMAGE_OPTIONAL_HEADER32.DataDirectory[0] 입니다.

 

 

000B5924가 IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress로 IMAGE_EXPORT_DIRECTORY 구조체의 시작 주소입니다.

 

해당 환경에서는 IMAGE_SECTOR_HEADER 구조체의 VirtualAddress와 PointerToRawData가 동일함으로 00B5924의 offset 역시 00B5924입니다.

 

00B5924로 이동하면 아래 그림과 같이 IMAGE_EXPORT_DIRECOTRY 구조체를 확인할 수 있습니다. 빨간색 Box 부분은 중요 멤버를 표시한 것입니다.

 

 

IMAGE_EXPORT_DIRECTORY 구조체는 다음과 같습니다. (크기 : 28 // 40 Bytes)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DESCRIPTOR {
  DWORD    Characteristics;
  DWORD    TimeDateStamp;
  DWORD    MajorVersion;                     
  DWORD    MinorVersion;                  
  DOWRD    Name;
  DOWRD    Base;
  DOWRD    NumberOfFunctions;           // 실제 Export 함수 개수
  DOWRD    NumberOfNames;               // Export 함수 중에서 이름을 가지는 함수 개수
  DOWRD    AddressOfFunctions;          // Export 함수 주소 배열 (배열의 원소 개수 = NumberOfFunctions)
  DOWRD    AddressOfNames;              // 함수 이름 주소 배열 (배열의 원소 개수 = NumberOfNames)
  DOWRD    AddressOfNameOrdinals;       // Ordinal 배열 (배열의 원소 개수 = NumberOfNames)
} IMAGE_EXPORT_DESCRIPTOR, *PIMAGE_EXPORT_DIRECTORY;
cs

 

중요 멤버의 값과 offset 값은 아래와 같습니다.

(VirtualAddress와 PointerToRawData가 동일함으로 RVA와 offset은 동일)

 

NumberOfFunctions     : 00000555

NumberOfNames         : 00000555

AddressOfFunctions    : 000B594C => B594C

AddressOfNames        : 000B6EA0 => B6EA0

AddressOfNameOrdinals : 000B83F4 => B83F4

 

 

3.1 함수 이름 배열

 

AddressOfName의 offset인 B594C로 이동하면 아래와 같이 Export 함수 이름의 주소로 이루어진 배열을 확인할 수 있고 배열 원소의 개수는 NumberOfNames(555)입니다.

 

 

 

3.2 함수 이름 찾기

 

위 그림의 Export 함수 주소 배열의 세 번째 원소의 값(000B8ED8)을 따라가 보면 아래 그림과 같이 "ActivateActCtx" 함수 이름의 문자열을 확인할 수 있습니다.

 

 

 

3.3 Ordinal 배열

 

"ActivateActCtx" 함수의 Ordinal 값을 알아내기 위해 AddressOfNameOrdinals의 offset으로 이동합니다. 아래 그림과 같이 2Bytes의 ordinal로 이루어진 배열을 확인할 수 있습니다.

 

"ActivateActCtx" 함수의 세 번째 Index(2)임으로 AddressOfNameOrdinals[2] = 4이고 따라서 Ordinal은 4입니다.

 

 

 

3.4 함수 주소 배열 - EAT

 

이제 "ActivateActCtx" 함수의 실제 주소를 확인하기 위해 AddressOfFunctions의 offset(B594C)으로 이동합니다. 아래 그림과 같이 실제 Export 함수 주소 배열을 확인할 수 있습니다.

3.3에서 구한 Ordinal을 배열 Index로 적용하면 0004556D RVA 값을 얻을 수 있고 해당 값에 kernel32.dll의 ImageBase(77DE0000)를 더하면 "ActivateActCtx" 함수의 실제 주소를 구할 수 있습니다.

 

 

 

 

[참고]

ReverseCore (www.reversecore.com)

Microsoft MSDN (https://msdn.microsoft.com)

 

 

 

'ReverseEngineering' 카테고리의 다른 글

PEB에서 로딩된 DLL 정보 찾기  (0) 2017.08.22