본문 바로가기
Security Book Study/x64dbg를 활용한 리버싱과 시스템 해킹의 원리

Chapter 6. 윈도우 실행 파일 구조

by 옹담 2025. 3. 14.

6.1. 윈도우 PE 파일

6.1.1. 윈도우 PE(Portabble Executable) 파일이란?

윈도우 실행파일 종류:

  • 실행 계열:
    • exe(실행 파일), scr(스크린 세이버), msi(윈도우즈 패키지 인스톨 프로그램), bat(명령어 배치파일), cmd(명령어 스크립트), vbx(VB 스크립트) 등
  • 라이브러리 계열:
    • dll(동적 라이브러리), ocx(OLE 개체 컨트롤), cpi(윈도우즈 컨트롤 판넬), drv(장치 관리), sys(시스템 디바이스) 등

PE 파일은 윈도우 운영체제에서 사용되는 표준 실행 파일 형식이다. PE(Portable Executable)라는 이름에서 알 수 있듯 다양한 운영체제에서의 이식성을 좋게 하려는 의도였으나 실제로는 Windows 계열의 운영체제에서만 사용한다. PE 파일은 윈도우에서 사용되는 실행파일, DLL 등을 실행, 또는 호출하는데 통용되는 하나의 형식이다. 윈도우 32비트 운영체제는 PE32라고도 부르고, 64비트 운영체제는 PE+ 또는 PE32+라 부른다.

6.1.2. PE 파일 관련 도구

PEView

PE 헤더 구조체에 맞추어 정보를 확인할 수 있다 가장 심플한 도구로 구조를 쉽게 파악할 수 있다. 그러나 업데이트가 진행되지 않아 최근 실행 파일은 제대로 읽지 못한느 경우가 많다.

PEView

 

Stud_PE

PE 구조를 세분화하여 볼 수 있고, 가상주소 공간을 확인하고 값을 수정할 수 있다. 또한 IAT나 Resource 정보를 검색할 수 있다.

Stud_PE

 

PE Tools

PE 헤더를 수정할 수 있다. 또한 실행 중인 프로세스를 선택하여 PE 헤더 정보를 분석할 수 있다.

PE Tools

 

PEiD

PE 헤더 정보와 각 섹션을 테이블 형태로 볼 수 있다. 간단한 디스어셈블 정보, 간단한 언패킹, 진입점(EP) 검색 기능도 지원한다.

PEiD

 

CFF Explorer Suite

PE 파일의 헤더 정보 보기, IAT와 EAT 보기, 주소 변환, Hex Editor 기능을 제공한다. 또한, PE 재구성(rebuild) 기능과 UPX 언패킹 기능을 제공한다.

CFF Explorer Suite

 

6.2. 윈도우 PE 파일 구조

6.2.1. 윈도우 실행 파일 형식

PE 파일은 기본적으로 MS-DOS 정보, Windows NT 정보, 그리고 섹션(Section) 정보로 구성된다.

  • MS-DOS 정보
    : DOS 헤더(IMAGE_DOS_HEADER)와 DOS Stub으로 구성되고, MS-DOS 버전과 호환성을 위해 존재한다.
  • Windows NT 정보
    : NT 헤더가 있으며 파일 헤더(IMAGE_FILE_HEADER)와 옵션 헤더(IMAGE_OPTIONAL_HEADER)로 구성된다. 파일 실행에 필요한 전반적인 정보로 실행 파일의 크기, 각 세그먼트 위치, 권한 등을 포함한다.
  • 섹션 정보
    : 섹션 헤더(IMAGE_SECTION_HEADER)와 섹션으로 구성된다.

PE 파일에는 PE 형식을 가진 파일임을 나타내는 매직(magic) 코드가 존재한다. 아래 그림과 같이 "MZ"와 "PE"가 이에 해당한다.

HxD

 

다음 그림은 Win32 플랫폼에서 PE 헤더의 구조를 보여준다. 왼쪽에 IMAGE_DOS_HEADER, IMAGE_NT_HEADER, IMAGE_SECTION_HEADER가 PE 파일의 전체 구조를 나타내고, IMAGE_NT_HEADERS에서 여러 가지 정보가 연결되어 있다.

