본문 바로가기

리버싱 핵심원리

[리버싱 핵심원리] 13장 - PE File Format(2)

13.4 RVA to RAW

PE 파일이 메모리에 로딩되었을 때 각 섹션에서 메모리의 주소(RVA)와 파일 옵셋을 매핑하는 것을 일반적으로 'RVA to RAW'라고 부른다. 해당 변환의 과정은 다음과 같다.

 

1. RVA(메모리에 로딩된 주소)가 속해 있는 섹션을 찾는다.

2. 간단한 비례식을 사용해서 RAW(파일 옵셋)을 계산한다.

 

IMAGE_SECTION_HEADER 구조체에 의하면 비례식은 아래와 같다.

RAW - PointerToRawData(파일에서 해당 섹션의 시작주소) = RVA - VirtualAddress (메모리에 해당 섹션의 시작 주소)

                                 RAW = RVA - VirtualAddress + PointerToRawData

 

ex) RVA = 5000일 때 File offset?

1. 해당 RVA는 첫 번째 섹션에 속한다.(.text)

2. RAW = 5000(RVA) - 1000(VirtualAddress) + 400(PointerToRawData) = 4400

 

ex) RVA = ABA8일 때 File offset?

1. 해당 RVA는 두 번째 섹션에 속한다.(.data)

2. RAW = ABA8 - 9000 + 7C00 = 97AB (해당 주소는 세번째 섹션에 해당된다.)

==> 이런 경우에는 해당 RVA에 대한 RAW의 값은 정의할 수 없다고 한다.

==> VirtualSize(메모리에서 .data 섹션 크기) 값이 SizeOfRawData(파일에서 .data 섹션 크기)보다 크기 때문에 이런 결과가 나온 것이다.

13.5 IAT

IAT는 Import Address Table의 약자로 프로그램이 어떤 라이브러리에서 어떤 함수를 사용하고 있는지를 기술한 table이다. IAT에는 Windows 운영체제의 핵심 개념인 process, memory, DLL 구조체 등에 대한 내용이 함축되어 있다. 

 

1.DLL

DLL은 우리말로 '동적 연결 라이브러리'(Dynamic Linked - List)라고 한다. 여러 프로그램이 동시에 실행되어야 하는 상황에서 모든 프로그램마다 동일한 라이브러리를 각각 포함해 실행하면 메모리 낭비가 심하다는 단점을 없애기 위해 해당 개념을 고안했다.

 

DLL의 개념
1. 프로그램 라이브러리를 포함시키지 말고 별도의 파일(DLL)로 구성해 필요할 때마다 불러 쓰자
2. 한 번 로딩된 DLL의 코드, 리소스는 Memory Mapping 기술로 여려 Process에서 공유해 쓰자
3. 라이브러리가 업데이트되었을 때 해당 DLL 파일만 교체하자

 

DLL의 로딩 방식

1. Explicit Linking : 사용되는 순간에 로딩하고 사용이 끝나면 메모리에서 해제되는 방법

2. Implicit Linking : 프로그램 시작할 때 같이 로딩되어 프로그램 종료시 메모리에서 해제되는 방법

 

IAT는 Implicit Linking에 대한 메커니즘을 제공하는 역할을 한다.

dll 파일 내부의 API를 호출할 때는 그 주소를 직접 쓰지 않고 미리 마련한 주소에 저장된 값(api의 주소)를 가져와 호출한다. 그 이유는 모든 환경(OS, 언어 등)에서 API의 호출을 보장하기 위해서이다. 이런 호출 방식은 해당 API의 실제 주소가 저장될 위치(ex.100)를 준비하고 CALL DWORD PTR DS:[100] 형식의 명령어를 적어두는 것이다. 파일이 실행되는 순간 PE 로더가 준비한 위치(100)에 해당 함수의 주소를 입력해준다. 그리고 이런 호출 방식을 사용하는 또 다른 이유는 DLL Relocation 때문이다. (DLL 파일은 PE 헤더에 명시된 ImageBase에 로딩되는 걸 보장하지 못하기 때문에 / 하지만 Windows 시스템 DLL 파일들은 자신만의 고유한 ImageBase가 있기 때문에 DLL Relocation이 발생하지 않는다.)

