알쓸전컴(알아두면 쓸모있는 전자 컴퓨터)

[reversing] PE 헤더 분석 (2) 본문

리버싱

[reversing] PE 헤더 분석 (2)

백곳 2018. 5. 4. 20:41

PE 헤더 분석 (2)


MS-DOS Stub Program 쪽 헤더는  


별 의미가 없으니 넘어가겠습니다. 



IMAGE_NT_HEADER  




의 구조체 부터 보겠습니다.  


typedef struct _IMAGE_NT_HEADERS {

    DWORD Signature;

    IMAGE_FILE_HEADER FileHeader;

    IMAGE_OPTIONAL_HEADER32 OptionalHeader;

} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;



Sigmature 는 PE Header 의 시작을 알려 줍니다.  0x00000450  이죠 . 




그 다음에 


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;



Machine: Machie 필드는 이 파일이 어떤 CPU에서 동작할 수 있는지, 실행될 수 있는 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  // ARM Thumb/Thumb-2 Little-Endian

#define IMAGE_FILE_MACHINE_ARMNT             0x01c4  // ARM Thumb-2 Little-Endian

#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



NumberOfSections: 이 필드는 PE 파일을 구성하는 섹션(Section)의 수로 섹션이 추가되면 이 값이 늘어나고 섹션이 줄어들면 이 값 역시 줄어듭니다. 이 값을 보고 이후에 등장할 섹션의 수를 알아낼 수 있으며 이 필드의 값은 0보다 커야 합니다. 이는 섹션이 한개라도 없는 경우가 존재하지 않는다는 것입니다. 한번 NumberOfSection 필드의 값을 보도록 해봅시다.




위와 같이 총 5개의 section 이 있는것을 알수 있습니다. 



TimeDateStamp: TimeDateStamp 필드는 PE 파일이 만들어진 시간, 즉 이 파일이 빌드된 날짜가 타임스탬프 형식으로 기록됩니다. 그러나 이는 확실히 신뢰할 수 있는 값이 아니며 변조가 가능하니 대략적인 값으로 생각을 하셔야 합니다. 한번 TimeDateStamp 필드의 값도 확인을 해보도록 합시다.



SizeOfOptionalHeader: SizeOfOptionalHeader 필드에는 옵셔널 헤더(IMAGE_OPTIONAL_HEADER32)의 크기가 담깁니다. IMAGE_OPTIONAL_HEADER의 크기는 정해져 있는것 같지만, 운영체제마다 크기가 가변적이기 때문에 PE 로더가 이 SizeOfOptionalHeader 필드의 값을 확인하고 IMAGE_OPTIONAL_HEADER의 크기를 처리합니다. 이것 역시 한번 직접 값이 어떤지 보도록 합시다.



Characteristics: Characteristics 필드는 현재 파일의 형식을 알려주는 역할을 하며, 이 필드의 값을 가지고 실행 가능한 파일인지, DLL 파일인지, 시스템 파일인지, 재배치 여부 등에 대한 정보가 들어있다고 할 수 있습니다.  참고로 OR 연산을 합니다. 


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


RVA,RAW



위에 헤더를 배우기 전에 RVA 라는 용어에 대해서 설명하겠습니다. 


VA(Virtual Address)는 프로세스 가상 메모리의 절대주소를 말하며,RVA(Rela-tive Virual Address)는 어느 기준 위치(ImageBase)에서

부터의 상대 주소를 말합니다. 


RVA + ImageBase = VA 


PE 헤더 내의 정보는 RVA 형태로 된 것이 많이 있습니다. PE파일(주로 DLL)이 프로세스 가동 메모리의 특정 위치로 로딩되는 순간 이미 그위치에 다른 PE 파일(DLL)이 로딩 되어 있을수 있습니다. 


