키보드 후킹 [Windows System Programming]

다루는 내용

유튜브 동영상 강의

메시지 후킹은 O/S에서 특정 메시지를 응용 큐에 전송하는 것을 가로채는 것을 말합니다.

여기에서는 메시지 후킹을 설정 및 해제하는 함수를 살펴봅니다.

여러 가지 후킹 중에 키보드 후킹을 살펴볼 것입니다.

그리고 전역 후킹하는 DLL을 만드는 것과 이를 사용하는 예를 살펴봅니다.

1. SetWindowsHookEx
2. UnhookWindowsHookEx
3. 전역 후킹 DLL 만들기
4. 전역 후킹 DLL 사용 예

1. SetWindowsHookEx

메시지 후킹 프로시저를 후킹 체인에 설정하는 메서드입니다.

가장 최근에 후킹 체인에 설정한 후킹 프로시저에게 먼저 전달합니다.

HHOOK
WINAPI
SetWindowsHookEx(
    int idHook,
    HOOKPROC lpfn,
    HINSTANCE hmod,
    DWORD dwThreadId);                       msdn 바로가기

반환 값

성공하면 후크 프로시저에 대한 핸들을 반환합니다.

실패하면 NULL을 반환합니다.

int idHook

설치할 후크 프로시저의 유형입니다.

#define WH_MIN              (-1)
#define WH_MSGFILTER        (-1)
#define WH_JOURNALRECORD    0
#define WH_JOURNALPLAYBACK  1
#define WH_KEYBOARD         2
#define WH_GETMESSAGE       3
#define WH_CALLWNDPROC      4
#define WH_CBT              5
#define WH_SYSMSGFILTER     6
#define WH_MOUSE            7
#if defined(_WIN32_WINDOWS)
#define WH_HARDWARE         8
#endif
#define WH_DEBUG            9
#define WH_SHELL           10
#define WH_FOREGROUNDIDLE  11
#if(WINVER >= 0x0400)
#define WH_CALLWNDPROCRET  12
#endif /* WINVER >= 0x0400 */

#if (_WIN32_WINNT >= 0x0400)
#define WH_KEYBOARD_LL     13
#define WH_MOUSE_LL        14
#endif // (_WIN32_WINNT >= 0x0400)

#if(WINVER >= 0x0400)
#if (_WIN32_WINNT >= 0x0400)
#define WH_MAX             14
#else
#define WH_MAX             12
#endif // (_WIN32_WINNT >= 0x0400)
#else
#define WH_MAX             11
#endif

#define WH_MINHOOK         WH_MIN
#define WH_MAXHOOK         WH_MAX

여기에서는 Low Level의 키보드 후킹을 할 것입니다. 따라서 WH_KEYBOARD_LL을 전달할 거예요.

HOOKPROC lpfn

후킹한 메시지를 처리하는 콜백 프로시저입니다.

typedef LRESULT (CALLBACK* HOOKPROC)(int code, WPARAM wParam, LPARAM lParam);

WH_KEYBOARD_LL일 때 code값은 언제나 0(HC_ACTION)입니다.

wParam은 WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP 중에 하나입니다.

#define WM_KEYDOWN                      0x0100
#define WM_KEYUP                        0x0101
#define WM_SYSKEYDOWN                   0x0104
#define WM_SYSKEYUP                     0x0105

lParam은 KBDLLHOOKSTRUCT 구조체에 대한 포인터입니다.

typedef struct tagKBDLLHOOKSTRUCT {
    DWORD   vkCode;
    DWORD   scanCode;
    DWORD   flags;
    DWORD   time;
    ULONG_PTR dwExtraInfo;
} KBDLLHOOKSTRUCT, FAR *LPKBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;    msdn 바로가기

HINSTANCE hmod

Hook 프로시저를 포함하는 모듈의 핸들입니다.

현재 프로세스에 Hook 프로시저가 있을 때 NULL로 지정합니다.

