티스토리 뷰

이 블로그는 제가 공부한 것을 바탕으로 정리 목적으로 사용되고 있습니다.

작성 내용중 부족한 부분이나 잘못된 부분을 지적해주시면 감사하겠습니다 (꾸벅)


PE 포맷


PE(Portable Executable)은 윈도우 운영체제에서 사용되는 실행 파일, DLL, object 코드, FON폰트 파일등을 위한 파일 형식이다. 

PE 포맷은 윈도우 로더가 실행 가능한 코드를 관리하는데 필요한 정보를 캡슐화한 데이터 구조체이다. 


  종류

 주요 확장자 

 종류 

 주요 확장자 

  실행 계열

 EXE, SCR 

 드라이버 계열 

 SYS, VXD 

  라이브러리 계열

 DLL, OCX, CPL, DRV

 오브젝트 파일 계열 

 OBJ 


설명을 위해서 노트패드 파일을 이용하여 헥스 에디터 HxD로 열어보도록 하겠다.

HxD로 노트패드를 여는 방법은 (기준은 윈도우 XP이다.) 메뉴 파일 탭에서 열기를 선택하면 아래와 같은 화면이 나온다.



찾는 위치에서 내컴퓨터 -> C:\ -> WINDOWS 로 들어가서 화면에서 보이는 NOTEPAD 를 선택하면 아래와 같은 화면이 다시 나오게 된다.



위의 그림은 노트패드 파일의 시작 부분이며, PE파일의 헤더 부분이다. 여기에는 파일이 실행되기 위해 필요한 모든 정보가 적혀있다.

어떻게 메모리에 적재되고, 어디서부터 실행되어야 하며, 실행에 필요한 DLL들은 어떤 것이 있고, 필요한 Stack / Heap 메모리의 크기를 얼마로 할건지 등등

수많은 정보가 여기에 저장되어 있다.


PE 파일의 기본 구조


<사진 출처 : http://goo.gl/6mjwhu >

위 그림은 노트패트 파일이 메모리에 적재될 때의 모습을 나타낸 것이다. 하나씩 살펴보면 DOS header 부터 Section header 까지를 PE header, 

그 아래의 Section 들을 합쳐서 PE body라 한다. 파일에서는 offset으로, 메모리에서는 VA(Virtual Address)로 위치를 표시하게 된다.

파일이 메모리에 적재되면 모양이 달라진다. 섹션 헤더에 각 Section에 대한 파일/메모리에서의 크기, 위치, 속성 등이 정의되어 있다.



VA 와 RVA 


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

RVA + ImageBox = VA 

이다.

PE 헤더 내의 정보는 RVA 형태로 된 것이 많다. 그 이유는 PE 파일이 프로세스 가상 메모리의 특정 위치에 적재되는 순간 이미 그 위치에 다른 PE 파일이 적재되어 있을 수 있다. 이런 오류의 상황에서 재배치 과정을 통해 비어있는 다른 위치에 적재된다. 이때 PE 헤더 정보들이 VA(절대주소)로 되어 있다면 정상적인 접근이 이루어지지 않을 것이다. 대신 정보를 RVA로 해두면 재배치가 발생해도 기준위치에 대해 상대주소가 변하지 않기 때문에 아무런 문제없이 원하는 정보에 접근 할 수 있을 것이다.



이제 우리가 확실히 알 수 있는 것은  PE 파일이 실행되기 위한 정보가 기록된 것이라는 것과, 그 정보가 제대로 기록되지 않으면 실행이 아예 되지 않는다는 점이다. PE는 일단 여러 구조체로 되어 있다. PE의 엔트리부터 각 구조체가 자리 잡고 있으며, 연이어 다음 구조체가 바이너리를 차지하고 있다. PE를 제대로 알고 있으려면 모두 외워야 하는 구조체이다. (여기서 살펴보는 코드들의 출처 : Microsoft Platform SDK - winnt.h )

IMAGE_DOS_HEADER
IMAGE_NT_HEADER
IMAGE_FILE_HEADER
IMAGE_OPTIONAL_HEADER
IMAGE_SECTION_HEADER (여기까지만 업데이트)
IMAGE_IMPORT_DESCRIPTION (차후 업데이트)
IMAGE_EXPORT_DIRECTORY
IMAGE_IMPORT_BY_NAME
IMAGE_THUNK_DATA32

IMAGE_DOS_HEADER 

마이크로소프트는 PE 파일 포맷을 만들 시에 널리 사용되던 DOS 파일에 대한 하위 호환성을 고려해 만들었다. 그 결과 PE헤더 제일 앞부분에 기존 DOS EXE header를 확장시킨 IMAGE_DOS_HEADER 구조체가 존재한다.