PE 헤더의 구조

 

6.2.2. MS-DOS 정보

MS-DOS 정보는 IMAGE_DOS_HEADER와 DOS Stub으로 구성된다. 이 정보는 MS-DOS 버전의 파일에 대한 호환성을 위해 존재한다. 다음은 IMAGE_DOS_HEADER 구조체 정보이다.

typedef struct _IMAGE_DOS_HEADER {
    short e_magic;           // DOS Signature "MZ"
    short e_cblp, e_cp;
    short e_crlc, e_cparhdr;
    short e_minalloc, e_maxalloc;
    short e_ss, e_sp;
    short e_csum, e_ip, e_cs;
    short e_lfarlc, e_ovno;
    short e_res[4], e_oemid;
    short e_oeminfo, e_res2[10];
    int e_lfanew;                // offset to NT header
} IMAGE_DOS_HEADER;

 

처음에 있는 매직 문자 "MZ"는 MS-DOS 실행 파일 설계자인 Mark Zbikowski의 이름에서 가져왔다. 마지막 값인 e_lfanew는 NT헤더 위치를 나타낸다. 아래 그림에서 DOS 헤더의 마지막 4바이트 값이 0x80으로 되어있으며, NT 헤더의 시작 위치와 같음을 알 수 있다.

 

DOS Stub은 실행에는 큰 영향이 없는 부분으로, Windows 실행 파일을 DOS 환경에서 실행할 때 "This program cannot be run in DOS mode라는 메세지를 보여주고 끝낸다. CFF Explorer로 실행 파일을 열고, [Hex Editor] 탭에서 DOS Stub 부분을 "x86(16bit) Assembly" 코드로 살펴보자.

CFF Explorer

 

아래와 같이 DOS Stub에 대한 어셈블리 코드를 확인할 수 있다. 문자열 "This program cannot be run in DOS mode"의 주소를 가리키는 0x0E와 WriteString() 함수를 가리키는 0x09를 매개변수로 넣고 인터럽트(INT 0x21)를 호출한다. 그리고 Exit() 함수를 가리키는 0x4C01을 인터럽트 호출하면서 종료한다.

 

6.2.3. Windows NT 정보

윈도우 NT 정보에는 다음과 같이 파일 헤더와 옵션 헤더로 구성된다. 그리고 "PE" 매직 문자가 있다. 64비트의 경우 IMAGE_OPTIONAL_HEADER64를 사용한다. 파일 헤더는 파일의 물리적 정보를 표현하고 옵션 헤더는 파일의 논리적 정보를 표현한다.

 

파일 헤더

파일 정보를 나타내는 IMAGE_FILE_HEADER 구조체는 위와 같다.

  • Machine : CPU 종류 (고유번호)
    • Intel 386 - 0x14C
    • Intel 64 - 0x0200
    • ARM - 0x01C0
    • AMD64 - 0x8664
  • NumberOfSections : 코드, 데이터, 리소스 등과 같은 섹션의 수
    • 0보다 크며, 정의된 섹션 수보다 실제 섹션이 적으면 오류가 발생하고, 실제 섹션이 많으면 정의된 개수만큼만 인식한다.
  • SizeOfOptionalHeader : 윈도우 옵션 헤더로, IMAGE_OPTIONAL_HEADER32의 크기를 나타내는 0xE0으로 정해져 있으나, 이 값을 다르게 설정할 경우 달라진 크기로 옵션 헤더의 크기를 인식한다.

CFF Explrer 도구를 활용하면 파일 헤더의 정보를 파악하기 편하다. 다음 그림은 이 도구로 실행 파일의 파일 헤더를 확인한다. Machine 값이 "intel 386"으로 되어 있고, 옵션 헤더의 크기도 0xE0로 되어있다.

CFF Explorer

 

