리버싱

PE File Format

111-000-111 2021. 9. 7. 13:54

 

 

 

 

PE파일이란?

 

PE(Portable Executable)파일은 Windows 운영체제에서 사용되는 실행 파일 형식이다.

PE 파일은 32비트 형태의 실행 파일을 의미하고, PE32라고도 한다.

64비트 형태의 실행 파일은 PE+ 또는 PE32+라고 한다.

 

 

 

 

PE파일의 종류

 

종류 주요 확장자 종류 주요 확장자
실행 계열 EXE, SCR 드라이버 계열 SYS, VXD
라이브러리 계열 DLL, OCX, CPL, DRV 오브젝트 파일 계열 OBJ

 

OBJ 파일을 제외한 모든 종류의 파일은 실행이 가능하나, DLL과 SYS 파일은 셸에서 직접 실행하는 것이 불가능하고 디버거 등을 통해 실행이 가능하다.

 

 

 

 

 

기본 구조

출처 : https://reversecore.com/category/study?page=4

 

 

DOS header부터 Section Header까지를 PE헤더, 그 밑의 Section들을 PE 바디라고 한다.

파일에서는 Offset으로, 메모리에서는 VA(Virtual Address, 절대주소)로 위치를 표현한다.

위 그림과 같이 파일이 메모리에 로딩이 되면 Section의 크기나 위치 등이 달라진다.

파일의 내용은 코드(.text), 데이터(.data), 리소스(.rsrc) 섹션에 나뉘어서 저장된다.

 

섹션 헤더에 각 섹션에 대한 파일/메모리에서의 크기, 위치, 속성 등이 정의되어 있다.

PE 헤더의 끝 부분과 각 섹션의 끝에는 NULL padding이라고 불리우는 영역이 존재한다.

이 영역은 컴퓨터에서 파일이나 메모리 등을 처리할때 효율을 높이기 위한 최소 기본 단위 개념이 적용된 것이다. 

파일/메모리에서 섹션의 시작 위치는 각 파일/메모리의 최소 기본 단위의 배수에 해당하는 위치여야 하고, 빈 공간은 NULL로 채운다.

 

 

 

 

 

VA & RVA

 

 

VA는 프로세스 가상 메모리의 절대 주소이다.

RVA(Rela-tive Virtual Address)는 ImageBase에서부터의 상대주소를 말한다.

둘의 관계는 아래와 같다.

RVA+ImageBase = VA

PE 헤더 내의 정보는 RVA 형태로 된 것이 많은데, 이는 DLL이 프로세스 가상 메모리의 특정 위치에 로딩되는 순간 이미 그 위치에 다른 DLL이 로딩되어 있을 수 있다. 이런 상황에서 DLL Relocation(DLL 재배치) 과정을 통해 비어있는 위치에 로딩시켜야 한다. 만약 VA(절대주소)로 되어있었다면 문제가 발생할수 있다. RVA는 상대적인 위치이므로 재배치가 발생해도 기준 위치에서의 상대적인 주소가 변하지는 않으므로 정상적으로 로딩이 될 것이다.

 

 

 

 

PE 헤더

 

 

DOS Header

 

IMAGE_DOS_HEADER 구조체의 크기는 40이고, 알아두어야 할 멤버는 e_magic과 e_lfanew이다.

