PE file format

ripemo·2025년 2월 12일

PE file(Portable Executable)

  • window 운영체제에서 실행되는 실행 파일 형식
  • 32bit : PE(32) , 64bit : PE(32)+

종류

  • 실행 계열 : exe, scr
  • 드라이버 계열 : sys, vxd
  • 라이브러리 계열 : dll, ocx, cpl, drv
  • 오브젝트 파일 계열 : obj

PE file 구조

EntryPoint, ImageBase, VA, RVA

  • EntryPoint : 프로그램이 실행될 때 처음으로 실행되는 코드의 주소(처음 실행되는 함수(?)의 주소)
  • ImageBase : 해당 파일이 메모리에 로드될 때 기본적으로 로드되는 가상 주소(로드된 base addr)
  • VA(Virtual Address) : 프로세스 가상 메모리의 절대주소(file이 (가상)메모리에 로드되었을 때 실제로 위치한 주소)
  • RVA(Relative Virtual Address) : ImageBase를 기준으로 한 오프셋.

PE header

1) DOS header
구조체 : IMAGE_DOS_HEADER

  • e_magic : DOS signature로 PE 파일을 나타내는 2byte, MZ
  • e_lfanew : NT header가 시작되는 위치의 offset. 가리키는 위치에 NT header가 존재해야 함.
    2) DOS stub
    DOS환경에서 실행되는 코드를 가지는 영역(없어도 됨)
    ex) "This program cannot be run in DOS mode"
    3) NT header
    구조체 : IMAGE_NT_HEADERS
  1. signature : "PE"값을 가짐.

  2. File header
    구조체 : IMAGE_FILE_HEADER

    • Machine : 파일의 실행 대상 플랫폼(I386,IA64,AMD64)
    • NumberOfSections : 파일에 존재하는 섹션의 개수(반드시 0보다 커야 함)
    • SizeOfOptionalHeader : Optional Header의 크기
    • Characteristics : 파일의 속성을 나타내는 값. (exe, dll ...)
    • TimeDateStamp : 파일의 빌드 시간
  3. Optional header
    구조체 : IMAGE_OPTIONAL_HEADER

    • Magic : IMAGE_OPTIONAL_HEADER32 인지 64인지 (32: 0x10B, 64: 0x20B)
    • AddressOfEntryPoint : EntryPoint의 RVA (실제 EntryPoint = ImageBase + AOEP)
    • ImageBase : PE파일이 로딩되는 시작 주소(exe : 0x400000, dll : 0x1000000)
    • SectionAlignment, FileAlignment : 메모리/파일에서 섹션의 최소단위 나타냄. 각 섹션은 반드시 Alignment의 배수여야 함(섹션의 크기가 남더라도 null 로 채워서 Alignment 맞춤)
    • SizeOfImage : (가상)메모리에서 PE Image가 차지하는 크기
    • SizeOfHeader : PE Header의 전체 크기
    • Subsystem : 동작환경 정의(sys파일 : 0x1, GUI파일 : 0x2, CLI파일 : 0x3)
    • NumberOfRvaAndSize : DataDirectory 배열의 개수. 일반적으로 16개의 디렉터리. 해당 멤버로 디렉터리 개수 정할 수 있음
    • DataDirectory : IMAGE_DATA_DIRECTORY 구조체의 배열. 디렉터리별로 각각의 정보를 담음
      typedef struct _IMAGE_DATA_DIRECTORY {
          DWORD VirtualAddress;
          DWORD Size;
      } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
      • Export Directory
      • Import Directory
      • TLS Directory
      • IAT Directory

4) Section Header

  • VirtualSize : 메모리에서의 섹션의 크기
  • VirtualAddress : 메모리에서의 섹션의 시작주소
  • SizeOfRawData : 파일에서의 섹션의 크기
  • PointerToRawData : 파일에서 섹션의 offset(시작주소)
  • Characteristics : 섹션의 속성

RVA to RAW

RVA to RAW 비례식

