[리버싱 - 5] : PE Header - ②

너무 중요한 IMAGE_OPTIONAL_HEADER 구조체 알아보기


IMAGE_OPTIONAL_HEADER

PE 구조체 중 가장 중요하다고 판단되는 IMAGE_OPTIONAL_HEADER 구조체이다. 코드는 아래와 같다.

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;

image-optional-header 너무 많은 멤버 중에서 중요한 멤버만 추려보자.

Magic

이미지 파일의 상태이다. 일종의 표식이라고 생각하면 되고 32비트의 경우 0x10b64비트의 경우 0x20b가 들어온다.

SizeOfCode

코드 양의 전체 크기를 가리킨다. 실제 개발자가 코딩한 양이 어느 정도인지 여기서 확인 가능하다.

MajorLinkerVersion / MinorLinkerVersion

어떤 버전의 컴파일러로 빌드했는지 알려주는 멤버이다.

ImageBase

해당 파일이 실행될 때 가상메모리에 로드될 번지를 가리킨다. 32비트의 경우 DLLImageBase0x10000000이고 EXE0x400000이고 64비트의 경우 DLLEXE의 ImageBase는 각각 0x180000000, 0x140000000이다.

AddressOfEntryPoint

실제 파일이 메모리에서 실행되는 시작 지점. 즉, Entry Point를 말한다.

IDA등 디버거를 통해 파일을 열었을 때 AddressOfEntryPoint의 값과 ImageBase의 값을 더한 주소에 처음 위치 시켜준다. GifCam이라는 프로그램을 PEview로 열었을 때 사진이 위에 있다. ImageBase0x400000, AddressOfEntryPoint의 값이 0x155538이라고 적혀있다. IDA로 GifCam 프로그램을 열면 ImageBaseAddressOfEntryPoint를 더한 값인 0x555538에 처음 위치로 잡아주는 것을 아래와 같이 확인 가능하다. entrypoint

BaseOfCode

실제 코드가 실행되는 번지이다. ImageBase는 PE 파일 전체에 대한 시작 주소이고 코드 영역이 시작되는 부분은 ImageBaseBaseOfCode 값을 더한 부분부터 시작된다.

웬만해선 BaseOfCode의 값은 0x1000으로 설정되어 있지만 컴파일로 변경할 수도 있다.

SectionAlignment / FileAlignment

각 섹션을 정렬하기 위한 저장 단위라고 생각할 수 있다. 보통은 0x1000으로 값이 채워져 있다. 이는 0x1000 단위로 섹션을 구분한다는 의미이다.

예를 들어 .text 섹션이 존재하고 그 다음에는 .rdata 섹션이 있다고 하자. .text 섹션의 크기가 0x800 바이트 정도라면 800바이트 이후에 바로 .rdata 섹션이 등장하는 것이 아닌 0x1000에서 0x800을 뺀 0x200은 0으로 채우고 0x1000 이후부터 .rdata 섹션이 등장하는 방식이다.

FileAlignment는 파일상의 간격, SectionAlignment는 메모리에 올라갔을 때의 간격이라고 생각하면 된다.

SizeOfImage

EXE나 DLL이 메모리에 올라갔을 때의 전체 크기라고 생각하면 된다. 로더가 PE를 메모리에 올릴 때 이 SizeOfImage의 값을 보고 공간을 확보한다.

보통 파일로 존재할 때와 실제 메모리에 올라갔을 때의 크기가 같은 경우도 있겠지만 대부분 다를 때가 더 많으므로, 특히 섹션의 위치가 각각 다른 위치에 매핑되면서 메모리에 올라간 PE의 크기가 더 큰 경우가 많다.

따라서 SizeOfImage의 값은 항상 SectionAlignment값의 배수가 된다.

SizeOfHeaders

PE 헤더의 크기를 알려주는 필드이다. 보통 0x1000의 값을 가지며 이럴 경우 ImageBase의 값만 더하면 돼서 간편하지만 0x400등의 경우(위 GifCam의 SizeOfHeaders의 값이 0x400이다.) 메모리 위치와 파일 위치가 단순 더해서만 되는 것이 아니라서 이 경우 직관적으로 번지를 계산할 수 없고 계산기등의 도움이 필요하다.

Subsystem

이 프로그램이 콘솔인지 GUI인지 확인할 때 이용할 수 있다.

  • 0x2 : Windows GUI 하위 시스템
  • 0x3 : Windows CUI 하위 시스템

DataDirectory

데이터 디렉터리의 첫 번째 IMAGE_DATA_DIRECTORY 구조체에 대한 포인터이다.

typedef struct _IMAGE_DATA_DIRECTORY {
  DWORD VirtualAddress;
  DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

VirtualAddressSize라는 필드가 포함되어 있다. Export 디렉터리나 Import 디렉터리, Resource 디렉터리, IAT 등 각각의 가상 주소와 크기를 이 필드를 통해 알 수 있다. 총 배열의 크기는 16개로 원소는 아래와 같다.

붉은색 글씨의 원소는 다른 원소에 비해 중요한 원소이다.

  • IMAGE_DIRECTORY_ENTRY_ARCHITECTURE(7) : 아키텍처별 데이터
  • IMAGE_DIRECTORY_ENTRY_BASERELOC(5) : 기본 재배치 테이블
  • IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT(11) : 바인딩된 가져오기 디렉터리
  • IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR(14) : COM 설명자 테이블
  • IMAGE_DIRECTORY_ENTRY_DEBUG(6) : 디버그 디렉터리
  • IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT(13) : 테이블 가져오기 지연
  • IMAGE_DIRECTORY_ENTRY_EXCEPTION(3) : 예외 디렉터리
  • IMAGE_DIRECTORY_ENTRY_EXPORT(0) : 디렉터리 내보내기
  • IMAGE_DIRECTORY_ENTRY_GLOBALPTR(8) : 전역 포인터의 상대 가상 주소
  • IMAGE_DIRECTORY_ENTRY_IAT(12) : 주소 테이블 가져오기
  • IMAGE_DIRECTORY_ENTRY_IMPORT(1) : 디렉터리 가져오기
  • IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG(10) : 구성 디렉터리 로드
  • IMAGE_DIRECTORY_ENTRY_RESOURCE(2) : 리소스 디렉터리
  • IMAGE_DIRECTORY_ENTRY_SECURITY(4) : 보안 디렉터리
  • IMAGE_DIRECTORY_ENTRY_TLS(9) : 스레드 로컬 스토리지 디렉터리

DataDirectory 위 사진을 보면 RVA(Relative Virtual Address)가 나오는데 상대 주소라고 생각하면 된다.

즉, 실제 주소가 0x403000이고 베이스 주소가 0x400000이라면, RVA는 0x3000이다.

실제로 계산 해보자.

위 사진에서 IAT 테이블RVA 값은 0x324A84이다.

베이스 주소(0x400000) + RVA(0x324A84) = 0x724A84이다.

IDA에서 0x724A84 주소게 가보자. rva-for-iat Import oleaut32.dll로 시작하는 것으로 보아 IAT가 시작되는 부분임을 알 수 있다.

RVA는 이처럼 베이스 주소를 기준으로 얼마만큼 떨어져 있는지 알려준다.