PE파일을 분석해서 프로그램 만들기

DOS Header

Dos환경에서 실행시키는 프로그램이 아니기 때문에, Dos stub 값은 생략할 수 있습니다.

  1. e_magic의 값을 4D 5A(MZ)로 설정합니다.
  2. NT Header 구조체의 위치를 가리키는 e_lfanew의 값을 00 00 00 40으로 설정합니다. (리틀 엔디언 : 40 00 00 00)

완성된 DOS Header의 모습은 다음과 같습니다.

NT Header

Signature

4바이트 값인 Signature값을 50450000(MZ)으로 설정합니다.

IMAGE FILE HEADER

다음으로 총 20바이트로 구성된 이미지 파일 헤더를 설정합니다.

  1. Machine(기계 종류, 2byte)으로, Intel x86 호환 칩셋 환경에서 동작하므로 014C로 설정합니다. (리틀 엔디언 : 4C 01)
  2. NumberOfSections(섹션의 수, 2byte)로 총 세 개의 섹션을 생성할 것이므로 0003으로 설정합니다. (리틀 엔디언 : 03 00)
  3. TimeDateStamp(타임스탬프, 4byte)로 PE파일이 마지막으로 수정된 시간을 나타냅니다. 타임스탬프는 1970년 1월 1일 00:00:00 UTC를 기준으로 시작하는 초 단위의 정수값이지만, 따로 설정하지 않습니다.
  4. PointerToSymbolTable(4byte), NumberOfSymobls(4byte)를 설정합니다. 심볼 테이블은 디버깅 도구에 사용되나, 따로 설정하지 않으므로 전부 00으로 표기합니다.
  5. SizeOfOptionalHeader(옵션 헤더 크기, 2byte)로, 옵션 헤더 크기를 E0으로 설정합니다. (리틀 엔디언 : E0 00)
  6. Characteristics(특성, 2byte)로, PE파일의 속성을 나타냅니다. 실행 가능한(0002) 32비트(0100)프로그램이므로 0102로 설정합니다. (리틀 엔디언 : 02 01)

IMAGE OPTIONAL HEADER

