본문 바로가기

리버싱 핵심원리

[리버싱 핵심원리] 21장 - Windows 메시지 후킹

21.1 훅(Hook)

원하는 것을 낚아채고 싶을 때 사용하는 도구인 갈고리를 말한다. 이 뜻이 확장되어 정보를 엿보거나 가로채는 경우에도 '훅'이라는 말을 쓴다. 중간에서 오고가는 정보를 엿보거나 가로채기 위해 초소를 설치하는 일을 훅을 건다(설치한다)라고 하고, 실제로 정보를 엿보고 조작하는 행위를 후킹(hooking)한다 라고 말한다.

컴퓨터 분야에서도 광범위하게 사용된다. 'OS - 애플리케이션 - 사용자' 사이에 오고가는 정보를 전부 엿보고 조작할 수 있다.(사용자 몰래도 가능하다...) 여러 방식 중 기본적인 메시지 훅에 대해 알아보자.

 

21.2 메시지 훅

Windows 운영체제는 GUI를 제공하고, 이는 Event Driven 방식으로 동작한다. 이번트가 발생할 때 OS는 미리 정의된 메시지를 해당 응용 프로그램으로 통보한다. 응용 프로그램은 해당 메시지를 분석해 필요한 작업을 진행하는 것이다. 즉, 키보드를 입력할 때에도 OS로부터 응용 프로그램으로 메시지가 이동한다. 메시지 훅이란 이런 메시지를 중간에서 엿보는 것이다. 

[일반적인 경우의 Windows 메시지 흐름]
1. 키보드 입력 이벤트가 발생하면 WM_KEYDOWN 메시지가 [OS message queue]에 추가됨
2. OS는 어느 응용 프로그램에서 이벤트가 발생했는지 파악해 queue에서 메시지를 꺼낸 후, 해당 응용 프로그램의 [application message queue]에 추가한다.
3. 응용 프로그램은 자신의 queue를 모니터링하고 있다가 WM_KEYDOWN 메시지가 추가된 것을 확인해고 해당 event handler를 호출한다.

만약 키보드 메시지 훅이 설치되었다면 해당 과정에서 OS의 message queue와 Application의 message queue 사이에 설치된 Hook chain의 키보드 메시지 훅들이 응용프로그램보다 먼저 해당 메시지를 볼 수 있다. 게다가 메시지 자체의 변경도 가능하며 메시지를 가로채서 Application message queue로 내려보내지 않게 할 수도 있다. 이러한 기능은 Windows 운영체제에서 제공하는 기본 기능이며, 대표적인 프로그램으로 MS Visual Studio에서 제공되는 SPY++가 있다.

 

+) HookChain : 같은 키보드 메시지 훅 여러 개가 동시에 설치될 수 있고 이때 설치 순서대로 호출되기 때문에 훅 체인이라고 말한다.

 

21.3 SetWindowsHookEx()

메시지 훅은 아래 API를 사용해서 간단히 구현할 수 있다.

HHOOK SetWindowsHookExA(
  int       idHook, //hook type
  HOOKPROC  lpfn,  // hook procedure
  HINSTANCE hmod,  // 위 hook procedure가 속해 있는 DLL 핸들
  DWORD     dwThreadId   // hook을 걸고 싶은 thread의 ID
); 
//출처 : https://learn.microsoft.com/ko-kr/windows/win32/api/winuser/nf-winuser-setwindowshookexa?redirectedfrom=MSDN

hook procedure는 운영체제가 호출해주는 콜백 함수이다. 메시지 훅을 걸 때 hook procedure은 DLL 내부에 존재해야 하며, 그 DLL의 인스턴스 핸들이 바로 hMod이다.

 

위 함수를 이용해 훅을 설치하면, 어떤 프로세스에서 메시지가 발생했을 때 운영체제가 해당 DLL 파일을 해당 프로세스에 강제로 인젝션하고 등록된 hook procedure를 호출한다.

 

+) 콜백 함수 : 특정 이벤트가 발생하면 호출되는 함수