DWORD dwThreadId

Hook 프로시저를 연결할 스레드의 식별자입니다.

만약 0이면 모든 스레드와 연결합니다.

2. UnhookWindowsHookEx

Hook 프로시저를 제거하는 함수입니다.

BOOL UnhookWindowsHookEx(
  HHOOK hhk
);

반환 값

성공하면 0이 아닌 값을 반환합니다.

실패하면 0을 반환합니다.

HHOOK hhk

제거할 Hook 핸들입니다.

3. 전역 후킹 DLL 만들기

Windows 데스크톱 마법사에서 동적 연결 라이브러리 유형의 빈 프로젝트(KeyHookLib)를 생성하세요.

프로젝트에 KeyHook.h 파일과 KeyHook.cpp 파일을 추가합니다.

KeyHook.h

Hook을 설치하는 함수와 해제하는 함수를 제공합니다.

#pragma once
#include <Windows.h>

#ifdef CIEMCVUEHNCEHC83764CYEHCTEH
#define KEY_DLL	__declspec(dllexport)
#else
#define KEY_DLL	__declspec(dllimport)
#endif

#define MWM_KEY	   (WM_USER+1)


extern "C" KEY_DLL void InstallHook(HWND hWnd);
extern "C" KEY_DLL void UninstacllHook();

KeyHook.cpp

다음은 KeyHook.cpp 소스 코드 전체 내용입니다.

#define CIEMCVUEHNCEHC83764CYEHCTEH
#include "KeyHook.h"
HMODULE hInstance;
HHOOK hkey_hook;
HWND gWnd;

LRESULT EHHookProc(int code, WPARAM wParam, LPARAM lParam)
{
	SendMessage(gWnd, MWM_KEY, wParam, lParam);	
	return CallNextHookEx(hkey_hook, code, wParam, lParam);
}

extern "C" KEY_DLL void InstallHook(HWND hWnd)
{
	hkey_hook = SetWindowsHookEx(WH_KEYBOARD_LL, EHHookProc, hInstance, 0);
	gWnd = hWnd; 
}
extern "C" KEY_DLL void UninstacllHook()
{
	UnhookWindowsHookEx(hkey_hook);
}
BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpRes)
{
	
	switch (fdwReason)
	{
	case DLL_PROCESS_ATTACH:hInstance = hInst; break;
	}
	return TRUE;
}

전역 변수 선언

DLL 모듈의 HINSTANCE와 Hook 핸들과 윈도우 핸들을 전역 변수로 선언합니다.

HMODULE hInstance;
HHOOK hkey_hook;
HWND gWnd;

EHHookProc

Hook 프로시저입니다.

후킹한 메시지를 윈도우에게 사용자 메시지로 전달합니다.

원래 수신해야 할 곳에서도 키보드 메시지를 정상적으로 수신하여 처리할 수 있게 CallNextHookEx를 호출합니다.

LRESULT EHHookProc(int code, WPARAM wParam, LPARAM lParam)
{
	SendMessage(gWnd, MWM_KEY, wParam, lParam);	
	return CallNextHookEx(hkey_hook, code, wParam, lParam);
}

InstallHook 메서드

Hook 프로시저를 설치하는 메서드입니다.

여기에서는 Low Level Keyboard 메시지를 후킹할 것입니다.

그리고 전역 후킹을 할 것이므로 SetWindowsHookEx메서드의 마지막 인자는 0으로 설정합니다.

입력인자로 전달받은 윈도우 핸들을 전역 변수에 설정합니다.

extern "C" KEY_DLL void InstallHook(HWND hWnd)
{
	hkey_hook = SetWindowsHookEx(WH_KEYBOARD_LL, EHHookProc, hInstance, 0);
	gWnd = hWnd; 
}

UninstacllHook 메서드

Hook 프로시저를 해제하는 메서드입니다.

