PE 포맷

안상준·2026년 1월 1일

Reversing

목록 보기
6/16

Ghidra Study

PE 포맷이란

PE(Portable Executable) 포맷 실행 파일은 윈도우에서 이용되는 형식으로 윈도우에서 .exe, .dll 파일이 메모리에 로드뒈어 실행되기 위한 정보를 담고 있다.

Header

PE 포맷 헤더에 담고 있는 정보에 대해서 알아보자.

PE 포맷 헤더의 구조는 위 사진과 같다.
프로세스 메모리 시작 주소, 코드 및 데이터의 저장 위치, 아키텍처 종류 (x86, x64, ARM), 함수 테이블 정보 등 실행을 위한 다양한 정보를 담고 있다.

DOS Header

IMAGE_DOS_HEADER 구조체로 정의되며 PE 파일을 분석할 때 가장 먼저 마주치게 된다.
모든 x64 윈도우 환경에서 PE 파일은 이 헤더로 시작한다. 리버스 엔지니어링에 중요한 필드 2개가 존재한다.

PEView 프로그램을 활용

  • e_magic (MZ Magic Number): 파일의 맨 시작 (Offset 0)에 위치하며, 값이 4D 5A(MZ)인지 확인해야 한다. 만약 시그니처가 다르다면 OS는 이를 실행파일로 간주하지 않는다.
  • e_lfanew (NT Header Offset): DOS 헤더의 맨 마지막 (Offset 3C)에 위치하며, 중요한 정보를 담고 있는 NT Header 파일의 시작 위치를 가리키는 포인터이다.

NT Header

IMAGE_NT_HEADER 구조체로 위 사진 처럼 PE(0x50, 0x45, 0x00, 0x00)이라는 값의 서명을 가지고 있다.

IMAGE_NT_HEADER 구조체는 IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER로 구성돼 있다.

File Header


위 사진은 File Header가 담고 있는 정보이다. 여기서 중요하게 볼 정보가 몇가지 있다.

  • Machine: 파일이 어떤 CPU 환경에서 동작하도록 설계되었는지 나타낸다. (I386 = x86)
  • Number of Sections: PE 헤더(NT Header) 바로 뒤에 오는 섹션 테이블의 크기를 의미한다.

Optional Header


Optional Header는 가장 방대하면서도 실제 프로그램이 메모리에서 어떻게 동작하는지를 결정하는 핵심 정보를 담고 있다.

  1. Magic: 파일이 32비트인지, 64비트인지 최종적으로 확정 짓는 값 (0x1B = 32bit, 0x20B = 64bit)

  2. AddressOfEntryPoint(EP): 리버싱의 시작점으로 프로그램이 메모리에 로드된 후, 가장 처음으로 실행될 코드의 주소를 가리킨다.

  3. ImageBase: 파일이 가상메모리의 어느 주소에 로드되는지 나타내는 정보 (.exe = 0x00400000, .dll = 0x10000000)

  4. SectionAlignment & FileAlignment: 파일이 하드디스크에 있을 때와 메모리에 올라갔을 때의 데이터 배치 간격
    섹션이 하드디스크에 있을 때와 가상 메모리에 있을 때 차지해야 하는 청크의 크기를 결정한다.
    이 둘을 다르게 하는 이유는 하드디스크와 가상 메모리(RAM)의 관리 효율성의 차이 때문이다.

  • 하드디스크: 섹션 사이의 빈 공간이 너무 크면 파일 용량이 불필요하게 커진다.
  • 가상메모리: CPU와 운영체젠는 메모리를 관리할 때 페이지라는 단위를 사용한다. 섹션이 페이지 단위와 딱 맞아 떨어져야 효율적인 실행이 가능.
  1. DataDirectory: Optional Header의 마지막 부분에 위치한 16개의 구조체 배열이다.
  • Export Table / Import Table: 어떤 함수를 내보내고 가져오는지에 대한 정보를 담긴 곳의 위치를 가리킨다.
  • Resource / TLS / Relocation: 각종 리소스와 보안 설정이 어디 있는지 알려주는 역할.

Section Header

IMAGE_SECTION_HEADR 구조체로 정의되며, 섹션 테이블에는 각 섹션 이름과 주소 정보가 저장되어 있다.

Section

Library

파일 조작이나 메모리 조작, 통신 등 대부분의 프로그램 동작에는 운영체제가 제공하는 라이브러리를 이용한다. 윈도우 실행 형식인 PE 포맷의 경우 윈도우 API가 라이브러리로 제공된다. 라이브러리 링크 방법에는 정적 링크와 동적 링크가 있다.

  • 정적 링크: 라이브러리의 함수 코드를 컴파일/링크 단계에서 실행 파일 내부에 직접 포함시키는 방식 ex) #include를 이용하여 라이브러리를 선언, printf 함수 호출 -> printf 함수 구현부가 실행파일 내부에 존재
  • 동적 링크: 함수의 실제 구현부를 파일에 넣지 않고, 실행 시간에 DLL 파일에서 찾아서 사용

    C++로 작성한 dll을 로드하여 호출하는 코드의 예

Import

PE 포맷에서 DLL 이 내보내는(Export) 함수를 임포트하여 사용. IAT(Import Address Table)에 임포트된 함수의 주소를 저장, 코드 내에서 함수를 호출할 때 IAT에 접근하여 함수의 주소로 이동.

Export

Import와 반대되는 개념으로 앞서 실행파일에서 DLL의 함수를 사용하기 위해서는 DLL 파일에서 함수를 Export 해줘야 한다.

Pactice

#include <windows.h>
#include <iostream>

using namespace std;

typedef int (*Add)(int, int);

int main() {
    HMODULE hDLL = LoadLibraryW(L"add.dll");
    if (hDLL == NULL) {
        cerr << "DLL 로드 실패" << endl;
        return 1;
    }

    Add AddNum = (Add)GetProcAddress(hDLL, "Add");
    if (AddNum == NULL) {
        cerr << "Add 함수 로드 실패" << endl;
        FreeLibrary(hDLL);
        return 1;
    }

    int result = AddNum(5, 10);
    cout << result << endl;

    FreeLibrary(hDLL);
    return 0;

}
#include <windows.h>

extern "C" __declspec(dllexport) int Add(int a, int b) {
    return a + b;
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    switch(ul_reason_for_call) {
        case DLL_PROCESS_ATTACH:
            break;
        case DLL_PROCESS_DETACH:
            break;
    }
    return true;
}

C++ 코드로 함수를 import, export 하는 코드를 작성해 주었다.
dll 파일을 생성하는 방법은

gcc -shared -o add.dll Export.cpp

싱행하는 방법은

g++ -o Import.exe Import.cpp

이렇게 컴파일하여 실행파일을 생성하였다.

생성한 실행파일을 분석하여 실제로 dll 파일이 동적 링크가 되는지 확인해 보았다.
PEView에서 볼 수 있는 ImportTable은 정적 링크된 함수들이기 때문에 코드를 실행하여 확인하여야 한다.
이를 위해 IDA를 사용해 주었다.

LoadLibrary이후에 BreakPoint를 걸고 실행하게 되면

직접 작성한 add.dll 파일이 로드된 것을 확인할 수 있다.

0개의 댓글