13.5.5 notepad.exe를 이용한 실습
목표 : 특정 DLL의 Import Table에서 함수를 찾고, 디버거로 실제 함수 위치 찾아보기
1. IMAGE_IMPORT_DESCRIPTOR 구조체 배열 찾기
실제 IMAGE_IMPORT_DESCRIPTOR 구조체는 PE 헤더가 아닌 PE 바디에 위치합니다. 그곳을 찾아가기 위한 정보는 역시 PE헤더에 있습니다. IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress 값이 실제 IMAGE_IMPORT_DESCRIPTOR 구조체 배열의 시작 주소입니다.
[그림 1]의 구조체 배열 정보를 보기 쉽게 나타내면 아래와 같습니다.
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입니다. File Offset을 구하기 위해선 IMAGE_SECTION_HEADER를 통해 각각 섹션의 정보를 알아야 합니다.
STEP 1 : DOS Header 분석
PE 파일은 IMAGE_DOS_HEADER로 시작됩니다. 중요한 정보는 다음과 같습니다:
- 0x3C 위치: e_lfanew 값으로, PE Header의 시작 오프셋을 나타냅니다.
- HEX 데이터: 0x3C = E0 00 00 00 → e_lfanew = 0xE0
STEP 2 : PE Header 확인
e_lfanew에서부터 PE Header가 시작됩니다.
- PE Header의 Signature는 PE\0\0입니다.
- HEX 데이터: 0xE0 = 50 45 00 00 (PE\0\0)
PE Header는 다음 섹션을 포함합니다:
- File Header (IMAGE_FILE_HEADER): PE Header에서 바로 뒤에 위치합니다.
- Machine (2바이트): 4C 01 (Intel x86)
- NumberOfSections (2바이트): 03 00 (섹션의 개수 = 3)
- SizeOfOptionalHeader (2바이트): E0 00 (Optional Header의 크기 = 0xE0)
- Optional Header (IMAGE_OPTIONAL_HEADER):
- 이 Header는 PE Header에서 중요한 데이터(RVA, 섹션 정보 등)를 포함합니다.
- 크기는 0xE0로 주어졌습니다.
STEP 3 : Section Header 위치 계산
Section Header는 Optional Header 바로 뒤에 위치합니다.
Section Header 위치 = e_lfanew + 24 + SizeOfOptionalHeader
- e_lfanew = 0xE0
- 24 (PE Header 크기)
- SizeOfOptionalHeader = 0xE0
- Section Header 위치 = 0xE0 + 24 + 0xE0 = 0x1D8
STEP 4 : IMAGE_SECTION_HEADER에서 정보 추출
각 IMAGE_SECTION_HEADER는 40바이트입니다. NumberOfSections = 3이므로, 3개의 섹션을 분석해야 합니다. 데이터는 다음과 같은 필드를 포함합니다:
- Name (8바이트): 섹션 이름
- VirtualSize (4바이트): 메모리에서 섹션의 크기
- VirtualAddress (4바이트): 섹션의 시작 RVA
- SizeOfRawData (4바이트): 파일에서 섹션의 크기
- PointerToRawData (4바이트): 파일에서 섹션의 시작 위치
- 기타 정보(16바이트)
1. 첫 번째 섹션 (.text)
- Name: .text → 74 65 78 74 00 00 00
- VirtualSize: 0x00487700 → 0x00007748
- VirtualAddress (RVA): 0x00001000
- SizeOfRawData: 0x00007800
- PointerToRawData: 0x00000400
- Characteristics: 0x60000020
- IMAGE_SCN_CNT_CODE
- IMAGE_SCN_MEM_EXECUTE
- IMAGE_SCN_MEM_READ
2. 두 번째 섹션 (.data)
- Name: .data → 2E 64 61 74 61 00 00 00
- VirtualSize: 0x00001BA8
- VirtualAddress (RVA): 0x00009000
- SizeOfRawData: 0x00000800
- PointerToRawData: 0x00007C00
- Characteristics: 0xC0000040
- IMAGE_SCN_CNT_INITIALIZED_DATA
- IMAGE_SCN_MEM_READ
- IMAGE_SCN_MEM_WRITE
3. 세 번째 섹션 (.rsrc)
- Name: .rsrc → 2E 72 73 72 63 00 00 00
- VirtualSize: 0x00008304
- VirtualAddress (RVA): 0x0000B000
- SizeOfRawData: 0x00008400
- PointerToRawData: 0x00008400
- Characteristics: 0x40000040
- IMAGE_SCN_CNT_INITIALIZED_DATA
- IMAGE_SCN_MEM_READ
※ 정리된 섹션 정보
Name | Virtual Size | Virtual Address (RVA) | Size of Raw Data | Pointer to Raw Data | Characteristics |
.text | 0x7748 | 0x1000 | 0x7800 | 0x0400 | Code, Execute, Read |
.data | 0x1BA8 | 0x9000 | 0x0800 | 0x7C00 | Initialized Data, RW |
.rsrc | 0x8304 | 0xB000 | 0x8400 | 0x8400 | Initialized Data, Read |
2. IMAGE_IMPORT_DESCRIPTOR 구조 이해하기
RVA가 7604의 섹션은 .text에 속하므로, RAW값은 7604 - 1000 + 400을 계산하여 6A04가 나오게 됩니다.
파일에서 6A04를 보겠습니다.
IMAGE_IMPORT_DESCRIPTOR 구조체는 다음 필드로 구성됩니다.
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
DWORD OriginalFirstThunk; // IAT의 RVA (INT로도 불림)
DWORD TimeDateStamp; // Time/Date 스탬프 (미사용 시 0 또는 -1)
DWORD ForwarderChain; // Forwarder Chain (미사용 시 -1)
DWORD Name; // DLL 이름의 RVA
DWORD FirstThunk; // IAT의 RVA
} IMAGE_IMPORT_DESCRIPTOR;
위 구조체 필드를 표로 정리하면 아래와 같습니다.
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)
FirstThunk 또는 IAT (Import Address Table)는 실행 시에 함수의 실제 주소가 저장되는 위치를 나타냅니다.
IAT의 RVA : 12C4는 RAW : 6C4입니다.
IAT의 크기는 DLL에서 가져올 함수의 개수에 따라 결정됩니다. 각 함수는 IAT에서 IMAGE_THUNK_DATA 구조체로 표현되며, 각 엔트리는 4바이트(32비트) 또는 8바이트(64비트)를 차지합니다.
6C4의 값들은 comdlg32.dll에서 참조된 함수들을 나타내며, 크기는 다음과 같이 정의됩니다:
- 가져올 함수의 개수: 6E10 ~ 6EAB에서 정의된 9개의 함수.
IAT의 크기는 다음 식으로 계산됩니다
- 크기 = (가져올 함수의 개수 + 1) × 엔트리 크기
따라서 [그림 6]에서 파일 오프셋 6C4 ~ 6EB 영역이 "comdlg32.dll" 라이브러리에 해당하는 IAT 배열 영역입니다.
INT와 마찬가지로 구조체 포인터 배열 형태로 되어있으며, 배열은 NULL로 끝납니다.
IAT의 첫 번째 원소 값은 이미 '76324906'으로 하드코딩 되어있습니다. 이 값은 의미 없는 값으로 notepad.exe 파일이 메모리에 로딩될 때 이 값은 정확한 주소 값으로 대체됩니다.
디버거를 이용해서 notepad.exe의 IAT를 확인해보겠습니다.
notepad.exe의 ImageBase 값은 01000000입니다. 따라서 IAT 주소는 010012C4이며, 그 값은 정확한 API의 시작 주소 값이 들어와 있습니다.
OS별로 다른 값이 세팅되어 있을 수 있습니다. 디버거에 들어와있는 값이 정확한 API 값이 들어와있습니다.
해당주소 75E66430으로 가면 아래의 그림과 같이 comdlg32.dll의 PageSetupDlgW 함수 시작이 나타납니다.
'게임해킹 > 리버싱 핵심원리(나뭇잎책)' 카테고리의 다른 글
2부. 13장 PE File Format(3) (0) | 2025.01.21 |
---|---|
2부. 13장 PE File Format(2) (0) | 2025.01.16 |
11장 Lena's Reversing for Newbies (2) | 2024.12.16 |
10장 함수 호출 규약 (2) | 2024.12.04 |
9장 Process Explorer - 최고의 작업 관리자 (0) | 2024.11.26 |