본문 바로가기
Reversing/리버싱 핵심 원리

[리버싱 핵심 원리] 13장, PE File Format

by Y06 2020. 9. 29.

13장, PE File Format

13.1. 소개

PE(Portable Excutable) 파일?

Windows 운영체제에서 사용되는 실행 파일 형식이다. 기존 UNIX에서 사용되는 COFF(Common Object File Format)를 기반으로 Microsoft에서 만들었다. 애초에는 다른 운영체제에 이식성을 좋게 하려고 만들었으나 현재는 Windows 계열의 OS에서만 사용되고 있다.

 

13.2.PE File Format

PE File Format

 

PE 파일의 종류
종류 주요 확장자
실행 계열 EXE, SCR
라이브러리 계열 DLL, OCX, CPL, DRV
드라이버 계열 SYS, VXD
오브젝트 파일 계열 OBJ

 

OBJ(오브젝트) 파일을 제외한 모든 것은 실행 가능한 파일이다. DLL, SYS 파일 등은 셀(Explorer.exe)에서 직접 수행할 수는 없지만, 다른 형태의 방법(디버거, 서비스, 기타)을 이용하여 실행이 가능한 파일이다.

 

 

노트패드(notepad.exe) 파일을 헥스 에디터(HxD)를 이용해서 열었다. 위에 사진은 notepad.exe 파일의 시작 부분이며, PE 파일의 헤더(PE header) 부분이다. 수 많은 정보(메모리 적재 방식, stack/heap 메모리의 크기 등)가 PE 헤더에 구조체 형식으로 저장되어 있다.

 

13.2.1.기본 구조

notepad.exe는 일반적인 PE 파일의 기본 구조(Basic Structure)이다.

위의 사진은 notepad.exe 파일이 메모리에 적재(loading 또는 mapping) 될 때의 모습을 나타낸 것이다.

DOS header부터 Section header까지를 PE 헤더, 그 밑의 Section들을 합쳐서 PE 바디(Body)라고 한다. 파일에서는 offset으로, 메모리에서는 VA(Virtual Address, 절대주소)로 위치를 표현한다. 파일이 메모리에 로딩되면 모양이 달라진다(Section의 크기, 위치 등). 파일의 내용은 보통 코드(.text), 데이터(.data), 리소스(.rsrc) 섹션에 나뉘어서 저장된다.

 

[참고]

개발도구(VB/VC++/Delphi/ect)와 빌드 옵션에 따라서 섹션의 이름, 크기, 개수, 저장 내용 등은 달라진다. 중요한 것은 각 용도별로 여러 섹션이 나뉘어서 저장된다는 것이다.

PE 헤더의 끝부분과 각 섹션의 끝에는 NULL padding이라고 불리우는 영역이 존재한다. 컴퓨터에서 파일, 메모리, 네트워크 패킷 등을 처리할 때 효율을 높이기 위해 최소 기본 단위 개념을 사용하는데, PE 파일에도 같은 개념이 적용된 것이다. 파일/메모리에서 섹션의 시작 위치는 각 파일/메모리의 최소 기본 단위의 배수에 해당하는 위치여야 하고, 빈 공간은 NULL로 채워버린다(위의 사진을 보면 각 섹션의 시작 주소가 어떤 규칙에 의해 딱딱 끊어지는 걸 볼 수 있다).

 

13.2.2. VA & RVA

VA(Virtual Address)는 프로세스 가상 메모리의 절대주소를 말하며, RVA(Relative Virtual Address)는 어느 기준 위치(ImageBase)에서부터의 상대주소를 말한다. VA와 RVA의 관계는 다음 식과 같다.

RVA + ImageBase = VA

PE 헤더 내의 정보는 RVA 형태로 된 것이 많다. 그 이유는 PE 파일(주로 DLL)이 프로세스 가상 메모리의 특정 위치에 로딩되는 순간 이미 그 위치에 다른 PE 파일(DLL)이 로딩되어 있을 수 있다. 그럴 때 재배치(Relocation) 과정을 통해서 비어 있는 다른 위치에 로딩되어야 하는데, 만약 PE 헤더 정보들이 VA(Virtual Address, 절대 주소)로 되어 있다면 정상적인 엑세스가 이루어지지 않을 것이다. 그러므로 정보를 RVA(Relative Virtual Address, 상대주소)로 해두면 Relocation이 발생해도 기준 위치에 대한 상대주소가 변하지 않기 때문에 아무런 문제 없이 원하는 정보에 엑세스할 수 있는 것이다.

 