파일 헤더에서 Characteristics는 exe, dll 등과 같은 파일 종류와 속성을 나타낸다. 예를 들어 0x0002는 실행 가능한 파일이고, 0x2000은 DLL 파일을 의미한다. 아래는 Characteristics에서 사용하는 값으로 비트 OR로 조합해서 사용할 수 있다.

Characteristics 의미
IMAGE_FILE_RELOCS_STRIPPED 0x0001 재배치를 하지 않음
IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 실행 가능함
IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 줄번호가 제거됨
IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 심볼 테이블이 제거됨
IMAGE_FILE_AGGRESSIVE_WS_TRIM 0x0010 사용하지 않음
IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 2GB보다 더 큰 주소 사용
IMAGE_FILE_BYTES_RESERVED_LO 0x0080 사용하지 않음
IMAGE_FILE_32BIT_MACHINE 0x0100 32비트 워드 지원
IMAGE_FILE_DEBUG_STRIPPED 0x0200 디버깅 정보가 제거됨
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 이동식 저장매체는 스웹공간에서 실행
IMAGE_FILE_SYSTEM 0x1000 시스템 파일
IMAGE_FILE_DLL 0x2000 DLL 파일
IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 단일 CPU에서만 실행
IMAGE_FILE_BYTES_RESERVED_HI 0x8000 사용하지 않음

 

CFF Explorer에서 파일 헤더의 Characteristics 정보에 있는 "Click Here"을 누르면 다음과 같이 파일 속성 정보 중에서 어떠한 것이 선택되어 있는지를 보여준다.

 

옵션 헤더

옵션 헤더 정보인 IMAGE_OPTIONAL_HEADER32의 구조체는 여러 정보가 있지만, 위에서는 중요한 정보만 표현하였다. 

  • Magic : 프로세스의 주소가 32비트이면 0x010B이고 64비트이면 0x020B 값을 가짐
  • ImageBase : PE 파일이 메모리에 로딩되는 주소
    • EXE, DLL - 0 ~ 0x7FFFFFFF (사용자 영역)
      • EXE - 0x00400000
      • DLL - 0x10000000
    • SYS - 0x8000000 ~ 0xFFFFFFFF (커널 영역)
  • AddressOfEntryPoint : 프로세스의 시작 주소로, 상대주소(RVA) 값으로 표현
    • PE 로더는 프로세스 메모리에 로딩한 후 EIP 레지스터 값을 "ImageBase + AddressOfEntryPoiont" 값으로 설정한다. 즉, 이 값이 프로세스의 EntryPoint이다.

구조체 정의에 배열 개수가 16개로 명시되어있지만, PE 로더는 NumberOfRvaAndSizes 값을 보고 배열 크기를 인식한다.

DataDirectory

Data Directory는 다른 DLL 파일에서 함수를 가져오거나 내보낼 시 해당 정보를 기록한다. 즉, Export Table, Import Table 등의 시작 위치와 크기가 저장된다. Data Directory는 IMAGE_DATA_DIRECTORY 구조체로 각 디렉터리 주소와 크기를 저장한다. 앞에 그림에서는 자주 사용되는 Import, Export,, Resource, TLS 공간을 표시하였다.

6.2.4. 섹션 정보

섹션 헤더의 구조

섹션은 특성이 동일한 데이터가 저장되어 있는 영역으로, 윈도우에서 사용하는 메모리 보호 메커니즘과 연관되어 있다. PE 로더 입장에서 데이터 속성을 구분할 방법으로 '실행 파일 생성 단계'에서 구분한다.

 

