[리버싱 - 4] : PE Header - ①

IMAGE_DOS_HEADER, IMAGE_NT_HEADER, IMAGE_FILE_HEADER 구조체 알아보기


우선 PE(Portable Executable File Format)는 무엇인가 단어의 뜻을 풀어서 해석하면 우리의 파일(File)이 이식 가능한 다른 곳(Portable)에 가도 실행이 가능 하도록(Executable) 만들어 놓은 포맷(Format)이란 뜻이다.

PE Header 안에는 실행 파일을 실행하기 위한 각종 정보가 기록돼 있다. EXE나 DLL을 실행하면 항상 개발자가 만든 코드가 실행되기 전에 PE의 정보부터 읽어와서 바이너리를 메모리에 올리기 위한 각종 데이터를 설정하는 작업을 하게 된다.

PE는 여러 구조체로 구성되어 있으며 중요한 구조체는 다음과 같다.

  • IMAGE_DOS_HEADER
  • IMAGE_NT_HEADER
  • IMAGE_FILE_HEADER
  • IMAGE_OPTIONAL_HEADER
  • IMAGE_SECTION_HEADER
  • IMAGE_IMPORT_DESCRIPTOR
  • IMAGE_EXPORT_DIRECTORY
  • IMAGE_IMPORT_BY_NAME
  • IMAGE_THUNK_DATA32

IMAGE_DOS_HEADER

IMAGE_DOS_HEADER 구조체는 WinNT.h 헤더 파일에 정의되어 있다.

