1. DLL (Dynamic Link Library)
DLL이란 여러 프로세스에서 공유하면서 쓰는동적 연결 라이브러리이다.
? Library
: 소프트웨어 개발에서 자주 쓰고, 기초적인 함수들을 중복 개발하는 것을 피하기 위해서 표준화 된 함수 및 데이터 타입을 만들어서 모아둔 것이다. 즉, 자주 사용되는 표준적인 함수를 매번 직접 작성해서 사용하는 것은 지나치게 시간 소모적이므로 표준화 할 수 있는 함수를 미리 만들어서 모아둔 것이다.
> 라이브러리를 한 번 더 구축해 놓기만 하면 다시 만들 필요없이 불러서 사용할 수 있기 때문에 개발 속도도 빨라지고 신뢰성도 확보할 수 있다.
예전 16비트 DOS 시절에는 DLL 이라는 개념이 없고 라이브러리만 존재했을 때는 함수 하나(예를 들면 printf)를 실행하고자 한다면, 라이브러리에서 바이너리 코드를 그대로 가져와서 프로그램에 삽입시켰다. 즉 실행 파일에 printf() 함수의 바이너리 코드를 가지고 있는 방식이다.
그런데 Windows OS로 넘어오면서 여러 프로그램들이 동시에 실행되어야 하는 멀티태스킹 환경이 지원되었다. 이 상황에서 각 프로그램마다 기본 라이브러리들과 프로그램을 위한 라이브러리들을 포함시키려니 중복으로 인한 메모리 낭비와 디스크 공간 낭비가 심해졌다. 그래서 탄생한 것이 DLL이다.
DLL(Dynamic Link Library)는 동적 링크라고 하며 실행 파일에서 해당 라이브러리의 기능을 사용 시에만 라이브러리 파일을 참조하여 (또는 다운로드 받아) 기능을 불러쓰는 형식이다. 메모리에 한 번 로딩된 DLL의 코드, 리소스를 프로세스마다 공유시키므로써 메모리를 더 효율적으로 사용할 수 있게 바뀌었다. 또한 이후에 라이브러리가 업데이트 된다 하더라도 해당 DLL 파일만 교체하면 되기 때문에 쉽고 편하다.
DLL의 로딩 방식에는 2가지가 있다. 프로그램이 시작할 때 같이 로딩되어 프로그램이 종료될 때 메모리에서 해제되는 로딩 방식을 기준으로 살펴본다.
프로그램 내에서 사용되는 순간에 로딩하고 사용이 끝나면 메모리를 해제 시키는 방법(Explicit Linking)과 프로그램 시작할 때 같이 로딩되어 프로그램 종료할 때 메모리에서 해제되는 방법(Implicit Linking)이 있다.
여기서 IAT가 바로 Implicit Linking에 대한 메커니즘을 제공하는 역할을 한다.
2. IAT (Import Address Table)
프로그램이 어떤 라이브러리에서 어떤 함수(API)를 사용하고 있는지를 기술한 테이블
IAT로 어떤 API가 쓰이고, 그 API는 어떤 DLL을 사용하는지까지 알 수 있다(이 주소들도 다 RVA값)
IAT를 쓰면 해당 프로그램 안에 사용하고자 하는 함수에 대한 모든 정보가 없어도, 주소만 가지고 DLL로 넘어가 해당 주소에 있는 함수 정보를 가져다가 쓰기 때문에 용량이 클 필요도, 메모리를 크게 점유할 필요도 없다
이는 다시 말해 어떤 함수나 API가 필요할 때 직접 호출하지 않고 어떤 주소에 있는 값을 갖와서 호출하는 식으로 동작한다는 의미이다.
왜 이렇게 두 번 거치는 작동 방식이 되었는지는 위에서 설명한 DLL을 통해 알 수 있다. DLL이 도입되면서 프로그램을 컴파일(생성)하는 순간에는 그 프로그램이 어떤 운영체제, 어떤 국가의 언어, 어떤 환경에서 실행될지 알 수가 없기 때문에 변화하는 환경에 맞춰 프로그램이 실행될 수 있도록 해야 했다. 그렇기 때문에 IAT개념을 넣어서 컴파일러는 함수의 실제 주소가 저장될 위치와 명령어만 가지고 있고, 파일이 실행되는 순간 PE로더가 실행되는 환경에 맞추어 해당 주소에 프로그램 주소를 입력해 주는 식으로 동작하게 만든 것이다
실제 주소를 하드코딩하지 못하는 이유로는 위처럼 실행 환경에 따른 문제도 있지만 DLL Relocation때문이기도 하다. 일반적인 DLL 파일의 ImageBase값은 10000000이다. 그런데 a.dll도 10000000을 쓰고 b.dll도 10000000을 쓰면 문제가 된다. 그래서 PE로더는 다른 비어있는 메모리 공간을 찾아서 뒤에 온 dll파일을 로딩한다. 그러므로 DLL은 PE헤더에 명시된 ImageBase에 로딩된다고 보장할 수가 없다
이는 PE헤더에서 주소를 나타낼 때 VA를 쓰지 못하고 RVA를 써야 하는 이유이기도 한다.
2-1. 관련 구조체 1
PE 파일은 자신이 어떤 라이브러리를 Import 하고 있는지 IMAGE_IMPORT_DESCRIPTOR 구조체에 명시하고 있습니다.
* Import : library 한테서 서비스(함수)를 제공 받는 일
* Export : library 입장에서 다른 PE 파일에게 서비스(함수)를 제공 하는 일
IMAGE_IMPORT_DESCRIPTOR 구조체는 아래와 같다.
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // INT(Import Name Table) address (RVA)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // library name string address (RVA)
DWORD FirstThunk; // IAT(Import Address Table) address (RVA)
} 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;
PE 파일이 어떤 라이브러리를 import 하고 있는지 이 구조체에 명시한다.
라이브러리 개수만큼 구조체의 배열 형식으로 존재하며, 구조체 배열의 마지막은 NULL 구조체로 끝나게 됩니다.
- OriginalFirstThunk: INT(Import Name Table)의 주소(RVA)
- Name: Library 이름 문자열 주소(RVA)
- FirstThunk : IAT(Import Address Table) 의 주소(RVA
NT 헤더의 Data Directory에서 Import Directory(즉, DataDiretory[1]) 구조체를 찾아간다.
* Data Directory는 IMAGE_DATA_DIRECTORY 구조체 배열로, 배열의 각 항목마다 정의된 값을 가진다.
Data Directory[0] = EXPORT Directory
Data Directory[1] = IMPORT Directory
...
Data Directory[0] = EXPORT Directory
[Data Directory_IMAGE_DATA_DIRECTORY]
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, * PIMAGE_DATA_DIRECTORY;
: PE Loader가 Import함수 주소를 IAT에 입력하는 순서
1. IID의 Name 멤버를 읽어서 라이브러리의 이름 문자열("kernel32.dll")을 얻는다
2. 해당 라이브러리(kernel32.dll)를 로딩한다.
3. IID의 OriginalFirstThunk 멤버를 읽어서 INT 주소를 얻는다.
4. INT에서 배열의 값을 하나씩 읽어 해당 IMAGE_IMPORT_BY_NAME주소(RVA)를 얻는다.
5. IMAGE_IMPORT_BY_NAME 의 Hint(ordinal) 또는 Name 항목을 이용하여 해당 함수("GetCurrentThreadId")의 시작 주소를 얻는다.
6.IID의 FirstThunk(IAT) 멤버를 읽어서 IAT의 주소를 얻는다.
7. 해당 IAT 배열 값에 위에서 구한 함수 주소를 입력한다.
8. INT가 끝날 때까지 (NULL을 만날때 까지) 4~7의 과정을 반복한다.

notepad.exe의 kernel32.dll에 대한 IMAGE_IMPORT_DESCRIPTOR 구조이다.
2-2. 실습


CFF Explorer를 사용한 이유..
PEView로 보니 화면이 IMPORT Directory를 알려주는 것이 아니라 Hex 값만 나와서...CFF Explorer라는 툴을 이용하게 되었다.

위 사진을 보면 RVA가 000C3F28이다. RVA to RAW 공식으로 file offset을 구하면 다음과 같다.

> RAW(FileOffset) = RVA - VitualAddress + PointerToRawData
000C 3F28 - 0000 1000(3F28은 .text영역) + 0000 0400 = 000C 3328
> Import Directory 시작 주소


File Offset | Member | RVA |
C 3328 | OriginalFirstThunk(INT) | 0000 0000 |
TimeDataStamp | 0000 0000 | |
ForWarderChain | 0000 00C9 | |
Name | 8000 0000 | |
FirstThunk(IAT) | 0600 0000 |

(2) IMAGE_IMPORT_BY_NAME
typedef struct _IMAGE_NAME_BY_NAME {
WORK Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_NAME;
(3) IMAGE_THUNK_DAT32
tydef struct _IMAGE_THUNK_DAT32 {
union{
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData;
} u1;
} IMAGE_THUNK_DAT32;
typedef IMAGE_THUNK_DAT32 * PIMAGE_THUNK_DAT32;
INT 와 IAT를 구성하고 있는 구조체이다. OriginalFirstThunk와 FirstThunk는 IMAGE_THUNK_DAT32 구조체 배열을 갖고 있다.
3. EAT
라이브러리 파일에서 제공하는 함수를 다른 프로그램에서 가져다 사용할 수 있도록 해주는 핵심 매커니즘이다.
라이브러리가 가진 함수를 다른 프로그램에서 사용할 수 있도록 한다.

3-1. 관련 구조체 1
: IMAGE_EXPORT_DIRECTORY
EAT는 IAT와 마찬가지로 PE 파일 내 특정 구조체 (IMAGE_EXPORT_DIRECTORY)에 정보를 저장한다. 이 구조체는 PE 파일에 하나만 존재한다.
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp; // creation time date stamp
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // address of library file name
DWORD Base; // ordinal base
DWORD NumberOfFunctions; // number of functions
DWORD NumberOfNames; // number of names
DWORD AddressOfFunctions; // address of function start addressarray
DWORD AddressOfNames; // address of functino name string array
DWORD AddressOfNameOrdinals; // address of ordinal array
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
중요 멤버
NumberofFunctions: 실제 Export함수 개수
NumberOfNames: export 함수 중에서 이름을 가지는 함수 개수 (<=NumberofFunctions)
AddressofFunctions: export함수 주소 배열 (배열의 원소 개수 = NumberOfFunctions)
AddressOfNames: 함수 이름 주소 배열 (배열의 원소 개수 = NumberOfNames)
AddressOfNamesOrdinals: Ordinal 배열 ( 배열의 원소 개수 = NumberOfNames)
EAT를 참조해서 원하는 API주소 구하기
(라이브러리에서 함수 주소를 얻는 API는 GetProcAddressl) 이다.)
1. AddressOfNames 멤버를 이용해 '함수 이름 배열' 로 간다.
2. '함수 이름 배열'은 문자열 주소가 저장되어 있다. 문자열비교(strcmp)를 통하여 원하는 함수 이름을 찾는다.(이때 배열의 inder를 name_ index로 하자.)
3.AddressofNameOrdinals 멤버를 이용해 'ordinal배열'로 간다.
4. 'ordinal배열'에서 name_ Index로 해당 ordinal값을 찾는다.
5. AddressofFunctions 멤버를 이용해 '함수 주소 배열(EAT)'로 간다.
6. 함수 주소 배열(EAT)'에서 아까 구한 ordinal을 배열 인덱스로 하여 원하는 함수의 시작 주소를 얻는다.
+ordinal은 export function 의 고유번호이다.
3-2. 실습
IMPORT Directory와 마찬자지로 NT 헤더의 Data Directory에서 EXPORT Directory (즉, DataDirectory[0]) 구조체를 찾아간다. (=IMAGE_EXPORT_DIRECTORY 구조체 배열의 시작 주소)


RVA는 0000 0190이다.
RVA to Raw 공식
> RAW(FileOffset) = RVA - VitualAddress + PointerToRawData

0000 0190 - 0008 2000(190은 .rdata 영역) + 0008 0A00 = Export Directory 시작 주소
근데 계산하면 음수가 나온다..

Export Directory도 없다. 아까 Value도 0인 것을 봐서 없는건가..?뭐지.......

NotepadXamlUI.dll 파일로 다시 분석해보았다.


RVA는 001BF1F0이다.
RAW(FileOffset)을 다시 구해보면,

001B F1F0 - 000C 7000 + 000C 6000 = 001B E1F0

계산한 결과와 같은 것을 확인할 수 있다!!!


HxD에서도 찾아봤다..

함수 이름 배열
AddressOfNames에는 RVA 001BF220가 있다.
이를 RAW로 바꾸면,
001B F220 - 000C 7000 + 000C 6000 = 001B E220

RAW로 갔더니 많은 문자열 주소(RVA)로 이루어져 있다.
첫 번째 원소 001B F23E를 RAW로 변환하여 찾아가 보겠다.
001B F23E - 000C 7000 + 000C 6000 = 001B E23E

이것이 함수 이름 목록이라고 한다.
'Reversing' 카테고리의 다른 글
[Reversing] Replace_WirteUp (0) | 2023.05.13 |
---|---|
[Reversing] Music Player_WirteUp (0) | 2023.05.13 |
[Reversing] ImagePrc_WirteUp (0) | 2023.04.29 |
[Reversing] Easy ELF_WirteUp (0) | 2023.04.29 |
[Reversing] Easy Keygen_WirteUp (0) | 2023.04.29 |