13.3.PE 헤더

PE 헤더는 많은 구조체로 이루어졌다.

 

13.3.1.DOS Header

Microsoft는 PE File Format을 만들 때 당시에 널리 사용되던 DOS 파일에 대한 하위 호환성을 고려해서 만들었다. 그 결과로 PE 헤더의 제일 앞부분에는 기존 DOS EXE Header를 확장시킨 IMAGE_DOS_HEADER 구조체가 존재한다.

 

typedef struct_IMAGE_DOS_HEADER {
     WORD e_magic;                // DOS signature :4D5A ("MZ")
     WORD e_cblp; WORD e_cp;
     WORD e_crlc; WORD e_parhdr;
     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_res2[10];
     WORD e_lfanew;             // offset to NT header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

IMAGE_DOS_HEADER 구조체의 크기는 40h(64byte)이다. 이 구조체에서 꼭 알아둬야 할 중요한 멤버는 e_magic과 e_lfanew이다.

 

  • e_magic: DOS signature (4D5A => ASCII 값 "MZ")
  • e_lfanew: NT header의 옵셋을 표시 (가변적인 값을 가짐)

모든 PE 파일은 시작 부분(e_magic)에 DOS signature ("MZ")가 존재하고, e_lfanew 값이 가리키는 위치에 NT header 구조체가 존재해야 한다(NT header 구조체의 이름은 IMAGE_DOS_HEADER이다)

 

과연 PE 스펙에 맞게 파일 2byte는 4D5A이며, e_lfanew 값은 000000E0이다.

 

13.3.2.DOS Stub

DOS Header 밑에는 DOS Stub이 존재한다. DOS Stub의 존재여부는 옵션이며 크기도 일정하지 않다. DOS Stub은 코드와 코드와 데이터의 혼합으로 이루어져 있으며, 아래 그림에 notepad.exe의 DOS Stub이 나타난다. 

 

위 그림에서 붉은색으로 표시된 부분은 16 bit 어셈블리 명령어이다. 32 bit 어셈블리 명령어이다. 32 bit 윈도우즈에서는 이쪽 명령어가 실행되지 않습니다. (PE 파일오 인식하기 때문에 아예 이쪽 코드를 무시한다)

 

DOS 환경에서 실행하거나, DOS용 디버거(debug.exe)를 이용해서 실행할 수 있다.

 

명령 프롬프트(cmd)를 뜨워서 아래와 같이 명령어를 입력한다.

 

C:\WINDOWS>debug notepad.exe
-u
0D1E:0000 0E        PUSH    CS
0D1E:0001 1F        POP     DS
0D1E:0002 BA0E00    MOV     DX,000E   ; DX = 0E : "This program cannot be run in DOS mode"
0D1E:0005 B409      MOV     AH,09
0D1E:0007 CD21      INT     21        ; AH = 09 : WriteString()

0D1E:0009 B8014C    MOV     AX,4C01
0D1E:000C CD21      INT     21        ; AX = 4C01 : Exit()

코드는 매우 간단하다. 문자열을 출력하고 종료한다.

 

즉, notpad.exe는 32 bit 용 PE 파일이지만, MS-DOS 호환 모드를 가지고 있어서 DOS 환경에서 실행하면 "This program cannot be run in DOS mode" 문자열을 출력하고 종료한다.

 

이 특성을 잘 이용하면 하나의 실행(EXE) 파일에 DOS와 Windows에서 모두 실행 가능한 파일을 만들 수도 있다. 실제로 세계적인 보안업체 McAfee에서 무료로 배포했던 scan.exe라는 파일이 이와 같은 특징을 갖고 있다.

(DOS 환경에서는 16 bit DOS용 코드가 Windows 환경에서는 32 bit Windows 코드가 각각 실행된다.)

 

앞에서 말씀드린대로 DOS Stub은 옵션이기 때문에 개발 도구에서 지원해야 한다.

 

13.3.3.NT Header

NT header 구조체 IMAGE_NT_HEADERS이다.

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;                          // PE Signature : 50450000 ("PE"00)
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

위 구조체는 32 bit 용이며, 64 bit 용은 세 번째 멤버가 IMAGE_OPTIONAL_HEADER64이다.

 

IMAGE_NT_HEADER 구조체는 3개의 멤버로 되어 있다.

제일 첫 멤버는 Signature로서 50450000h ("PE"00) 값을 가진다(변경불가).

그리고 FileHeader와 OptionalHeader 구조체 멤버가 있다.

 

notepad.exe의 IMAGE_NT_HEADERS의 내용을 헥스 에디터(HxD)로 살펴보자.

 IMGE_NT_HEADERS 구조체의 크기는 F8h이다. 상당히 큰 구조체이다.

FileHeader와 OptionalHeader 구조체를 살펴볼 수 있다.

 

13.3.4.NT Header_File Header

파일의 개략적인 속성을 나타내는 IMAGE_FILE_HEADER 구조체이다.

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;

    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;

} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