해당 섹션은 메모리에서 VirtualAddress 값을 시작으로 VirtualSize 크기로 할당된다. 파일 상태에서는 파일의 PointerToRawData 위치부터 SizeOfRawData 값만큼 저장된다. 즉 파일에서 메모리로 섹션이 로드될 때 크기가 바뀔 수 있다.

 

  • Name : 섹션의 이름이 저장됨
    • .text : 실행되는 코드
    • .data : 초기화된 전역 변수 (읽고 쓰기 가능)
    • .rdata : 읽기 전용의 데이터 섹션, 문자열 표현이나 C++/com 가상 함수 테이블 포함
    • .bss : 초기화되지 않은 전역변수를 위한 섹션
    • .idata : 다른 DLL 에서 가져다쓰는 함수들의 정보를 담고 있는 섹션
    • .edata : 다른 모듈이 이 PE 파일에 정의되어 있는 함수를 사용할 수 있도록 함수 목록을 담고 있는 섹션
  • Characteristics : 각 섹션의 속성 정의
    • 아래 값의 비트 OR 조합으로 이루어짐
    • 코드(.text) - 실행, 읽기 권한
    • 데이터(.data) - 비실행과 읽기/쓰기 권한
    • 자원(.rsrc) - 비실행과 읽기 권한
플래그 의미
IMAGE_SCN_CNT_CODE 0x00000020 실행 코드 포함
IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 초기화된 데이터
IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 초기화되지 않은 데이터
IMAGE_SCN_MEM_SHARED 0x10000000 메모리 공유 가능
IMAGE_SCN_MEM_EXECUTE 0x20000000 실행 허용
IMAGE_SCN_MEM_READ 0x40000000 읽기 허용
IMAGE_SCN_MEM_WRITE 0x80000000 쓰기 허용

 

6.2.5. 메모리 로딩 시 주소 변환

실행 파일은 메모리에 로드될 때, ImageBase를 기준으로 배치된다. 그러나 실행 파일의 헤더와 섹션은 ImageBase를 기준으로 같은 오프셋으로 배치되지는 않는다. 위의 그림처럼 섹션의 시작 위치가 달라지거나 섹션의 크기가 달라질 수 있다.

 

RVA(Relative Virtual Address)는 메모리의 상대주소를 나타내고, VA(Virtual Address)는 메모리의 절대주소를 나타낸다. RVA와 같이 상대주소를 사용하는 것은 PE 헤더에서 PE 파일이 메모리 어느 곳에 재배치(relocation) 되더라도 주소 매핑(mapping)을 쉽게 하기 위한 것이다. 즉 ImageBase 값을 기준으로 상대적인 위치를 나타낸다.

 

RVA + ImageBase = VA (VirtualAddress)

 

파일 오프셋(File Offset)을 나타내는 RAW는 디스크 상의 파일에서 주소이다. 파일에서는 오프셋이라 부른다. 이것은 재배치 과정을 통해 RVA로 변환된다. (PointerToRawData와 VirtualAddress는 섹션 헤더에 있다.)

 

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

 

예를 들어, 다음 그림에서 RVA가 0x5500이면, "RAW = 5500 - 2000 + 1400 (.text 섹션)" 이므로 RAW는 0x4900이 된다.

 

Stud_PE와 같은 도구로 주소 변환을 간단히 할 수 있다. 이러한 주소 변환은 정적 분석을 통한 결과를 바탕으로 동적 분석을 수행할 때 사용될 수 있다.

Stud_PE

CFF Explorer에서도 "Address Converter" 기능을 사용하면 다음과 같이 주소 변환을 할 수 있다.

CFF Explorer

 

6.3. IAT (Import Address Table)

IAT는 프로그램에서 사용하는 라이브러리 테이블이다. IAT에는 프로세스, 메모리, DLL(Dynamic Linked Library) 구조를 내포하고 있다. DLL은 "동적 연결 라이브러리"이며, 프로그램에 포함되지 않고 별도의 파일로 구성하여 필요할 때 불러서 쓸 수 있다.

 

DLL을 사용하는 이유:

  • 먼저 로드하지 않고 해당 코드가 필요할 때 로딩하여 사용할 수 있다.
  • 로드된 DLL 코드는 여러 프로세스에서 공유할 수 있어서 메모리를 절약할 수 있다.
  • 라이브러리 업데이트가 필요하면 해당 DLL 파일만 교체하면 된다.

DLL은 일반 모듈의 재사용성을 강화하고, 보안 모듈의 지속성을 증가시킨다.

 