RAW - PointerToRawData = RVA - VirtualAddress
RAW = RVA - VirtualAddress + PointerToRawData
  • PointerToRawData : 파일에서의 해당 섹션(RVA가 해당하는 섹션)의 시작 주소

VA와 'RVA to RAW 에서의 Virtual Address'의 차이

  • 기존에 말하던 VA는 말 그대로 절대적인 주소. 가상 메모리에서의 절대주소
  • 하지만 'RVA to RAW 에서의 Virtual Address'는 가상 메모리에서의 상대주소(RVA와 같은 것)
    즉 메모리에서의 해당 섹션(RVA가 해당하는 섹션)의 상대 주소를 의미

RVA to RAW 연산

  • RVA = 3000 일 때 RAW의 값은?
    -> 해당 그림의 image base는 01000000. 현재 RVA의 섹션은 text 영역.
    -> RAW = RVA - Virtual Address + PointerToRawData
    -> 2400 = 3000 - 1000 + 400

IAT, EAT

DLL(Dynamic Linked Library)

  • 동적링크로 실행파일에서 해당 라이브러리 기능을 사용시에만 참조해 기능을 호출할 수 있는 방법
  • SLL, DLL
    1) static link library
    • 정적 링크
    • 컴파일 시점에 라이브러리가 링크에 의해 실행파일의 일부분이 됨
      2) dynamic link library
    • 동적 링크
    • 실행 파일에서 해당 라이브러리의 기능을 사용할 때 라이브러리를 참조(또는 다운로드)하여 기능 호출
  • Implicit linking, Explicit linking
    1) Implicit linking
    • Implicit Load Time Linking
    • 해당 프로그램에서 사용될 DLL 정보를 프로그램에 내장하는 방법
    • 컴파일 시점에 외부 라이브러리에 링크. 실행 파일이 실행될 때 운영체제 로더가 자동으로 필요한 라이브러리 로드
    • 프로그램이 시작할 때 같이 로딩되고 종료될 때 메모리에서 해제
      2) Explicit linking
    • 컴파일 시점에 로드하지 않고, 런타임 중에 특정 API 호출을 통해 수동으로 로드
    • 실행 파일에서 해당 라이브러리의 기능을 사용할 때 라이브러리를 참조(또는 다운로드)하여 기능 호출

DLL과 IAT 이해

  • notepad.exe에서 CreateFileW를 호출 할 때 직접 호출하지 않고 01001104 주소에 있는 값을 호출
    그리고 해당 주소에 있는 7c8107f0 이라는 주소가 CreateFileW의 함수 주소
    -> ? 그러면 굳이 두번 호출하지 않고 한 번에 'CALL 7c8107f0' 하면 되지 않냐
    -> 실행파일이 어떤 환경에서 실행될지 알 수 없음. 환경에 따라 dll 버전과 CreateFileW 함수의 위치가 달라짐. 따라서 모든 환경을 보장하기 위해 CreateFileW의 주소가 저장될 위치를 준비하고 해당 주소를 call
    -> 파일이 실행되는 순간 PE loader가 01001104 위치에 CreateFileW의 주소 입력
  • DLL Relocation
    두 가지 dll을 사용한다면 첫번째 dll이 10000000 위치를 사용하고 있다면 두번째 dll은 다른 위치에 로딩시켜야 하므로 상대적인 주소가 필요(실제 주소를 하드코딩하는 형식, 절대적인 주소 불가)

IAT(Import Address Table)

  • 프로그램에서 사용되는 라이브러리에서 어떤 함수들을 사용하고 있는지, 함수명, 함수시작 주소 등에 대한 정보를 기술한 테이블

[notepad.exe의 kernel32.dll에 대한 IMAGE_IMPORT_DESCRIPTOR 구조]

Import Directory Table

  • optional header에 Import Table의 RVA의 값으로 Import Directory Table의 주소 저장하고 있음
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            
        DWORD   OriginalFirstThunk;       // INT(Import Name Table) address (RVA)
    };
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain; 
    DWORD   Name;                         // library name string address (RVA)
    DWORD   FirstThunk;                   // IAT(Import Address Table) address (RVA)
} IMAGE_IMPORT_DESCRIPTOR;

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;                         // ordinal
    BYTE    Name[1];                      // function name string
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME; 

