0%

Windows API Hooking - IAT hooking

일명 나뭇잎 책이라고 불리는 리버싱 핵심 원리 책을 보면서, 배운 내용을 정리하는 포스팅입니다.

해당 포스팅에서는 API 후킹중 IAT 후킹에 대한 내용 정리입니다.


IAT Hooking

이번 포스팅에서는 DLL 인젝션을 이용해서 IAT를 후킹해서 프로세스에서 호출되는 특정 API의 기능을 변경을 해보는 것을 할 것입니다.

API 후킹중에서 IAT 후킹 기법은 동작 원리와 비교적 구현이 간단한 것이 특징이라고 합니다. 단순히 후킹 하고 싶은 API를 사용자 DLL에 재정의하고 프로세스에 인젝션하면 끝입니다. 하지만, IAT에 실제 함수의 주소를 조작해서 함수 실행 흐름을 바꾸는 것이니 후킹을 원하는 API 대상 프로세스의 IAT에 존재하지 않다면 사용할 수 없습니다. 즉, 동적으로 DLL을 로딩해서 사용하는 API의 경우에는 해당 방법으로는 후킹을 할 수가 없다는 것입니다.


후킹 API 선정

어떤 작업을 할건지에 따라서 후킹 대상 API를 선정을 해야 합니다. 하지만 이것은 만만치 않은 작업입니다. 내가 조작하고 싶은 부분이 어떤 API를 통해서 작동되는지, 어떤 알고리즘을 통해서 작동되는지 파악하기 어렵기 때문입니다. 해당 부분은 개발/리버싱 경험이 많아야지 쉽게 선택할 수 있습니다.


IAT 후킹 동작 원리

IAT는 PE 파일을 공부했으면 알다시피 프로그램에서 호출되는 API들의 실제 주소가 저장이 되는 영역입니다. IAT 후킹이란 IAT에 저장된 API들의 실제 주소 값을 주소를 바꾸는 것입니다.

정상적인 프로그램 같은 경우에는 본래 호출하고자 했던 API를 호출하고, 함수 실행이 완료되면 돌아오는 것이 일반적입니다. 하지만 IAT 후킹을 당한 프로세스 같은 경우에는 본래 호출하고자 했던 API의 주소가 바뀌었기 때문에 후킹 API가 먼저 호출되고, 작업에 따라서 본래 API의 기능을 무시하고 새로운 기능을 만들 수 있고, 값을 조작할 수 있고, 여러가지 일을 할 수 있게 됩니다.

정리하자면, 타겟 프로세스에 후킹 DLL을 인젝션하고 타켓 프로세스의 IAT영역에서 API의 실제 주소 크기만큼만 변경하면 API 후킹을 할 수 있다는 말입니다.


분석

기본적으로 DLL 인젝션을 통해 API 후킹을 하기 위해서는 DLL Injector는 필요합니다. 어떤 DLL Injector를 사용해도 상관없습니다. 여기서 사용한 DLL Injector는 나뭇잎 책에서 제공하는 것을 사용했습니다.

여기서 후킹할 API는 ExitProcess 입니다. ExitProcess API 같은 경우에는 프로그램이 종료될 때 호출되는 API 인데, 대부분의 프로그램이 종료될 때 해당 함수를 호출합니다.

API 후킹을 통해서 MyExitProcess API를 만들고, 메시지 박스를 표시한 다음에 본래의 ExitProcess API를 호출하도록 실습을 할 것입니다.

소스 코드를 보면서, 실습을 진행해봅시다.


DllMain()

실행 파일에서 늘 작성하던 main() 함수처럼 DLL 파일에서도 마찬가지로 늘 작성해야하는 DllMain() 함수가 있습니다. DllMain() 함수에서 늘 시작한다고 보면 됩니다. DllMain() 함수에서는 다음과 같은 작업을 합니다.

DllMain() 함수에서는 DLL이 인젝션이 되면, original API를 어딘가에 저장을 하고 IAT 후킹을 통해서 Hooking API의 주소로 대체합니다. DLL이 이젝션 되면 original API의 주소를 가지고 와서 본래의 IAT로 백업을 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
// original API 주소 저장
g_pOrgFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "ExitProcess");