IMAGE_FILE_HEADER 구조체에서 아래 4가지 멤버들이 중요하다.

이 값들이 정확하게 세팅되어 있지 않으면 파일은 정상적으로 실행되지 않습니다.

 

#1.Machine

Machine 넘버는 CPU 별로 고유한 값이며 32 bit Intel 호환 칩 14ch의 값을 가진다. 아래는 winnt.h 파일에 정의된 Machine 넘버의 값들이다.

#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000             0x0166  // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000            0x0168  // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2         0x0169  // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA             0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3               0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB             0x01c2
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64

#2. NumberOfSections

PE 파일은 코드, 데이터, 리소스 등이 각각의 섹션에 나뉘어서 저장된다고 설명했었다.

NumberOfSections는 바로 그 섹션의 개수를 나타낸다.

 

이 값은 반드시 0보다 커야 한다.

 

정의된 섹션 개수보다 실제 섹션이 적다면 실행 에러가 발생한다.

정의된 섹션 개수보다 실제 섹션이 많다면 정의된 개수만큼만 인식된다.

 

#3. SizeOfOptionalHeader

IMAGE_NT_HEADER 구조체의 마지막 멤버는 IMAGE_OPTIONAL_HEADER32 구조체이다.

SizeOptionalHeader 멤버는 바로 이 IMAGE_OPTIONAL_HEADER32 구조체의 크기를 나타낸다.

 

IMAGE_OPTIONAL_HEADER32는 C언어의 구조체이기 때문에 이미 그 크기가 결정되었다.

그런데 Windows의 PE Loader는 IMAGE_FILE_HEADER의 SizeOfOptionalHeader 값을 보고 IMAGE_OPTIONAL_HEADER32 구조체를 인식한다.

 

[참고]

IMAGE_DOS_HEADER 의 e_lfanew 멤버와 IMAGE_FILE_HEADER 의 SizeOfOptionalHeader 멤버 때문에
일반적인(상식적인) PE 파일 형식을 벗어나는 일명 '꽈배기' PE 파일(PE Patch) 이 만들 수 있다.

#4. Characteristics

파일의 속성을 나타내는 값으로써, 실행이 가능한 형태인지(executable or not) 혹은 DLL 파일인지 등의 정보들이 bit OR 형식으로 조합된다.

 

아래는 winnt.h 파일에 정의된 Characteristics 값들이다. (0002h와 2000h 의 값을 기억해야 한다.)

 

#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  
                                                     // (i.e. no unresolved externel references).

#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008  // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED            0x0200  // Debugging info stripped from 
                                                     // file in .DBG file

#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // If Image is on removable media, 
                                                     // copy and run from the swap file.

#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // If Image is on Net, 
                                                     // copy and run from the swap file.

#define IMAGE_FILE_SYSTEM                    0x1000  // System File.
#define IMAGE_FILE_DLL                       0x2000  // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // Bytes of machine word are reversed.

Q.참고로 PE 파일 중에 Characteristics 값에 0002h가 없는 경우(not executable)가 있을까?

A.있다. 예를 들어 *.obj와 같은 object 파일 및 resource DLL 같은 파일이 그런 경우이다.

 

IMAGE_FILE_HEADER의 TimeDateStamp 멤버에 대해서 설명하면, 이 값은 파일의 실행에 영향을 미치지 않는 값으로써 해당 파일의 빌드 시간을 나타낸 값이다.

단, 개발 도구에 따라서 이 값을 세팅해주는 도구(VB, VC++)가 있고, 그렇지 않은 도구(Delphi)가 있다.

 

IMAGE_FILE_HEADER

 

위 사진은 헥스 에디터(HxD)로 봤을 때, 이를 알아보기 쉽게 구조체 멤버로 표현하면 아래와 같다.

