본문 바로가기

Security/Reversing

리버싱 핵심원리 13장(1)

13장(1) PE File Format

 

PE(Portable Executable) 파일은 Windows 운영체제에서 사용되는 실행 파일 형식이다. 기존 UNIX에서 사용되는 COFF(Common Object File Format)를 기반으로 MS에서 만듦. 애초에는 이름마냥 다른 운영체제에 이식성을 좋게 하려는 의도였으나 실제로는 Windows 계열의 운영체제에서만 사용되고 있음.

PE파일은 32비트 형태의 실행 파일을 의미하며 PE32라는 용어를 사용하기도 한다. 64비트 형태의 실행파일은 PE+ 또는 PE32+라고 부르며 PE파일의 확장 형태이다.

 

PE File Format

실행계열 : EXE, SCR

라이브러리 계열 : DLL, OCX, CPL, DRV

드라이버 계열 : SYS, VXD

오브젝트 파일 계열 : OBJ

*PE파일의 종류들이며 OBJ말고는 사실상 실행 가능.

 

PE파일의 헤더(PE header)부분에 파일이 실행되기 위해 필요한 모든 정보가 적혀있다. 수많은 정보가 PE헤더에 구조체 형식으로 저장되어 있다. 즉 PE File Format을 공부한다는 것은 PE헤더 구조체를 공부한다는 것과 같은 말임.

 

DOS header부터 Section header까지를 PE헤더, 그 밑의 Section들을 합쳐서 PE Body라고 한다. 파일에서는 offset(상대주소같은 개념)으로 메모리에서는 VA(Virtual Address, 절대주소)로 위치를 표현함. 파일이 메모리에 로딩되면 모양이 달라지는데 파일의 내용은 보통 코드(.text), 데이터(.data), 리소스(.rsrc) 섹션에 나뉘어서 저장된다.

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

PE 헤더의 끝부분과 각 섹션의 끝에는 NULL Padding이라고 불리우는 영역이 존재한다. 컴퓨터에서 파일, 메모리, 네트워크 패킷 등을 처리할 때 효율을 높이기 위해 최소 기본 단위 개념을 사용하는데, PE파일에도 같은 개념이 적용된 것이다. 파일/메모리에서 섹션의 시작 위치는 각 파일/메모리의 최소 기본단위의 배수에 해당하는 위치여야 하고 빈 공간은 NULL로 채워버림.(위 사진을 보면 각 섹션의 시작 주소가 어떤 규칙에 의해 딱딱 끊어지는 걸 볼 수 있다.)

 

 

VA&RVA(절대 주소, 상대 주소)

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

RVA + ImageBase = VA

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

 

 

PE헤더

PE헤더는 많은 구조체로 이루어져 있다. 리버싱에서 중요한 의미를 가지는 구조체들도 있으니 주의.

 

 

DOS Header - IMAGE_DOS_HEADER

MS는 PE File Format을 만들 때 당시 널리 사용되던 DOS파일에 대한 하위 호환성을 고려해서 만듦. 그 결과로 PE헤더의 제일 앞부분에는 기존 DOS EXE Header를 확장시킨 IMAGE_DOS_HEADER 구조체가 존재함.

IMAGE_DOS_HEADER 구조체의 크기는 40임.(00~3F) 이 구조체에서 꼭 알아야 할 중요한 멤버는 e_magic e_lfanew임.

e_magic : DOS signature(4D5A -> 아스키값 "MZ")

e_lfanew : NT header의 offset을 표시(파일에 따라 가변적인 값을 가짐)

모든 PE파일은 시작부분(e_magic)에 DOS signature("MZ")가 존재하고 끝부분(e_lfanew)값이 가리키는 위치에 NT Header 구조체가 존재해야 한다. *intel계열 cpu는 자료를 리틀 엔디언 표기법으로 자료를 저장함.

 

DOS Header 밑에는 DOS stub이 존재함. DOS stub의 존재 여부는 옵션이며 크기도 일정하지 않음.(DOS stub이 없어도 파일 실행에는 문제가 없음) DOS stub은 코드와 데이터의 혼합으로 이루어져 있다. offset 40-D0영역은 16비트 어셈블리 명령어이다. 32비트나 64비트 windows OS에서는 이쪽 명령어가 실행되지 않는다.

 

 

NT header - IMAGE_NT_HEADERS

IMAGE_NT_HEADERS 구조체는 3개의 멤버로 되어있는데 제일 첫 멤버는 Signature로 50450000("PE"00) 값을 가짐. 그리고 FileHeader와 Optional Header 구조체 멤버가 있다. 총 구조체의 크기는 F8임.

1. NT header  -  IMAGE_FILE_HEADER

 

