본문 바로가기

아이티 :D/Reversing

PE format 상세분석

안녕하세요 :)

저번 포스팅에서는 RVA to RAW 관련 개념들을 정리했었는데 

이번 포스팅에선 PE file format을 상세분석 하도록 하겠습니다.

사실 이 포스팅이 먼저였어야 하는데 말이죠!ㅎㅎ

아직까지도 PE에 대해서 초보지만 PE 분석하는 프로그램을 만들면서 

하나하나 파싱해나가니깐 그래도 좀 친숙하게 PE가 다가오더라구여

그래서 이번에도 자신감을 가지고 가보도록 하겠습니다.


1. PE란 무엇인가?

-Portable Executable의 약자로 Win32 운영체제가 돌아가는 시스템이면 어디서든 실행이 가능한 파일을

 말합니다.

-PE 파일의 종 류는 아래와 같습니다.

 드라이브: SYS, 오브젝트파일: OBJ, 라이브러리: DLL, OCX, 실행파일: EXE, SCR


2. PE구조 한눈에 보기!


    출처 - <Windows 시스템 실행파일의 구조와 원리 8page>



1) IMAGE_DOS_HEADER

2) DOS stub code

3) IMAGE_NT_HEADER

3-1) PE signature    

3-2) IMAGE_FILE_HEADER

3-3) IMAGE_OPTIONAL_HEADER

4) IMAGE_DATA_DIRECTORY

5) IMAGE_SECTION_HEADERS

5-1) IMAGE_SECTION_HEADER

5-2) IMAGE_SECTION_HEADER

5-3) IMAGE_SECTION_HEADER

5-4) IMAGE_SECTION_HEADER

     ........

6) SECTION_DATA


PE구조는 간단히 위와같이 구성되어 있습니다.

그럼 지금부터 하나하나 살펴보도로 하겠습니다.



3. IMAGE_DOS_HEADER

- IMAGE_DOS_HEADER 구조체

typedef struct _IMAGE_DOS_HEADER {

WORD e_magic; 

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;

} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;


     -IMAGE_DOS_HEADER 는 "64byte"이며, 눈여겨 볼 필드는 아래와 같습니다


1) WORD e_magic; = IMAGE_DOS_HEADER의 시그니쳐이자 식별자로써 ASCII "MZ"로 고정.

 여기서 "MZ"는 도스를 설계한 사람중 한명인 Mark Zbikowski의 이니셜입니다               

2) LONG e_lfanew; = IMAGE_NT_HEADER 의 시작 offset을 가지고 있습니다.

        개인적으로 PE 분석 프로그램 만들기 위해 파싱할때

        이 값을 기준으로 잡을정도로 중요한 필드입니다!


<IMAGE_DOS_HEADER>


 

4. DOS stub code 

- 별로 의미있는 부분은 아닙니다만은 설명을 하고 넘어 가자면 예전 DOS시절에 쓰였던 부분으로

          ASCHII 값으로 "This program cannot be run in DOS mode." 라는 문장을 나타냅니다.

   시작점에서 IMAGE_DOS_HEADER의 크기인 64byte만큼 떨어진 곳에서부터 시작이 되며

          크기가 가변적이라 IMAGE_DOS_HEADER 구조체의 e_lfanew 필드를 참조하여 끝을 알 수 있습니다.



<DOS stub code>



5. IMAGE_NT_HEADER

-IMAGE_NT_HEADER 구조 체

typedef struct _IMAGE_NT_HEADERS{

DWORD Signature;

IMAGE_FILE_HEADER FileHeader;

IMAGE_OPTIONAL_HEADER OptionalHeader; 

} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;


5-1) PE signature

DWORD signate; 필드를 나타내며, 4byte로 구성됩니다.

PE를 나타내는 식별자이며, IMAGE_DOS_HEADER에 MZ와 마찬가지로

ASCHII값으로 "PE\x00\x00"의 고정값을 지닙니다.

IMAGE_NT_HEADER의 시작점이자 식별자이며, 그렇기에 당연히 이전 e_lfanew 필드가 가르키는