typedef struct _IMAGE_DOS_HEADER // DOS .EXE header
{
    WORD e_magic;       // Magic number
    WORD e_cblp;        // Byte on last page of file
    WORD e_cp;          // Pages in file
    WORD e_crlc;        // Relocations
    WORD e_cparhdr;     // Size of header in paragraphs
    WORD e_minalloc;    // Minimum extra paragraphs needed
    WORD e_maxalloc;    // Maximum extra paragraphs needed
    WORD e_ss;          // Initial (relative) SS value
    WORD e_sp;          // Checksum
    WORD e_ip;          // Initital IP value
    WORD e_cs;          // Initial (relative) CS value
    WORD e_lfarlc;      // File address of relocation table
    WORD e_ovno;        // Overlay number
    WORD e_res[4];      // Reserved words
    WORD e_oemid;       // OEM identifier (for e_oeminfo)
    WORD e_oeminfo;     // OEM information; e_oemid specific
    WORD e_res2[10];    // Reserved words
    LONG e_lfanew;      // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

멤버 수가 많지만 쓸모 있는 멤버는 딱 두 개로 첫 번째 필드인 e_magic과 마지막 필드인 e_lfanew이다.

e_magic

e_magic필드는 현재 파일이 PE 파일인지 체크하는 것 말고는 쓸 일이 없다. PE 파일에서 가장 먼저 MZ 스트링(4D 5A)가 등장한다면 이는 PE 파일이라는 것을 나타내는 것이다. MZ 헤더를 통해 MS-DOS 헤더의 시작을 알리고, 이 바이너리가 PE 파일인지 아닌지를 체크한다.

직접 확인해 보자. 내 컴퓨터에 있는 exe 파일 한 개를 PEview라는 프로그램으로 열어보자. mz-header PE 파일의 시작이 MZ 스트링(4D 5A)로 시작하는 것을 직접 확인할 수 있다.

e_lfanew

다음으로 확인해볼 필드는 e_lfanew이다. 이는 IMAGE_NT_HEADER의 구조체 위치를 알아 내는 데 사용되는 값이다. 실질적인 PE의 오프셋이 어디인지 이 필드를 통해 지정하게 된다. 참고로 다음과 같은 코드로 사용하며, PIMAGE_NT_HEADERS 사용을 위한 준비 작업 정도이다.

PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpFileBase;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((DWORD) pDosHeader + pDosHdr->e_lfanew);

IMAGE_NT_HEADER

다음으로 살펴볼 구조체는 IMAGE_NT_HEADER이다.

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

첫 번째 멤버인 SignaturePE\0\0을 표시하는 4바이트 값으로 50 45 00 00이 된다. 이도 직접 확인해 보자. Signature PEview에서 IMAGE_NT_HEADERS를 클릭하면 제일 먼저 시작하는 부분이 50 45 00 00이다.

옛날에는 이 signature 부분에 해커들이 자신의 표식을 남기두는 용도로도 사용했지만 현재 출시되는 운영체제에서는 이 부분이 50 45 00 00이 아니면 실행이 안되도록 설계했다고 한다.

IMAGE_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 멤버들 하나씩 알아보자.

Machine

어떤 CPU에서 이 파일이 실행될 수 있는지를 알려준다. 만약 일반 데스크톱이나 노트북에서만 리버싱을 한다면 별로 눈여겨볼 필요는 없다. 이 멤버는 다음 값 중 하나를 가질 수 있다.

  • IMAGE_FILE_MACHINE_I386(0x014c) : x86를 의미
  • IMAGE_FILE_MACHINE_IA64(0x0200) : Intel Itanium를 의미
  • IMAGE_FILE_MACHINE_AMD64(0x8664) : X64를 의미

NumberOfSections

섹션이 몇 개 있는지 알려주는 멤버이다. 섹션이란 .text나 .data 같은 코드 섹션이나 데이터 섹션 등을 일컫는다. MFC로 별다른 옵션 변경 없이 빌드한 파일의 경우에는 .text, .rdata, .data, .rsrc 4개의 섹션이 존재하며 NumberOfSections의 값도 당연히 4로 표시된다.

마이크로소프트 공식 문서에 따르면 Windows 로더는 섹션 수를 96개로 제한한다고 한다.

TimeDateStamp

현재 파일이 빌드된 날짜를 표시하는 멤버이다. 정확히는 obj파일이 컴파일러를 통해 EXE로 생성된 시간이라고 할 수 있는데 obj 파일만 따로 만들고 나중에 EXE로 컴파일하는 경우는 거의 없으므로 뭉뚱그려서 개발자가 EXE/DLL를 만든 시간이라고 보면 된다.

변조가 쉬우므로 항상 신뢰할 수 있는 값은 아니다. 또한 델파이의 경우 이 멤버 값을 항상 1992년으로 표시하므로 델파이로 만든 파일은 이 필드를 통해서는 빌드 날짜를 확인할 수 없다.

즉, TimeDateStamp의 값이 1992로 작성된 파일은 델파이로 만들어졌구나로 생각하면 된다. 1992년 프로그램이 지금 내 컴퓨터에 있을 확률은 거의 없으므로..

SizeOfOptionalHeader

SizeOfOptionalHeader멤버는 IMAGE_OPTIONAL_HEADER32구조체의 크기를 나타낸다. IMAGE_OPTIONAL_HEADER32는 PE를 로딩하기 위한 굉장히 중요한 구조체를 담고 있는데 이 구조체는 OS마다 다를 수도 있기 때문에 PE 로더는 SizeOfOptionalHeader 값을 먼저 확인한 뒤 IMAGE_OPTIONAL_HEADER32 구조체의 크기를 처리한다.

Characteristics

이 필드는 현재 파일이 어떤 형식인지 알려준다. DLL인지 EXE인지 구분하는 용도 정도로 생각하면 된다. 그러나 DLL인지 EXE인지 구분하는 방법은 이것 이외에도 많이 있으므로(파일 확장자만 봐도 구별 가능..) 눈 여겨볼 가치가 큰 멤버는 아닌 것 같다.


이번장에서는 PE 헤더의 IMAGE_DOS_HEADER, IMAGE_NT_HEADER, IMAGE_FILE_HEADER 까지만 살펴보고 다음장에서 나머지 IMAGE_OPTIONAL_HEADER, IMAGE_SECTION_HEADER를 다뤄보자.