[ IMAGE_FILE_HEADER ] - notepad.exe

 offset   value   description

-------------------------------------------------------------------------------
000000E4     014C machine
000000E6     0003 number of sections
000000E8 48025287 time date stamp (Mon Apr 14 03:35:51 2008)
000000EC 00000000 offset to symbol table
000000F0 00000000 number of symbols
000000F4     00E0 size of optional header
000000F6     010F characteristics
                      IMAGE_FILE_RELOCS_STRIPPED
                      IMAGE_FILE_EXECUTABLE_IMAGE
                      IMAGE_FILE_LINE_NUMS_STRIPPED
                      IMAGE_FILE_LOCAL_SYMS_STRIPPED
                      IMAGE_FILE_32BIT_MACHINE

13.3.5.NT Header_Optional Header

PE 헤더 구조체 중에서 가장 크기가 큰 IMAGE_OPTIONAL_HEADER32이다.

 

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;

    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;
    DWORD   ImageBase;

    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

IMAGE_OPTIONAL_HEADER32 구조체에서 주목해야 할 멤버는 아래와 같다.

이 값들 역시 파일 실행에 필수적인 값들이라서 잘못 세팅되면 파일이 정상 실행 되지 않는다.

 

#1. Magic

IMAGE_OPTIONAL_HEADER32인 경우, 10Bh, IMAGE_OPTIONAL_HEADER64인 경우 20Bh 값을 가지게 된다.

 

#2. AddressOfEntryPoint

EP(Entry Point)의 RVA(Relative Virtual Address) 값을 가지고 있다.

 

#3. ImageBase

프로세스의 가상 메모리는 0 ~ FFFFFFFFh 범위이다(32 bit의 경우). ImageBase는 이렇게 광활한 메모리내에서 PE 파일이 로딩(매핑)되는 시작 주소를 나타낸다.

 

EXE, DLL 파일은 user memory 영역인 0~7FFFFFFFh 범위에 위치하고, SYS 파일은 kernel memory 영역인 80000000h~FFFFFFFh 범위에 위치한다.

 

일반적으로 개발도구(VB/VC++/Delphi)들이 만들어내는 EXE 파일의 ImageBase 값은 00400000h이고, DLL 파일의 ImageBase 값은 01000000h이다.

 

PE loader는 PE 파일을 실행시키기 위해 프로세스를 생성하고 파일을 메모리에 로딩(매핑) 시킨 후, EIP 레지스터 값을 ImageBase + AddressOfEntryPoint 값으로 세팅한다.

 

#4.SectionAlignment, FileAlignment

PE 파일은 섹션으로 나뉘어져 있는데 파일에서 섹션의 최소단위를 나타내는 것이 FileAlignment 이고 메모리에서 섹션의 최소단위를 나타내는 것이 SectionAlignment 이다.

따라서 파일/메모리의 섹션 크기는 반드시 각각 FileAlignment/SectionAlignment 의 배수가 되어야 한다.

#5. SizeOfImage
PE 파일이 메모리에 로딩되었을 때 가상 메모리에서 PE Image 가 차지하는 크기를 나타낸다.
일반적으로 파일의 크기와 메모리에 로딩된 크기는 다르다. 각 섹션의 로딩 위치와 메모리 점유 크기는 나중에 소개할 Section Header에 정의 되어 있다.

#6. SizeOfHeader

PE header의 전체 크기를 나타낸다. 이 값은 FileAlignment의 배수여야 한다. 파일 시박에서 SizeOfHeader 옵셋만큼 떨어진 위치에 첫 번째 섹션이 위치한다.

 

#7. Subsystem
1 : Driver file (*.sys)
2 : GUI (Graphic User Interface) 파일 -> notepad.exe 와 같은 윈도우 기반 어플리케이션
3 : CUI (Console User Interface) 파일 -> cmd.exe 와 같은 콘솔 기반 어플리케이션


#8. NumberOfRvaAndSizes

마지막 멤버인 DataDirectory 배열의 개수

 

구조체 정의에 분명히 배열 개수가 IMAGE_NUMBEROF_DIRECTORY_ENTRIES (16)이라고 명시되어 있지만, PE loader는 NumberOfRvaAndSizes의 값을 보고 배열의 크기를 인식한다.

 

#9. DataDirectory

IMAGE_DATA_DIRECTORY 구조체의 배열로써, 배열의 각 항목마다 정의된 값을 가지게 된다