그럴 때 재배치(Relocation) 과정을 통해서 비어 있는 다른 위치에 로딩 되어야 하는데 만약 PE 정보들이 VA(Virual Address,절대 주소

로 되어 있다면 정상적인 엑세스가 이루어 지지 않을 것입니다. 


그러므로 정보를 RVA 로 해두면 메모리 재배치 (Relocation ) 가 발생해도 기준위치에 대한 상대 주소가 변하지 않기 때문에 

아무런 문제 없이 원하는 정보에 엑세스 할수 있는 것입니다. 


RAW


RAW는 PE 파일이 로딩되기 전에 File Offset 의미합니다.

만약에 Image base 01000000 일때  메모리상에 올라 갈때 메모리 맵 입니다. 어떻게 아래와 같은 주소가 결정 되는지는 


세션의 메모리 주소는  IMAGE_OPTIONAL_HEADER 와 IMAGE_SECTION_HEADER 을 참조 해서 정해 줍니다 







 IMAGE_OPTIONAL_HEADER



파라 메터중에 분석할때 쓰이는 항목만 알아보도록 하겠습니다.


typedef struct _IMAGE_DATA_DIRECTORY {

    DWORD   VirtualAddress;

    DWORD   Size;

} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

 

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;





Magic: 32비트(IMAGE_OPTIONAL_HEADER32)인 경우에는 값이 10B이며, 64비트(IMAGE_OPTIONAL_HEADER64)인 경우에는 20B라는 값을 가집니다. 


SizeOfCode: 코드 영역의 전체 크기가 이곳에 들어갑니다. 이는 .text 섹션의 크기가 들어간다는 것입니다.


ImageBase: PE 파일이 메모리에 로드될 때의 시작 주소를 가리킵니다. 기본적으로 EXE 파일의 경우에는 0x400000 번지가, DLL 파일인 경우에는 0x10000000 번지로 지정되어 있으며 그렇다고 해서 항상 이 번지로 고정되어 있는게 아니라는 점을 주의하셔야 합니다. (이는 링커 옵션을 통해서 시작 주소를 지정할 수 있습니다) DLL의 경우는 기본 ImageBase의 값이 0x10000000 번지로 지정되어 있지만, 다른 DLL이 이 번지를 차지하고 있을 경우에는 다른 곳에 배치되는 재배치가 이루어집니다. 


이 ImageBase는 RVA의 기준이 되며, 여기서 RVA(Relative Virtual Address, 상대 가상 주소)란 ImageBase를 기준으로 하여 어느만큼 떨어져 있는지를 나타내는 값으로 파일의 오프셋(Offset)과 같은 개념이라고 할 수 있습니다. 다만, RVA는 파일이 아닌 메모리 공간에서의 상대적인 값으로 예를 들면, ImageBase가 0x400000 번지일 경우 .text 섹션의 RVA 값이 0x3000 이라면 실제로 .text 섹션이 로드되는 위치는 0x403000이 되는 것입니다.


AddressOfEntryPoint: 프로그램이 메모리에서 실행 되는 시작 지점이며, 이는 진입점(Entry Point)를 말하는 것입니다. 위치는 RVA 값으로 저장되어 있으며, WinMain 혹은 DllMain의 번지라고 생각할 수 있습니다. (정확하게는 Start up의 번지라고 할 수 있습니다.) 실제로 다음 실행할 명령이 들어있는 메모리의 번지를 가지는 EIP 레지스터의 값을 파일이 메모리에 로딩되고 나서 ImageBase + AddressOfEntryPoint로 지정합니다. 


BaseOfCode: 코드 영역이 시작되는 상대 주소(RVA)가 담깁니다. BaseOfCode가 RVA니 ImageBase + BaseOfCode의 값은 실제 코드 영역의 주소가 됩니다.


BaseOfCode의 값이 00001000(0x1000)으로, 만약 ImageBase가 0x400000이라면 0x401000이 실제 코드 영역의 주소입니다.



SectionAlignment: 메모리에서 섹션의 최소단위를 나타냅니다. 메모리에서 섹션의 크기는 반드시 SectionAlignment의 배수가 되어야 합니다.


SectionAlignment의 값은 00001000(0x1000) 입니다. 이는 메모리 공간에서 섹션의 크기가 0x1000의 배수라고 할 수 있습니다. 섹션의 크기에서 구조체 크기를 제외한 빈 공간은 모두 0으로 채워지며 이 것을 패딩(padding)이라고 합니다


FileAlignment: 파일에서 섹션의 최소단위를 나타냅니다. 파일에서 섹션의 크기는 반드시 FileAlignment의 배수가 되어야 합니다.



FileAlignment의 값은 00000200(0x200) 입니다. 이는 파일에서 섹션의 크기가 0x200의 배수라고 할 수 있습니다. SectionAlignment와 마찬가지로 섹션의 크기에서 구조체 크기를 제외한 빈 공간은 모두 0으로 채워집니다.



SizeOfImage: PE 파일이 메모리에 로딩되었을 때의 전체 크기를 담고 있습니다. 이 값은 파일의 크기와 같을 때도 있으며, 다를때도 있으나 다른 경우가 더 많습니다. PE 파일이 메모리에 로딩되고 나서는 SectionAlignment의 영향을 받아 패딩이 따라붙으며, SizeOfImage 역시 SectionAlignment의 영향을 받는다고 할 수 있습니다.


SizeOfHeaders: 이름 그대로 모든 헤더의 크기를 담고 있습니다. 즉, 도스 헤더, 도스 스텁, PE 헤더, 섹션 헤더의 크기를 모두 더한 값이라고 할 수 있으며 파일의 시작점에서 SizeOfHeaders 만큼 떨어진 Offset에 첫번째 섹션이 존재합니다.


Subsystem: 이 값을 통해 시스템 드라이버 파일인지, 프로그램이 GUI 혹은 CUI 인지 알아낼 수 있습니다.


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


DataDirectory: IMAGE_DATA_DIRECTORY 구조체를 보시면 VirtualAddress와 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_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_OPTIONAL_HEADER의 DataDirectory 필드는 익스포트 디렉터리, 임포트 디렉터리, 리소스 디렉터리, 예외 디렉터리, 보안 디렉터리 영역 등에 접근할 수 있는 주소와 크기를 지니고 있는 배열로, IMAGE_DATA_DIRECTORY 구조체의 VirtualAddress를 통해 가상 주소를 알 수 있으며, Size를 통해 크기를 알 수 있습니다. 여기서 중요한 값은 EXPORT, IMPORT, RESOURCE, TLS, IAT인데 우선은 이것들을 잘 기억해두시기 바랍니다. 이부분에 대해서는 추후에 다시 설명하도록 하겠습니다.


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;

 

#define IMAGE_SIZEOF_SECTION_HEADER          40



Name: 필드명 그대로 섹션의 이름을 나타냅니다. 저기 상수 IMAGE_SIZEOF_SHORT_NAME의 값인 8, 섹션의 이름은 최대 8바이트까지 가능하다는 겁니다. 그리고 이 필드의 값은 NULL로 비어있을 수 있으며, 다 꽉 채울수도 있습니다. 기본적인 섹션의 이름에 대한 용도는 아래에 정리해 두었습니다.


섹션명


.text

코드, 실행, 읽기 속성을 지니며 컴파일 후의 결과가 이곳에 저장됩니다. 즉, 이 섹션은 실행되는 코드들이 들어가는 섹션입니다.


.data

초기화, 읽기, 쓰기 속성을 지니며 초기화된 전역 변수를 가집니다.


.rdata

초기화, 읽기 속성을 지니며 문자열 상수나 const로 선언된 변수처럼 읽기만 가능한 읽기 전용 데이터 섹션입니다.


.bss

비초기화, 읽기, 쓰기 속성을 지니며 초기화되지 않은 전역 변수의 섹션입니다.


.edata

초기화, 읽기 속성을 지니며 EAT와 관련된 정보가 들어가 있는 섹션입니다.


.idata

초기화, 읽기, 쓰기 속성을 지니며 IAT와 관련된 정보가 들어가 있는 섹션입니다.


.rsrc

초기화, 읽기 속성을 지니며 리소스가 저장되는 섹션입니다.


VirtualSize: PE 로더를 통해 PE 파일이 메모리에 로드되고 나서의 메모리에서 섹션이 차지하는 크기를 가집니다. 위 그림에서 VirtualSize와 PhysicalAddress를 필드로 갖는 공용체가 존재하지만, 여기서 PhysicalAddress는 현재 사용되지 않는 필드고 VirtualSize 필드만 사용됩니다. 


VirtualAddress (RVA): PE 로더를 통해 PE 파일이 메모리에 로드되고 나서의 해당하는 섹션의 RVA 값입니다. 즉, RVA는 이미지 베이스를 기준으로 하는 것이기에 예를 들어서 ImageBase의 값이 0x400000이고, VirtualAddress의 값이 0x1000이라면 로더는 0x401000에 섹션을 올리게 됩니다. 즉 ImageBase + VirtualAddress는 해당 섹션의 실제 주소값이라고 할 수 있습니다. 


PointerToRawData: 파일 상에서의 해당 섹션이 시작하는 위치(파일 오프셋)를 담고 있습니다. 이 값 역시도 옵셔널 헤더 구조체의 FileAlignment 값의 배수가 되어야 합니다. 





어떠한 RVA 의 메모리에 적재된(목표) 세션이 실제로 File의 어디정도 있는지(RAW) 알고 싶을때 


RAW = RVA(목표) - VirtualAddress + PointerToRawData 가 됩니다. 

Characteristics: 섹션의 속성 정보를 플래그로 지니며, 여기서는 6가지의 속성만 알아보도록 하겠습니다.


#define IMAGE_SCN_CNT_CODE                   0x00000020  // 코드로 채워진 섹션

#define IMAGE_SCN_CNT_INITIALIZED_DATA       0x00000040  // 데이터가 초기화된 섹션

#define IMAGE_SCN_CNT_UNINITIALIZED_DATA     0x00000080  // 데이터가 비초기화된 섹션

#define IMAGE_SCN_MEM_EXECUTE                0x20000000  // 실행 가능한 섹션

#define IMAGE_SCN_MEM_READ                   0x40000000  // 읽기가 가능한 섹션

#define IMAGE_SCN_MEM_WRITE                  0x80000000  // 쓰기가 가능한 섹션






출처: http://blog.eairship.kr/270 [누구나가 다 이해할 수 있는 프로그래밍 첫걸음]









Comments