DLL 로딩 방법은 암시적 연결(Implicit Linking)과 명시적 연결(Explicit Linking)이 있다.

  • 암시적 연결
    : 프로그램의 시작과 함께 로드되어 프로그램이 끝날 때 메모리에서 해제된다.
    이때 DLL은 라이브러리 폴더에 있거나 프로그램과 같은 폴더에 있어야 한다.
  • 명시적 연결
    : 라이브러리가 필요할 때 로딩하고, 사용이 끝나면 메모리에서 해제된다. 즉, DLL을 로드할 시점을 명시한다.
    PE로더가 DLL 프로그램을 메모리 공간에 로드하면, DLL 프로그램은 재배치가 이루어진다.

IAT 생성 과정

Import 라이브러리(IMAGE_IMPORT_DESCRIPTOR) 구조체

IAT를 통한 라이브러리 로딩 과정:

  1. IID(IMAGE_IMPORT_DESCRIPTOR)의 Name 주소로부터 문자열("kernel32.dll")을 가져옴.
  2. 해당 라이브러리를 로딩: LoadLibrary("kernel32.dll")
  3. IID의 OriginalFirstThunk에서 INT(Import Name Table) 주소를 가져옴
  4. INT 배열 값을 하나씩 읽어 IIBN(IMAGE_IMPORT_BY_NAME) 주소를 가져옴
  5. IIBN의 Name이나 Hint 항목을 이용하여 함수의 시작주소를 구함: GetProcAddress("GetCurrentThreadId")
  6. IID의 FirstThunk로 IAT 주소를 구함
  7. 해당 IAT 배열 값에 위 주소를 넣음
  8. INT가 끝날 때까지 4~7 과정 반복

IAT를 통한 라이브러리 로딩 과정:

IAT에 각 라이브러리 주소는 이 구조체의 FirstThunk에 저장된다.

 

IAT에서 라이브러리 검색하기

IAT에서 "kernel32.dll"에 속한 API "Sleep"을 직접 찾아보자. 

IMAGE_OPTIOONIAL_HEADER32.DataDirectory[1].VirtualAddress 값이 IMAGE_IMPORT_DESCRIPTOR 구조체 배열(Import Directory Table)의 시작 주소이다. CFF Explorer에서 "kernel32.dll"을 검색해 보자. OFTs(OriginalFirstThunk)의 값이 0x6050이고, 첫 번째 값으로 0x61F88이 저장되어 있다. 디버거로 해당 주소를 찾아가보면, 같은 내용을 볼 수 있다.

디버거

INT(Import Name Table) 주소는 변환해서 0x4061F8이며, 이곳에서 함수 이름을 검색하여 색인(id)를 찾아낸다. 첫 번째 API 이름은 "Beep"임을 확인할 수 있다. 이제 INT에서 API 이름을 찾아보자. IAT 주소에서 색인(id) 번째 데이터가 찾고자 하는 함수의 주소에 해당된다.

 

"kernel32.dll"의 API 리스트를 찾아보면, 17번째에 "Sleep"이 존재한다. 따라서 이 API의 주소는 IAT의 위치를 나타내는 FTs(FirstThunk)에서 17번째 데이터이다. FTs 값이 0x6124이므로 실제 주소는 ImageBase를 더한 0x406124이다. 그리고 17번째 이므로 0x406124 + (4바이트 * 16) = 0x406174에 "Sleep"의 주소가 저장되어 있다.

 

저장된 주소 0x76C80C10으로 찾아가면 다음과 같이 코드 내용을 볼 수 있다. "jmp" 명령어로 0x76CE0BE8에 저장된 데이터를 목표 주소로 이동하는 코드이다.

위 주소에 저장된 값으로 이동하면, 주소 0x748F11D0가 저장되어 있다. 즉 IAT에는 이 주소로 이동하여 API를  수행하게 된다.

 

실제로 위 주소로 이동하면 아래와 같은 Sleep() 함수의 코드를 볼 수 있다.