아래에 각 배력 항목을 나열한다.

DataDirectory[0] = EXPORT Directory         
DataDirectory[1] = IMPORT Directory         
DataDirectory[2] = RESOURCE Directory       
DataDirectory[3] = EXCEPTION Directory      
DataDirectory[4] = SECURITY Directory       
DataDirectory[5] = BASERELOC Directory      
DataDirectory[6] = DEBUG Directory          
DataDirectory[7] = COPYRIGHT Directory      
DataDirectory[8] = GLOBALPTR Directory      
DataDirectory[9] = TLS Directory            
DataDirectory[A] = LOAD_CONFIG Directory    
DataDirectory[B] = BOUND_IMPORT Directory   
DataDirectory[C] = IAT Directory            
DataDirectory[D] = DELAY_IMPORT Directory   
DataDirectory[E] = COM_DESCRIPTOR Directory 
DataDirectory[F] = Reserved Directory  

여기서 말하는 Directoty란 어떤 구조체의 배열이라고 생각하면 된다.

 

빨간색으로 표시한 EXPORT, IMPORT, RESOURCE, TLS Directory를 눈여겨 보자.

특히 IMPORT와 EXPORT Directory 구조는 PE header에서 매우 중요하다.

 

아래 사진에서 실제로 notepad.exe의 IMAGE_OPTIONAL_HEADER32를 확인할 수 있다.

구조체 멤버별 값과 설명은 아래와 같다.

[ IMAGE_OPTIONAL_HEADER ] - notepad.exe

 offset   value   description
-------------------------------------------------------------------------------
000000F8     010B magic
000000FA       07 major linker version
000000FB       0A minor linker version
000000FC 00007800 size of code
00000100 00008C00 size of initialized data
00000104 00000000 size of uninitialized data
00000108 0000739D address of entry point
0000010C 00001000 base of code
00000110 00009000 base of data
00000114 01000000 image base
00000118 00001000 section alignment
0000011C 00000200 file alignment
00000120     0005 major OS version
00000122     0001 minor OS version
00000124     0005 major image version
00000126     0001 minor image version
00000128     0004 major subsystem version
0000012A     0000 minor subsystem version
0000012C 00000000 win32 version value
00000130 00014000 size of image
00000134 00000400 size of headers
00000138 000126CE checksum
0000013C     0002 subsystem
0000013E     8000 DLL characteristics
00000140 00040000 size of stack reserve
00000144 00011000 size of stack commit
00000148 00100000 size of heap reserve
0000014C 00001000 size of heap commit
00000150 00000000 loader flags
00000154 00000010 number of directories
00000158 00000000 RVA  of EXPORT Directory
0000015C 00000000 size of EXPORT Directory
00000160 00007604 RVA  of IMPORT Directory
00000164 000000C8 size of IMPORT Directory
00000168 0000B000 RVA  of RESOURCE Directory
0000016C 00008304 size of RESOURCE Directory
00000170 00000000 RVA  of EXCEPTION Directory
00000174 00000000 size of EXCEPTION Directory
00000178 00000000 RVA  of SECURITY Directory
0000017C 00000000 size of SECURITY Directory
00000180 00000000 RVA  of BASERELOC Directory
00000184 00000000 size of BASERELOC Directory
00000188 00001350 RVA  of DEBUG Directory
0000018C 0000001C size of DEBUG Directory
00000190 00000000 RVA  of COPYRIGHT Directory
00000194 00000000 size of COPYRIGHT Directory
00000198 00000000 RVA  of GLOBALPTR Directory
0000019C 00000000 size of GLOBALPTR Directory
000001A0 00000000 RVA  of TLS Directory
000001A4 00000000 size of TLS Directory
000001A8 000018A8 RVA  of LOAD_CONFIG Directory
000001AC 00000040 size of LOAD_CONFIG Directory
000001B0 00000250 RVA  of BOUND_IMPORT Directory
000001B4 000000D0 size of BOUND_IMPORT Directory
000001B8 00001000 RVA  of IAT Directory
000001BC 00000348 size of IAT Directory
000001C0 00000000 RVA  of DELAY_IMPORT Directory
000001C4 00000000 size of DELAY_IMPORT Directory
000001C8 00000000 RVA  of COM_DESCRIPTOR Directory
000001CC 00000000 size of COM_DESCRIPTOR Directory
000001D0 00000000 RVA  of Reserved Directory
000001D4 00000000 size of Reserved Directory