값에서부터 4byte의 입니다!

아래 사진에서 0x000000D8 지점(e_lfanew값)이 DWORD signature의 시작지점이고 값은 PE인걸 확인할 수 있습니다.

<DWORD signature>




5-2) IMAGE_FILE_HEADER

-IMAGE_FILE_HEADER 구조체

typedef struct _IMAGE_FILE_HEADER {

WORD Machin;

WORD NumberOfSections; 

DWORD TimeDateStamp; 

DWORD PointToSymbolTable; 

DWORD NumberOfSymbols; 

WORD SizeOfOptionalHeader; 

   WORD Characteristics;

} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;


5-2-1) Machin

- 해당필드는 파일이 어떤 CPU에서 동작하고 있는지를 알려줍니다.

  대표적으로 3가지만 살펴보겠습니다.

  Intel386: 0x14c

  Intel64:  0x0200

  AMD64: 0x8664


5-2-2) NumberOfSections

- 해당필드는 IMAGE_SECTION_HEADERS 구조체내의 섹션의 수를 나타냅니다.

  그럼 확인 해볼까요?



 


<NumberOfSections>

 

 필드의 값은 4며, PEview로 확인해본 결과 섹션의 개수도 4인걸 확인 할 수 있습니다.



5-2-3) SizeOfOptionalHeader

- 해당 필드는 IMAGE_FILE_HEADER 다음에 이어지는 IAMGE_OPTIONAL_HEADER

  구조체의 바이트 수를 알려줍니다.

  OBJ파일이면 해당값이 0이며, 실행 파일일 경우 32비트는 0xE0(224)byte

  64비트는 0xF0(240)byte입니다. 


5-2-4) Characteristics

- 해당 필드는 파일의 특성을 알려줍니다.

  특성의 종류에 대해서 아래에서 살펴보고 저희가 분석중인 calc.exe는 어떤 특성을 가지고

  있는지 알아보겠습니다.

                         <WinNT.h 의 Characteristics 값>

0x0001  // Relocation info stripped from file.  

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

0x0004  // Line nunbers stripped from file.  

0x0008  // Local symbols stripped from file.  

0x0010  // Agressively trim working set  

0x0020  // App can handle >2gb addresses  

0x0080  // Bytes of machine word are reversed.  

0x0100  // 32 bit word machine.  

0x0200  // Debugging info stripped from file in .DBG file  

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

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

0x1000  // System File.  

0x2000  // File is a DLL.  

0x4000  // File should only be run on a UP machine  

0x8000  // Bytes of machine word are reversed.  

그럼 calc.exe의 특성값을 확인해보겠습니다.


<calc.exe의 characteristics>

0x0102의 값을 가지고 있고, 내용은 실행파일이며 32비트를 사용한다네요 :)

그럼 이제 다음으로 중요한 헤더인 IMAGE_OPTIONAL_HEADER로 가보겠습니다!!


5-3) IMAGE_OPTIONAL_HEADER

 -IMAGE_OPTIONAL_HEADER 구조체

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;  

 

5-3-1) Magic

-해당 필드는 IMAGE_OPTIONAL_HEADER를 나타내는 시그니처 정보를 담고있습니다.

 32비트는 0x010B이며, 64비트는 0x020B입니다. ROM파일은 0x0107입니다.

 

5-3-2,3) AddressOfEntryPoint, BaseOfCode

-해당 필드는 Entry Point의 RVA값을 나타냅니다.

 즉 프로그램에서 "최초로 실행되는 코드"라고 생각하시면 편해요.

 여기서 코드의 시작점과 최초로 실행되는 코드는 다릅니다.

 최초의 코드는 아래에서 설명할 BaseOfCode필드인데 코드의 시작점이고,

 최초로 실행되는 코드는 코드의 맨 처음은 아니지만 디버거 실행시 최초로 실행되는

 코드입니다. 즉 디버거 실행시 젤 위에 있는 코드 주소값이 아래에서 설명 할 

 BaseOfCode이며, 디버거에서 Module Entry Point라고 지정되있는 최초 실행지점이

 바로 AddressOfEntryPoint입니다.

 근데 중요한게 AddressOfEntryPoint나, BaseOfCode 둘다  RVA 값이니깐 

 Imagebase값을 더해줘야 VA 값이 된다는 사실!!

 <이전 포스팅에서 RVA to RAW, VA, RVA, RAW등을 정리했습니다.

         참고하시면 좋을 것 같아요..!>



