(Win32 : WindowsProgramming-27) API Hooking

Posted by : at

Category : win32   WindowsProgramming


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를 찾아볼 것
  • 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); 
}

About Taehyung Kim

안녕하세요? 8년차 현업 C++ 개발자 김태형이라고 합니다. 😁 C/C++을 사랑하며 다양한 사람과의 협업을 즐깁니다. ☕ 꾸준한 자기개발을 미덕이라 생각하며 노력중이며, 제가 얻은 지식을 홈페이지에 정리 중입니다. 좀 더 상세한 제 이력서 혹은 Private 프로젝트 접근 권한을 원하신다면 메일주세요. 😎

Star
Useful Links