13.3.6.섹션 헤더

섹션의 속성(property)을 정의한 것이 섹션 헤더이다.

E 파일을 여러개의 section 구조로 만들었을때 (제가 생각하는) 장점은 바로 프로그램의 안정성이다.

가령 문자열 data 에 값을 쓰다가 어떤 이유로 overflow 가 발생(버퍼 크기를 초과해서 입력) 했을때 
바로 다음의 code (명령어) 를 그대로 덮어써버릴 것이다. 

즉, code/data/resource 마다 각각의 성격(특징, 엑세스 권한)이 틀리다는 것을 알게 된 것이다.

  • code - 실행, 읽기 권한
  • data - 비실행, 읽기, 쓰기 권한
  • resource - 비실행, 읽기 권한


그래서 PE 파일 포멧 설계자들은 비슷한 성격의 자료를 section 이라고 이름 붙인 곳에 모아두기로 결정하였고,
각각의 section 의 속성을 기술할 section header 가 필요하게 된 것이다.
(section 의 속성에는 file/memory 에서의 시작위치, 크기, 엑세스 권한 등이 있어야 한다.)

 

IMAGE_SECTION_HEADER

 

섹션헤더는 각 섹션 별 IMAGE_SECTION_HEADER 구조체의 배열로 되어있다.

#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;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;

    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

 

IMAGE_SECRION_HEADER 구조체를 알아야 할 중요 멤버

  • VirtualSize      : 메모리에서 섹션이 차지하는 크기
  • VirtualAddress   : 메모리에서 섹션의 시작 주소 (RVA)
  • SizeOfRawData    : 파일에서 섹션이 차지하는 크기
  • PointerToRawData : 파일에서 섹션의 시작 위치
  • Characteristics  : 섹션의 특징 (bit OR)

VirtualAddress와 PointerToRawData의 값은 아무 값이나 가질 수 없고. 각각 (IMAGE_OPTIONAL_HEADER32에 정의된) SectionAlignment와 FileAlignment에 맞게 결정된다.

 

VirtualSize와 SizeOfRawData는 일반적으로 서로 다른 값을 가진다. 즉 파일에서의 섹션 크기와 메모리에 로딩된 섹션의 크기는 다른다는 것이다.

Characteristics는 표시된 값들의 조합(bit OR)으로 이루어진다.

 

#define IMAGE_SCN_CNT_CODE                   0x00000020  // Section contains code.
#define IMAGE_SCN_CNT_INITIALIZED_DATA       0x00000040  // Section contains initialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA     0x00000080  // Section contains uninitialized data.
#define IMAGE_SCN_MEM_EXECUTE                0x20000000  // Section is executable.
#define IMAGE_SCN_MEM_READ                   0x40000000  // Section is readable.
#define IMAGE_SCN_MEM_WRITE                  0x80000000  // Section is writeable.

마지막으로 name 항목을 보면 Name 멤버는 언어의 문자열처럼 NULL로 끝나지 않는다. 또한 ASCII 값만 와야 한다는 제한도 없다. PE 스펙에는 섹션 Name에 대한 어떠한 명시적인 규칙도 없기 때문에 어떠한 값을 넣어도 되고 심지어 NULL로 채워도 된다. 따라서 섹션의 Name은 그냥 참고용이기 때문에 어떤 정보로써 활용하기에는 100% 장담할 수 없다.

notepad.exe의 IMAGE_SECTION_HEADER 구조체 배열

[ IMAGE_SECTION_HEADER ]

 offset   value   description
-------------------------------------------------------------------------------
000001D8 2E746578 Name (.text)

000001DC 74000000
000001E0 00007748 virtual size
000001E4 00001000 RVA
000001E8 00007800 size of raw data
000001EC 00000400 offset to raw data
000001F0 00000000 offset to relocations
000001F4 00000000 offset to line numbers
000001F8     0000 number of relocations
000001FA     0000 number of line numbers
000001FC 60000020 characteristics
                    IMAGE_SCN_CNT_CODE
                    IMAGE_SCN_MEM_EXECUTE
                    IMAGE_SCN_MEM_READ

