Windows Message Hooking

컴컴한해커·2025년 1월 8일

리버스 엔지니어링

목록 보기
9/18
post-thumbnail

📌 Message Hooking이란?

Windows 운영체제는 GUI를 제공하고, Event Driven 방식으로 동작한다. 이런 이벤트가 발생할 때, OS는 메세지를 응용 프로그램으로 보낸다. 이런 메세지를 중간에서 엿보는 것을 Message Hooking이라고 한다.

📢 일반적인 Message 흐름

  1. 키보드 입력 이벤트가 발생하면 WM_KEYDOWN 메세지가 [OS message queue]에 추가된다.
  2. OS는 어느 응용 프로그램에서 이벤트가 발생했는지 파악해서 [OS message queue]에서 메세지를 꺼내어 해당 응용 프로그램의 [application message queue]에 추가한다.
  3. 응용 프로그램은 자신의 message queue를 모니터링하고 있다가 WM_KEYDOWN 메시지가 추가된 걸 확인하고 해당 event handler를 호출한다.

📢 SetWindowsHookEx() API

여기서 hook procedure란 운영체제가 호출하는 콜백 함수이며, DLL 내부에 존재해야 한다. 이 API를 이용하여 훅을 설치하면 메세지가 발생했을 때, 운영체제가 DLL 파일을 프로세스에 강제로 인젝션하고 등록된 hook procedure를 호출한다. 이것이 훗날 DLL Injection인 것 같다.

  1. HookMain.cpp
#include "stdio.h"
#include "conio.h"
#include "windows.h"

#define	DEF_DLL_NAME		"KeyHook.dll"
#define	DEF_HOOKSTART		"HookStart"
#define	DEF_HOOKSTOP		"HookStop"

typedef void (*PFN_HOOKSTART)();
typedef void (*PFN_HOOKSTOP)();

void main()
{
	HMODULE			hDll = NULL;
	PFN_HOOKSTART	HookStart = NULL;
	PFN_HOOKSTOP	HookStop = NULL;
	char			ch = 0;

    // KeyHook.dll 로딩
	hDll = LoadLibraryA(DEF_DLL_NAME);
    if( hDll == NULL )
    {
        printf("LoadLibrary(%s) failed!!! [%d]", DEF_DLL_NAME, GetLastError());
        return;
    }

    // export 함수 주소 얻기
	HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
	HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);

    // 후킹 시작
	HookStart();

    // 사용자가 'q' 를 입력할 때까지 대기
	printf("press 'q' to quit!\n");
	while( _getch() != 'q' )	;

    // 후킹 종료
	HookStop();
	
    // KeyHook.dll 언로딩
	FreeLibrary(hDll);
}

main 함수가 중요한데, 이는 간단해 보인다.
KeyHook.dll 파일을 로딩한 후 Hookstart와 HookStop 함수의 주소를 얻어 후킹 진행

  1. KeyHook.cpp
#include "stdio.h"
#include "windows.h"

#define DEF_PROCESS_NAME		"notepad.exe"

HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
HWND g_hWnd = NULL;

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{
	switch( dwReason )
	{
        case DLL_PROCESS_ATTACH:
			g_hInstance = hinstDLL;
			break;

        case DLL_PROCESS_DETACH:
			break;	
	}

	return TRUE;
}

LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	char szPath[MAX_PATH] = {0,};
	char *p = NULL;

	if( nCode >= 0 )
	{
		// bit 31 : 0 => press, 1 => release
		if( !(lParam & 0x80000000) )
		{
			GetModuleFileNameA(NULL, szPath, MAX_PATH);
			p = strrchr(szPath, '\\');

            // 현재 프로세스 이름을 비교해서 만약 notepad.exe 라면 0 아닌 값을 리턴함
            // => 0 아닌 값을 리턴하면 메시지는 다음으로 전달되지 않음
			if( !_stricmp(p + 1, DEF_PROCESS_NAME) )
				return 1;
		}
	}

    // 일반적인 경우에는 CallNextHookEx() 를 호출하여
    //   응용프로그램 (혹은 다음 훅) 으로 메시지를 전달함
	return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

#ifdef __cplusplus
extern "C" {
#endif
	__declspec(dllexport) void HookStart()
	{
		g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
	}

	__declspec(dllexport) void HookStop()
	{
		if( g_hHook )
		{
			UnhookWindowsHookEx(g_hHook);
			g_hHook = NULL;
		}
	}
#ifdef __cplusplus
}
#endif

전처리 한 부분이 main으로 봐야 할 코드들이다. HookStart 함수가 호출되면 SetWindowHookEx()가 호출되고 이로 인해 키보드 훅 체인에 KeyboardProc()이 추가된다. KeyboardProc()은 키보드 입력이 발생했을 때 현재 프로세스 이름과 "notepad.exe" 문자열과 비교하여 만약 같다면 1을 리턴해서 KeyboardProc()를 종료시킨다. 즉, 프로그램의 메세지 큐에 전달되지 않는다.

📢 디버깅 과정 중 핵심 코드 찾기

  1. 한 줄씩 트레이싱
  2. 관련 API 검색
  3. 관련 문자열 검색

1번은 최후의 보루로 사용하는 거고 보통 API 또는 문자열을 검색한다. 일단 "press 'q' to quit!" 문자열을 우리가 알고 있으므로 검색해 본다.

그 전에 있는 HookStart 함수가 호출되는 코드로 따라가보면 다음 KeyHook.dll의 HookStart() 함수 영역으로 갈 수 있다.

이때 push 명령어가 4번 일어나는데 모두 SetWindowsHookExW() 함수의 파라미터 4개라고 볼 수 있다. 참고로 스택에 push하기 때문에 역순으로 입력하고 있다. 우리가 주의깊게 봐야 할 부분은 Hook procedure의 주소이다. 이는 SetWindowsHookExW() 함수의 두번재 파라미터이므로 1001020임을 알 수 있다. 확인해보면 아래와 같다.

0개의 댓글