// ntdll!RtlExitUserProcess() 를 hook!hooking() 로 후킹
hook_iat("kernel32.dll", g_pOrgFunc, (PROC)MyExitProcess);
break;

case DLL_PROCESS_DETACH:
// IAT를 원래 대로 복원
hook_iat("kernel32.dll", (PROC)MyExitProcess, g_pOrgFunc);
break;
}
return TRUE;
}

코드를 보시면, DLL이 로딩될 때, kernel32.dll에서 ExitProcess API의 주소를 가지고 와서 저장을 하고, hook_iat() 함수를 호출을 통해서 ExitProcess API를 후킹하는 것을 알 수 있습니다. 그리고 DLL이 언로딩될 때, IAT를 원래 대로 복원하는 것을 볼 수 있습니다.


MyExitProcess 후킹 함수 구현

ExitProcess API를 대신 사용할 MyExitProcess 함수를 구현을 해야 합니다. 본래 ExitProcess API 대신 호출이 되는 것이기 때문에 함수 인자 같은 경우에는 동일하게 해줘야 합니다. 그래야지 MyExitProcess 함수를 호출하고 나서 정상적으로 ExitProcess API를 호출할 수 있습니다.

ExitProcess API는 원래 리턴 타입이 void 타입이지만 직접 만든 MyExitProcess 함수는 ExitProcess API를 호출 할 수 있도록 하기 위해서 BOOL 타입으로 만들었습니다.

MyExitProcess 후킹 함수는 다음과 같습니다.

1
2
3
4
5
BOOL WINAPI MyExitProcess(UINT uExitCode)
{
MessageBoxA(NULL, "IAT hooking", "hook", MB_OK);
return ((PFEXITPROCESS)g_pOrgFunc)(uExitCode);
}

후킹 로직 구현

hook_iat() 함수는 실질적인 후킹 로직이 담겨 있습니다. 해당 함수에서 실질적으로 ExitProcess API를 후킹을 합니다.

구현하고자 하는 API 후킹 기법은 IAT를 이용해서 후킹을 하는 것이기 때문에 메모리에 매핑된 IAT의 주소를 알아야 합니다. 구하는 방법은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
hMod = GetModuleHandle(NULL);			// hMod = ImageBase
pAddr = (PBYTE)hMod; // pAddr = ImageBase
pAddr += *((DWORD*)&pAddr[0x3C]); // pAddr = "PE" Signature
#ifdef _WIN32
#ifdef _WIN64
dwRVA = *((DWORD*)&pAddr[0x90]); // dwRVA = RVA of IMAGE_IMPORT_DESCRIPTOR -> 64bit
#else
dwRVA = *((DWORD*)&pAddr[0x80]); // dwRVA = RVA of IMAGE_IMPORT_DESCRIPTOR -> 32bit
#endif
#endif
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD_PTR)hMod + dwRVA);

먼저, GetModuleHandle API를 통해서 프로세스의 Image base address를 구하고, IMAGE_DOS_HEADER 구조체의 e_lfanew 멤버를 통해서 IMAGE_NT_HEADER 구조체를 구합니다. 그다음으로 IMAGE_IMPORT_DESCRIPTOR의 RVA를 구하기 위해서 해당 위치에서 32bit 기준 0x80, 64bit 기준 0x90 만큼 offset값을 계산해서 IMAGE_IMPORT_DESCRIPTOR의 RVA를 구한 다음에 Image base Address + RVA 를 더해서 IMAGE_IMPORT_DESCRIPTOR 구조체 배열의 주소를 찾습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for (; pImportDesc->Name; pImportDesc++)
{
szLibName = (LPCSTR)((DWORD_PTR)hMod + (DWORD)pImportDesc->Name);

if (!_stricmp(szLibName, szDllName))
{
pThunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)hMod + (DWORD)pImportDesc->FirstThunk);
for (; pThunk->u1.Function; pThunk++)
{
if (pThunk->u1.Function == (DWORD_PTR)pfnOrg)
{
VirtualProtect((LPVOID)&pThunk->u1.Function, 8, PAGE_EXECUTE_READWRITE, &dwOldProtect);
pThunk->u1.Function = (DWORD_PTR)pfnNew;
VirtualProtect((LPVOID)&pThunk->u1.Function, 8, dwOldProtect, &dwOldProtect);

return TRUE;
}
}
}
}