00000200 2E646174 Name (.data)
00000204 61000000
00000208 00001BA8 virtual size
0000020C 00009000 RVA
00000210 00000800 size of raw data
00000214 00007C00 offset to raw data
00000218 00000000 offset to relocations
0000021C 00000000 offset to line numbers
00000220     0000 number of relocations
00000222     0000 number of line numbers
00000224 C0000040 characteristics
                    IMAGE_SCN_CNT_INITIALIZED_DATA
                    IMAGE_SCN_MEM_READ
                    IMAGE_SCN_MEM_WRITE

00000228 2E727372 Name (.rsrc)
0000022C 63000000
00000230 00008304 virtual size
00000234 0000B000 RVA
00000238 00008400 size of raw data
0000023C 00008400 offset to raw data
00000240 00000000 offset to relocations
00000244 00000000 offset to line numbers
00000248     0000 number of relocations
0000024A     0000 number of line numbers
0000024C 40000040 characteristics
                             IMAGE_SCN_CNT_INITIALIZED_DATA
                             IMAGE_SCN_MEM_READ

notepad.exe.의 IMAGE_SECTION_HEADER 구조체 배열의 실제 값

 

[참고]

PE 파일 설명에서 자주 등장하는 이미지(Image)라는 용어를 잘 알아야 한다. PE 파일이 메모리에 로딩될 때 파일이 그대로 올라가는 것이 아니라, 섹션 헤더에 정의된 대로 섹션 시작 주소, 섹션 크기 등에 맞춰서 올라간다. 따라서 파일에서의 PE와 메모리에서의 PE는 서로 다른 모양을 가진다. 이를 구별하기 위해서 메모리에 로딩된 상태를 이미지라는 용어를 사용해서 구별한다.

13.4. RVA to RAW

PE 파일이 메모리에 로딩되었을 때 각 섹션에서 메모리의 주소(RVA)와 파일 옵셋을 잘 매핑할 수 있어야 하는데, 이러한 매핑을 일반적으로 "RVA to RAW"라고 한다.

 

[방법]

1) RVA가 속해 있는 섹션을 찾는다.

2) 간단한 비례식을 사용해서 파일 옵셋(RAW)을 계산한다.

 

IMAGE_SECTION_HEADER 구조체에 의하면 비례식은 아래와 같다.

RAW - PointerToRawData = RVA - VirtualAddress
                   RAW = RVA - VirtualAddress + PointerToRawData 

Q1)  RVA = 5000h 일때 File Offset = ?
A1) 먼저 해당 RVA 값이 속해 있는 섹션을 찾아야 한다.
      => RVA 5000 는 첫번째 섹션(.text)에 속해있다. (ImageBase 01000000h 를 고려하세요.)

      비례식 사용
      => RAW = 5000h(RVA) - 1000h(VirtualAddress) + 400h(PointerToRawData) = 4400

Q2) RVA = 13314h 일때 File Offset = ?
A2) 해당 RVA 값이 속해 있는 섹션을 찾는다.
      => 세번째 섹션(.rsrc)에 속해있다.

      비례식 사용
      => RAW = 13314(RVA) - B000(VA) + 8400(PointerToRawData) = 10714

Q3) RVA = ABA8h 일때 File Offset = ?
A2) 해당 RVA 값이 속해 있는 섹션을 찾는다.
      => 두번째 섹션(.data)에 속해있다.

 비례식 사용
      => RAW = ABA8(RVA) - 9000(VA) + 7C00(PointerToRawData) = 97A8 (X)
      => 계산 결과로 RAW = 97A8가 나왔지만, 이 옵셋은 세번째 섹션(.rsrc)에 속해 있다.

           RVA 는 두번째 섹션이고, RAW 는 세번째 섹션이라면 말이 안 된다.
           이 경우에 "해당 RVA(ABA8h)에 대한 RAW 값은 정의할 수 없다" 라고 해야 한다.
           이런 이상한 결과가 나온 이유는 위 경우에 두번째 섹션의 VirtualSize 값이 SizeOfRawData 값 보다 크기 때문이다.

 

13.5_IAT

IAT(Import Address Table) : 프로그램이 어떤 라이브러리에서 어떤 함수를 사용하고 있는지 기술한 테이블

 

DLL(Dynamic Linked Library) :

프로그램에 라이브러리를 포함시키지 말고 별도의 파일(DLL)로 구성하여 필요할 때마다 불러 쓸 수 있다. 일단 한 번 로딩된 DLL의 코드, 리소스는 Memory Mapping 기술로 려어 프로세스에서 공유해서 쓸 수 있다. 라이브러리가 업그레이드 되었을 때 해당 DLL 파일만 교체하면 되기 때문에 쉽고 편하다. 

 