파일의 개략적인 속성을 나타내는 IMAGE_FILE_HEADER 구조체

위의 IMAGE_FILE_HEADER 구조체에서 빨간 4가지 멤버가 중요함.(이 값이 정확히 세팅되어있지 않으면 파일 실행 X)

 

1-1. Machine

 Machine넘버는 CPU별로 고유한 값이며 32비트 Intel x86 호환 칩은 14C의 값을 가짐.(32비트는 14C, 64비트는 200)

winnt.h 파일에 정의된 Machine넘버값들

1-2. Number Of Sections

 PE파일은 코드, 데이터, 리소스 등이 각각의 섹션에 나뉘어서 저장된다고 했는데 NumberOfSections는 바로 그 섹션의 개수를 나타냄. 그런데 이 값은 반드시 0보다 커야하고 정의된 섹션 개수와 실제 섹션이 다르면 실행 에러가 발생함.

 

1-3. Size Of Optional Header

 IMAGE_NT_HEADERS구조체의 마지막 멤버는 IMAGE_OPTIONAL_HEADER32 구조체인데 SizeOfOptionalHeader 멤버는 바로 이 IMAGE_OPTIONAL_HEADER32 구조체의 크기를 나타낸다. IMAGE_OPTIONAL_HEADER32 구조체는 C언어의 구조체이기 때문에 이미 그 크기가 결정되어있다. 근데 windows의 PE로더는 IMAGE_FILE_HEADER의 SizeOfOptionalHeader 값을 보고 IMAGE_OPTIONAL_HEADER32 구조체 크기를 인식함.

 PE32+ 형태의 파일인 경우에는 IMAGE_OPTIONAL_HEADER32 대신 IMAGE_OPTIONAL_HEADER64 구조체를 사용함. 두 구조체의 크기는 다르기 때문에 SizeOfOptionalHeader 멤버에 구조체 크기를 명시하는 것.

 *IMAGE_DOS_HEADER의 e_lfanew 멤버와 IMAGE_FILE_HEADER의 SizeOfOptionalHeader멤버 때문에 일반적인 PE 파일 형식을 벗어나는 '꽈배기" PE 파일을 만들 수 있다.

 

1-4. Characteristics

 파일의 속성을 나타내는 값으로 실행 가능한 형태인지(executable or not) 혹은 DLL파일인지 등의 정보들이 bit OR 형식으로 조합.

winnt.h 파일에 정의된 Characteristics 값

0002h와 2000h의 값을 기억하자. 참고로 PE 파일 중에 Characteristics 값에 0002h가 없는 경우(not executable)도 있다. 예를 들면 *.obj와 같은 object 파일 및 resource DLL 같은 파일을 들 수 있다.

 

2. NT header  -  IMAGE_OPTIONAL_HEADER32(64)

 

IMAGE_OPTIONAL_HEADER32 구조체

2-1. Magic

IMAGE_OPTIONAL_HEADER32인 경우엔 10B, IMAGE_OPTIONAL_HEADER64의 경우엔 20B의 값을 가짐.(운영체제의 비트수에 따라 결정)

 

2-2. Address Of Entry Point

AddressOfEntryPoint는 EP의 RVA(상대주소)값을 가지고 있으며 프로그램에서 최초로 실행되는 코드의 시작주소로 매우 중요한 값(ImageBase 값이랑 더해 EIP 레지스터의 값을 정한다.)

 

2-3. Image Base

ImageBase는 메모리에서 PE파일이 로딩되는 시작 주소를 나타냄. PE로더는 PE파일을 실행시키기 위해 프로세스를 생성하고 파일을 메모리에 로딩한 후 EIP레지스터 값을 ImageBase + AddressOfEntryPoint 값으로 세팅함.(ImageBase는 RVA(상대주소)의 기준이 됨.)

 

2-4. Section Alignment, File Alignment

 PE파일의 Body부분은 섹션으로 나뉘어져 있는데 이 파일에서 섹션의 최소단위를 나타내는 것이 FileAilgnment이고 메모리에서 섹션의 최소단위를 나타내는 것이 SectionAilgnment이다. 파일/메모리의 섹션 크기는 각각 이 최소단위의 배수가 되어야함.

 

2-5. Size Of Image

 PE파일이 메모리에 로딩되었을때 가상 메모리에서 PE Image(PE)가 차지하는 크기를 나타냄. 일반적으로 파일의 크기와 메모리에 로딩된 크기는 다름.(SectionAlignment와 FileAilgnment가 다르기 때문에 파일의 크기와 메모리의 크기가 다름.)

 

2-6. Size Of Headers

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

 

2-7. Subsystem

 이 값을 보고 시스템 드라이버 파일(.sys)인지 일반 실행 파일(.exe, .dll)인지 구분할 수 있음. Subsystem멤버의 값은 다음과 같다.