IMAGE_IMPORT_DESCRIPTOR

  • Import Directory Table의 구조체. 즉 IID는 IDT의 배열

IMAGE_IMPORT_BY_NAME

  • INT(Image Name Table)에 존재.
  • 각 함수의 이름에 대한 정보 제공

<PE Loader가 Import 함수 주소를 IAT에 입력하는 순서>

  1. IID(IMAGE_IMPORT_DESCRIPTOR)의 Name 멤버를 읽어서 라이브러리의 문자열("kernel32.dll") 획득
  2. 해당 라이브러리("kernel32.dll") 로딩 - LoadLibrary("kernel32.dll")
  3. IID의 OriginalFirstThunk 멤버를 읽어서 INT(Import Name Table) 주소 획득
  4. INT에서 배열의 값을 하나씩 읽어 해당 IMAGE_IMPORT_BY_NAME 주소 획득
  5. IIBN의 Hint 또는 Name 항목을 이용하여 해당 함수의 시작 주소 획득 - GetProcAddress(함수주소)
  6. IID의 FirstThunk 멤버를 읽어서 IAT 주소 획득
  7. 해당 IAT 배열 값에 위에서 구한 함수 주소 입력
  8. INT가 끝날 때까지 (NULL을 만날 때 까지) 4 ~ 7 과정 반복
왜 IMAGE_IMPORT_DESCRIPTOR를 도는게 아니라 INT를 돌면서 확인하는건가?
  • IMAGE_IMPORT_DESCRIPTOR 하나가 dll 하나임. 즉 "kernel32.dll" 를 로드하면 "kernel32.dll"에 대한 IMAGE_IMPORT_DESCRIPTOR 하나가 있는 것임. 따라서 해당 INT에는 "kernel32.dll" 안의 함수들의 이름에 대한 정보가 들어가있다.

notepad.exe 에서 IAT 확인

  • optional header에서 Import Table RVA 확인

  • RVA 이므로 RAW로 변환

    왜 RVA를 그대로 안 쓰고 RAW로 변환해야 하나?

    -> 현재 상태는 notepad.exe가 메모리에 로드되지 않은 디스크에 존재하는 상태이기 때문에 RVA를 RAW로 바꿔줘야 함.

    아직 로드되지 않은 상태인데 RVA가 왜 존재하는가?

    -> RVA는 메모리에 로드된 후의 가상 주소를 나타내는 개념은 맞다.
    하지만 로드되지 않았더라도 디스크 상의 pe 파일에서도 일전에 설명한 이유로 절대적인 주소가 아닌 RVA로 파일이 구성된다. 따라서 메모리에 로드되기 이전에 파일에서도 RVA가 존재.

  • 암튼 그래서 IMPORT Table의 RAW 계산

    참고로 IAT는 .idata section에 존재. 따라서 Virtual Address, PointToLawData 의 값은.idata의 section header에서 획득

    • (.idata의) Virtual Address(RVA to RAW에서는 RVA와 같다고 말했었다.) = 2B000, PTRD = 28800
    • 따라서 RAW = 2B5AC - 2B000 + 28800 = 28dac
    • 한 번 찾아가보자
  • 위 그림의 IMAGE_IMPORT_DESCRIPTOR와 동일하다. Name에서 dll의 이름을 획득하고 로드한다.

  • 다음으로는 Original First Thunk(INT)의 주소를 읽는다. RAW 계산 후 이동해보자.

  • RAW = 2B9C4 - 2B000 + 28800 = 291C4

  • GDI32.dll 의 함수 리스트 확인. 맨 앞의 SetMapMode를 한 번 따라가보자

  • RAW = 2C004 - 2B000 + 28800 = 29804

  • 해당 위치에 SetMapMode 확인