우리가 볼 화면은 EXE나 DLL을 열어서 봤을 때 가장 맨 처음 부분에 해당하는 버퍼이다. 맨 위 부터 PE가 시작된다. ReadFile() 또는 메모리 맵 파일로 볼 경우, 파일의 맨 처음 부분에 해당한다. 아래 코드에 의하면  MapViewOfFile()에서 리턴한 lpFileBase가 PE의 시작이라고 볼 수 있다. 그곳을 IMAGE_DOS_HEADER 구조체로 형변환한 뒤부터 PE의 정보가 시작된다. (4D 5A로 시작하는 부분)


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

                2376 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;


IMAGE_DOS_HEADER안에는 많은 멤버 변수가 존대한다. 이 중에서 우리가 봐야하는 것은 딱 두가지이다.
첫 번째가 e_magic과 마지막 e_lfanew가 그것이다. 나머지는 리버스 엔지니어링, 시스템 프로그래밍을 할 때도 언패킹에 대해 강도 높게 공부하지 않는 한 앞으로 영원히 볼 일이 없다고 생각해도 무방하다.

먼저 e_magic 필드는 현재 파일이 PE파일인지 체크하는 것 말고는 쓸 일이 없다. e_magic이 등장한다면 전부 다음과 같은 코드로 시작될 것이다. 

PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpFileBase;
if (pDosHeader -> e_magic != IMAGE_DOS_SIGNATUE){
     printf(“It’s not PE File format.\n” );
}

e_magic필드가 IMAGE_DOS_SIGNATURE인지 확인하는 코드이다. IMAGE_DOS_SIGNATURE은 5A4D라는 값으로, PE의 가장 첫 번째 값이다.

#define IMAGE_DOS_SIGNATURE     0x5A4D     //MZ

먼저 PE는 “MZ 헤더를 가지고 있다.”라는 표현을 쓰는 사람도 많은데 이 말이 무슨 뜻인지부터 알아보자. MZ헤더란 PE파일에서 가장 먼저 등장하는 표식으로, MZ(4D 5A)스트링이 기록된 곳이다. MZ헤더를 통해 MS-DOS헤더의 시작을 알리고, 이 바이너리가 PE파일인지 아닌지를 체크하는 코드에 해당한다. 
다음 e_flanew 필드에 대해 살펴보자. 이 필드는 IMAGE_NT_HEADER의 구조체 위치를 알아내는 데 사용되는 값이라고 보면 된다. 실질적인 PE의 오프셋이 어딘지 이 필드를 통해 지정하게 된다. 


IMAGE_NT_HEADER

한 개의 DWORD 변수와 두 개의 중요한 구조체로 나눠서 설명할 수 있다.

typedef struct _IMAGE_NT_HEADERS64 {
                2672   DWORD Signature;
                2673   IMAGE_FILE_HEADER FileHeader;
                2674   IMAGE_OPTIONAL_HEADER64 OptionalHeader;

                2675 } IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;


Signature는 “PE\0\0”를 표시하는 4바이트 값으로, 50 45 00 00이 된다. PE파일임을 표시하는 용도 말고는 딱히 사용할 일이 없으며,
바이러스 제작자들이 자신의 값을 표시하는데 자주 이용되어 왔다. PIMAGE_DOS_HEADER에서 e_magic을 검사하는 것과 비슷하다.


IMAGE_FILE_HEADER

이 구조체는 파일을 실행하기 위한 가장 기본적인 데이터가 담겨 있는 구조체라 할 수 있다. 반드시 알아둬야 할 중요한 필드만 하나씩 간단히 알아보것다.

typedef struct _IMAGE_FILE_HEADER {
                2622   WORD  Machine;
                2623   WORD  NumberOfSections;
                2624   DWORD TimeDateStamp;
                2625   DWORD PointerToSymbolTable;
                2626   DWORD NumberOfSymbols;
                2627   WORD  SizeOfOptionalHeader;
                2628   WORD  Characteristics;

                2629 } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;


Machine : 어떤 CPU에서 이 파일이 실행될 수 있는지를 알려준다. 만약 일반 데스크톱이나 노트북에서만 리버스 엔지니어링을 수행할 것이라면, 별로 눈여겨볼 필요는 없다.

