본문 바로가기
분석의 묘미

PE File Format에 좀 더 자세히 접근해보자!

by Sweeny 2008. 7. 28.
반응형
※ 흐릿한 그림은 클릭(Click)하면 원본크기로 나옵니다.



PE 파일 형식 예와 구성

그림 1-5는 EXE 파일을 바이너리로 읽어 들인 것입니다.
맨 앞의 MZ로 시작하는 부분이 MS-DOS의 헤더입니다. "This Program cannot be run is dos mode" 라는 부분의 앞 부분이 도스에서 윈도우 프로그램이 실행되는 것을 막는 역할을 합니다. PE 헤더는 Winnt.h 안에 들어 있습니다.

사용자 삽입 이미지
 
<그림 1-5>


아래 그림 1-6은 PE형식의 구성도입니다.

사용자 삽입 이미지
 
<그림 1-6>


PE 구조 첫 부분이자 첫 시작! 도스 스텁(Dos Stub)과 도스 헤더(Dos Header)

이 도스스텁과 도스헤더는 DOS 시절에 사용되었지만 현재 윈도우에서는 사용되고 있지는 않습니다.
다만, 이 부분을 모르고 넘어간다면 다음에 나올 DOS_HEADER에 대한 구조체를 이해하기 힘들기 때문에 반드시 짚고 넘어가야 하는 부분입니다. (단, 현재 악성코드들이 DOS 헤더를 이용하는 경우가 가끔 있음)

사용자 삽입 이미지
<그림 1-7>

아래는 구조체 필드의 정보를 적어봤습니다. 현재 윈도우에서 사용되고 있는 필드는 거의 두개뿐이므로 이 두개에 대한 정보만을 적겠습니다.

1. e_magic
IMAGE_DOS_HEADER의 시작을 나타내는 것으로 항상 'MZ'라는 문자열로 시작 합니다. 그리고 e_lfanew 필드는 IMAGE_DOS_HEADER 다음에 나오는 헤더 파일의 오프셋 값을 가지고 있습니다. 그 외의 필드들은 Windows에서 파일을 실행시켰을 때 이용되지 않는다. 즉, DOS헤더의 MZ라는 정의입니다.

2. e_lfanew
위 그림에서 마지막 빨간색 네모칸의 e_lfanew는 PE헤더가 있는 곳의 실제 오프셋 값을 나타내며 실제 PE 형식의 주소를 가리키고 있는 상대 주소 값입니다. e_lfanew는 나중에 나올 추후 나올 IMAGE_NT_HEADER의 시작위치를 알려줍니다.

실제주소를 얻기 위해서 아래 코드와 같이 이용하게 합니다.