IAT와 INT가 같은데 무슨 차이가 있는가?
  • INT는 함수의 이름을 저장하고 IAT도 프로그램이 로드되기 전까지는 동일하지만 로드되고 난 후에는 dll 안에 있는 실제 함수 주소가 덮어씌워지게 된다.

EAT(Export Address Table)

IMAGE_EXPORT_DIRECTORY

typedef struct _IMAGE_EXPORT_DIRECTORY {  
    DWORD   Characteristics;  
    DWORD   TimeDateStamp;          // creation time date stamp  
    WORD    MajorVersion;  
    WORD    MinorVersion;    
    DWORD   Name;                   // address of library file name
    DWORD   Base;                   // ordinal base  
    DWORD   NumberOfFunctions;      // number of functions  
    DWORD   NumberOfNames;          // number of names  
    DWORD   AddressOfFunctions;     // address of function start addressarray  
    DWORD   AddressOfNames;         // address of functino name string array  
    DWORD   AddressOfNameOrdinals;  // address of ordinal array
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
  • NumberOfFuntions : 실제 export 함수 갯수
  • NumberOfNames : export 함수 중에서 이름을 가지는 함수 갯수 (<= NumberOfFunctions)
  • AddressOfFunctions : export 함수들의 시작 위치 배열의 주소
  • AddressOfNames : 함수 이름 배열의 주소
  • AddressOfOrdinals : ordinal 배열의 주소

[EAT 구조]

GetProcAddress() 함수가 함수 이름을 이용해 함수 주소를 얻어내는 순서

  1. AddressOfNames 멤버를 이용해 함수이름배열로 이동
  2. 함수이름배열에는 문자열 주소가 저장되어 있음. 문자열비교(strcmp)를 통해 원하는 함수 이름 검색. 그 때의 배열 인덱스를 name_index라고 가정
  3. AddressOfNameOrdinals 멤버를 이용해 ordinal배열로 이동
  4. ordinal배열에서 name_index를 이용해 ordinal_index 값 획득
  5. AddressOfFunctions 멤버를 이용해 함수주소배열-EAT로 이동
  6. 함수주소배열-EAT에서 ordinal_index를 배열 인덱스로 하여 함수의 시작 주소 획득
dll의 파일은 모든 함수의 이름이 존재하지 않을 수 있고, AddressOfNameOrdinals 배열의 값에서 index 값과 ordinal 값이 다를 수 있다.(아래의 실습은 함수의 이름은 모두 존재하지만 index값과 ordinal 값이 다름)

-> 따라서 위 순서를 따라야 정확한 함수 주소를 얻을 수 있음

kernel32.dll에서 EAT로 AddAtomA 주소 확인

  • 동일하게 optional header에 EXPORT Table 존재. 추가로 EXPORT Table은 .rdata 섹션에 존재

  • (.rdata의) Virtual Address : 80000, PTRD : 6D000

  • RAW = 93DF0 - 80000 + 6D000 = 80DF0
    ![[Pasted image 20240910161811.png]]

  • IMAGE_EXPORT_DIRECTORY에 해당 주소 확인.

  • AddressOfNames(Name Pointer Table RVA)의 RAW를 계산해 이동해보자.

  • RAW = 957C4 - 80000 + 6D000 = 827C4

  • EXPORT Name Point Table에서 해당 주소 확인.

  • AcquireSRWLockExclusive의 RAW 계산 후 이동

  • RAW = 97EB2 - 80000 + 6D000 = 84EB2

  • AcquireSRWLockExclusive 문자열 확인

  • AddAtomA가 5번째 인덱스 임을 확인

  • AddressOfOrdinals(Ordinal Table RVA) RAW 계산 후 이동

  • RAW = 97170 - 80000 + 6D000 = 84170

  • 5번째 인덱스에 AddAtomA 확인. 해당 ordinal 값 8 확인

  • AddressOfFunctions(Address Table RVA)의 RAW를 계산해 이동해보자

  • RAW = 93E18 - 80000 + 6D000 = 80E18

  • EXPORT Address Table에서 8번째 인덱스에 AddAtomA가 있는 것을 확인

profile
hackyFrog

0개의 댓글