NumberOfSections : 섹션이 몇 개 있는지 알려준다. 섹션이란 .text나 .data 같은 코드 섹션이나 데이터 섹션 등을 일컫는다. 보통 비쥬얼 스튜디오에서 MFC로 별다른 옵션 변경 없이 빌드한 파일의 경우엔 아래와 같이 .text .rdata .data .rsrc로 4개의 섹션이 존재하며, NumberOfSection 값도 당연히 4로 표시된다. 
만약 패킹이나 프로텍팅 등의 이유로 섹션 수가 증가한다면 이 값의 수치도 변경된다. 각 섹션에 대한 설명과 역할이 리버스 엔지니어링을 할 때는 매우 중요한데, 그 내용은 IMAGE_SECTION_HEADER 부분에서 다시 설명하것다.

TimeDateStamp : 이 파일을 빌드한 날짜가 표시된다. obj파일이 컴파일러를 통해 EXE로 생성한 시간이라고 할 수 있다.
obj 파일만 따로 만들어 뒀다가 EXE로 컴파일 하는 경우는 사실 거의 없으므로 그냥 대충 개발자가 EXE/DLL을 만든 시간이라고 생각하면 된다. 

SizeOfOptionalHeader : IMAGE_OPTIONAL_HEADER32의 구조체 크기다. IMAGE_OPTIONAL_HEADER32는 PE를 로딩하기 위한 굉장히 중요한 구조체를 담고 있는데, 이 구조체는 운영체제마다 크기가 다를 수도 있기 때문에,  PE로더에서는 SizeOfOptionalHeader 값을 먼저 확인 한 뒤, IMAGE_OPTIONAL_HEADER32 구조체의 크기를 처리한다.

Characteristics : 현재 파일이 어떤 파일 형식인지 알려준다. DLL인지 EXE인지 구분하는 용도라고 생각하면 된다. 


IMAGE_OPTIONAL_HEADER


이번에는 PE구조체 중 중요한 값을 가장 많이 담긴 IMAGE_OPTIONAL_HEADER에 대해 알아보자. 대략적인 레이아웃은 다음과 같다. 중요한 값은 굵게 표시했다.

typedef struct _IMAGE_OPTIONAL_HEADER64 {

                2639   WORD  Magic/* 0x20b */
                2640   BYTE MajorLinkerVersion;
                2641   BYTE MinorLinkerVersion;
                2642   DWORD SizeOfCode;
                2643   DWORD SizeOfInitializedData;
                2644   DWORD SizeOfUninitializedData;
                2645   DWORD AddressOfEntryPoint;

                2646   DWORD BaseOfCode;

                2647   ULONGLONG ImageBase;
                2648   DWORD SectionAlignment;
                2649   DWORD FileAlignment;
                2650   WORD MajorOperatingSystemVersion;
                2651   WORD MinorOperatingSystemVersion;
                2652   WORD MajorImageVersion;
                2653   WORD MinorImageVersion;
                2654   WORD MajorSubsystemVersion;
                2655   WORD MinorSubsystemVersion;
                2656   DWORD Win32VersionValue;
                2657   DWORD SizeOfImage;
                2658   DWORD SizeOfHeaders;
                2659   DWORD CheckSum;
                2660   WORD Subsystem;
                2661   WORD DllCharacteristics;
                2662   ULONGLONG SizeOfStackReserve;
                2663   ULONGLONG SizeOfStackCommit;
                2664   ULONGLONG SizeOfHeapReserve;
                2665   ULONGLONG SizeOfHeapCommit;
                2666   DWORD LoaderFlags;
                2667   DWORD NumberOfRvaAndSizes;
                2668   IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

                2669 } IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

Magic : 표식이라 생각하면 될 것 같다. 32비트인 경우 0x10B가 들어오고 64비트인 경우 0x20B가 된다.

SizeOfCode : 코드 양의 전체 크기를 가리킨다. 실제 개발자가 만든 코드의 양이 얼마나 되는지는 바로 이 필드에 들어간다.
바이러스나 악성코드는 이 필드를 읽어서 자신의 코드를 복제할 위치를 기준 잡기도 하고, 보안 솔루션에서는 코드 섹션의 무결성 검사를 수행시 이 섹션의 값을 얻어와서 검사 크기를 잡기도 한다.

MajorLinkerVersion, MinorLinkerVersion : 어떤 버전의 컴파일러로 빌드했는지 알려준다. 

ImageBase : 해당 파일이 실행될 경우, 실제 가상 메모리에 올라가는 번지를 가리킨다. EXE 파일의 경우는 특별한 번지 지정 옵션이 없는 한 대부분 0x400000이 된다.
올리디버거 등으로 어떤 프로그램 크랙이나 코드 패치 등을 할 때 0x403456 같은 번지로 이동해 점프문을 고치거나 하는 등의 작업을 해본 적이 있을 것이다. 좀더 고급스러운 표현으로는 PE파일이 실제 메모리에 번지에 매핑되는 번지라고 할 수 있다.