hFile = CreateFile( "C:\\winnt\\system32\\KERNEL32.DLL", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
hFileMap = CreateFileMapping( hFile, NULL, PAGE_READONLY, 0, 0, NULL);
dwSize = GetFileSize( hFile, 0);
pBaseOfFile = (char*)MapViewOfFile( hFileMap, FILE_MAP_READ, 0, 0, dwSize );

PIMAGE_DOS_HEADER          pIDH = (PIMAGE_DOS_HEADER)pBaseOfFile;
PIMAGE_NT_HEADERS          pIDH = (PIMAGE_NT_HEADERS)((DWORD)pIDH + pIDH -> e_lfanew);

사용자 삽입 이미지



PE 두번째 부분 PE 헤더IMAGE_NT_HEADERS 입니다.

사용자 삽입 이미지


IMAGE_NT_HEADERS는 위 그림과 같은 구조를 지니고 있습니다.

typedef struct _IMAGE_NT_HEADERS {
  DWORD Signature;
  IMAGE_FILE_HEADER FileHeader;
  IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;


첫번째 시그너처(Signature)의 값은 PE\0\0라는 값을 갖습니다. 이 값을 체크해서 PE포멧 파일인지 아닌지 구별할 수 있습니다. 윈도우에서는 이 값을 식별하기 위하여 IMAGE_NT_SIGNATURE 라는 상수를 정의해 놓았습니다. 이 값은 항상 4바이트로 정의되어 있습니다.

< 참고할 IMAGE_NT_HEADERS의 주소 위치 >
사용자 삽입 이미지


이번엔 PE 형식 세번째 구조 IMAGE_FILE_HEADER 입니다.

사용자 삽입 이미지


파일헤더는 그림 1-5 에서 0xD0 번지부터는 "PE\0\0"을 볼 수 있는데 이는 PE라는 것을 밝히는 PE의 사인입니다.  이 구조체에는 파일에 대한 기본적인 정보가 담겨 있습니다.


typedef struct _IMAGE_FILE_HEADER {
  WORD Machine;
  WORD NumberOfSections;
  DWORD TimeDateStamp;
  PointerToSymbolTable;
  DWORD NumberOfSymbols;
  
 WORD SizeOfOptionalHeader;
  WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

아래는 구조체의 정보들입니다.

1. Machine

어떤 플랫폼에서 동작하기 위한것인지 나타냄. 그리고 또 한가지 눈에 띄는 점은 필자의 닉네임택과 Tistory 주소 택 이미지가 추가 되었습니다. 그 전까진
twister1018@naver.com 이라는 제 이메일로 대신했지만... 앞으론 저 택들이 쭉 함께 있을것입니다.

 
사용자 삽입 이미지
<그림 1-8>

2. NumberOfSections;

섹션을 분석하기 위해 사용되는 값입니다. 파일을 Hex파일등으로 열어 하드코딩시에 이 값을 변경시켜 섹션수를 늘리고 코드를 추가할 수 있습니다. 즉, 섹션 개수라고 생각하면 됩니다.


3. TimeDateStamp;
파일을 만든 날짜와 시간입니다.

4. PointerToSymbolTable;
COFF 심볼 테이블의 주소입니다.

5. NumberOfSymbols;
COFF 심볼 테이블의 심볼의 개수
입니다.

6. SizeOfOptionalHeader;

IMAGE_FILE_HEADER 바로 다음에 위치한 IMAGE_OPTIONAL_HEADER 구조체의 크기입니다. 32-bit 윈도우에선 0xE0라는 어마어마한 크기를 갖고 있습니다.


7. Characteristics;
파일이 exe인지, dll인지에 대한 플래그를 갖고 있는 변수입니다.


헤더의 멤버중 SizeOfOptionalHeader(옵션 IMAGE_OPTIONAL_HEADER(예제 1-2) 이것이 가르킴) 엔트리 포인트 주소, 스택 사이즈 등을 이 헤더에서 관리합니다.

7번째 AddressOfEntryPoint는 프로그램 코드의 시작 주소를 나타내며 절대 주소가 아닌 상대 주소(RVA, Relative Virtual Address, 특정 주소로부터 얼만큼 떨어져있는지 나타낸 값을 칭함. 만약 주소가 0x50이고 실제 주소가 0x30이라면 RVA는 0x20 됨. 단, 대상이 가상메모리임을 명심) 시작 점입니다.

AddressOfEntryPoint 값이 가리키는 값은 .text에 위치하며, IMAGE_OPTIONAL_HEADER는 프로그램이 구동하기 위한 기본 정보들을 담고 있으므로 중요한 구성원은 굵은 글씨를 보면 됩니다.

사용자 삽입 이미지

<그림 1-9>

 

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 SzieOfImage;
  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_HEADER, *PIMAGE_OPTIONAL_HEADER;


이렇게 PE 사인과 IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER를 합쳐 IMAGE_NT_HEADERS라 합니다. 아래는 IMAGE_NT_HEADERS의 구조체입니다.

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



PE의 마지막 구조체인 섹션 헤더(Section Header)

이제 마지막인 섹션 헤더입니다. 섹션 헤더에서의 IMAGE_SECTION_HEADER은 .text, .data, .idata, .reloc 등의 구역(Section)의 시작 주소와 사이즈에  관한 정보가 들어 있습니다. 우선 .text는 실제 코드가 들어 있습니다. .data 구역은 전역이나 정적 변수들이 존재합니다. 그리고 아래쪽에 .idata 테이블이 있는데 이 테이블은 DLL로부터 가져다 쓴 함수들의 실제 주소가 적혀 있습니다.

섹션 테이블의 구조체 정의는 아래와 같습니다.

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;

1. Name[IMAGE_SIZEOF_SHORT_NAME];
섹션의 이름이 들어 있습니다.이 멤버는 섹션이름을 나타내는 문자열을 저장하게 되는데 IMAGE_SIZEOF_SHORT_NAME 은 8바이트 크기를 나타내는 상수입니다. .text나 .data 같은 이름을 갖지만 섹션의 이름으로 섹션의 성격을 파악해선 안됩니다. 이 멤버의 값은 null일 수도 있으며 섹션이름이 같다고 하여 항상 같은 속성을 지니진 않습니다. 섹션의 속성을 파악하기 위해선 Characteristics 멤버를 참조하여야 합니다.


2. PhysicalAddress/VirtualSize
PE로더에 의해 이미지가 메모리에 올려진 후에 해당 섹션이 얼마만큼의 크기를 가지고 있게 되는지의 정보입니다. 이 멤버는 Misc 유니온 구조체의 멤버이며 이 이름은 변수 의미에 대해 충분한 오해의 소지가 있습니다. 이는 물리적주소를 의미하는게 아니라 가상주소 상에서 해당 섹션의 크기를 나타내는 멤버라는 점을 주의하기 바랍니다.

3. VirtualAddress
PE로더에 의해 이미지가 메모리에 올려진 후에 해당 섹션이 어느 주소에 위치하는지의 RVA 주소를 값으로 가지고 있습니다. 이 멤버는 항상 IMAGE_OPTIONAL_HEADER의 멤버인 SectionAlignment 의 배수값을 가집니다. PE로더가 메모리에 섹션을 올릴때 이 멤버의 값을 참조하게 됩니다.
이 멤버의 값이 0x1000h 이고 IMAGE_OPTIONAL_HEADER의 ImageBase 값이 0x00400000h 라면 PE로더는 해당 섹션을 0x00401000h 번지에 올립니다. 항상 이 값은 ImageBase를 기준으로 하는 RVA값이라는 걸 주의하기 바랍니다.

4.
.text 섹션
.text 섹션에는 일반적으로 실행되는 코드들이 들어 있습니다. 따라서 어셈블러나 컴파일러가 만드는 코드들이 들어가게 됩니다. .idata는 .text와 DLL주소간의 매핑 테이블이며, 이런 .idata의 기능 덕분에 .text는 읽기 전용이 가능하다는 결론입니다. .text은 IAT를 참조하여 함수,변수를 호출하고 사용합니다.

5. .data 섹션
.data 섹션은 초기 데이터들이 있으며 변수들이 컴파일하며 초기화 됩니다. 지역 변수의 경우 쓰레드 스택에 저장되며 .data 영역에는 존재하지 않습니다. 즉, .text섹션과 함게 프로그램 코드와 데이터를 포함한 영역입니다.


6. .idata 섹션
다른 DLL로부터 가져다 쓰는 함수들의 정보가 담겨 있습니다. 정적 임포트 데이터 섹션으로 런타임 동적으로 로드된것은 (LoadLibrary API 또는 GetProcAddress API) 임포트 섹션이 필요가 없습니다. 로드타임 동적 링크인 경우에 사용되어 집니다. .idata역시 import directory table(IDT)가 있으며 임포트 하려는 DLL들의 엔트리가 저장 되어 있습니다.

7. .edata 섹션
함수의 리스트들이 들어 있는 섹션입니다. export directory table(edt)가 있으며 주 내용으로 "순서수 베이스", EAT(export address table)의 엔트리수, ENPT(export name pointer table)의 엔트리수, EAT의 RVA, ENPT의 RVA, 순서수 테이블의 RVA가 있습니다.


 

마치며...

PE의 시작부터 끝까지 모두 잘 살펴보았습니다.
Win32 PE의 구조를 살펴본다는 것은 컴퓨터를 만지는 전산장이에게는 상당히 즐거운 일일수도, 반대로 재미 없는 일일 수도 있겠습니다만, 컴퓨터를 활용하는데에 있어서 네트워크 분야나 프로그래밍의 한 분야만을 고수하기 보다는 여러 분야의 장단점을 생각하여 본인만의 컴퓨터 학습 방법을 따로 익히는 것이 좋을것 같습니다.

참고 자료 - MSDN, An In-Depth Look info the Win32 PE File FOrmat - Part 1
참고 도서 - 해킹, 파괴의 광학(출판사 : 와이미디어, 김성우 저자)
 

반응형