[ DLL의 로딩 방식 ]

첫 번째, 프로그램에서 사용되는 순간에 로딩하고 사용이 끝나면 메모리에서 해제되는 방법(Explicit Linking)

두 번째, 프로그램이 시작할 때 같이 로딩되어 프로그램 종료할 때 메모리에서 해제되는 방법(Implicit Linking)

 

- IAT는 Implicit Linking에 대한 매커니즘을 제공하는 역할을 한다.

 

typedef struct _IMAGE_IMPORT_DESCRIPTIOR
{
    union{
        DWORD Characteristics;

        DWORD OriginalFirstThunk;            // INT(Import Name Table)의 주소(RVA)

    };

 
DWORD TimeDataStamp;
DWORD ForwarderChain;
DWORD Name;                                   // Library 이름 문자열의 주소(RVA)
DWORD FirstThunk;                             // IAT(Import Address Table)의 주소(RVA) => Table == 배열
}IMAGE_IMPORT_DESCRIPTOR;


typedef struct _IMAGE_IMPORT_BY_NAME
{
    WORD Hint;                                    // ordinal
    BYTE Name[1];                                 // function name string 

}IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
항목 항목
OriginalFirstThunk INT(Import Name Table)의 주소(RVA)
Name Library 이름 문자열의 주소(RVA)
FirstThunk IAT(Import Address Table)의 주소(RVA)

- INT와 IAT의 크기는 같아야 한다.

- INT와 IAT는 long type(4바이트 자료형) 배열이고 NULL로 끝난다. (크기가 따로 명시되어 있지 않다.)

 

1. 라이브러리 이름(Name)

Name 항목: 임포트 함수가 소속된 라이브러리 파일의 이름 무나열 포인터

 

2. OrigialFirstThunk - INT(Import Name Table)

INT : 임포트 하는 함수의 정보(ordinal, Name)가 담긴 구조체 포인터

 

3. FirstThunk - IAT(Import Address Table)

 


13.6_EAT

1. EAT (Export Address Table)

라이브러리 파일에서 제공하는 함수를 다른 프로그램에서 가져다 사용할 수 있도록 해주는 핵심 메커니즘이다.

PE 파일 내의 큭정 구조체 IMAGE_EXPORT_DIRECTORY에 익스포트 정보를 저장하고 있다.

typedef struct _IMAGE_EXPORT_DIRECTORY
{
    DWORD Characteristics; 
    DWORD TimeDateStamp; //creation time data stamp
    WORD MajorVersion;
    WORD MinorVersion;
    DWORD Name; //address of library file name
    DWORD Base; //ordinal base
    DWORD NumberOfFunctions; //number of functions  //실제 Export 함수 개수
    DWORD NumberOfNames; //number of names   //Export 함수 중에서 이름을 가지는 함수 개수
    DWORD AddressOfFunctions; //address of function start address array 
                       //Export 함수 주소 배열(배열의 원소 개수 == NumberOfFunctions)

    DWORD AddressOfNames; //address of function name string array 
                      //함수 이름 주소 배열(배열의 원소 개수 == NumberOfNames)

    DWORD AddressOfNameOrdinals; //address of ordinal array
                      //Ordianl 배열(배열의 원소 개수 == NumberOfNames)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

[ 참고]

 

GetProcAddress() 동작원리

1. AddressOfNames 멤버를 이용해 '함수 이름 배열'로 간다.

2. '함수 이름 배열'은 문자열 주소가 저장되어 있다. 문자열 비교(strcmp)를 통하여 원하는 함수 이름을 찾는다. (이때 매열의 인덱스를 name_index라고 한다.)

3. AddressOfNameOrdinals 멤버를 이용해 'ordinal 배열'로 간다.

4. 'ordinal 배열'에서 name_index로 해당 ordinal 값을 찾는다.

5. AddressOfFunctions 멤버를 이용해 '함수 주소 배열(EAT)'로 간다.

6. '함수 주소 배열(EAT)'에서 아까 구한 ordinal을 배열 인덱스로 하여 원하는 함수의 시작 주소를 얻는다.


 

13.7_Advanced PE

PE 스펙 : 권장 스펙

Patched PE : PE 스펙에 어긋나지는 않지만 굉장히 창의적인 PE 헤더를 가진 파일 (PE 헤더를 이러저리 꼬아놓은 형태)