먼저 적기에 앞서 winnt.h 헤더 파일을 올려놓겠습니다.


PE File Format

이번에 정리할 것은 윈도우 리버싱에서 알아두어야할 PE 파일이다.
PE는 Portable Excutable)의 줄임말이며,
윈도우에서 사용되는 실행 가능한 파일 형식입니다.
PE 파일은 EXE 파일을 말하는 것이 아니라 SCR, DLL, OCX, SYS, OBJ 들도 포함이 됩니다.
PE 구조를 이해는 API 후킹, 압축 실행 등과 같은 고-오-급 리버싱 기법들의 기본 바탕이 되기도 합니다.
고로 리버싱을 하겠다 하면 이 PE 구조에 대해 빡삭한 지식을 가지고 있어야됩니다.

Preview


어느 실행파일의 PE 구조입니다.

이 PE 헤더에는 이 실행파일을 실행하기 위한 여러가지 정보가 담겨있으며,

PE의 내용을 가지고 DLL을 로드하거나, 리소스를 할당하고 그 외에도 많은 정보들이 담겨있습니다.

이렇게 중요한 정보들이 담겨있는 만큼 PE파일이 잘못된 데이터로 누락하게 되면 실행파일은 실행이 안될 수도 있습니다.


Basic PE File

PE 구조에 대해 알아보도록 하겠습니다.

먼저 이미지를 첨부하고... (쮸글쮸글)
아, 참고로 구글에 검색하고 괜찮은 이미지 긁어왔습니다.

[ 출처 : rednooby.tistory.com ]


PE 구조는 차례대로 DOS Header, DOS Stub, NT Header(File Header, Optional Header), Section Header로 나뉘며,

그 뒤로는 코드를 포함하는 (.text) Section, 

(전역, 정적) 변수를 포함하고 있는 (.data) Section, 

문자열이나 리소스 데이터를 포함하는 (.rsrc) Section이 등장합니다.

그리고 이러한 섹션들 끝에는 NULL padding 이라는 것이 존재합니다.


VA & RVA

섹션마다 
가상메모리에서 해당 섹션을 차지하는 크기(VirtualSize), 
가상 메모리 오프셋(VirtualOffset), 
파일에서 해당 섹션이 차지하는 크기(RawSize), 
파일 오프셋(RawOffset) 들이 있습니다.

가상 메모리의 주소(Virtual Address)

응용 프로그램의 가상 메모리의 절대 주소를 의미합니다.
즉, 이 VA는 로딩되었을 때의 가상 메모리에서의 절대주소입니다.

ImageBase에서부터의 상대주소(Relative Virtual Address)

응용 프로그램의 가상 메모리에서의 상대주소를 의미합니다.
상대주소라고 하면 ImageBase의 기준으로부터 상대주소를 의미합니다.

RVA와 VA의 관계식(?)

RVA + ImageBase = VA

RVA와 RAW의 비례식

RAW - PointerToRawData = RVA - Virtual Address
RAW = RVA - Virtual Address + PointerToRawData

이 비례식에 존재하는 VirtualAddress는 우리가 지금껏 말하고 있던 VA(Virtual Address)가 아닙니다.
VA는 프로그램의 메모리에서의 절대주소라고 적었습니다.
위에 있는 RAW를 구하기 위한 식에서 나오는 VA(Virtual Address)는 Section Header의 멤버인 Virtual Address를 의미합니다. 
Section Header 구조체의 멤버인 VirtualAddress 멤버는 메모리에서 섹션의 시작 주소(RVA)를 의미합니다. 
결론적으로 RAW 비례식에서의 VirtualAddress는 우리가 앞에서 계속 얘기하던 RVA (프로세스 메모리에서의 상대주소)를 말하는 것입니다. 

[ 주황색 말 출처 : 리버싱 핵심원리 ] 

PE 헤더 (Portable Executable Header)