2. IMAGE_IMPORT_DESCRIPTOR

해당 구조체는 PE 파일이 어떤 라이브러리를 임포트(Import)하고 있는지를 명시한 것이다.

 

+)Import : 라이브러리에게 서비스(함수)를 제공받는 일

+)Export : 라이브러리 입장에서 다른 PE 파일에게 서비스(함수)를 제공하는 일

typedef struct _IMAGE_IMPORT_DESCRIPTOR{
	union {
    	DWORD Characteristics;
        DWORD OriginalFirstThunk;
        };
	DWORD TimeDataStam;
    DWORD ForwarderChain;
    DWORD Name;
    DWORD FirstThunk;
    
} IMAGE_IMPORT_DESCRIPTOR;

typdef struct _IMAGE_IMPORT_BY_NAME{
	WORD Hint;
    BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

일반적으로 프로그램에서 여러 개의 라이브러리를 임포트하기 때문에 로딩되는 라이브러리 개수만큼 위 구조체의 배열 형식으로 존재한다. 그리고 구조체 배열의 마지막은 NULL 구조체로 끝난다. (PE 헤더에서 table이라고 하면 배열을 의미하는 것이다.)

 

IMAGE_IMPORT_DESCRIPTOR 구조체의 중요 멤버는 다음과 같다.

항목 의미
OriginalFirstThunk INT(Import Name Table)의 주소(RVA)
Name Library 이름 문자열의 주소(RVA)
FirstThunk IAT(Import Address Table)의 주소(RVA)

INT의 원소 값은 IMAGE_IMPORT_DESCRIPTOR 구조체의 포인터를 의미한다. 그리고 INT과 IAT의 크기는 같아야 한다.

IAT 입력 순서
1. llD의 Name 멤버를 읽어서 라이브러리의 이름 문자열을 얻는다.
2. 해당 라이브러리를 로딩한다.
3. IID의 OrigianlFirstThunk 멤버를 읽어서 INT 주소를 얻는다.
4. IMAGE_IMPORT_BY_NAME의 Hint(ordinal) 또는 Name 항목을 이용해 해당 함수의 시작주소를 얻는다.
5. IID의 FirstThunk(IAT) 멤버를 읽어 IAT의 주소를 얻는다.
6. 해당 IAT 배열 값에 앞에서 구한 함수 주소를 입력한다.
7. INT가 끝날 때까지(NULL을 만날 때까지) 위 과정을 반복한다.(4~7과정)

IMAGE_IMPORT_DIRECTORY 구조체 배열은 PE 바디에 위치한다. 해당 구조체의 주소는 PE 헤더의 IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress 값이다. 그리고 IMAGE_IMPORT_DIRECTORY 구조체 배열을 다른 용어로 IMPORT Directory Table이라고 한다.

 

IMAGE_IMPORT_DESCRIPTOR 구조체를 각 멤버별로 살펴보자.

1.라이브러리 이름(Name)

  : 임포트 함수가 소속된 라이브러리 파일의 이름 문자열 포인터이다.

2.OrigianlFirstThunk - INT(Import Name Table)

  : 임포트하는 함수의 정보(Ordinal,Name)가 담긴 구조체 포인터 배열이다. 원소 각각이 IMAGE_IMPORT_MY_NAME 구조체를 가리키고 있다.

3. IMAGE_IMPORT_MY_NAME

  : [라이브러리에서 함수의 고유 번호(Ordinal - 2byte) + 함수의 이름] 으로 구성된다.

4.FirstThunk - IAT(Import Address Table)

  : INT와 마찬가지로 구조체 포인터 배열 형태로 되어 있다. 파일에서 해당 배열의 원소들은 하드코딩 되어 있고 메모리에 로딩될 때 정확한 주소 값으로 대체된다.

 

13.6 EAT

EAT란 Export Address Table의 약자로 라이브러리 파일에서 제공하는 함수를 다른 프로그램에서 가져다 사용할 수 있도록 해주는 핵심 메커니즘이다. 즉 EAT를 통해서만 해당 라이브러리에 익스포트하는 함수의 시작 주소를 정확히 구할 수 있다. 라이브러리를 설명하는 EAT는 IAT와는 다르게 PE 파일 내에 구조체 배열이 아닌 하나의 구조체 (IMAGE_EXPORT_DIRECTORY)로 존재한다.

 

구조체의 시작 주소는 PE헤더의 IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress에서 구할 수 있다.

1. IMAGE_EXPORT_DIRECTORY

typedef struct _IMAGE_EXPORT_DIRECTORY{
	DWORD Characteristics;
    DWORD TimeDataStamp;
    WORD MajorVersion;
    WORD MinorVersion;
    DOWRD Name;
    DWORD Base;
    DWORD NumberOfFuncations;
    DWORD NumberOfNames;
    DWORD AddressOfFunctions;
    DWORD AddressOfNames;
    DWORD AddressOfNmaeOrdinals;
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY

 

위 구조체의 주요 멤버들은 아래와 같다.

항목 의미
NumberOfFunctions 실제 Export 함수 개수
NumberOfNames Export 함수 중에서 이름을 가지는 함수 개수
AddressOfFunctions Export 함수 주소 배열 (배열 원소 개수 = NumberOfFunctions)
AddressOfNames 함수 이름 주소 배열 (배열 원소 개수 = NumberOfNames)
AddressOfNameOrdinals Ordinal 배열 (배열 원소 개수 = NumberOfNames)

라이브러리에서 함수 주소를 얻는 API는 GetProcAddress()이다. 이 API가 EAT를 참조해서 원하는 API의 주소를 구하는 것이다. 따라서 GetProcAddress() API의 동작원리를 이해하면 EAT를 이해할 수 있다.

 

GetProcAddress() 동작 원리
1. AddressOfNames 멤버를 이용해 '함수 이름 배열'로 간다.
2. '함수 이름 배열'은 문자열 주소가 저장되어 있다. 문자열 비교를 통해 원하는 함수 이름을 찾는다.
3. AddressOfNamesOrdinals 멤버를 이용해 'ordinal 배열'로 간다.
4. 'ordinal 배열'에서 name_index(문자열 비교로 찾은 배열의 인덱스)로 해당 ordinal 값을 찾는다.
 ==> AddressOfNameOrdinals[index] = ordinal
5. AddressOfFunctions 멤버를 이용해 '함수 주소 배열(EAT)'로 간다.
6. '함수 주소 배열(EAT)'에서 앞서 구한 ordinal을 배열 인덱스로 하여 원하는 함수의 시작 주소(RVA)를 얻는다.
==> AddressOfFunctions[ordinal] = RVA

[Ordinal - IMAGE_EXPORT_DIRECTORY.Base]를 함수 주소 배열의 인덱스로 하여 함수 이름 없이 Ordinal로만 익스포트된 함수의 주소를 찾을 수도 있다.

13.7 Advanced PE

1. PE View : PE Viewer이다. PE 헤더를 각 구조체별로 보기 쉽게 표현해주고 RVA와 File Offset 변환을 수행해준다.

2. Patched PE : PE 스펙은 권장 스펙이기 때문에 내부에 사용되지 않는 멤버가 많다. 그리고 말 그대로 스펙만 맞추면 PE 파일이 되기 때문에 상식을 벗어난 PE 파일도 존재하고 그런 파일을 Patched PE라고 한다. 

 

출처 : 리버싱 핵심원리 (이승원, 인사이트)