extern "C" KEY_DLL void UninstacllHook()
{
	UnhookWindowsHookEx(hkey_hook);
}

DllMain 진입점

DLL 진입점으로 선택적으로 정의할 수 있습니다.

여기에서는 DLL 핸들을 전역 변수에 설정하기 위해 정의합니다.

BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpRes)
{
	
	switch (fdwReason)
	{
	case DLL_PROCESS_ATTACH:hInstance = hInst; break;
	}
	return TRUE;
}

4. 전역 후킹 DLL 사용 예

데스크톱 애플리케이션 유형의 빈 프로젝트로 생성합니다.

리소스에 대화 상자를 추가하고 ListBox를 배치하세요.

대화 상자의 ID는 컨트롤 IDD_DIALOG_MAIN으로 정의합니다.

List Box의 ID는 IDC_LIST_KEY로 정의합니다.

키보드 메시지 후킹
키보드 메시지 후킹

Program.cpp 소스 파일을 추가합니다.

Program.cpp

다음은 Program.cpp 소스 코드의 전체 내용입니다.

#include <Windows.h>

#include "resource.h"
#include "../KeyHookLib/KeyHook.h"
#pragma comment(lib,"..\\x64\\Debug\\KeyHookLib.lib")


void OnInit(HWND hDlg)
{
	InstallHook(hDlg);
}
void CancelProc(HWND hDlg)
{
	UnInstacllHook();
	EndDialog(hDlg, 0);
}
void OnCommand(HWND hDlg,WORD cid, WORD cmsg,HWND cWnd)
{
	switch (cid)
	{
	case IDCANCEL: CancelProc(hDlg); return;
	}
}

void OnKey(HWND hDlg, WPARAM wParam, LPARAM lParam)
{	
	HWND lWnd = GetDlgItem(hDlg, IDC_LIST_KEY);
	KBDLLHOOKSTRUCT* pks = (KBDLLHOOKSTRUCT*)lParam;
	DWORD key = pks->vkCode;
	if (wParam == WM_KEYDOWN)
	{
		wchar_t buf[256];
		if ((isalnum(key)| isblank(key) | (key==VK_RETURN)) == FALSE)
		{
			return;
		}
		
		if (GetKeyState(VK_CAPITAL)==FALSE)
		{
			key = tolower(key);
			
		}
		switch (key)
		{
		case VK_TAB:wsprintf(buf, TEXT("TAP")); break;
		case VK_SPACE:wsprintf(buf, TEXT("SPACE")); break;
		case VK_RETURN:wsprintf(buf, TEXT("ENTER")); break;
		default: wsprintf(buf, TEXT("%c"), key); break;
		}
		

		SendMessage(lWnd, LB_ADDSTRING, 0, (LPARAM)buf);
		DWORD cnt =(DWORD) SendMessage(lWnd, LB_GETCOUNT, 0, 0);
		SendMessage(lWnd, LB_SETCURSEL, cnt-1, 0);
	}	
}

LRESULT CALLBACK DlgProc(HWND hDlg, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
	switch (iMessage)
	{
	case WM_INITDIALOG: OnInit(hDlg); return TRUE;
	case MWM_KEY:OnKey(hDlg, wParam, lParam); return TRUE;
	case WM_COMMAND: OnCommand(hDlg, LOWORD(wParam), HIWORD(wParam), (HWND)lParam); return TRUE;
	}
	return FALSE;
}
INT APIENTRY WinMain(HINSTANCE hIns, HINSTANCE hPrev, LPSTR cmd, INT nShow)
{
	DialogBox(hIns, MAKEINTRESOURCE(IDD_DIALOG_MAIN), 0, DlgProc);
	return 0;
}

키보드 후킹 DLL 포함 및 참조

앞에서 만든 KeyHoolLib를 사용하기 위해 헤더 포함문과 참조문을 추가합니다.