image file header에서 image optional header의 크기를 E0(224byte)로 정했기 때문에, 총 224byte의 크기를 차지합니다.

  1. Magic(2byte) 필드에서는 PE파일의 종류를 나타내며 32비트 이미지이므로 010B로 설정합니다. (리틀 엔디언 : 0B 01)
  2. 다음 2바이트는 1바이트씩 링커 버전 정보를 나타내는데 링커를 사용하지 않으므로 0000으로 설정합니다. (리틀 엔디언 : 00 00)
  3. SizeOfCode(코드 섹션의 크기, 4byte)입니다. 200 크기로 설정합니다. 총 512바이트입니다. (리틀 엔디언 : 00 02 00 00)
  4. SizeOfInitializedData(4byte)와 SizeOfUninitializedData(4byte)입니다. 둘 모두에 해당 사항이 없으므로 00으로 채웁니다.
  5. AddressOfEntryPoint(진입점, 4byte)는 이름 그대로 프로그램의 진입점을 나타내며 code섹션의 virtual address를 3000으로 설정할 것이므로 3000으로 설정합니다. (리틀 엔디언 : 00 30 00 00)
  6. BaseOfCode(코드의 시작점, 4byte) 마찬가지로 code섹션의 시작점인 3000으로 설정합니다. (리틀 엔디언 : 00 30 00 00)
  7. BaseOfData(데이터 시작점, 4byte) string 데이터를 저장하는 섹션의 virtual address를 2000으로 설정할 것이므로 2000으로 설정합니다. (리틀 엔디언 : 00 20 00 00)
  8. ImageBase(기본 주소, 4byte) PE파일이 메모리에 로드될 때 기본 주소를 나타내는데, 여기서는 400000으로 설정합니다. (리틀 엔디언 : 00 00 40 00)
  9. SectionAlignment(섹션 정렬, 4byte)는 메모리 내에서의 섹션의 간격을 의미합니다. 여기서는 1000을 설정하였으므로, 메모리 상에서 각 섹션의 주소가 1000의 단위로 구분됩니다. (리틀 엔디언 : 00 10 00 00)
  10. FlieAlignment(파일 정렬, 2byte)는 파일 내에서의 섹션의 간격을 의미합니다. 여기서는 200을 설정하였으므로, RAW 섹션의 주소가 200의 단위로 구분됩니다. (리틀 엔디언 : 00 02 00 00)
  11. 다음 16바이트는 윈도우 버전과 관련한 정보, 그리고 Win32 런타임 버전을 나타냅니다.
  12. SizeOfImage(이미지 크기, 4byte)는 이미지 전체 크기를 나타내며, 총 4000의 크기를 가진 이미지를 구성할 것이므로 4000으로 설정합니다. (리틀 엔디언 : 00 40 00 00)
  13. SizeOfHeaders(헤더 크기, 4byte)는 섹션 헤더들의 총 크기를 나타냅니다. 200씩 할당합니다. (리틀 엔디언 : 00 02 00 00)
  14. CheckSum(체크섬, 4byte)은 PE파일의 무결성을 확인하는 체크섬 값을 나타냅니다. PE파일의 실제 체크섬과 해당 값을 비교합니다. 현재는 따로 설정하지 않았습니다.
  15. Subsystem(서브시스템, 2byte)은 실행 중인 프로그램의 서브시스템을 나타냅니다. GUI와 콘솔 서브시스템을 선택할 수 있습니다. 과제에서는 GUI환경을 사용하므로 0002 값을 설정합니다. (리틀 엔디언 : 02 00)
  16. DllCharacteristics(DLL 특성, 2byte)는 DLL의 특성을 지정합니다. 이 PE 프로그램은 실행 가능한 응용 프로그램이므로 따로 설정하지 않았습니다. (리틀 엔디언 : 00 00)
  17. SizeOfStackReserve(스택 예약 크기, 4byte)는 스택의 최대 크기를 나타냅니다. 즉 해당 크기만큼의 스택 메모리를 예약합니다. 10000으로 설정합니다. (리틀 엔디언 : 00 00 01 00)
  18. SizeOfStackCommit(스택 커밋 크기, 4byte)는 물리적으로 할당되는 스택의 실제 메모리 크기를 나타냅니다. 1000으로 설정합니다. (리틀 엔디언 : 00 10 00 00)
  19. SizeOfHeapReserve(힙 예약 크기, 4byte)는 스택 예약 크기와 마찬가지로 힙의 최대 크기를 나타냅니다. 10000으로 설정합니다. (리틀 엔디언 : 00 00 01 00)
  20. SizeOfHeapCommit(힙 커밋 크기, 4byte)도 물리적으로 할당되는 힙의 실제 메모리 크기를 나타냅니다. 1000으로 설정합니다. (리틀 엔디언 : 00 10 00 00)
  21. LoaderFlags(로더 플래그, 4byte)는 로더가 사용하는 추가 플래그를 지정합니다. 로더를 사용하지 않으므로 00으로 설정합니다. (리틀 엔디언 : 00 00 00 00)
  22. NumberOfRvaAndSizes(RVA 및 크기의 개수, 4byte)는 데이터 디렉토리 항목의 개수를 나타냅니다. 일반적으로 16개를 가집니다. Hex로 표현하면 10입니다. (리틀 엔디언 : 10 00 00 00)
  23. DataDirectory(데이터 디렉토리, 128byte)는 각 8바이트로 16개로 이루어진 배열입니다. 이 중에서 필요한 시스템 콜 함수를 불러오기 위해, Import Table 정보를 설정합니다. Import Table은 두 번째 배열에 위치하며, 첫 4바이트로 import의 virtual address를 설정하고, 다음 4바이트로 그 크기를 설정합니다. 각각 1000과 28로 설정합니다. (리틀 엔디언 : 00 10 00 00 28 00 00 00)

따라서 완성된 NT 헤더의 모습은 다음과 같습니다.

Section Headers

섹션 헤더에서는 각 섹션들의 정보를 정의합니다. 각 헤더가 나타내는 정보는 동일합니다.
Name(8byte), VirtualSize(4byte), VirtualAddress(4byte), SizeOfRawData(4byte), PointerToRawData(4byte), 그리고 12바이트를 건너뛰어 마지막 4바이트에 섹션의 Characteristics를 정의합니다.

imports Section Header

import섹션의 VirtualAddress는 1000으로 설정했습니다. 또한 import의 경우 initialized data이며 읽기가 가능하므로 40000040을 설정합니다. (리틀 엔디언 : 40 00 00 40)

strings Section Header

strings섹션의 VirtualAddress는 2000으로 설정했습니다. 또한 strings의 경우 initialized data이며 읽기와 쓰기가 모두 가능하므로 C0000040을 설정합니다. (리틀 엔디언 : 40 00 00 C0)