+) 인스턴스 핸들 : 모든 실행 파일과 DLL 파일은 프로세스의 메모리 공간 상에 로드될 때 고유의 '인스턴스 핸들' 값을 할당 받는다.(리소스 참조에 사용된다.)

+) 인젝션 : 공격자가 신뢰할 수 없는 입력을 프로그램에 주입하도록 하는 공격

 

21.4 키보드 메시지 후킹 실습

위 설명을 이해하기 위해 간단한 키보드 후킹을 해본다.

KeyHook.dll : 파일 훅 프로시저(KeyboardProc)가 존재하는 DLL 파일

HookMain.exe : KeyHook.dll을 최초로 로딩해 키보드 훅을 설치하는 프로그램

 

HookMain.exe에서 KeyHook.dll을 최초로 로딩해서 키보드 훅을 설치하고 SetWindowsHookEx() 함수를 이용해서 키보드 훅(KeyboardProc)을 설치한다. 위 그림처럼 다른 프로세스에서 Key event가 발생하면 OS에서 해당 프로세스의 메모리 공간에 KeyHook.dll을 강제로 로딩하고 KeyboardProc()함수가 호출된다.

 

OS가 프로세스에게 dll 파일을 강제로 로딩 ==> 메시지 후킹 기법은 DLL 인젝션 기법의 하나로 사용된다.

 

1. 실습 예제 HookMain.exe

HookMain.exe 실행 - 키보드 훅 설치

HookMain.exe를 실행시키면 다음과 같은 화면이 나오면서 후킹이 시작된다.

 

Notepad.exe 실행

notepad.exe를 실행해서 키보드 입력을 실행해보면, 입력이 안되는 것을 볼 수 있다. Process Explorer를 통해 notepad.exe 프로세스를 확인해보면, KeyHook.dll 파일이 로딩되어 있는 것을 볼 수 있다.

[Find] - [Handle or DLL substring] 로 들어가 keyhook.dll을 검색하면 해당 dll 파일이 인젝션된 프로세스들을 볼 수 있다. 키보드 입력이 있는 모든 프로세스들에 인졕션이 되었지만, notepad를 제외하곤 키보드 입력이 정상적으로 작동하는 것을 볼 수 있다. (keyhook.dll 검색을 위해서 키보드 입력을 했을 때 notepad처럼 사라지지 않고 잘 입력되었다.)

 

HookMain.exe 종료 - 키보드 훅 제거

실행시켰던 HookMain.exe에서 'q'를 입력해 종료시킨다. (책과 다르게 q를 입력하자마자 창이 사라졌다...왤까...)

키보드 훅을 제거 했기 때문에 notepad.exe에 키보드 입력이 정상적으로 작동하는 것을 볼 수 있다. keyhook.dll 파일이 인젝션된 프로세스들을 찾아보면 전과 다르게 하나도 없는 것을 볼 수 있다.

키보드 훅이 제거되면서 관련 프로세스에서 KeyHook.dll 파일이 모두 언로딩되었다.

 

2. 소스코드 분석

HookMain.cpp - HookMain.exe의 소스코드

#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);
}

 매우 간단한 소스코드이다. (C++을 모르는 나는.... 조금 당황스럽긴 했지만.. 간단..은 하다..)

KeyHook.dll 파일을 로딩해서 HookStart() 함수를 호출하면 후킹이 시작되고, HookStop() 함수를 호출하면 후킹이 종료된다.

KeyHook.cpp - KeyHook.dll 파일의 소스코드

#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

익스포트 함수인 HookStart() 함수가 호출되면 SetWindowsHookEx()에 의해서 키보드 훅 체인에 KeyboardProc()가 추가된다. 키보드 훅이 실행되는 상황에서 프로세스에서 키 입력 이벤트가 발생하면 KeyHook.dll이 인젝션되고, 인젝션 당한 프로세스에서 키보드 이벤트가 발생하면 KeyHook.KeyboardProc()이 먼저 호출된다.

 