1 - Driver file - 시스템 드라이버(ntfs.sys 등

2 - GUI(Graphic User Interface)파일 - 창 기반 애플리케이션(notepad.exe 등)

3 - CUI(Console User Interface)파일 - 콘솔 기반 애플리케이션(cmd.exe 등)

 

2-8. Number Of Data Directories

 IMAGE_OPTIONAL_HEADERS32구조체의 마지막 멤버인 DataDirectory 배열의 개수를 나타낸다. 구조체 정의에 배열 개수가 IMAGE_NUMBEROF_DIRECTORY_ENTRIES (16) 이라고 선언되어 있지만 PE로더는 이 멤버를 보고 배열의 크기를 인식함. 따라서 16이 아닐 수도 있다는 뜻.

 

2-9. DataDirectory(배열)

 IMAGE_DATA_DIRECTORY 구조체의 배열로 각 항목마다 정의된 값을 가짐. Directory는 그냥 어떤 구조체의 배열이라고 생각하면 됨.

IMAGE_DATA_DIRECTORY

EXPORT, IMPORT, RESOURCE, TLS Directory가 중요. 특히 IMPORT와 EXPORT Directory 구조는 PE 헤더에서 매우 중요하기 때문에 나중에 따로 설명함.

 

 

Section Header - IMAGE_SECTION_HEADER(섹션 헤더)

각 섹션의 속성(property)을 정의한 것이 섹션 헤더이다. PE파일은 code, data, resource 등을 각각 섹션에 나누어서 저장함.

 

IMAGE_SECTION_HEADER 구조체

1. Virtual Size

 메모리에서 섹션이 차지하는 크기

 

2. VirtualAddress(RVA)

 메모리에서 섹션의 시작 주소

 

3. Size Of Raw Data

 파일에서 섹션이 차지하는 크기

 

4. Pointer To Raw Data

 파일에서 섹션의 시작 위치

 

5. Characteristics

 섹션의 속성(bit OR)

 

VirtualAddress(RVA)와 PointerToRawData는 아무 값이나 가질 수 없고 각각 Optional Header에 정의된 SectionAilgnment와 FileAilgnment에 맞게 결정됨.(즉 메모리/파일의 섹션 시작 주소는 최소 단위에 따라 결정됨.)

VirtualSize와 SizeOfRawData는 일반적으로 서로 다른 값을 가짐. 즉 파일에서의 섹션 크기와 메모리에 로딩된 섹션 크기는 다르다.(즉 메모리/파일의 섹션이 차지하는 크기는 최소 단위가 다르기 때문에 최소 단위의 배수로 이루어진 두 값이 다름.)

 

 *PE파일이 메모리에 로딩될 때 파일이 그대로 올라가는 것이 아니라 섹션 헤더에 정의된 대로 섹션 시작 주소, 크기 등에 맞춰서 올라감. 따라서 파일에서의 PE와 메모리에서의 PE는 서로 모양이 다르다. 이를 구분하기 위해 메모리에 로딩된 상태를 IMAGE라는 용어를 사용하여 구분함.

 

RVA to RAW

PE파일이 메모리에 로딩되었을 때 각 섹션에서 메모리 주소와 파일 옵셋을 매핑하는 것을 RVA to RAW라고 부름. 즉 파일이 메모리에 로딩되기 전의 file offset과 로딩되고 나서의 주소를 매칭하는것을 의미함.

 *RVA는 메모리에 로딩되었을때의 주소, RAW는 로딩되기 전의 file offset을 의미

RVA to RAW 비례식

여기서 VirtualAddress는 절대주소(VA)가 아닌 Section Header의 멤버인 VirtualAddress(메모리에서의 섹션 시작 주소)이다. 그리고 offset을 구했을 땐 그 offset에 해당 섹션이 있는지 확인해야함. 메모리에서 포함되었던 섹션에서 벗어난 값이라면 '해당 RVA에 대한 RAW 값은 정의할 수 없다' 라고 해야함. 이러한 결과가 나오는 이유는 섹션마다VirtualSize와 SizeOfRawData의 값이 다르기 때문이다. RVA - RAW(file offset) 변환 작업은 PE 헤더를 공부할 때 가장 기본이 되므로 잘 익혀놔야함.

'Security > Reversing' 카테고리의 다른 글

리버싱 핵심원리 14장  (0) 2023.03.27
리버싱 핵심원리 13장(2)  (0) 2023.03.27
리버싱 핵심원리 10장  (0) 2023.03.27
리버싱 핵심원리 8장  (0) 2023.03.27
리버싱 핵심원리 7장  (0) 2023.03.27