앞의 접근 과정으로 IAT가 어떤 형태로 구성되었는지 알 수 있다. 디버거를 활용하면 IAT를 쉽게 찾아볼 수 있다. x64dbg의 "기호" 탭에서 kernel32.dll을 검색해보면 Sleep 함수를 찾아볼 수 있다. 코드를 분석할 때는 앞의 복잡한 과정 없이 디버거를 이용하여 원하는 API를 검색하면 된다.

6.4. EAT(Export Address Table)

Import는 라이브러리로부터 서비스(함수)를 제공받는 것이며, Export는 라이브러리를 다른 PE 파일에게 서비스(함수)를 제공하는 것을 말한다. 즉, 라이브러리 파일에서 제공하는 함수를 다른 프로그램에서 사용할 수 있도록 한다. 따라서 EAT는 일반적으로 DLL 파일에 존재한다.

 

EAT에 해당하는 IMAGE_EXPORT_DIRECTORY 구조체의 주소는 PE NT헤더의 Optional 헤더 내에 있는 DataDirectory[0] 값에 있다. EAT에 대한 구조체 내용 중에서 중요한 정보는 다음과 같다.

EAT의 구조체

라이브러리 함수 주소 가져오기

EAT에서 라이브러리 함수 주소를 찾는 방법:

  1. AddressOfNames를 이용해 함수 이름 배열로 찾아간다. 함수 이름을 비교하여 원하는 함수 이름을 찾고 그 색인(index)을 구한다.
  2. AddressNameOridnals를 이용해 ordinal 배열로 찾아간다. Ordinal 배열에서 index에 해당하는 ordinal 값(ord)을 찾는다.
  3. AddressOfFunctionon를 이용해 함수 주소 배열(EAT)로 찾아간다. 함수 주소 배열(EAT)에서 ordinal 값(ord)을 배열 index로 하여 원하는 함수의 시작 주소를 가져온다.

다음 그림은 EAT에서 라이브러리 함수 주소를 구하는 과정을 보여준다. GetProcAddress() API는 실제로 EAT를 참조하여 라이브러리 함수 주소를 구한다.

DLL 파일의 EAT에서 API 함수를 찾기

IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress 값이 IMAGE_EXPORT_DESCRIPTOR 구조체 배열(Export Directory Table)의 시작 주소이다. IMAGE_EXPORT_DESCRIPTOR 구조체 멤버의 Name(라이브러리 이름)으로 원하는 "keyHook.dll" 프로그램을 찾는다. CFF Explorer에서는 "Export Directory"에서 정보를 찾을 수 있다. DLL 파일에 포함되어 있는 함수의 이름은 AddressOfNames(ⓑ)에서 찾을 수 있고, 함수의 주소는 AddressOfFunctioons(ⓐ)에서 찾을 수 있다.

 

AddressOfNames 값인 0x80E0(ⓑ)에 ImageBase 값을 더한 실제 주소를 찾아가본다. 여기에 저장된 값은 0x80F8이며, 이 주소에서 함수 이름들을 차례로 찾아볼 수 있다. 여기서 Ordinal 값은 0x80E8 부분에 0x00으로 저장되어 있다. 첫 번째 함수 이름은 "HookStart"이고, 두 번째 함수의 이름은 "HookStop"이다. 각각의 Ordinal 값은 차례로 0x00과 0x01이다.

 

이제 AddressOfFunctions 값(ⓐ)에서 해당 순서의 함수에 대한 주소 값을 찾아보자. 첫  번째 함수 주소는 0x10E0이고, 두 번째는 0x1100임을 확인할 수 있다. 따라서 "HookStart" 함수의 실제 주소는 0x100010E0가 된다.

 

이러한 EAT의 함수들은 다른 실행 파일의 IAT에 포함될 수 있다. 디버거의 [기호] 탭에서 DLL 파일의 내역을 보면 앞에서 확인했던 두 함수의 주소 값을 확인할 수 있다. 그리고 그 주소를 더블클릭하면 해당 함수의 코드 위치로 이동한다. 이 주소를 찾아가 실제 코드를 확인해본다.

최근댓글

최근글

skin by © 2024 ttutta