5-3-4) Imagebase

-해당 필드는 가상 메모리에 로드된 파일의 시작주소로써, 즉 가상메모리에 PE파일이

 mapping된 시작주소이자 RVA의 기준이되는 주소값입니다.

 일반적으로 실행파일은 0x00400000의 값을 가지며 DLL은 0x10000000의 값을 

 가집니다.

 절대적인 값이 아니며, 링크 시 옵션 /BASE를 함으로써 변경 될 수 있습니다.


                                             <Imagebase>

 

5-3-5) SectionAlignment

-"Alignment"라는 단어는 가지런함, 정렬의 의미를 지닙니다.

 즉 섹션을 정렬할것이다라고 필드명을 보고 추측할 수 있겠네요.

 메모리상에서 섹션 크기의 최소단위입니다.

 아래에서 보면  0x1000값을 가지는데 4096byte로 계속 정렬된다고 보면 됩니다.

<SectionAlignment>

 

   

5-3-6) FileAlignment

-해당 필드는 SectionAlignment와 비슷합니다. 하드디스크상에서 섹션크기의

 최소단위 입니다.


5-3-7) SizeOfImage

-해당 필드는 PE 파일을 메모리에 로드할 때 확보 해야 할 크기를 나타냅니다.

 Imagebase가 가르기는 값에서 PE 파일의 마지막 섹션 끝까지의 크기이며, 혹 PE 파일이

 메모리에 매핑될 때 크기가 조금씩 틀려질 수 있기 때문에 보통은 PE 파일의 크기보다

 크다. 그리고 이 필드의 값은 SectionAlignment 값의 배수가 되어야 합니다.


5-3-8) SizeOfHeader

-해당 필드는 IMAGE_DOS_HEADER부터 Section_table들의 바이트를 모두 합친 값.

 시작주소에서 이 필드의 값만큼 떨어진 곳에 첫번째 섹션의 데이터가 시작합니다.

 이 필드의 값은 FileAlignment 값의 배수가 되어야 합니다.


5-3-9) MajorSubsystemVersion, MinorSubsystemVersion

-해당 필드들은 PE를 실행할 때 필요한 subsystem의 최소 버전을 나타냅니다.

 값이 4인데 window NT 4.0이상의 기반에서 PE가 돌아갈 수 있기때문입니다.

 /SUBSYSTEM이라는 명령어로 해당 값을 변경 할 수 있습니다.


5-3-10) Subsystem

-해당 필드는 PE파일이 어떤 기반인지를 알려줍니다. 3가지정도만 알면 되는데

 0x01: .sys 류의 드라이파일

 0x02: GUI (그래픽 유저 인터페이스)

 0x03: CUI (콘솔 유저 인터페이스)


6. IMAGE_DATA_DIRECTORY

-IMAGE_DATA_DIRECTORY 구조체

typedef struct _IMAGE_DATA_DIRECTORY {  

    DWORD   VirtualAddress;  

    DWORD   Size;  

} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16


이 구조체 배열의 엔트리는 16이며, 그 중에서 사용하는 엔트리는 15개입니다.

구조체를 보니 각 요소요소들은 주소값과 크기를 가지고 있겠네요 :)


-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  

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

//중요한 몇몇 필드만 살펴보도록 하겠습니다. 이 배열의 엔트리들에 대해서는

  차후에 좀 더 상세하게 포스팅 하도록 하겠습니다.//


6-1) IMAGE_DIRECTORY_ENTRY_EXPORT

-EXPORT_TABLE의 시작번지와 크기를 가지고 있습니다.

6-2) IMAGE_DIRECTORY_ENTRY_ IMPORT