이 헤더라는 것들은 여러가지 필드로 이루어진 하나의 구조체라고 보시면 됩니다.
PE에는 처음에 봤듯이 여러가지 헤더들이 앞에 자리잡고 있으므로 이 PE는 많은 구조체 덩어리라고 말 할 수 있습니다.


먼저 이 헤더들(IMAGE_DOS_HEADER, MS-DOS Stub Program, IMGAGE_NT_HEADER)을 차례대로 살펴보도록하겠습니다.


IMAGE_DOS_HEADER

가장 처음으로 등장하는 IMAGE_DOS_HEADER 입니다.
아래의 코드는 winnt.h 헤더에 있는 도스 헤더의 구조체입니다.


#define IMAGE_DOS_SIGNATURE                 0x5A4D      // MZ

typedef struct _IMAGE_DOS_HEADER {
    WORD  e_magic;      /* 00: MZ Header signature */
    WORD  e_cblp;       /* 02: Bytes on last page of file */
    WORD  e_cp;         /* 04: Pages in file */
    WORD  e_crlc;       /* 06: Relocations */
    WORD  e_cparhdr;    /* 08: Size of header in paragraphs */
    WORD  e_minalloc;   /* 0a: Minimum extra paragraphs needed */
    WORD  e_maxalloc;   /* 0c: Maximum extra paragraphs needed */
    WORD  e_ss;         /* 0e: Initial (relative) SS value */
    WORD  e_sp;         /* 10: Initial SP value */
    WORD  e_csum;       /* 12: Checksum */
    WORD  e_ip;         /* 14: Initial IP value */
    WORD  e_cs;         /* 16: Initial (relative) CS value */
    WORD  e_lfarlc;     /* 18: File address of relocation table */
    WORD  e_ovno;       /* 1a: Overlay number */
    WORD  e_res[4];     /* 1c: Reserved words */
    WORD  e_oemid;      /* 24: OEM identifier (for e_oeminfo) */
    WORD  e_oeminfo;    /* 26: OEM information; e_oemid specific */
    WORD  e_res2[10];   /* 28: Reserved words */
    DWORD e_lfanew;     /* 3c: Offset to extended header */
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;


위 구조체에서는 데이터 타입이 WORD가 16개, WORD 배열이 2개, DWORD가 1개가 존재합니다.
이 헤더에서는 2개의 필더만 유심히 보시면 됩니다.
e_magic, e_lfanew 2개 입니다.


e_magic 

e_magic 필드는 PE 파일이 맞는지 아닌지 체크할 때 사용되며,
처음부터 2byte를 보면 MZ(4D 5A)인 IMAGE_DOS_SIGNATURE로 시작되는 부분이 emagic이 차지하는 공간입니다.

e_lfanew

e_lfanew 필드는 IMAGE_NT_HEADER의 시작 오프셋을 가지고 있으며, 
이 값은 고정되어 있는 값이 아니라 파일에 따라 값이 변경됩니다.
즉 PE 헤더(IMAGE_NT_HEADER)의 주소는 IMAGE_DOS_HEADER의 e_lfanew 필드를 참조하여 알아낼 수 있다는 것입니다.
위에 있는 이 프로그램의 e_lfanew 필드의 값은 0x000000F0이라는 것을 알 수 있습니다.

DOS Stub

스텁 코드(Stub Code)


위의 영역이 DOS Stub 영역입니다.
저 영역을 자세히 보시면 "This program cannot be run in DOS mode" 라는 문자열을 볼 수 있으며,
도스 모드에서 이 파일이 실행되는 것을 막기 위한 것 입니다.
그냥 이 영역은 그다지 신경을 안써도 되는 영역이라고 생각합니다. 

IMAGE_NT_HEADER

이번에는 IMAGE_DOS_HEADER에 이어서 IMAGE_NT_HEADER를 알아보겠습니다.
아래의 코드는 winnt.h 헤더에 있는 NT 헤더의 구조체입니다.


#define IMAGE_NT_SIGNATURE                  0x00004550  // PE00

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;



먼저 적기 전에 IMAGE_DOS_HEADER의 e_lfanew 필드의 값이 IMAGE_NT_HEADER의 오프셋이 맞음을 알 수 있었습니다.


Signature

IMAGE_NT_HEADER의 필드를 살펴보시면 Signature 필드가 가장 처음으로 등장합니다.
데이터 타입이 DWORD이기 때문에 4byte를 차지하며, 이 Signature의 값을 가지고 PE 파일 구조인지 아닌지를 체크를 할 수 있습니다.
Signature의 값은 IMAGE_NT_SIGNATURE 상수 그대로 PE 00(50 45 00 00) 라는 값을 가지고 있습니다.

그리고 Signature 말고도 FileHeader와 OptionalHeader필드가 있는데,
이는 IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER32 구조체를 가지고 있습니다.
이들을 알아보도록 하겠습니다.

IMAGE_FILE_HEADER

이번에는 IMAGE_NT_HEADER에 이어서 IMAGE_FILE_HEADER를 알아보겠습니다.
아래의 코드는 winnt.h 헤더에 있는 FILE 헤더의 구조체입니다.


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_NT_HEADER보다 필드가 많은 것을 볼 수 있습니다.
차례대로 알아보도록 하겠습니다.

Machine

Machine 필드는 이 파일이 어떤 CPU에서 실행될 수 있는지를 알 수 있습니다.
아래는 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_SH3               0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP            0x01a3
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5               0x01a8  // SH5
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB             0x01c2
#define IMAGE_FILE_MACHINE_AM33              0x01d3
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP         0x01f1
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE           0x0520  // Infineon
#define IMAGE_FILE_MACHINE_CEF               0x0CEF
#define IMAGE_FILE_MACHINE_EBC               0x0EBC  // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64             0x8664  // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R              0x9041  // M32R little-endian
#define IMAGE_FILE_MACHINE_CEE               0xC0EE


이 프로그램에 Machine 필드의 값을 확인해보도록 하겠습니다.


4C 01로 되있으며,

#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.
값과 일치합니다.
그렇다면 intel x86 CPU와 호환한다는 소리가 됩니다.

NumberOfSections

이 필드는 PE 파일을 구성하는 섹션의 수를 나타냅니다.
섹션이 추가되면 이 값은 증가하고 섹션이 없어지면 감소합니다.
이 값을 보고 섹션의 개수를 알아낼 수 있으며, 무조건 0보다는 커야됩니다.
그렇다면 직접 NumberOfSections의 값을 보도록 하겠습니다.


섹션이 9개가 있다고 합니다.



실제로 9개가 존재하는 것을 볼 수 있었습니다.

TimeDateStamp

TimeDateStamp 필드는 이 PE파일이 생성된 시간,
즉 이 파일이 생성된 날짜가 타임스탬프 형식으로 기록이 됩니다.
그러나 이는 확실히 신뢰할 수 없는 값이며, 언제든지 변조가 가능하므로 "아 그렇구나~" 로 보셔야 됩니다.
TimeDateStamp 필드의 값을 확인하도록 하겠습니다.


TimeDateStamp의 값은 0x578F684B로 알수 있으며,

0x578F684B(16)를 10진수로 바꾸면 1469016139(10) 입니다.


http://www.epochconverter.com/
의 사이트에서 표준 시간으로 바꾸게 되면 

실제 파일 시간과 동일함을 볼 수 있습니다.



PointerToSymbolTable

PointerToSymbolTable 필드는 파일의 심볼 내용을 담고 있는 테이블의 오프셋을 저장하고 있고, 없으면 0을 담고 있습니다.


NumberOfSymbols

NumberOfSymbols 필드는 심볼 테이블에 저장된 심볼의 개수를 저장하고 있고, 없으면 0을 담고 있습니다.


SizeOfOptionalHeader

SizeOfOptionalHeader 필드는 IMAGE_OPTIONAL_HEADER 필드에 있는 OPTIONAL 헤더(IMAGE_OPTIONAL_HEADER32)의 크기를 담고 있습니다.

이 필드의 크기는 정해져있는 것 같고 운영체제에 따라 값이 다르기 때문에 

PE로더는 이 필드의 값을 확인하고 IMAGE_OPTIONAL_HEADER의 크기를 처리합니다.



위 그림에서 SizeOfOptionalHeader 필드의 값을 확인할 수 있으며,

값은 0xE0(16)으로 224(10) byte 만큼 차지한다는 것을 알 수 있습니다.


Characteristics

Characteristics 필드는 현재 파일의 형식을 알려주는 역할을 하며,
이 필드의 값을 가지고 실행 가능한 파일인지,
DLL 파일인지, 시스템 파일인지 등의 정보가 들어있습니다.
아래는 winnt.h에 정의된 Characteristics 상수입니다.


#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.


위 상수의 값은 비트 플래그를 사용한 것으로, 2진수 형식으로 증가합니다.


Characteristics 필드의 값이 0x0102라는 것을 볼 수 있으며, 이는 0100과 0002를 합한 값이랑 같습니다.

#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.

#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved externel references).


즉, 32비트 머신을 필요로 하고, 실행 가능한 파일이라는 것을 알 수 있습니다.


IMAGE_OPTIONAL_HEADER

이번에는 IMAGE_FILE_HEADER에 이어서 IMAGE_OPTIONAL_HEADER를 알아보겠습니다.
아래의 코드는 winnt.h 헤더에 있는 OPTIONAL 헤더의 구조체입니다.


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

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

//
// Optional header format.
//

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    //
    // NT additional fields.
    //

    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_HEADER 구조체는 PE구조체 중에 가장 크기가 큰 구조체로서, 필드가 상당히 많은 것을 볼 수 있습니다.
이 구조체에서 주목해야할 필드만 쏙쏙 간략하게 보도록 하겠습니다.

Magic

이 필드는 IMAGE_OPTIONAL_HEADER32일 경우에는 0x0B01, IMAGE_OPTIONAL_HEADER64일 경우에는 0x0B02 값을 가지게 됩니다.


위 그림에서 Magic 필드의 값이 0x0B01 값을 가지고 있는 것을 볼 수 있으며,
이는 IMAGE_OPTIONAL_HEADER32 구조체임을 알 수 있습니다.

AddressOfEntryPoint

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


ImageBase

PE 파일이 메모리에 로드될 때의 주소를 가르킵니다.
32비트일 경우 가상 메모리의 범위는 0 ~ FFFFFFFF 범위입니다.
ImageBase는 이런 광범위한 메모리 내에서 PE 파일이 로딩되는 시작 주소를 가지고 있습니다.

EXE, DLL 파일은 user memory 영역인 0 ~ FFFFFFF7 범위에 위치하고,
SYS 파일은 kernel memory 영역인 00000008 ~ FFFFFFFF 범위에 위치합니다.

보통은 실행 파일들의 ImageBase는 0x00400000, DLL 파일의 ImageBase는 0x01000000입니다. ( 물론 무조건 이 값을 가지고 있는 것은 아닙니다. )


위의 사진에서 ImageBase의 값은 0x00040000임을 알 수 있습니다.


SectionAlignment & FileAlignment

SectionAlignment는 메모리에서의 섹션의 최소 단위츨 나타내고, FileAlignment는 파일에서의 섹션의 최소 단위를 나타냅니다.
따라서 파일/메모리의 섹션 크기는 반드시 각각 SectionAlignment, FileAlignment 들의 배수가 되어야합니다.


초랭이 : SecionAlignment

주랭이 : FileAlignment


SizeOfHeader

PE 헤더의 전체 크기를 나타냅니다.
파일 시작점에서 SizeOfHeaders Offset만큼 떨어진 위치에 첫번째 섹션이 존재합니다.  


위 사진에서 SizeOfHeader 값은 0x0001F000(16)으로, 헤더의 총 크기는 126976(10)입니다.


Subsystem

이 값을 통해 시스템 드라이버 파일인지, GUI인지 CUI인지 확인이 가능합니다.
아래의 코드는 winnt.h 헤더에 있는 SUBSYSTEM의 정의된 상수입니다.


// Subsystem Values

#define IMAGE_SUBSYSTEM_UNKNOWN              0   // Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE               1   // Image doesn't require a subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_GUI          2   // Image runs in the Windows GUI subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_CUI          3   // Image runs in the Windows character subsystem.
#define IMAGE_SUBSYSTEM_OS2_CUI              5   // image runs in the OS/2 character subsystem.
#define IMAGE_SUBSYSTEM_POSIX_CUI            7   // image runs in the Posix character subsystem.
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS       8   // image is a native Win9x driver.
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI       9   // Image runs in the Windows CE subsystem.
#define IMAGE_SUBSYSTEM_EFI_APPLICATION      10  //
#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER  11   //
#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER   12  //
#define IMAGE_SUBSYSTEM_EFI_ROM              13
#define IMAGE_SUBSYSTEM_XBOX                 14
#define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16


직접 보게 되면.


위 사진에서는 Subsystem의 값이 0x0003으로 CUI 파일이라는 것을 알 수 있습니다.


NumberOfRvaAndSizes

마지막 필드인 DataDirectory 배열의 갯수를 가지고 있습니다.
PE 로더는 NumberOfRvaAndSizes의 값을 보고 배열의 크기를 인식합니다.


DataDirectory

IMAGE_DATA_DIRECTORY 구조체를 보면 Virtual Address와 Size라는 필드가 존재합니다.
아래의 코드는 winnt.h 헤더에 있는 DataDirectory의 정의된 상수입니다.


// Directory Entries

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
//      IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor


IMAGE_DATA_DIRECTORY 구조체의 Virtual Address를 통해 가상 주소를 알 수 있으며, Size를 통해 크기를 알 수 있습니다.

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory

#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table

앗, 그렇다면 잠깐 IAT에 대해 알아보기 전에 IMAGE_SECTION_HEADER 먼저 알아보도록 하겠습니다.

이유는 IMAGE_IMPORT_DESCRIPTOR의 주소를 알아내기 위해서는 

RAW 비례식을 이용하여 RVA - IMAGE_SECTION_HEADER의 필드인 Virtual Address를 구하여야됩니다.


IMAGE_SECTION_HEADER

이 구조체는 섹션 테이블이라고도 불리며, 섹션에 대한 정보를 관리하는 구조체라고도 합니다.
이 구조체를 보고 ".text" 섹션, ".code" 섹션, ".rdata" 섹션 등의 대한 정보들을 알 수 있다는 겁니다.
아래의 코드는 winnt.h 헤더에 있는 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;


위의 코드를 보면 필드는 총 11개가 있습니다.

이 중에 중요한 필드들만 간략하게 보도록 하겠습니다.


Name

필드 명 그대로 섹션의 이름을 나타냅니다.
#define IMAGE_SIZEOF_SHORT_NAME              8
이 상수의 값만큼 이름의 길이가 가능합니다.
말 그대로 최대 8byte까지 가능합니다.
이 필드는 NULL로 비워있을 수도 있으며, 8byte 모두가 차있을 수도 있습니다.

VirtualSize

PE 로더를 통해 PE 파일이 메모리에 로드되고 나서의 메모리에서 섹션이 차지하는 크기를 가지고 있습니다.

VirtualAddress

PE 로드를 통해 PE 파일이 메모리에 로드되고 나서의 해당하는 섹션의 RVA 값입니다.

SizeOfRawData

파일 상에서의 해당 섹션이 차지하는 크기를 가집니다.

PointerToRawData

파일 상에서의 해당 섹션이 시작하는 위치를 담고 있습니다.

Characteristics

섹션의 속성 정보를 플래그로 지닙니다.
아래는 winnt.h에 정의된 Characteristics 상수입니다.


//
// Section characteristics.
//
//      IMAGE_SCN_TYPE_REG                   0x00000000  // Reserved.
//      IMAGE_SCN_TYPE_DSECT                 0x00000001  // Reserved.
//      IMAGE_SCN_TYPE_NOLOAD                0x00000002  // Reserved.
//      IMAGE_SCN_TYPE_GROUP                 0x00000004  // Reserved.
#define IMAGE_SCN_TYPE_NO_PAD                0x00000008  // Reserved.
//      IMAGE_SCN_TYPE_COPY                  0x00000010  // Reserved.

#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_LNK_OTHER                  0x00000100  // Reserved.
#define IMAGE_SCN_LNK_INFO                   0x00000200  // Section contains comments or some other type of information.
//      IMAGE_SCN_TYPE_OVER                  0x00000400  // Reserved.
#define IMAGE_SCN_LNK_REMOVE                 0x00000800  // Section contents will not become part of image.
#define IMAGE_SCN_LNK_COMDAT                 0x00001000  // Section contents comdat.
//                                           0x00002000  // Reserved.
//      IMAGE_SCN_MEM_PROTECTED - Obsolete   0x00004000
#define IMAGE_SCN_NO_DEFER_SPEC_EXC          0x00004000  // Reset speculative exceptions handling bits in the TLB entries for this section.
#define IMAGE_SCN_GPREL                      0x00008000  // Section content can be accessed relative to GP
#define IMAGE_SCN_MEM_FARDATA                0x00008000
//      IMAGE_SCN_MEM_SYSHEAP  - Obsolete    0x00010000
#define IMAGE_SCN_MEM_PURGEABLE              0x00020000
#define IMAGE_SCN_MEM_16BIT                  0x00020000
#define IMAGE_SCN_MEM_LOCKED                 0x00040000
#define IMAGE_SCN_MEM_PRELOAD                0x00080000

#define IMAGE_SCN_ALIGN_1BYTES               0x00100000  //
#define IMAGE_SCN_ALIGN_2BYTES               0x00200000  //
#define IMAGE_SCN_ALIGN_4BYTES               0x00300000  //
#define IMAGE_SCN_ALIGN_8BYTES               0x00400000  //
#define IMAGE_SCN_ALIGN_16BYTES              0x00500000  // Default alignment if no others are specified.
#define IMAGE_SCN_ALIGN_32BYTES              0x00600000  //
#define IMAGE_SCN_ALIGN_64BYTES              0x00700000  //
#define IMAGE_SCN_ALIGN_128BYTES             0x00800000  //
#define IMAGE_SCN_ALIGN_256BYTES             0x00900000  //
#define IMAGE_SCN_ALIGN_512BYTES             0x00A00000  //
#define IMAGE_SCN_ALIGN_1024BYTES            0x00B00000  //
#define IMAGE_SCN_ALIGN_2048BYTES            0x00C00000  //
#define IMAGE_SCN_ALIGN_4096BYTES            0x00D00000  //
#define IMAGE_SCN_ALIGN_8192BYTES            0x00E00000  //
// Unused                                    0x00F00000
#define IMAGE_SCN_ALIGN_MASK                 0x00F00000

#define IMAGE_SCN_LNK_NRELOC_OVFL            0x01000000  // Section contains extended relocations.
#define IMAGE_SCN_MEM_DISCARDABLE            0x02000000  // Section can be discarded.
#define IMAGE_SCN_MEM_NOT_CACHED             0x04000000  // Section is not cachable.
#define IMAGE_SCN_MEM_NOT_PAGED              0x08000000  // Section is not pageable.
#define IMAGE_SCN_MEM_SHARED                 0x10000000  // Section is shareable.
#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.


그렇다면 이제 IAT를 ... !


IAT (Import Address Table)

PE 헤더의 최대의 벽은 IAT(Import Address Table)이라고 생각합니다.


두둥!!!


이 IAT는 윈도우 운영체제의 핵심 개념인 프로세스, 메모리, DLL 구조 등에 대한 내용이 함축되어있습니다.

고로 DLL(Dynamic Linked Library)에 대한 지식이 필요합니다.

쉽게 IAT에 말하자면 어떤 라이브러리에서 어떠한 함수를 사용하고 있는지를 기술한 테이블입니다.

[ 말 출처 : 리버싱 핵심 원리 ]

그렇다면 DLL에 대해 먼저 알아보도록 하겠습니다.


DLL (Dynamic Linked Library)

IAT를 알려면 DLL 개념을 짚고 가야됩니다.

동적 링크 라이브러리(영어: dynamic-link library, DLL)는 마이크로소프트 윈도우에서 구현된 동적 라이브러리이다. 
내부에는 다른 프로그램이 불러서 쓸 수 있는 다양한 함수들을 가지고 있는데, 확장DLL인 경우는 클래스를 가지고 있기도 한다. 
DLL은 COM을 담는 그릇의 역할도 한다.

사용하는 방법에는 두 가지가 있는데,

묵시적 링킹(Implicit linking) 

실행 파일 자체에 어떤 DLL의 어떤 함수를 사용하겠다는 정보를 포함시키고 운영체제가 프로그램 실행 시 해당 함수들을 초기화한 후 그것을 이용하는 방법

명시적 링킹(Explicit linking) 

프로그램이 실행 중일 때 API를 이용하여 DLL 파일이 있는지 검사하고 동적으로 원하는 함수만 불러와서 쓰는 방법이 있다.
전자의 경우는 컴파일러가 자동으로 해주는 경우가 많으며, 후자의 경우는 사용하고자 하는 DLL이나 함수가 실행 환경에 있을지 없을지 잘 모르는 경우에 사용된다. (때때로 메모리 절약을 위해 쓰이기도 한다.)

[ 출처 : https://ko.wikipedia.org/wiki/%EB%8F%99%EC%A0%81_%EB%A7%81%ED%81%AC_%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC ]



여기서 IAT는 묵시적 링킹에 대한 매카니즘을 제공하는 역할을 합니다.

아! DLL은 넘어가고!!


Hell o IAT


어서와~!


이 IAT 설명에서 사용할 코드는 아래와 같습니다.



#include <stdio.h>

#include <Windows.h>


int main(int argc, char *argv[])

{

HWND hWnd = FindWindow(NULL, TEXT("Untitled - Notepad"));


if (hWnd) {

printf("있음\n");

}

else

{

printf("없음\n");

}


return 0;

}

 


그렇다면 이제 진짜 확인을 위해 올리디버거로 프로그램을 열어보겠습니다.



0x011A17C7 번지를 보면 FindWindowA를 직접 호출하는 것이 아니라



0x011AA098 번지에 있는 0x7787FFE6값을 가져와 호출을 하는 간접적인 호출 방식이라는 것을 볼 수 있습니다.

(모든 API 호출은 이런 방식으로 되어있습니다.)


0x011AA98 번지는 이 프로그램의 ".text" 섹션 메모리 영역입니다.

0x011AA98 번지의 값은 0x7787FFE6이라는 것을 위에서 볼 수 있었으며,

0x7787FFE6 주소는 바로 이 프로그램의 프로세스 메모리에 로딩된 user32.dll 내에 있는 FindWindowA 함수 주소입니다.


그렇다면 여기서 의문점이 하나가 생기게 됩니다.

엥 이거 완전 바로 0x7787FFE6를 Call하면 좋지 않냐~?!


이 방법은 DOS 시대의 방법이였습니다.

이 프로그램이 생성되는 순간에 이 프로그램이 어떤 윈도우 환경에 실행되는지 알 수가 없으며,

이런 환경에 따라 kernel32.dll, user32.dll 등의 버전이 틀려지게 되고,

FindWindowA함수의 주소가 달라지게 됩니다.


그렇기 때문에 어떠한 환경에서도 FindWindowA 함수의 호출을 보장하기 위해 

컴파일러가 0x011AA98 번지에 미리 공간을 마련해두고 파일이 실행이 된 직후에 PE 로더가 이 공간에 FindWindowA의 주소를 넣어주게 됩니다.

이러한 DLL 내의 함수 주소를 모아놓은 테이블을 만들어놓고 코드 섹션에서 만들어 놓은 테이블을 쓰는 방식으로 관리를 하여 이 테이블을 IAT라고 합니다.


이정도만 알아두면 IAT는 절반은 알았다! 라고 할 수 있을 것 같습니다.


아! PE 파일은 자신이 어떤 라이브러리를 import하고 있는 지 어떠한 구조체에 명시하고 있습니다.

이 아래에서는 그 구조체에 대해 알아보도록 하겠습니다!


IMAGE_IMPORT_DESCRIPTOR

아래의 코드는 winnt.h 헤더에 있는 IMAGE_IMPORT_DESCRIPTOR의 구조체입니다.


typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;


이 구조체는 6개의 필드 중 중요한 3개의 필드만 보도록 하겠습니다.


앗, 먼저 필드를 보기 전에 IMAGE_IMPORT_DESCRIPTOR의 위치를 찾아야됩니다.

위에서 IMAGE_OPTIONAL_HEADER의 DataDirectory가 기억나시나요?


어이쿠 안나신다고요??!

올리기 귀찮으실테니 이미지를 첨부해드리겠습니다!

(저는 압니다... 올리다가 다 까먹으실 것을요!)



2번째 요소가 Import Directory을 볼 수 있습니다.



초랭이 : VirtualAddress(RVA)

주랭이 : VirtualSize


VirtualAddress(RVA) : 0x0001A000

VirtualSize : 0x000001B4


나머지 Virtual Address를 구하려면 현재 RVA가 어디 섹션에 해당하는지를 알아야됩니다.



위에서 알려드린 IMAGE_SECTION_HEADER 기억나시죠?

앗... RVA가 이 섹션에 숨어있었습니다!



핫핑꾸 색깔로 표시해두었습니다.

위와 같이 0x0001A000이 실제적으로 ".idata" 섹션에 위치하는 옵셋이 됩니다.


".idata" 섹션의 이름은 0x6164692E 이며,

VirtualSize : 0x00000A6A

RVA : 0x0001A000

Size of Raw Data : 0x00000C00

Pointer to Raw Data : 0x00007600


앗, Raw 옵셋을 찾았습니다.



위의 영역이 모두 IMAGE_IMPORT_DESCRIPTOR 구조체 배열이며, 

이 영역 처음부터 20byte까지는 구조체 배열의 첫번째 요소입니다.


DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)

를 알아보기 전에 IMAGE_THUNK_DATA라는 구조체를 보도록 하겠습니다.


IMAGE_THUNK_DATA

아래는 winnt.h 헤더파일의 IMAGE_THUNK_DATA 구조체입니다.



typedef struct _IMAGE_THUNK_DATA64 {

    union {

        ULONGLONG ForwarderString;  // PBYTE 

        ULONGLONG Function;         // PDWORD

        ULONGLONG Ordinal;

        ULONGLONG AddressOfData;    // PIMAGE_IMPORT_BY_NAME

    } u1;

} IMAGE_THUNK_DATA64;

typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;


#include "poppack.h"                        // Back to 4 byte packing


typedef struct _IMAGE_THUNK_DATA32 {

    union {

        DWORD ForwarderString;      // PBYTE 

        DWORD Function;             // PDWORD

        DWORD Ordinal;

        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME

    } u1;

} IMAGE_THUNK_DATA32;

typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;



IMAGE_THUNK_DATA 필드는 총 4개로 모두 공용체의 멤버이며, 4바이트 공간을 4개의 필드가 공유합니다.


이 이상은 제 지식이 딸려서 적을 수가 없네요 :'(

덕분에 작성하면서 깨달음을 많이 얻었습니다!

생각정리하는데는 글 쓰기가 꿀이네요.

'0x20 Security > 0x22 Reversing' 카테고리의 다른 글

UPX Pakking Principle (UPP)  (1) 2017.07.07

+ Recent posts