1. PE 파일이란?

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

 PE 파일은 헤더 + 바디로 구성.

  헤더에는 파일이 실행되기 위해 필요한 정보가 저장됨(메모리 적재 방식, EP 주소 등).

  바디에는 명령어(코드), 데이터, 리소스 등이 저장됨.

 (Linux는 ELF(Executable and Linkable Format)를 실행파일의 형식으로 사용)

 

2. PE 파일의 종류

 실행계열 : EXE, SCR

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

 드라이버 계열 : SYS, VXD

 오브젝트 파일 계열 : OBJ

 

3. PE 기본 구조

 PE 파일은 헤더 + 바디로 구성.

  헤더에는 파일이 실행되기 위해 필요한 정보가 저장됨(메모리 적재 방식, EP 주소 등).

  바디에는 명령어(코드), 데이터, 리소스 등이 저장됨.

PE 파일의 메모리 로딩

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

PE 헤더의 끝부분과 각 섹션의 끝에는 NULL padding 영역이 존재하며 이는 파일, 메모리, 패킷 등을 처리할 때 효율을 높이기 위해 기본 단위 개념으로 사용.

 

4. VA & RVA

 VA(Virtual Address) : 프로세스 가상 메모리의 절대주소.

 RVA(Relative VA) : 어느 기준 위치에서부터의 상대주소.

   ImageBase + RVA = VA

   RVA는 이미 그 위치에 다른 PE 파일이 로딩된 경우 재배치를 효율적으로 할 수 있기때문에 사용.

 

5. PE 헤더

 5.1 DOS Header

  PE파일의 가장 앞 부분에 존재하며 제일 앞 부분에는 MZ 시그니처가 존재.

  IMAGE_DOS_HEADER 구조체의 크기는 40이며, 가장 중요한 멤버는 e_magic과 e_lfanew 멤버임.

   e_magic : 앞에서 말한 DOS Signature (4D5A => "MZ")

   e_lfanew : NT header의 옵셋을 표시

 

 5.2 DOS Stub

  DOS Stub의 존재 여부는 옵션이며 현재는 잘 사용되지 않음.

 

 5.3 NT Header

  3개의 멤버를 가지며, 제일 첫 번째 멤버는 Signature로 "PE" 값을 가짐.

  나머지는 File Header와 Optional Header 구조체 멤버가 존재.

 

  File Header

   파일의 개략적인 속성을 나타냄. 아래는 4개의 중요한 멤버를 순서대로 나타냄.

   #1. Machine : Machine 넘버는 CPU별로 고유한 값을 가지며 32비트 Intel x86 호환칩은 14C의 값을 가짐.

   #2. NumberOfSection : 섹션의 수를 나타내며, 반드시 0보다 커야함. 정의된 섹션 개수와 다를 시 에러 발생.

   #3. SizeOfOptionalHeader : NT Header의 마지막 멤버 IMAGE_OPTIONAL_HEADER32의 크기를 나타냄.

                                       위의 구조체는 크기가 고정돼 있지만 32비트와 64비트의 크기가 달라 크기를 명시함.

   #4. Characteristics : 파일의 속성을 나타내는 값으로, 실행이 가능한 형태인지, DLL 파일인지의 정보를 bit or 로 조합.

 

 Optional Header

  PE 헤더 중 가장 크기가 크며, 잘못 세팅되면 파일이 정상 실행 되지 않음. 아래는 중요 멤버를 순서대로 나타냄.

  #1. Magic : 32비트의 경우 10B, 64비트의 경우 20B의 값을 가진다. IMAGE_OPTIONAL_HEADER의 종류에 따라 달라짐.

  #2. AddressOfEntryPoint : RVA 값을 가짐. 프로그램에서 최초로 실행되는 코드의 시작 주소로 매우 중요함.

  #3. ImageBase : PE파일이 로딩되는 시작 주소를 나타냄. 주로 exe는 00400000, dll은 10000000값이며 EIP= ImageBase + AddressOfEntryPoint 값으로 세팅됨.

  #4. SectionAlignment, FileAlignment : 각각 메모리와, 파일을 관리하는 최소 단위이며 각각의 섹션의 크기는 이 멤버의 배수가 되어야 함.

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

  #6. SizeOfHeader : PE 헤더의 전체 크기를 나타내며, 이 값 역시 FileAlignment의 배수여야 함.

  #7. Subsystem : 이 값을 보고 시스템 드라이버 파일인지, 일반 실행파일 인지 구분할 수 있음.

                       ex) 1, Driver file    2, GUI file    3, CUI file

  #8. NumberOfRvaAndSize : IMAGE_OPTIONAL_HEADER32 구조체의 마지막인 DataDirectory 배열의 개수를 나타냄.

  #9. DataDirectory : IMAGE_DATA_DIRECTORY 구조체의 배열로 각 항목마다 정의된 값을 가짐.

       DataDriectory[0] = EXPORT Directory

       DataDriectory[1] = IMPORT Directory

       DataDriectory[2] = RESOURCE Directory

       DataDriectory[3] = EXCEPTION Directory

       DataDriectory[4] = SECURITY Directory

       DataDriectory[5] = BASERELOC Directory

       DataDriectory[6] = DEBUG Directory

       DataDriectory[7] = COPYRIGHT Directory

       DataDriectory[8] = GLOBALPTR Directory

       DataDriectory[9] = TLS Directory

       DataDriectory[A] = LOAD_CONFIG Directory

       DataDriectory[B] = BOUND_IMPORT Directory

       DataDriectory[C] = IAT Directory

       DataDriectory[D] = DELAY Directory

       DataDriectory[E] = COM_DESCRIPTOR Directory

       DataDriectory[F] = Reserved Directory

 

