13.4 RVA to RAW
PE 파일이 메모리에 로딩되었을 때 각 섹션에서 메모리의 주소(RVA)와 파일 옵셋을 잘 매핑할 수 있어야 하는데, 이러한 매핑을 일반적으로 "RVA to RAW"라고 한다.
- RVA가 속해 있는 섹션을 찾는다.
- 간단한 비례식을 사용해서 파일 오프셋을 계산합니다.
IMAGE_SECTION_HEADER 구조체의 비례식
RAW - PointerToRawData = RVA - VirtualAddress
RAW = RVA - VirtualAddress + PointerToRawData
간단한 퀴즈를 풀어보며 RAW를 이해해보도록 하겠습니다. 그림 13.9는 notepad.exe의 File과 Memory 에서의 모습입니다.
Q1. RVA = 5000 일 때, File Offset = ?
A1. 먼저 해당 RVA 값이 속해 있는 섹션을 찾습니다.
- RVA 5000은 첫 번째 섹션(.text)에 속해 있습니다.(ImageBase 01000000을 고려)
- RAW = 5000(RVA) - 1000(Virtual Address) + 400(PointerToRawData) = 4400
Q2. RVA = 13314 일 때, File Offset = ?
A2. 해당 RVA 값이 속해 있는 섹션을 찾습니다.
- 세 번째 섹션(.rsrc)에 속해 있습니다.
- RAW = 13314(RVA) - B000(VA) + 8400(PointerToRawData) = 10714
Q3. RVA = ABA8 일 때, File Offset = ?
A3. 해당 RVA 값이 속해 있는 섹션을 찾습니다.
- 두 번째 섹션(.data)에 속해 있습니다.
- RAW = ABA8(RVA) - 9000(VA) + 7C00(PointerToRawData) = 97A8(X)
- 계산 결과로 97A8이 나왔지만 이 오프셋은 세 번째 섹션(.rsrc)에 속해 있습니다. RVA는 두 번째 섹션이고 RAW는 세 번째 섹션이라면 말이 안됩니다. 따라서 이 경우에는 "해당 RVA(ABA8)에 대한 RAW 값은 정의할 수 없다." 라고 해야합니다.
- 이러한 결과가 나온 이유는 두 번째 섹션의 VirtualSize 값이 SizeOfRawData 값보다 크기 때문입니다.
PE 파일 섹션에는 Q3)의 경우와 같이 VirtualSize와 SizeOfRawData 값이 서로 달라서 벌어지는 이상하고 재미있는 일들이 많이 있습니다.
13.5 IAT(Import Address Table)
IAT에는 Windows 운영체제의 핵심 개념인 process, memory, DLL 구조 등에 대한 내용이 함축되어 있습니다. 즉, 쉽게 말해서 프로그램이 어떤 라이브러리에서 어떤 함수를 사용하고 있는지를 기술한 테이블입니다.
IAT(Import Address Table)는 PE(Portable Executable) 파일 형식에서 외부 DLL 함수에 대한 주소를 저장하는 테이블입니다. 프로그램이 실행될 때, 다른 DLL에 정의된 함수를 호출할 수 있도록 해당 함수들의 실제 메모리 주소를 저장하는 역할을 합니다.
13.5.1 IAT의 역할
- 동적 링크:
- 프로그램이 실행되면서 필요한 DLL 파일을 로드하고, 해당 DLL에 정의된 함수들의 주소를 IAT에 기록합니다.
- 프로그램은 이 IAT에 저장된 주소를 참조하여 외부 함수를 호출할 수 있습니다.
- 간접 호출:
- 프로그램 코드에서는 외부 함수의 실제 주소를 직접 사용하지 않고, IAT에 저장된 주소를 통해 간접적으로 호출합니다.
- 이렇게 하면 프로그램이 실행될 때마다 DLL이 로드된 주소가 달라도, IAT에 기록된 함수 주소만 참조하기 때문에 문제가 발생하지 않습니다.
13.5.2 DLL (Dynamic Linked Library)
16비트 DOS 시절에는 DLL 개념이 없이 그냥 'Library'만 존재하였습니다. 예를들어 C언어에서 printf() 함수를 사용할 때 컴파일러는 C 라이브러리에서 해당 함수의 binary 코드를 그대로 가져와 프로그램에 삽입 시켜버렸습니다. 즉, 실행 파일에 printf() 함수의 바이너리 코드를 가지고 있는 것입니다. Windows OS에서는 멀티 태스킹을 지원하기 때문에 이러한 라이브러리 포함 방식이 비효율적으로 바뀌었습니다.
32비트 Windows 환경을 제대로 지원하기 위해 기본적으로 많은 라이브러리 함수(process, memory, window, message 등)을 사용해야 합니다. 여러 프로그램이 동시에 실행되어야 하는 상황에서 모든 프로그램마다 위와 같이 동일한 라이브러리가 포함되어서 실행된다면 심각한 메모리 낭비를 불러옵니다. 그래서 Windows OS 설계자들은 아래와 같은 DLL 개념을 고안해냈습니다.
- 프로그램에 라이브러리를 포함시키지 말고 별도의 파일(DLL)로 구성하여 필요할 때마다 불러쓰자.
- 일단 한 번 로딩된 DLL의 코드, 리소스는 Memory Mapping 기술로 여러 process에서 공유해 쓰자.
- 라이브러리가 업데이트되었을 때 해당 DLL 파일만 교체하면 되니 쉽고 편해서 좋다.
실제 DLL 로딩 방식은 2가지가 있습니다.
- Explicit Linking : 프로그램에서 사용되는 순간에 로딩하고 사용이 끝나면 메모리에서 해제되는 방법
- Implicit Linking : 프로그램 시작할 때 같이 로딩되어 프로그램 종료할 때 메모리에서 해제되는 방법
IAT는 Implicit Linking에 대한 메커니즘을 제송하는 역할을 합니다. x32dbg를 통해 notepad.exe를 열어보겠습니다.
그림 13.10은 kernel32.dll의 CreateFileW를 호출하는 코드입니다.
◆ 덤프에서 값 표시하는 방법
덤프 창에서 오른쪽 버튼 클릭 -> 주소 클릭
CreateFileW를 호출할 때 직접 호출하지 않고 01001104 주소에 있는 값을 가져와서 호출합니다. 01001104 주소는 notepad.exe에서 '.text' 섹션의 메모리 영역입니다. 01001104 주소의 값은 765733E0의 값을 가지며 이 765733E0 주소가 바로 notepad.exe 프로세스 메모리에 로딩된 kernel32.dll의 CreateFileW 함수 주소입니다.
Q. "그런데 그냥 'CALL 765733E0'로 호출하는게 더 편하고 좋지 않나요?"
위 질문에 사용된 방식이 바로 DOS 시절의 방식입니다.
notepad.exe 제작자가 프로그램을 컴파일 하는 순간에는 이 notepad.exe 프로그램이 어떤 환경, 어떤 언어, 어떤 서비스팩에서 실행될 지 알 수 없습니다. 위에서 열거한 모든 환경에서 kernel32.dll의 버전이 달라지고, CreateFileW 함수의 위치(주소)가 달라집니다.
모든 환경에서 CreateFileW 함수 호출을 보장하기 위해서 컴파일러는 CreateFileW의 실제 주소가 저장될 위치(01001104)를 준비하고 CALL DWORD PTR DS:[1001104] 형식의 명령어를 적어두기만 합니다. 파일이 실행되는 순간 PE로더가 01001104의 위치에 CreateFileW 주소를 입력해줍니다.
위 방식을 사용하지 않는 또 다른 이유는 DLL Relocation 때문입니다. 만약 a.dll과 b.dll을 사용한다고 했을 때 PE 로더는 먼저 ImageBase 값인 메모리 10000000에 로딩합니다. 그 다음 b.dll를 10000000에 로딩하려고 봤더니 이미 그 주소에 a.dll이 할당되어 있습니다. 그래서 PE로더는 다른 비어있는 메모리 공간을 찾아서 b.dll을 로딩시켜 줍니다. 이것이 DLL Relocation이며 실제 주소를 하드코딩 할 수 없는 이유입니다. 또한 PE헤더에서 주소를 나타낼 때 VA를 쓰지 못하고 RVA를 써야하는 이유이기도 합니다.
※ DLL 파일의 단점
- DLL 지옥 (DLL Hell):
- 프로그램이 실행될 때 필요한 DLL의 버전이 맞지 않거나, 잘못된 DLL이 로드될 경우 프로그램이 비정상적으로 동작하거나 실행되지 않는 문제입니다.
- 이는 동일한 이름의 DLL이 서로 다른 버전을 가지거나, 시스템 디렉터리에 위치한 DLL이 우선적으로 로드되면서 발생할 수 있습니다.
- 의존성 문제:
- 프로그램이 여러 DLL에 의존할 경우, 모든 DLL이 정상적으로 설치되어 있어야 프로그램이 실행될 수 있습니다.
- 일부 DLL이 누락되거나 손상되면 프로그램이 실행되지 않습니다.
- 보안 문제:
- 공격자는 DLL 파일을 변조하거나, 프로그램이 참조하는 DLL을 악성 DLL로 교체하여 악성 코드를 실행할 수 있습니다.
- 이를 DLL 하이재킹(DLL Hijacking) 공격이라고 합니다.
※ DLL 하이재킹 (DLL Hijacking)
- DLL 하이재킹은 프로그램이 특정 DLL을 로드할 때, 정상적인 DLL이 아닌 공격자가 만든 악성 DLL을 로드하도록 유도하여, 악성 코드를 실행하게 만드는 공격 기법입니다.
- Windows 운영 체제에서는 DLL 로드 순서를 통해 먼저 찾은 DLL을 로드하기 때문에, 공격자는 프로그램이 참조하는 DLL과 같은 이름의 악성 DLL을 프로그램 디렉터리에 배치하여 공격할 수 있습니다.
13.5.3 IMAGE_IMPORT_DESCRIPTOR
PE파일은 자신이 어떤 라이브러리를 Import 하고 있는지 IMAGE_IMPORT_DESCRIPTOR 구조체에 명시하고 있습니다. IMAGE_IMPORT_DESCRIPTOR 구조체는 아래와 같습니다.
typedef struct _IMAGE_IMPORT_DESCRIPTIOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // INT(Import Name Table)의 주소(RVA)
};
DWORD TimeDataStamp;
DWORD ForwarderChain;
DWORD Name; // Library 이름 문자열의 주소(RVA)
DWORD FirstThunk; // IAT(Import Address Table)의 주소(RVA) => Table == 배열
} 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;
일반적인 프로그램에선 여러 개의 라이브러리를 임포트하기 때문에 라이브러리 개수만큼 위 구조체의 배열 형식으로 존재하며, 구조체 배열의 마지막은 NULL 구조체로 끝나게 됩니다.
- Name : DLL의 이름.
- Original First Thunk : IAT를 초기화하기 위해 사용되는 Hint/Name Table을 가리키는 포인터.
- First Thunk : 실제 IAT가 위치한 포인터.
- TimeDateStamp : 바인딩 정보를 나타내는 필드.
- ForwarderChain : 포워딩된 함수가 있을 경우 이를 나타냅니다.
★ IAT 입력 순서
- 로더가 Import Descriptor Table에서 Name 필드를 읽어 "kernel32.dll"과 같은 문자열을 얻고, 해당 DLL을 LoadLibrary("kernel32.dll")로 로드합니다.
- Import Descriptor의 OriginalFirstThunk 필드를 통해 INT(Import Name Table)의 시작 주소를 얻고, 배열의 값을 하나씩 읽습니다.
- INT의 각 엔트리를 통해 IMAGE_IMPORT_BY_NAME 구조체의 주소를 얻고, 이를 이용해 함수 이름을 확인합니다.
- 함수 이름("GetCurrentThreadId" 등)을 이용하여 GetProcAddress를 호출하여 해당 함수의 주소를 얻습니다.
- Import Descriptor의 FirstThunk 필드를 통해 IAT의 시작 주소를 얻고, 여기에서 얻은 함수 주소를 기록합니다.
- INT 배열의 끝에 도달할 때까지 위 과정을 반복합니다.
13.5.4 IAT에 대한 보안적 측면
IAT는 코드 인젝션이나 후킹과 같은 공격 기법에 자주 사용됩니다. 공격자는 프로그램이 외부 함수를 호출할 때 사용하는 IAT를 변조하여, 자신이 원하는 악성 코드를 실행하게 만들 수 있습니다.
IAT 후킹 (IAT Hooking)
- IAT 후킹은 공격자가 IAT에 기록된 외부 함수의 주소를 악성 함수의 주소로 바꾸는 기법입니다.
- 예를 들어, printf 함수가 호출될 때마다 원래의 printf가 아닌 공격자가 만든 악성 함수가 호출되도록 만들 수 있습니다.
IAT 후킹 과정
- 프로그램이 실행되면서 IAT가 초기화됩니다.
- 공격자는 IAT에 기록된 함수 주소를 자신이 원하는 함수 주소로 바꿉니다.
- 이후 프로그램이 해당 함수를 호출할 때, 변조된 주소로 인해 공격자의 코드가 실행됩니다.
방어 기법
- 코드 서명과 무결성 검증을 통해 IAT가 변조되지 않았는지 확인할 수 있습니다.
- 최신 보안 솔루션은 실행 중인 프로그램의 IAT를 감시하고, 비정상적인 변조가 감지되면 차단하는 방식으로 보호합니다.
13.5.5 notepad.exe를 이용한 실습
실제 IMAGE_IMPORT_DESCRIPTOR 구조체는 PE 헤더가 아닌 PE 바디에 위치합니다. 그곳을 찾아가기 위한 정보는 역시 PE헤더에 있습니다. IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress 값이 실제 IMAGE_IMPORT_DESCRIPTOR 구조체 배열의 시작 주소입니다.
그림 13.12의 구조체 배열 정보를 보기 쉽게 나타내면 아래와 같습니다.
offset | value | description |
00000158 | 00000000 | RVA of EXPORT Directory |
0000015C | 00000000 | size of EXPORT Directory |
00000160 | 00007604 | RVA of IMPORT Directory |
00000164 | 000000C8 | size of IMPORT Directory |
00000168 | 0000B000 | RVA of RESOURCE Directory |
0000016C | 00008304 | size of RESOURCE Directory |
- 이 PE 파일에는 Export Directory가 없고,
- Import Directory가 RVA 0x7604에 있으며, 크기는 200 바이트입니다.
- 또한, Resource Directory가 RVA 0xB000에 있으며, 크기는 33,028 바이트입니다.
위 정보에서 RVA가 7604이므로 File Offset은 6A04입니다. 파일에서 6A04를 보겠습니다.
File Offset | Member | RVA | RAW |
6A04 | OriginalFirstThunk(INT) | 00007990 | 00006D90 |
6A08 | TimeDateStamp | FFFFFFFF | - |
6A0C | ForwarderChain | FFFFFFFF | - |
6A10 | Name | 00007AAC | 00006EAC |
6A14 | FirstThunk(IAT) | 000012C4 | 000006C4 |
1. 라이브러리 이름(Name)
Name 항목은 임포트 함수가 소속된 라이브러리 파일의 이름 문자열 포인터 입니다.
RVA : 7AAC -> RAW : 6EAC
파일 Offset 6EAC에 "comdlg32.dll" 문자열이 보입니다.
2. OriginalFirstThunk - INT(Import Name Table)
INT는 임포튼 하는 함수의 정보(Ordinal, Name)가 담긴 구조체 포인터 배열입니다. 이 정보를 얻어야 프로세스 메모리에 로딩된 라이브러리에서 해당 함수의 시작 주소를 정확히 알 수 있습니다.
OriginalFirstThunk 멤버를 따라갑니다.(RVA : 7990 -> RAW : 6D90)
주소 배열 형태로 되어있습니다. 주소 값 하나하나가 각각의 IMAGE_IMPORT_BY_NAME 구조체를 가리키고 있습니다. 첫 번째 값인 7A7A를 따라가보면 임포는 하는 API 함수 이름 문자열이 나타날겁니다.
3. IMAGE_IMPORT_BY_NAME
RVA : 7A7A는 RAW : 6E7A 입니다.
파일 오프셋 6E7A의 최초 2바이트 값(000F)은 Ordinal로, 라이브러리에서 함수의 고유 번호입니다. 그 뒤로 "PageSetupDlgW" 함수 이름 문자열이 보입니다.
INT는 IMAGE_IMPORT_BY_NAME 구조체 포인터 배열입니다. 즉, 배열의 첫 번째 원소가 가리키는 함수의 Ordinal 값은 000F이고 함수의 이름은 "PageSetupDlgW" 입니다.
4. FirstThunk - IAT(Import Address Table)
IAT의 RVA : 12C4는 RAW : 6C4입니다.
그림 13.17에서 파일 오프셋 6C4 ~ 6EB 영역이 "comdlg32.dll" 라이브러리에 해당하는 IAT 배열 영역입니다. INT와 마찬가지로 구조체 포인터 배열 형태로 되어있으며, 배열은 NULL로 끝납니다.
IAT의 첫 번째 원소 값은 이미 '76324906'으로 하드코딩 되어있습니다. 이 값은 의미 없는 값으로 notepad.exe 파일이 메모리에 로딩될 때 이 값은 정확한 주소 값으로 대체됩니다.
디버거를 이용해서 notepad.exe의 IAT를 확인해보겠습니다.
notepad.exe의 ImageBase 값은 01000000입니다. 따라서 IAT 주소는 010012C4이며, 그 값은 정확한 API의 시작 주소 값이 들어와 있습니다.
해당주소 75E66430으로 가면 아래의 그림과 같이 comdlg32.dll의 PageSetupDlgW 함수 시작이 나타납니다.
'게임해킹 > 리버싱 핵심원리(나뭇잎책)' 카테고리의 다른 글
13장. IAT - 실습 해보기 (1) | 2025.01.22 |
---|---|
2부. 13장 PE File Format(3) (2) | 2025.01.21 |
11장 Lena's Reversing for Newbies (2) | 2024.12.16 |
10장 함수 호출 규약 (4) | 2024.12.04 |
9장 Process Explorer - 최고의 작업 관리자 (2) | 2024.11.26 |