KeyboardProc() 함수를 살펴보면, stricmp 함수를 통해 DEF_PROCESS_NAME(=notepad.exe)과 현재 프로세스 이름을 비교해 같다면 1을 리턴하고 종료한다. 이것은 메시지를 가로채서 없애버리는 것을 의미하고 결국 키보드 메시지는 notepad.exe의 message queue에 전달되지 않고 사라져 화면에 보이지 않게 된다. (이름이 같지 않다면 CallNextHookEx 명령이 실행되며 메시지는 응용 프로그램으로 잘 전달되거나 훅 체인의 또 다른 훅 함수로 전달된다.)

 

21.5 디버깅 실습

1. HookMain.exe 디버깅

OllyDbg로 HookMain.exe를 열어 디버깅을 진행한다. 위 사진은 HookMain.exe의 EP 코드이다. 우리가 찾아야 하는 것은 키보드 후킹을 시작하는 부분이다. 

 

핵심 코드 찾기

1. 한 줄씩 트레이싱 // 최후의 방법....
2. 관련 API 검색
3. 관련 문자열 검색 ==> 이 방식을 사용한다.

 

앞에서 메시지 훅 실행 실습을 통해 이 프로그램의 기능과 출력 문자열을 이미 알고 있다. 출력 문자열을 이용해 핵심 코드를 찾아보면, [search for] - [All referencd text strings]에 들어가 출력 문자열("press 'q' to quit!")을 찾는다.

우리가 찾는 문자열이 40104D 주소의 명령어에서 참조된다는 것을 알 수 있다. 더블클릭을 통해 들어가면 HookMain.exe 프로그램의 main() 함수를 찾을 수 있다. (앞의 소스코드와의 비교를 통해 알 수 있다.)

 

main() 함수 디버깅

main() 함수의 처음 부분에 BP를 걸고 실행을 시켜 디버깅을 시작한다. 코드를 차례대로 내려오면 흐름을 알 수 있다. 401006 주소에서 LoadLibrary를 통해 KeyHook.dll을 호출하고 40104B 주소의 명령어를 통해 KeyHook.HookStart() 함수가 호출되는 것을 알 수 있다. 해당 명령어를 따라 들어가 보면 아래와 같다.

파란 네모 안에 있는 값들은 SetWindowsHookEx() 함수의 파라미터들이다. 순서대로 dwThreadId, hMod, lpfn, idHook이다.(스택에 입력되기 때문에 역순이라는 것을 명심하자.) idHook의 값은 2로 WH_KEYBOARD이다. 그리고 두 번째 파라미터(lpfn)의 값은 10001020이며, 이 값이 훅 프로시저의 주소이다.

 

2. Notepad.exe 프로세스 내의 KeyHook.dll 디버깅

notepad.exe를 ollydbg에서 열고 실행[F9]시켜 정상적으로 동작하도록 한다.

 

그리고 ollydbg의 옵션 [Break on new module(DLL)]을 check 상태로 변경한다. 해당 옵션은 디버기 프로세스(notepad.exe)에 새로운 DLL이 로딩될 때 자동으로 디버깅을 멈추는 옵션이다. (DLL이 인젝션되는 순간부터 디버깅할 때 유리함)

이 상태에서 HookMain.exe를 실행하고 notepad.exe에서 키보드 입력을 하면 OllyDbg가 멈추면서 아래와 같은 Executable modules 창이 뜬다.

10000000 주소에 KeyHook.dll이 로딩된 것을 확인할 수 있다. 이것을 더블클릭하면 KeyHook.dll의 EP로 간다. 앞의 main() 함수 디버깅 과정에서 파라미터로 넘긴 주소를 통해 훅 프로시저의 주소(10001020)를 알고 있기 때문에 바로 이동한다.

위와 같이 훅 프로시저에 BP를 설치하면 notepad.exe에 키보드 입력 이벤트가 발생할 때마다 이곳에서 멈춰진다. 스택에 표시된 곳의 값은 KeyboardProc() 함수의 파라미터이다. 훅 프로시저의 내용은 소스코드에서 설명한 내용과 같다.

 

출처 : 리버싱 핵심원리(이승원, 인사이트)