01 윈도우 PE 파일
02 윈도우 PE 파일 구조
03 IAT(Import Address Table)
04 EAT(Export Address Table)
1. 윈도우 PE 파일
1.1 윈도우 PE(Portable Executable) 파일이란?
- 윈도우에서 실행 파일은 PE 파일의 구조를 가지여, 여러 가지 확장자로 사용되고 있다. 파일이 실행되는 정보를 볼 수 있고 어느 메모리에서 로딩되는지 등의 정보를 알 수 있다.
- 윈도우에서 사용되는 실행 파일 종류
- 실행 계열 : exe(실행파일), scr(스크린세이버), msi(윈도우즈 패키지 인스톨 프로그램), bat(명령어 배치파일), cmd(명령어 스크립트), vbs(VB 스크립트) 등..
- 라이브러리 계열 : dll(동적 라이브러리), ocx(OLE 개체 컨트롤), cpl(윈도우즈 컨트롤 판넬), drv(장치 관리), sys(시스템 디바이스) 등
- PE(Portable Executable) 파일 : 윈도우 운영체제에서 사용되는 표준 실행 파일 형식이다.
다양한 운영체제에서의 이식성을 좋게 하려는 의도였으나 실제 windows 계열의 운영체제에서만 사용한다.
PE 파일은 윈도우에서 사용되는 실행 파일, DLL 등을 실행, 또는 호출하는데 통용되는 하나의 형식이다.
윈도우에는 다양한 프로그램과 파일들이 존재하지만, 실행 파일은 PE 구조를 가지기 때문에 PE를 이해하고 활용할 수 있다면 윈도우에서 작동하는 파일들을 제대로 다룰 수 있게 된다.
1.2 PE 파일 관련 도구
- PEView(http://wjradburn.com/software/) : PE 헤더 구조체에 맞추어 정보를 확인 가장 심플한 도구로 구조를 쉽게 파악
- PE Tools : 실행중인 프로세스를 분석.
- Stud_PE (https://www.cgsoftlabs.ro/zip/Stud_PE.zip) : PE 구조를 세분화하여 볼 수 있고, 가상 주소 공간을 확인하고 값을 수정할 수 있다.
- PEiD (https://www.softpedia.com/get/Programming/Packers-Crypters-Protectors/) : PE 헤더 정보와 각 섹션을 테이블 형태로 볼 수 있다.
- CFF Explore : PE 파일의 헤더 정보 보기, IAT와 EAT 보기, 주소변환, Hex Editor 기능을 제공한다.
2. 윈도우 PE 파일 구조
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)로
- 파일 실행에 필요한 전반적인 정보로 실행 파일의 크기, 각 섹먼트 위치, 권한 등을 포함한다.
PE 파일에는 PE 형식을 가진 파일임을 나타내는 매직 코드("MZ","PE")가 존재한다.
- win32 플랫폼에서 PE 헤더의 구조
IMAGE_DOS_HEADER, IMAGE_NT_HEADERS, IMAGE_SECTION_HEADER가 PE 파일의 전체 구조,
IMAGE_NT_HEADERS에서 여러 가지 정보가 연결되어 있다.
2.2 MS-DOS 정보
- MS-DOS 정보는 IMAGE_DOS_HEADER와 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;
-> 4D 5A가 매직 문자 "MZ"로 실행 파일 설계자인 Mark Zbikowski의 이름에서 가져왔다.
-> 위부터 DOS Header / Dos Stub / NT Header
마지막 값인 e_lfanew는 NT헤어 위치를 나타낸다. DOS 헤더의 마지막 4바이트 값이 0x80으로 되어 있고 NT 헤더의 시작 위치와 같음을 알 수 있다.
DOS 스텁은 실행에는 영향이 없고 실행파일을 DOS 환경에서 실행할 때 "This program cannot be run in DOS mode"라는 메시지를 보여주는 게 끝이다.
- CFF Explorer로 실행 파일을 보자
파일을 열고 스텁 부분을 어셈블리 언어로 볼 수 있다.
int 0x21은 인터럽트 호출.
2.3 Windows NT 정보
- 윈도우 NT 정보는 다음과 같이 파일 헤더와 옵션 헤더로 구성
64비트의 경우 IMAGE_OPTIONAL_HEADER64를 사용
파일 헤더는 파일의 물리적 정보를 표현 / 옵션 헤더는 파일의 논리적 정보를 표현
typedef struct _IMAGE_NT_HEADERS {
int signature; //PE signature "PE"
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
}; IMAGE_NT_HEADERS32;
#파일 헤더
typedef struct _IMAGE_FILE_HEADER {
short Machine; // CPU 종류(고유 번호)
short NumberOfSections; // 섹션의 수
int TimeDateStamp;
intNumberOfSymbols;
short SizeOfOptionalHeader; //옵션 헤더의 크기
short Characteristics; //파일 속성(종류)
} IMAGE_FILE_HEADER;
- Machine은 CPU 종류를 나타내는 고유번호로 Intel386(0x014 C), intel64비트(0x0200), ARM(0x01 C0), AMD64비트(0x8664)로 표시
- NumberOfSections 값은 코드, 데이터, 리소스 등과 같은 섹션의 수를 나타내며, 이 값은 0보다 크고 정의된 섹션 수보다 실제 섹션이 적으면 오류가 발생한다. 그리고 실제 섹션이 많다면 정의된 개수만큼만 인식한다.
- 윈도우 옵션 헤더는 IMAGE_OPTIONAL_HEADER32의 크기를 나타내는 0xE0로 정해져 있으나 sizeOfOptionalHeader 값을 다르게 설정할 경우 달라진 크기로 옵션 헤더의 크기를 인식한다.
- characteristics에서 사용하는 값
-> 16비트로 16가지 특징을 OR비트로 조합해서 사용할 수 있다.
#옵션 헤더
typedef struct _IMAGE_OPTIONAL_HEADER {
short Magic; //주소 크기
...
int AddressOfEntryPoint; // 시작 주소
...
int ImageBase; // 메모리 로딩 주소
...
int SizeOfImage; //메모리에 로딩되는 크기
int SizeOfHeader; //PE 헤더의 크기
...
int NumberOfRvaAndSizes; //DataDirectory 배열 개수
IMAGE_DATA_DIRECTORY DataDirectory [16];
} IMAGE_OPTIONAL_HEADER32;
- 옵션 헤더 정보인 IMAGE_OPTIONAL_HEADER32 구조체에서 Magic은 프로세스의 주소가 32비트이면 0x010B이고 64비트이면 0x020 B 값을 가진다.
프로세스의 가상 메모리는 32비트의 경우 0~0 xFFFFFFFF 범위이다. ImageBase 값은 PE 파일이 메모리에 로딩되는 주소를 나타낸다. EXE나 DLL 파일은 사용자 영역으로 0~0x7 FFFFFFF에 위치하고 SYS 파일은 커널 영역으로 0x80000000 ~ 0 xFFFFFFFF에 위치한다.
보통 EXE 파일은 0x00400000이고, DLL 파일은 0x10000000이다. AddressOfEntryPoint는 프로세스의 시작 주소로 상대 주소(RVA) 값으로 표현한다. PE 로더는 프로세스 메모리에 로딩한 후에 EIP 레지스터 값을
"ImageBase + AddressOfEntryPoint" 값으로 설정한다. [코드 실행 진입점이다]
- 구조체 배열 개수가 16개로 명시되어 있지만 PE로더는 NumberOfRvaAndSizes 값을 보고 배열 크기를 인식한다.
다음은 DataDirectory 배열로 다른 DLL 파일에서 함수를 가져오거나 내보낼 시에 해당 정보를 기록한다.
Export Table, Import Table 등의 시작 위치와 크기가 저장된다. DataDirectory는 IMAGE_DATA_DIRECTORY 구조체로 각 디렉터리 주소와 크기를 저장한다.
2.4 섹션 정보
typedef struct _IMAGE_SECTION_HEADER {
char Name [8]; // 섹션 이름(. text 등)
Union {
int PhysicalAddress;
int VirtualSize; // 메모리의 섹션 크기
} Misc;
int VirtualAddress; // 섹션 시작 주소
int SizeOfRawData; //파일의 섹션 크기
int PointerToRawData; // 파일의 섹션 위치
...
int Characteristics; //섹션의 속성
} IMAGE_SECTION_HEADER;
- 섹션은 특성이 동일한 데이터가 저장되어 있는 영역으로 윈도우에서 사용하는 메모리 보호 메커니즘과 연관되어 있다. PE 로더 입장에서 데이터 속성 구분할 방법으로 "실행 파일 생성 단계"에서 구분
해당 섹션은 메모리에서 VirtualAddress 값을 시작으로 VirtualSize 크기로 할당된다. 파일 상태에서 파일의 PointerToRawData 위치부터 SizeOfRawData 값만큼 저장된다. 즉 파일에서 메모리로 섹션이 로드될 때 크기가 바뀔 수 있다.
- Name은 섹션의 이름이 저장된다.
- . text : 실행되는 코드를 담고 있는 섹션
- . data : 초기화된 전역 변수를 담고 있으며, 읽고 쓰기가 가능한 섹션
- . rdata : 읽기 전용의 데이터 섹션, 문자열 표현이나 C++/com 가상 함수 테이블을 담고 있음
- . bss : 초기화되지 않은 전역 변수를 위한 섹션
- . idata : 다른 DLL에서 가져다 쓰는 함수들의 정보를 담고 있는 섹션
- . edata : 다른 모듈이 이 PE 파일에 정의되어 있는 함수를 사용할 수 있도록 함수 목록을 담고 있는 섹션
- Characteristics는 각 섹션의 속성을 정의하고 있다. 값은 아래 값의 비트 OR 조합으로 이뤄진다.
일반적인 코드(. text) 섹션은 실행과 읽기 권한, 데이터(. data)는 비실행과 읽기/쓰기, 자원(/. rsrc)은 비실행과 읽기 권한을 준다.
2.5 메모리 로딩 시 주소 변환
- 실행 파일은 메모리에 로드될 때, ImageBase를 기준으로 배치된다. 그러나 실행 파일의 헤더와 섹션은 ImageBase를 기준으로 같은 오프셋으로 배치되지는 않는다.
RVA(Relative Virtual Address) : 메모리의 상대 주소를 나타내고, VA(Virtual Address) : 메모리의 절대 주소를 나타낸다.
RVA와 같이 상대 주소를 사용하는 것은 PE헤더에서 PE 파일이 메모리 어느 곳에 재배치(Relocation)되더라도 주소 매핑(mapping)을 쉽게 하기 위한 것이다. 즉 ImageBase 값을 기준으로 상대적인 위치를 나타낸다.
- RVA + ImageBase = VA
- 파일 오프셋을 나타내는 RAW는 디스크 상의 파일에서 주소이다. 파일에서는 오프셋이라 부른다.
오프셋은 재배치 과정을 통해서 RVA로 변환된다.
- RAW - PointerToRawData = RVA = VirtualAddress
- RAW = RVA - VirtualAddress + PointToRawData
- RVA = RAW + VirtualAddress - PointToRawData
ex) RVA가 0x5500이면, "RAW = 5500 - 2000 + 1400(. text 섹션)" -> RAW는 0x4900
3. IAT(Import Address Table)
- IAT(Import Address Table) : 프로그램에서 사용하는 라이브러리 테이블
프로세스, 메모리, DLL(Dynamic Linked Library) 구조를 내포하고 있다. DLL은 동적 연결 라이브러리로 프로그램에 포함되지 않고 별도의 파일로 구성하여 필요할 때 불러서 쓸 수 있다.
- DLL 사용 이유
- 먼저 로드하지 않고 해당 코드가 필요할 때 로딩하여 사용할 수 있다.
- 로드된 DLL 코드는 여러 프로세스에서 공유할 수 있어서 메모리를 절약
- 라이브러리 업데이트가 필요하면 해당 DLL 파일만 교체하면 된다.
- DLL 로딩 방법
암시적 연결과 명시적 연결이 있다.
- 암시적 연결(Implicit Linking) - 프로그램의 시작과 함께 로드되어 프로그램이 끝날 때 메모리에서 해제된다. 이때 DLL은 라이브러리 폴더에 있거나 프로그램과 같은 폴더에 있어야 한다.
- 명시적 연결(Explicit Linking) - DLL을 로드할 시점을 명시한다. 라이브러리가 필요할 때 로딩하고, 사용이 끝나면 메모리에서 해제된다. PE 로더가 DLL 프로그램을 메모리 공간에 로드하면 DLL 프로그램은 재배치가 이뤄진다.
# IAT 생성 과정(IMAGE_IMPORT_DESCRIPTOR)
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
Union {int Characteristics;
IMAGE_THUNK_DATA *OriginalFirstThunk;
}; //INT의 주소(RVA)
...
int Name; //라이브러리 이름 문자열 주소 (RVA)
IMAGE_THUNK_DATA *FirstThunk; // IAT 주소 (RVA)
} IMAGE_iMPORT_DESCRIPTOR;
typedef struct _IMAGE_THUNK_DATA {
Union {
...
IMAGE_IMPORT_BY_NAME *AddressOfData; // 함수 정보 주소
};
} IMAGE_THUNK_DATA;
typedef struct _IMAGE_IMPORT_BY_NAME {
short Hint;
char Name [1]; //함수 이름 문자열
} IMAGE_IMPORT_BY_NAME;
- 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 항목을 이용하여 함수의 시작 주소 구함: GetProAddress("GetCurrentThreadId")
6. IID의 FirstThunk로 IAT 주소를 구함
7. 해당 IAT 배열 값에 위 주소를 넣음
8. INT가 끝날 때까지 4~7을 반복
# IAT에서 라이브러리 검색하기
- x64 dbg를 사용해도 된다.
4. EAT(Export Address Table)
Import가 라이브러리로부터 서비스(함수)를 제공받는 것이면,
Export는 라이브러리를 다른 PE 파일에게 서비스를 제공하는 것이다. 라이브러리 파일에서 제공하는 함수를 다른 프로그램에서 사용할 수 있도록 하는데 EAT는 일반적으로 DLL 파일에 존재.
EAT에 해당하는 IMAGE_EXPORT_DIRECTORY 구조체의 주소는 PE NT헤더의 Optional 헤더 내에 있는 DataDirectory [0] 값에 있다.
typedef struct _IMAGE_EXPORT_DIRECTORY {
...
int Name; //라이브러리 파일 이름의 주소
int Base; //Ordinal Base
int NumberOfFunctions; //실제 Export 함수 계열의 개수
int NumberOfNames; //Export 함수 이름 배열의 개수
int AddressOfFunctions; //함수 시작 주소 배열
int AddressOfNames; // 함수 이름 배열의 주소
int AddressOfNameOrdinals; // Ordinal 배열은 주소
} IMAGE_EXPORT_DIRECTORY;
# 라이브러리 함수 주소 가져오기
- EAT에서 라이브러리 함수 주소를 찾는 방법
- 1) AddressOfNames를 이용해 함수 이름 배열로 찾아간다. 함수 이름을 비교하여 원하는 함수 이름을 찾고 그 색인(index)을 구한다.
- 2) AddressONameOrdinals를 이용해 ordinal 배열로 찾아간다. Ordinal 배열에서 색인에 해당하는 ordinal 값(ord)을 찾는다.
- 3) AddressOfFunction를 이용해 함수 주소 배열(EAT)로 찾아간다. 함수 주소 배열에서 ordinal 값을 배열 색인으로 하여 원하는 함수의 시작 주소를 가져온다.
# DLL 파일의 EAT에서 API 함수를 찾기
'리버싱 > x64dbg 디버거를 활용한 리버싱과 시스템 해킹의 원리' 카테고리의 다른 글
ch08 - 안티 리버싱 / WindDgb (0) | 2021.10.13 |
---|---|
ch07 - 파일 패킹과 언패킹 (0) | 2021.10.06 |
ch05 - 코드 분석과 패치 (0) | 2021.09.22 |
ch04 - 어셈블리어의 이해 (0) | 2021.09.15 |
ch03 - 디버거의 활용 방법(x64DBG/OllyDBG) (0) | 2021.09.09 |