AddressOfEntryPoint : 실제 파일이 메모리에서 실행되는 시작 지점, 즉 엔트리 포인트를 말한다. 우리가 올리디버거를 통해 파일을 실행했을 때 디버거는 첫 실행 지점을 이곳과 이미지 베이스를 합산한 위치에 지정해서 멈춰준다.

BaseOfCode : 실제 코드가 실행되는 번지라고 할 수 있다. 이미지베이스는 PE파일 전체에 대한 시작 주소이고, 코드 영역이 시작되는 베이스 주소는 BaseOfCode를 더한 값부터 시작된다. 특별한 일이 없는 한 0x1000으로 지정돼 있다.

SectionAlignment, FileAlignment : 각 세션을 정렬하기 위한 저장 단위라고 생각할 수 있다. 보통은 0x1000이 저장되며, 0x1000 단위로 구분된다. 에를 들어 .text 섹션이 있고, 다음에는 .rdata 섹션이 있다고 해보자. 그리고 .text의 실제 크기가 0x800바이트 정도라면 800바이트 이휴에 바로 .rdata섹션이 등장하는 것이 아니고 SectionAlignmet가 0x1000이므로 나머지 0x200은 0으로 채워진다.
그리고 그렇게 0x1000을 채운 뒤, 다음 섹션인 .rdata가 시작되는 등 섹션 정렬에 대한 최소 단위라고 생각 할 수 있다. 

SizeOfImage : 이 EXE/DLL가 메모리에 로딩됐을 때의 전체 크기라고 생각하면 된다. 로더가 PE를 메모리에 올릴 때 SizeOfImage필드를 보고 이 공간만큼 확보하게 된다.

SizeOfHeaders : PE 헤더의 크기를 알려주는 필드다. 0x1000이라면 메모리에 올라갔을 떄의 번지 계사니 매우 간편해진다. 만약 다른 값인 경우에는 계산을 해야하므로 SizeOfHeader로 해당 값을 확인해야 한다. 


IMAGE_SECTION_HEADER

섹션 헤더는 주로 각 섹션에 대한 이름을 비롯해 시작 주소와 시이즈 등의 정보를 관리는 구조체로, 우리가 말하는 코드 세견이니 데이터 섹션이니 하는 것들에 대한 정보를 구할 수 있다.

 2736 #define IMAGE_SIZEOF_SHORT_NAME 8
                2737 
                2738 typedef struct _IMAGE_SECTION_HEADER {
                2739   BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
                2740   union {
                2741     DWORD PhysicalAddress;
                2742     DWORD VirtualSize;
                2743   } Misc;
                2744   DWORD VirtualAddress;
                2745   DWORD SizeOfRawData;
                2746   DWORD PointerToRawData;
                2747   DWORD PointerToRelocations;
                2748   DWORD PointerToLinenumbers;
                2749   WORD  NumberOfRelocations;
                2750   WORD  NumberOfLinenumbers;
                2751   DWORD Characteristics;

                2752 } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;


VirtureSize : 메모리에서 섹션이 차지하는 크기를 의미한다. 

VirtureAddress : 메모리에서 섹션의 시작 주소를 의미한다. (RVA 상대 주소값)

SizeOfRawData : 파일에서 섹션이 차지하는 크기를 의미한다. 

PointerToRawData : 실제 섹션 데이터가 파일 내에 존재하는 오프셋, 즉 파일에서 섹션의 시작 위치를 의미한다.

Characteristic : 섹션의 속성 표시

VirtureAddress, PointerToRawData 는 아무 값이나 가질 수 없고, 각각 SectionAlignment와 FilAlignment에 맞게 설정된다. VirtureSize 와 SizeOfRawData 는 일반적으로 서로 다른 값을 가진다. 즉 파일에서의 섹션 크기와 메모리에 적재된 섹션의 크기가 다르다는 의미이다.
Characteristic는 아래의 값들의 조합으로 이루어진다. 이때 사용된 조합은 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_CNT_EXECUTE                             0x20000000 // Section is executable

#define IMAGE_SCN_CNT_READ                                   0x40000000 // Section is readable

#define IMAGE_SCN_CNT_WRITE                                  0x80000000 // Section is writale



< 출처 : 리버싱 핵심 원리 ; 저자 : 이승원 >


'Reversing > Reverse Engineering' 카테고리의 다른 글

실행 압축 테스트  (0) 2016.04.26
Huffman Algorithm에 대해서  (0) 2016.04.23
abex’ crackme #1  (0) 2015.12.26
OllyDbg를 이용한 스택 공부하기  (0) 2015.12.23
PACKING  (0) 2015.12.21
댓글