5.4 섹션 헤더

 각 섹션의 속성을 정의한 것.

 프로그램의 안정성을 위함임(ex. Code 영역과 Data영역이 하나로 되어있으면 overflow 시 코드를 덮게됨).

 섹션마다 다른 액세스 권한을 가짐.

  code : 실행, 읽기 권한

  data : 비실행, 읽기, 쓰기 권한

  resource : 비실행, 읽기 권한

 

5.4.1 IMGAE_SECTION_HEADER

 섹션 헤더는 각 섹션 별 IMAGE_SECTION_HEADER 구조체 배열로 되어 있음.

 해당 구조체에서 중요 멤버

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

  VirtualAddress : 메모리에서 섹션의 시작 주소(RVA)

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

  PointerToRawData : 파일에서 섹션의 시작 위치

  Characteristics : 섹션의 속석(bit OR)

 

5.5 RVA to Raw

 PE 파일이 메모리에 로딩되었을 때 각 섹션에서 메모리의 주소와 파일 옵셋을 매핑하는 방법.

 RAW - PointerToRawData = RVA - VirtualAddress

                            RAW = RVA - VirtualAddress + PointerToRawData

 

5.6 IAT

 Windows 운영체제의 핵심 개념인 process, memory, DLL 구조 등에 대한 내용이 함축되어 있음.

 프로그램이 어떤 라이브러이에서 어떤 함수를 사용하고 있는지를 기술한 테이블.

 

5.6.1 DLL

 여러 프로그램에서 동시에 사용할 수 있는 코드와 데이터를 포함하는 라이브러리. 

 메모리를 적게 사용하며, 모듈식 프로그램을 손 쉽게 개발할 수 있으며, 또한 배포가 용이.

 

5.6.2 IMAGE_IMPORT_DESCRIPTOR

 PE 파일은 자신이 어떤 라이브러리를 임포트하고 있는지 IMAGE_IMPORT_DESCRIPTOR 구조체에 명시.

 구조체의 중요 멤버

  OriginalFirstThunk : INT(Import Name Table)의 주소(RVA)

  Name : Library 이름 문자열의 주소(RVA)

  FirstThunk : IAT(Import Address Table)의 주소(RVA)

 

 PE 로더가 임포트 함수 주소를 IAT에 입력하는 순서:

  1. IID의 Name 멤버를 읽어 라이브러리의 이름 문자열(kernel32.dll)을 얻음.

  2. 해당 라이브러리를 로딩. => LoadLibrary("kernel32.dll")

  3. IID의 OriginalFirstThunk 멤버를 읽어 INT 주소를 얻음.

  4. INT에서 배열의 값을 하나씩 읽어 해당 IMAGE_IMPORT_BY_NAME 주소(RVA)를 얻음.

  5. IMAGE_IMPORT_BY_NAME의 Hint(ordinal) 또는 Name 항목을 이용하여 해당 함수의 시작 주소를 얻음.

     => GetProcAddress("GetCurrentThreadId")

  6. IID의 FirstThunk(IAT) 멤버를 읽어서 IAT 주소를 얻음.

  7. 해당 IAT 배열 값에 위에서 구한 함수 주소를 입력.

  8. INT가 끝날 때까지 위 4~7 과정을 반복.

 

5.7 EAT

 라이브러리 파일에서 제공하는 함수를 다른 프로그램에서 가져다 사용할 수 있도록 해주는 핵심 메커니즘.

 EAT를 통해서만 해당 라이브러리에서 익스포트하는 함수의 시작 주소를 정확히 구할 수 있음.

 

5.7.1 IMAGE_EXPORT_DIRECTORY

 임포트와 마찬가지로 어떤 라이브러리르 임포트하고 있는지를 해당 구조체에 명시.

 구조체의 중요 멤버

  NumberOfFunctions : 실제 Export 함수 개수

  NumberOfNames : Export 함수 중에서 이름을 가지는 함수 개수 (<= NumberOfFunctions)

  AddressOfFunction : Export 함수 주소 배열(배열의 원소 개수 = NumberOfFunctions)

  AddressOfNames : 함수 이름 주소 배열(배열의 원소 개수 = NumberOfNames)

  AddressOfNameOrdinals : Ordinal 배열(배열의 원소 개수 = NumberOfNames)

 

5.7.2 GetProcAddress()

 라이브러리에서 함수 주소를 얻는 API. 동작 원리는 아래와 같음

  1. AddressOfNames 멤버를 이용해 함수 이름 배열로 감.

  2. 함수 이름 배열은 문자열 주소가 저장되어 있으며, 문자열 비교를 통하여 원하는 함수 이름을 찾음.(name_index)

  3. AddressOfNameOrdinals 멤버를 이용해 ordinal 배열로 감.

  4. ordinal 배열에서 name_index로 해당 ordinal 값을 찾음.

  5. AddressOfFunctions 멤버를 이용해 함수 주소 배열(EAT)로 감.

  6. EAT에서 아까 구한 ordinal을 배열 인덱스로 하여 원하는 함수의 주소를 얻음.

Posted by 회색세계

블로그 이미지
회색세계

공지사항

Yesterday
Today
Total

달력

 « |  » 2025.5
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31

최근에 올라온 글

최근에 달린 댓글

글 보관함