#include "../KeyHookLib/KeyHook.h"
#pragma comment(lib,"..\\x64\\Debug\\KeyHookLib.lib")

OnInit 메시지 처리기

대화 상자 초기에 수행할 메서드입니다.

Hook 프로시저를 설치합니다.

void OnInit(HWND hDlg)
{
	InstallHook(hDlg);
}

CancelProc 메서드

대화 상자의 닫기를 눌렀을 때 수행하는 메서드입니다.

Hook 프로시저를 해제하고 대화 상자를 끝냅니다.

void CancelProc(HWND hDlg)
{
	UnInstacllHook();
	EndDialog(hDlg, 0);
}

OnCommand 메시지 처리기

WM_COMMAND 메시지 처리기입니다.

여기에서는 IDCANCEL일 때 CancelProc을 호출하는 부분을 구현합니다.

void OnCommand(HWND hDlg,WORD cid, WORD cmsg,HWND cWnd)
{
	switch (cid)
	{
	case IDCANCEL: CancelProc(hDlg); return;
	}
}

OnKey 메시지 처리기

사용자 정의 메시지 MWM_KEY를 수신하였을 때 처리하는 메서드입니다.

설치한 키보드 Hook 프로시저에 위해 키보드 메시지를 후킹하면 전달받는 메시지입니다.

여기에서는 wParam이 키 누룸일 때 가상 키 코드를 List Box에 추가합시다.

여기에서는 알파벳이나 숫자, 공백(탭 포함), 엔터가 아닐 때는 ListBox에 추가하지 않고 있습니다.

그리고 CAPS Lock (VK_CAPITAL)이 눌러진 상태인지 확인하여 대소문자를 구분합니다.

void OnKey(HWND hDlg, WPARAM wParam, LPARAM lParam)
{	
	HWND lWnd = GetDlgItem(hDlg, IDC_LIST_KEY);
	KBDLLHOOKSTRUCT* pks = (KBDLLHOOKSTRUCT*)lParam;
	DWORD key = pks->vkCode;
	if (wParam == WM_KEYDOWN)
	{
		wchar_t buf[256];
		if ((isalnum(key)| isblank(key) | (key==VK_RETURN)) == FALSE)
		{
			return;
		}
		
		if (GetKeyState(VK_CAPITAL)==FALSE)
		{
			key = tolower(key);
			
		}
		switch (key)
		{
		case VK_TAB:wsprintf(buf, TEXT("TAP")); break;
		case VK_SPACE:wsprintf(buf, TEXT("SPACE")); break;
		case VK_RETURN:wsprintf(buf, TEXT("ENTER")); break;
		default: wsprintf(buf, TEXT("%c"), key); break;
		}
		

		SendMessage(lWnd, LB_ADDSTRING, 0, (LPARAM)buf);
		DWORD cnt =(DWORD) SendMessage(lWnd, LB_GETCOUNT, 0, 0);
		SendMessage(lWnd, LB_SETCURSEL, cnt-1, 0);
	}
	
}

진입점과 대화상자 Proc

진입점에서는 대화상자를 띄웁니다.

대화상자 프로시저는 초기화, WM_KEY, WM_COMMAND 메시지를 처리하는 처리기를 호출합니다.

LRESULT CALLBACK DlgProc(HWND hDlg, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
	switch (iMessage)
	{
	case WM_INITDIALOG: OnInit(hDlg); return TRUE;
	case MWM_KEY:OnKey(hDlg, wParam, lParam); return TRUE;
	case WM_COMMAND: OnCommand(hDlg, LOWORD(wParam), HIWORD(wParam), (HWND)lParam); return TRUE;
	}
	return FALSE;
}
INT APIENTRY WinMain(HINSTANCE hIns, HINSTANCE hPrev, LPSTR cmd, INT nShow)
{
	DialogBox(hIns, MAKEINTRESOURCE(IDD_DIALOG_MAIN), 0, DlgProc);
	return 0;
}