e_magic : DOS signature (4A5D -> ASCII 값으로 "MZ"

e_lfanew : NT header의 오프셋을 표시(파일에 따라 가변적인 값을 가진다)

 

모든 PE 파일은 시작 부분에 MZ가 존재하고 e_lfanew 값이 가리키는 위치에 NT header 구조체가 존재해야 한다.

Intel 계열 CPU는 리틀 엔디언 방식으로 값을 저장한다.

 

 

⦁ DOS Stub

 

DOS Stub의 존재 여부는 필수가 아니고, 크기도 일정하지 않다.

 

notepad.exe의 DOS Stub

 

40~4D는 16비트 어셈블리 명령어이다. 32비트 OS에서는 이 부분이 실행되지 않는다.

notpad.exe를 DOS 환경에서 실행하면 해당 코드를 실행시킬 수 있다.

 

debug 명령을 통해 확인한 DOS Stub


화면에 This program cannot be run in DOS mode 라는 문자열을 출력시키고 프로그램이 종료된다.

즉, notepad.exe는 32비트용 PE파일이지만 MS-DOS 호환 모드가 있어 DOS 환경에서도 해당 문자열을 확인할 수 있다.

이 특성을 이용해서 하나의 실행 파일에서 DOS와 Windows 환경에서 모두 실행 가능한 파일을 만들 수 있다.

어디까지나 이는 옵션이므로 개발 도구에서 지원해줘야 가능하다.

 

 

 

 

NT Header

 

NT header 구조체 IMAGE_NT_HEADERS

 

첫 번째 멤버 Signature는 50450000h("PE"00) 값을 가진다.

두 번째, 세 번째 멤버는 구조체이다.

 

IMAGE_NT_HEADERS의 크기는 F8이다.

 

 

NT Header - File Header

NT 헤더의 두 번째 멤버인 File Header는 아래와 같이 정의되어 있다.

 

IMAGE_NT_HEADERS의 _IMAGE_FILE_HEADER

 

IMAGE_FILE_HEADER 구조체에서는 4가지 멤버가 중요한데, 해당 멤버들은 정확히 세팅되어있지 않으면 파일이 정상적으로 실행되지 않는다.

 

  1. Machine - CPU별로 고유한 값이며 아래와 같이 정의되어 있다. 
    정의된 Machine의 값
  2. NumberOfSections - PE 파일의 섹션의 개수로, 반드시 0보다 커야한다.

  3. SizeOfOptionalHeader - 해당 멤버는 IMAGE_OPTIONAL_HEADER32 구조체의 크기를 나타낸다. 그런데 IMAGE_OPTIONAL_HEADER32는 이미 크기가 정해져있으나 Windows의 PE 로더는 IMAGE_FILE_HEADER의 SizeOfOptionalHeader 값을 보고 IMAGE_OPTIONAL_HEADER32의 크기를 인식한다.
    왜냐하면 PE32+의 IMAGE_OPTIONAL_HEADER64 구조체 크기가 다르기 때문이다. 이러한 특성을 이용하여 일반적인 PE 형식을 벗어나는 PE 파일을 만들 수 있다.

  4. Characteristics - 파일의 속성을 나타내는 값으로 bit OR 형식으로 조합된다. 아래와 같이 정의되어 있다.
    Charateristics

IMAGE_FILE_HEADER 구조체

 

4C01 - machine
0004 - number of sections
559EA6FF - time date stamp
00000000 - offset to symbol table
00000000 - number of symbols
00E0 - size of optional header
0102 - charateristics (IMAGE_FILE_EXECUTABLE_IMAGE, IMAGE_FILE_32BIT_MACHINE)

 

 

NT Header - Optional Header

 

IMAGE_OPTIONAL_HEADER32는 PE 헤더 구조체 중에서 가장 크기가 크다.

 

 

주목해야 할 멤버들은 아래와 같다.

 

  1. Magic
    IMAGE_OPTIONAL_HEADER32 구조체인 경우 10B, 64비트인 경우 20B의 값을 갖는다.

  2. AddressOfEntryPoint
    EP(Entry Point)의 RVA 값을 갖는다. 프로그램에서 최초 실행되는 코드의 시작 주소이다.

  3. ImageBase
    가상 메모리에서 PE 파일이 로딩되는 시작 주소를 나타낸다. EXE, DLL 파일은 user memory 영역인 0~7FFFFFFF 범위에 로딩되고(32비트 기준), SYS 파일은 kernel memory 영역인 80000000~FFFFFFFF 범위에 로딩된다. 일반적으로 ImageBase는 00400000이고 DLL의 ImageBase는 10000000이다. 
    PE로더는 PE 파일을 실행시키기 위해서 프로세스를 생성하고 파일을 메모리에 로딩한 후 EIP 레지스터 값을 ImageBase + AddressOfEntryPoint  값으로 세팅한다.

  4. SectionAlignment, FileAlignment 
    파일에서의 섹션의 최소 단위를 나타내는 것이 FileAlignment이고 메모리에서 섹션의 최소단위를 나타내는 것이 SectionAlignment이다. 파일과 메모리의 섹션 크기는 각각 FilAlignment, SectionAlignment와 같아야 한다.

  5. SizeOfImage
    PE 파일이 메모리에 로딩되었을 때 가상 메모리에서 PE Image가 차지하는 크기를 나타낸다.

  6. SizeOfHeader
    PE 헤더의 전체 크기를 나타낸다. 이 값도 FileAlignment의 배수여야 한다. 파일 시작에서 SizeOfHeader offset만큼 떨어진 위치에 첫 번째 섹션이 위치한다.

  7. SubSystem

    의미 비고
    1 Driver File 시스템 드라이버 (예 : ntfs.sys)
    2 GUI(Graphic User Interface) 파일 창 기반 애플리케이션 (예 : notepad.exe)
    3 CUI(Console User Interface) 파일 콘솔 기반 애플리케이션 (예 : cmd.exe)
  8. NumberOfRvaAndSizes
    DataDirectory 배열의 개수를 나타낸다. 해당 배열은 분명 16개라고 명시되어 있지만, PE로더는 이 값을 보고 배열 크기를 인식한다.

  9. DataDirectory

 

 

메모리 속성별 액세스 권한

종류 액세스 권한
code 실행, 읽기 권한
data 비실행, 읽기, 쓰기 권한
resource 비실행, 읽기 권한

 

 

 

 

⦁ IMAGE_SECTION_HEADER

 

 

 

이 구조체에서 중요한 멤버는 아래와 같고, 해당 멤버들 외에는 사용되지 않는다.

항목 의미
VirtualSize 메모리에서 섹션이 차지하는 크기
VirtualAddress 메모리에서 섹션의 시작 주소(RVA)
SizeOfRawData 파일에서 섹션이 차지하는 크기
PointerToRawData 파일에서 섹션의 시작 위치
Characteristics 섹션의 속성(bit OR로 조합)
  1. VirtualAddress, PointerToRawData
    SectionAlignment와 FileAlignment에 맞게 결정된다.

  2. VirtualSize와 SizeOfRawData는 일반적으로 서로 값이 다르다. 파일에서의 섹션 크기와 메모리에 로딩되고 난 후의 섹션의 크기가 다르다는 뜻이다.

  3. Characteristics는 아래 값들을 bit OR 연산으로 조합한 값을 갖는다. 아래 값들 외에도 많은 값들이 정의되어 있다.
  4. NAME의 멤버는 일반적인 C언어 문자열처럼 NULL로 끝나지 않고, 어떠한 값이 와야한다는 제약이 없다.
    따라서 참고용일 뿐 정보로써의 효용성이 100%라고 장담할 수 없다.

 

notepad.exe의 IMAGE_SECTION_HEADER

 

※ 이미지(Image) - PE파일이 메모리에 로딩된 상태를 이미지라고 한다.

 

 

 

 

⦁ RVA to RAW

 

PE 파일이 메모리에 로딩되었을 때 각 섹션에서 메모리의 주소(RVA)와 파일 offset을 매핑할 수 있어야 한다.

과정은 아래와 같다.

 

1. RVA가 속해 있는 섹션을 찾는다.
2. 간단한 비례식을 사용해서 파일 offset(RAW)를 계산한다.

 

RAW - PointerToRawData = RVA - VirtualAddress
RAW = RVA - VirtualAddress + PointerToRawData

 

출처 : https://reversecore.com/category/study?page=4

위 상황에서 만약 RVA가 5000이라면 File Offset은 아래와 같이 구할 수 있다.

1. RVA 5000이 속한 섹션은 .text 섹션이다.
2. RAW = 5000(RVA) - 1000(VirtualAddress) + 400(PointerToRawData) = 4400

 

VirtualSize와 SizeOfRawData 값이 서로 다르면 File Offset을 정의할 수 없는 경우도 존재한다.