codes Section Header

codes섹션의 VirtualAddress는 3000으로 설정했습니다. 또한 codes의 경우 code이며 읽기와 실행이 모두 가능하므로 60000020을 설정합니다. (리틀 엔디언 : 20 00 00 60)

따라서 완성된 섹션 헤더는 다음과 같습니다.

그리고 파일 정렬에 의해 정해진 위치에서 섹션을 시작하기 위해 File Offset기준 200까지 NULL byte를 추가합니다.

imports Section

첫 20바이트는 image import descriptor입니다. 다음은 image import descriptor의 상세 사양입니다.

  1. OriginalFirstThunk(4byte)로, INT의 주소를 나타냅니다. (리틀 엔디언 : 28 10 00 00)
  2. TimeDateStamp와 ForwarderChain(각 4byte)을 설정합니다. 설정 사항이 없으므로 00을 입력합니다. (리틀 엔디언 : 00 00 00 00 00 00 00 00 00)
  3. Name(4byte)으로 불러올 DLL의 이름의 상대적 위치를 설정합니다. 00001040에 위치하므로 이 값과 같게 설정합니다. (리틀 엔디언 : 40 10 00 00)
  4. FirstThuck(4byte)는 IAT의 주소를 나타냅니다. INT는 가져올 함수의 이름 또는 ordinal값을, IAT는 실제로 호출할 DLL함수들의 주소를 가지지만 이 경우 함수의 이름을 기반으로 호출하므로 두 값이 같습니다. (리틀 엔디언 : 28 10 00 00)

그리고 다음 20바이트는 NULL로 유지합니다. 그 다음 8바이트는 각각 INT와 IAT주소 리스트입니다. 함수의 이름을 기반으로 호출하므로 IAT는 NULL로 설정되며 INT의 값은 1030입니다. (리틀 엔디언 : 30 10 00 00)

다음으로 RVA상 1030에 해당하는 위치에 함수의 이름을 설정합니다. 필요한 함수는 MessageBoxW이므로, Null로 설정한 Hint값 뒤에 입력합니다. (리틀 엔디언 : 4D 65 73 73 61 67 65 42 6F 78 57)

그 다음 RVA상 1040에 해당하는 위치에 DLL의 이름을 설정합니다. 필요한 DLL은 USER32.dll입니다. (리틀 엔디언 : 55 53 45 52 33 32 2E 64 6C 6C)

완성된 imports Section은 다음과 같습니다.

마찬가지로 파일 정렬을 만족하기 위해 NULL byte를 채웁니다.

strings Section

미리 정해진 조건 없이 프로그램이 실행되는 데 필요한 문자열들을 저장합니다. MessageBoxW함수를 실행하기 위해, “2021350034 이주호”, “KU Reverse Engineering”의 문자열을 NULL로 구분지어 저장했습니다. VA기준 문자열의 시작 위치는 이후 code가 실행되는 데 필요합니다. VA = ImageBase + RVA라는 식으로 쉽게 유도할 수 있습니다.

완성된 strings Section은 다음과 같습니다.

마찬가지로 파일 정렬을 만족하기 위해 NULL byte를 채웁니다.

codes Section

codes Section에서는 프로그램 실행에 필요한 기계어를 입력합니다.
실행할 함수는 MessageBoxW(NULL, “2021350034 이주호”, “KU Reverse Engineering”, 0x40)입니다. 이를 어셈블리어로 표현하면 다음과 같습니다.

PUSH 40 ; 메시지 박스의 타입(파란색 i모양)
PUSH 0x00402020 ; 제목이 저장된 VA
PUSH 0x00402000 ; 내용이 저장된 VA
PUSH 0
CALL [0x00401028] ; INT의 VA
XOR EAX, EAX
RETN

시스템 콜을 위해 CALL [0x00401028]을 수행하는 이유는 PE loader에 함수가 위치하는 주소로 수정될 것이기 때문입니다.

이를 리틀 엔디언 HEX로 대응하고, Section에 값을 입력하면 다음과 같습니다.

Result

MessageBoxW함수를 활용해 제목이 KU Reverse Engineering이고, 내용이 2021350034 이주호이며 파란색 i형 동그라미 모양을 가지는 메시지 박스를 출력하는 데 성공했습니다.
또한 조건에 따라 세 개의 섹션을 가지며, 학번이 2021350034로 짝수 번쨰로 끝나므로 imports, strings, codes 순의 섹션을 설정했습니다.