API Hooking 이란
#include <Windows.h>
#include <stdio.h>
int main()
{
MessageBoxA(0, "API Hooking", "AAA", 0);
}
위 코드 처럼 MessageBoxA
API를 부르기 직전 foo
라는 함수를 끼워 넣고싶다? -> API Hooking
API Hooking 기술
- IAT Hook (우리가 사용할 것)
- Detour
- Debugging API
- Trojan horse
IAT Hook
일단 DLL 함수의 호출 원리를 먼저 알아야 한다.
# main에서 MessageBoxA은 어떻게 부를까?
|----------------------------|
| |
| |
| |
| |
|------(user32.dll))---------|
| |
| MessageBoxA() | <- 0x7717 0F40
| |
| |
| |
| |
|-----------(.exe)-----------|
| int main() |
| MessageBoxA() | <- call 0x7717 0F40을 할까?
| |
| |
| |
Process Virtual Address Space
알고 싶은점이 MessageBoxA()
를 호출하면 무조건 0x7717 0F40
를 콜하냐는 문제이다
.exe에서 MessageBoxA()
가 0x7717 0F40
에 있을꺼라는 보장이 없다.
DLL이 호출되다 메모리 공간의 부족으로 Relocation되었을 확률이 있기 때문이다.
따라서 DLL내의 함수의 주소는 exe가 실행되고 DLL이 완전히 로드 된 후 결정된다.
- DLL에는 자신이 노출하는 함수의 Offset 정보를 가지고 있다(Export Address Table)
- PE포맷의
.edata
를 확인시 함수 Offset정보가 나오는데 .dll의 시작점에서 얼마나 떨어져 있는지에 대한 정보이다. - 참고로
.edata
가 없다면.rdata
를 찾아볼 것
- PE포맷의
- DLL을 사용하는 모듈(exe)에는 DLL의 이름과 함수 이름, 함수 주소를 담을 수 있는 테이블을 가지고 있다.
.idata
를 확인 단, 파일로 있을때는 의미없는 정보를 채워 넣음 이후에 실행될 시 실제 함수의 주소가 들어간다.- 참고로
.idata
가 없다면.rdata
를 찾아볼 것
이걸 왜 이리 길게 설명하냐면,
- 결국 exe에는 사용하는 DLL 함수의 주소를 테이블로 관리
- exe에서 DLL의 함수를 사용시 관리중인 테이블에서 주소를 꺼내어 사용
- 만약 이 꺼내는 주소를 가로챌 수 있다면??? -> IAT Hook(Import Addres Table Hook)
Example
- 테스트를 위해 몇가지 제약사항을 둔다.
- 32bit debug
- 프로젝트 -> 속성 -> 링커 -> 고급 -> 임의 기준 주소 -> 아니요
- dll, exe주소를 실행마다 바꾸는지 여부를 아니오로 설정
// 32bit debug 모드
#include <Windows.h>
#include <stdio.h>
// MessageBoxA를 후킹할 함수는 함수의 모양이 동일해야 한다.
UINT _stdcall foo(HWND hwnd, const char* s1, const char* s2, UINT btn)
{
printf("foo : %s, %s\n", s1, s2);
return 0;
}
int main()
{
MessageBoxA(0, "API Hook", "AAA", 0); // 여기 브레이크 포인트를 걸고
// 프로젝트 -> 속성 -> 링커 -> 고급 -> 임의 기준 주소 -> 아니요(dll, exe주소를 실행마다 바꾸는지 여부)
// Ctrl + Alt + d : 디스 어셈블리창
// call dword ptr [__import__MessageBoxA@16(0D4b098h)]
// Ctrl + Alt + m : 메모리창
// 메모리 창에서 확인된 주소가 MessageBoxA의 주소이다.
}
- 참고로 ASLR(Address Space Layout Randomization) 실행시 exe, dll, stack, hep의 주소를 임의로 변경하는 보안개념을 알고있자
여기까지하면 MessageBoxA
의 주소를 알았다
int main()
{
// MessageBoxA의 주소를 담은 IAT 항목을 덮어쓴다.
// 우선 MessageBoxA 주소의 보호속성을 변경해야 다른주소를 쓸 수 있다.
DWROD old;
VirtualProtect((void*)0x041B098, sizeof(void*), PAGE_READWRITE, &old);
*((int*)0x041B098) = (int)&foo; // 앞에서 구한 MessageBoxA의 주소
MessageBoxA(0, "API Hook", "AAA", 0);
}
여기까지하면 foo
가 호출은 되지만 MessageBox
는 호출이 되지 않는다
MessageBox
까지 호출해보자
typedef UINT (_stdcall *F)(HWND hwnd, const char* s1, const char* s2, UINT btn);
UINT _stdcall foo(HWND hwnd, const char* s1, const char* s2, UINT btn)
{
printf("foo : %s, %s\n", s1, s2);
// return MessageBoxA(hwnd, s1, s1, btn); // 재귀 호출
HMODULE hDll = GetModuleHandleA("user32.dll"); // Dll의 주소를 구하고
F f = (F)GetProcAddress(hDll, "MessageBoxA"); // 함수의 주소가 어딘지 구해서
return f(hwnd, s2, s1, btn); // 직접 호출해준다.
}
단, 여기까지는 실행파일이 항상 같은 메모리공간에 올라간다는 가정이 있었다.
Hooking할 주소를 모른다면?
#include <Windows.h>
#include <stdio.h>
#include <DbgHelp.h> // for ImageDirectoryEntryToData
#pragma comment( lib, "DbgHelp.lib")
typedef UINT(__stdcall* FUNC)(HWND, const char*, const char*, UINT);
UINT __stdcall foo(HWND hwnd, const char* s1, const char* s2, UINT btn)
{
printf("foo : %s, %s\n", s1, s2);
HMODULE hDll = GetModuleHandleA("user32.dll");
FUNC f = (FUNC)GetProcAddress(hDll, "MessageBoxA");
return f(hwnd, s1, s2, btn);
}
void Replace(HMODULE hExe, const char* dllname, void* api, void* func)
{
// 1. .exe에서 import(.idata) 섹션의 주소를 얻어낸다.
// DbgHelp.dll의 ImageDirectoryEntryToData함수를 이용한다
ULONG sz;
IMAGE_IMPORT_DESCRIPTOR* pImage =
(IMAGE_IMPORT_DESCRIPTOR*)::ImageDirectoryEntryToData(hExe,
TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &sz);
// hExe의 ImageDirectory를 리턴해주세요
printf("Address Import Directory : %p\n", pImage);
// 2. 해당 DLL의 정보를 가진 항목을 찾아낸다.
for (; pImage->Name; pImage++)
{
char* s = ((char*)hExe + pImage->Name);
if (_strcmpi(s, dllname) == 0) break;
}
if (pImage->Name == 0)
{
printf("can not found %s\n", dllname);
return;
}
printf("%s import table : %p\n", dllname, pImage);
// 3. 이제 함수주소를 담은 table을 조사합니다.
IMAGE_THUNK_DATA* pThunk =
(IMAGE_THUNK_DATA*)((char*)hExe + pImage->FirstThunk);
for (; pThunk->u1.Function; pThunk++)
{
if (pThunk->u1.Function == (DWORD)api)
{
DWORD* addr = &(pThunk->u1.Function);
DWORD old;
VirtualProtect(addr, sizeof(DWORD), PAGE_READWRITE, &old);
// 이렇게 덮어써도 되지만
//*addr = (DWORD)func;
// WriteProcessMemory() 를 이용해서 덮어쓰는 것을 추천
DWORD len;
WriteProcessMemory(GetCurrentProcess(), addr, &func, sizeof(DWORD), &len);
VirtualProtect(addr, sizeof(DWORD), old, &old);
break;
}
}
}
int main()
{
Replace(GetModuleHandle(0), //= API 함수를 사용하는 모듈(exe)의 주소
"user32.dll", //= 후킹할 함수가 있는 dll이름
&MessageBoxA, //= 후킹할 API 함수 주소
&foo); //= 사용자 함수
MessageBoxA(0, "aaa", "bbb", 0);
}