IMAGE_IMPORT_DESCRIPTOR 구조체 배열의 주소 까지 찾으면 다 한거나 마찬가지입니다. 다음으로, 내가 후킹하고 싶은 API가 들어있는 DLL 이름을 찾아내고, pImportDesc->FirstThunk를 이용해서 IAT의 RVA 값을 찾아내서 IAT의 주소를 구하고, 후킹하고 싶은 ExitProcess API의 주소를 찾아 낸다음 후킹을 하면 됩니다.

이때, VirtualProtect API를 사용하는 이유는 일반적으로 IAT는 쓰기 권한이 없기 때문에 IAT의 값을 변조를 할수가 없습니다. 그래서 해당 API를 이용하면 일시적으로 쓰기 권한이 생기기 떄문에 IAT 영역내 ExitProcess API의 주소값을 MyExitProcess API의 주소값으로 바꿀수 있게 되고, 후킹을 성공할 수 있게 됩니다.


실행


전체 소스 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include "stdio.h"
#include "tchar.h"
#include "windows.h"

typedef int (WINAPI* PFEXITPROCESS)(UINT uExitCode);

FARPROC g_pOrgFunc = NULL;

BOOL WINAPI MyExitProcess(UINT uExitCode)
{
MessageBoxA(NULL, "IAT hooking", "hook", MB_OK);
return ((PFEXITPROCESS)g_pOrgFunc)(uExitCode);
}

BOOL hook_iat(LPCSTR szDllName, PROC pfnOrg, PROC pfnNew)
{
HMODULE hMod;
LPCSTR szLibName;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
PIMAGE_THUNK_DATA pThunk;
DWORD dwOldProtect, dwRVA;
PBYTE pAddr;

hMod = GetModuleHandle(NULL); // hMod = ImageBase
pAddr = (PBYTE)hMod; // pAddr = ImageBase
pAddr += *((DWORD*)&pAddr[0x3C]); // pAddr = "PE" Signature
#ifdef _WIN32
#ifdef _WIN64
dwRVA = *((DWORD*)&pAddr[0x90]); // dwRVA = RVA of IMAGE_IMPORT_DESCRIPTOR -> 64bit
#else
dwRVA = *((DWORD*)&pAddr[0x80]); // dwRVA = RVA of IMAGE_IMPORT_DESCRIPTOR -> 32bit
#endif
#endif
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD_PTR)hMod + dwRVA);

for (; pImportDesc->Name; pImportDesc++)
{
szLibName = (LPCSTR)((DWORD_PTR)hMod + (DWORD)pImportDesc->Name);

if (!_stricmp(szLibName, szDllName))
{
pThunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)hMod + (DWORD)pImportDesc->FirstThunk);
for (; pThunk->u1.Function; pThunk++)
{
if (pThunk->u1.Function == (DWORD_PTR)pfnOrg)
{
VirtualProtect((LPVOID)&pThunk->u1.Function, 8, PAGE_EXECUTE_READWRITE, &dwOldProtect);
pThunk->u1.Function = (DWORD_PTR)pfnNew;
VirtualProtect((LPVOID)&pThunk->u1.Function, 8, dwOldProtect, &dwOldProtect);

return TRUE;
}
}
}
}
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
// original API 주소 저장
g_pOrgFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "ExitProcess");

// ntdll!RtlExitUserProcess() 를 hook!hooking() 로 후킹
hook_iat("kernel32.dll", g_pOrgFunc, (PROC)MyExitProcess);
break;

case DLL_PROCESS_DETACH:
// IAT를 원래 대로 복원
hook_iat("kernel32.dll", (PROC)MyExitProcess, g_pOrgFunc);
break;
}
return TRUE;
}