-IMPORT_TABLE의 시작번지와 크기를 가지고 있습니다.

     6-3) IMATE_DIRECTORY_ENTRY_RESOURCE

-RESOUSE_TABLE의 시작번지와 크기를 가지고 있습니다.

6-4) IMAGE_DIRECTORY_ENTRY_TLS 

-Thread Local Storage(TLS)섹션에 대한 포인터를 가지고 있습니다.

6-5) IMAGE_DIRECTORY_ENTRY_IAT

-IAT(Import Address Table)의 시작 번지를 가르킵니다.


7. IMAGE_SECTION_HEADER 

-IMAGE_SECTION_HEADER 구조체

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_FILE_HEADER안에 NumberOfSection의 

 값으로 알 수 있습니다.

 이제 각각의 섹션테이블에 대해 알아보도록 하겠습니다!!


7-1)  Name

-해당 필드는 섹션의 이름을 ASCII값으로 표현하고 있습니다.

 이름을 나타내는 ASCII값은 8자로 제한이됩니다.


<Section Name>


7-2) PhysicalAddress or Virtual Size

-union이라는 사실에 주목을 해야합니다.

 왜냐하면 PE파일인지 OBJ파일인지 두가지 경우에 따라서 값이 틀려지기 때문이죠

 OBJ의 경우는 값이 0으로 고정되고, PE인 경우는 코드와 데이터의 실제 바이트 수를 담고 있는데

 FileAlignment로 정렬되기 전의 실제 바이트 수를 담고 있습니다.


7-3) VirtualAddress

-해당 필드는 메모리상에 파일이 로드되었을때 섹션이 시작되는 주소를 RVA값으로 담고 있습니다.

 제 파일에서는 0x00001000의 값을 가지고 있네요.


7-4) PointerToRawData

-해당 필드는 파일상에서 해당 섹션이 시작하는 실제 Offset값입니다.

 VirtualAddress가 가르기는 주소값과 같을 수도 다를 수도 있습니다.

 그럼 실제로 파일상에서 해당 섹션의 offset이 맞는지 아래 그림에서 확인 해보겠습니다


<PointerToRawData>

<File상에서 실제 해당 섹션의 시작점>

확인 결과 PointerToRawData가 가르키고 있는 주소 값이 파일 상에서 해당 섹션이 시작하는 주소값

과 동일하다는 것을 알 수 있습니다.


7-5) SizeOfRawData

-해당 필드는 파일상에서 섹션의 크기에 대한 정보를 가지고 있는데 7-2) 필드에서 살펴본

 VirtualSize의 값은 FileAlignment로 정렬하지 않은 실제 데이터의 바이트 수를 나타낸거라면

 SizeOfRawData의 값은 Filealignment로 정렬한 값의 배수를 나타내고 있습니다.

 PE파일이 아닌 OBJ파일의 경우라면 값은 0으로 고정됩니다.


7-6) Characteristics

-해당 필드는 해당 섹션의 특성들을 나타내고 있습니다.

 아래 그림에서 해당 필드의 값을 확인 해보겠습니다.

           <Characteristics>

 0x00000020 - IMAGE_SCN_CNT_CODE: 섹션에 코드가 포함되어 있다는것을 의미합니다.

 0x20000000 - IMAGE_SCN_MEM_EXECUTE: 섹션이 실행 가능하다는 것을 의미합니다.

 0x40000000 - IMAGE_SCN_MEM_READ: 섹션이 읽기가 가능하다는 것을 의미합니다.

 이 특성들 이외에 전체를 알고 싶다면 아래 링크의 웹페이지 마지막 Characteristics를 확인하세요.

 http://msdn.microsoft.com/en-us/library/windows/desktop/ms680341(v=vs.85).aspx


8. 상세분석을 바탕으로 만들어본 PEprint.exe

- 이론으로만 가지고 있으면 금방 까먹을 것 같아서 파이썬으로 간단하게 만들어봤습니다 :D


9. 참고문헌

-Windows 시스템 실행파일의 구조와 원리 (이호동 저)




'아이티 :D > Reversing' 카테고리의 다른 글

RVA to RAW  (0) 2016.11.02