. 전역 훅

전역 훅은 시스템에서 발생하는 모든 메시지를 가로챌 수 있으므로 강력하고 활용성도 높다. 하지만 보호된 Win32 환경의 특수성으로 인해 프로그래밍하기는 무척 어려운 편인데 전역 훅을 제대로 이해하려면 Win32의 메모리 구조, DLL, IPC, 스레드, 메시지 전달 체계, PE 파일의 구조에 대한 이해가 있어야 한다. 그래서 전역 훅을 제대로 이해하려면 이런 선수 과목을 먼저 공부한 후에 볼 것을 권장한다.

메시지들은 발생한 사건에 대한 정보를 wParam, lParam 인수로 보내는데 이 두 인수로 전달되는 값은 보통 정수나 핸들 등의 단순값이지만 일부 복잡한 메시지(WM_CREATE, WM_DRAWITEM)는 구조체의 포인터를 전달하기도 한다. 전역 훅 프로시저는 시스템의 모든 스레드에서 발생하는 메시지들을 감시하는데 훅 프로시저가 메시지에 대한 정보를 읽으려면 전달된 구조체 포인터로부터 멤버를 읽을 수 있어야 한다.

그러나 Win32 환경에서는 프로세스들의 주소 공간이 분리되어 있기 때문에 일반적인 함수로는 이 문제를 해결할 수 없다. 훅 프로시저가 전달받은 포인터로는 이 메시지를 받을 프로세스의 주소 공간을 액세스할 수 없는 것이다. 그래서 전역 훅 프로시저는 반드시 분리된 DLL에 있어야 한다. DLL은 공유되는 모듈이며 연결된 프로세스의 주소 공간에서 실행되므로 모든 스레드의 메시지를 자유롭게 읽을 수 있다. DLL안에 훅 프로시저를 작성해 놓으면 시스템이 훅 프로시저를 호출하기 전에 이 DLL을 메시지 목표 프로세스의 주소 공간으로 먼저 로드한다.

32비트 환경이 보호된 환경이기 때문에 전역 훅 프로시저가 DLL에 있어야 한다는 것은 어렵지 않게 이해가 될 것이다. 훅 프로시저를 DLL로 작성할 때 곤란한 문제가 하나 더 있는데 바로 공유 데이터의 문제이다. DLL은 어디까지나 메시지를 가로채기 위한 훅 프로시저를 제공할 뿐이며 가로챈 메시지를 실제로 처리하는 작업은 훅 서버가 하는 것이 일반적이다. 훅 DLL은 가로챈 메시지를 훅 서버에게 IPC나 메시지 등의 방법으로 전달해야 하며 그러기 위해서는 DLL이 훅 서버의 핸들을 항상 가지고 있어야 한다. 또한 훅 핸들도 전역으로 유지해야 훅 프로시저의 끝에서 CallNextHookEx 함수를 호출하여 메시지가 원활하게 흘러가도록 할 수 있다.

DLL은 연결된 프로세스의 주소 공간에 맵핑되며 각 DLL 인스턴스별로 고유한 데이터를 가진다. DLL은 코드는 공유하지만 데이터는 공유하지 않도록 되어 있다. 결국 훅 서버의 핸들이나 훅 핸들 등 동작에 꼭 필요한 정보를 각각의 DLL이 따로 가지게 되며 이렇게 되면 훅 DLL과 훅 서버가 지속적인 통신을 할 수 없을 것이다. 각 프로세스에 연결된 DLL마다 훅 서버에 대한 정보나 훅 핸들이 달라져 버리기 때문이다.

훅 프로시저가 어떤 프로세스의 주소 공간에서 실행되고 있든지 가로챈 메시지를 전달할 훅 서버는 항상 일정해야 한다. 그래서 훅 DLL은 각각의 프로세스에 연결되더라도 동작에 필요한 전역 변수는 DLL인스턴스끼리 공유해야 하며 그러기 위해 각 DLL 인스턴스끼리 IPC로 통신하거나 또는 공유섹션이나 파일맵핑, 레지스트리같은 약속된 장소에 전역 변수를 저장해야 한다.

다음은 설치와 해제의 문제에 대해 점검해 보자. 훅을 설치하는 응용 프로그램(훅 서버)은 훅 프로시저를 가진 DLL(훅 드라이버)을 로드한 후 GetProcAddress 함수로 훅 프로시저의 번지를 조사하고 SetWindowsHookEx함수로 훅을 설치할 수 있다. 훅을 해제할 때는 UnhookWindowsHookEx 함수를 호출하는데 이 함수에 의해 훅은 체인에서 제거되지만 각 프로세스의 주소 공간으로 로드되어 버린 DLL들은 명시적으로 제거할 기회를 가지지 못하게 된다.

또한 전역 훅을 설치할 때 SetWindowsHookEx 함수로 훅 DLL의 핸들을 전달해야 하는데 훅 서버가 훅 DLL의 핸들을 알려면 LoadLibrary 함수로 DLL을 실행중에 읽어와야 한다. 이렇게 되면 훅 서버와 훅 DLL이 묵시적으로 연결될 수 없다. 그래서 통상 전역 훅은 DLL 자체에 설치, 해제 함수를 작성하고 훅 서버는 DLL의 이 함수를 호출하여 설치와 해제 작업을 한다.

이론이 점점 복잡해지고 있는데 이쯤에서 실제 예제를 만들어 보고 분석하면서 이론을 정리해 보도록 하자. 다음 예제는 전역 키보드 훅을 설치하여 키보드가 눌러질 때마다 효과음을 낸다. 전역 훅이므로 모든 응용 프로그램으로 전달되는 키보드 메시지를 가로챌 수 있다. 먼저 훅 프로시저를 제공하는 DLL부터 작성해 보자.

 

#include <windows.h>

 

#pragma data_seg(".kbdata")

HINSTANCE hModule=NULL;

HHOOK hKeyHook=NULL;

HWND hWndBeeper=NULL;

#pragma data_seg()

#pragma comment (linker, "/SECTION:.kbdata,RWS")

 

LRESULT CALLBACK KeyHookProc(int nCode, WPARAM wParam, LPARAM lParam)

{

   if (nCode>=0) {

      SendMessage(hWndBeeper,WM_USER+1,wParam,lParam);

   }

   return CallNextHookEx(hKeyHook,nCode,wParam,lParam);

}

 

extern "C" __declspec(dllexport) void InstallHook(HWND hWnd)

{

   hWndBeeper=hWnd;

   hKeyHook=SetWindowsHookEx(WH_KEYBOARD,KeyHookProc,hModule,NULL);

}

 

extern "C" __declspec(dllexport) void UninstallHook()

{

   UnhookWindowsHookEx(hKeyHook);

}

 

BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpRes)

{

   switch (fdwReason) {

   case DLL_PROCESS_ATTACH:

      hModule=hInst;

      break;

   case DLL_PROCESS_DETACH:

      break;

   }

   return TRUE;

}

 

세 개의 전역 변수를 가지는데 이 변수들은 .kbdata라는 이름의 섹션에 선언되어 있으며 이 섹션에 공유 속성을 주었다. 섹션이란 동일한 특성을 가지는 연속된 메모리 공간인데 .data 섹션에는 초기화된 데이터들이 저장되며 .bss 섹션에는 초기화되지 않은 데이터들이 저장된다. #pragma data_seg지시자는 이후 선언된 변수가 저장될 섹션을 지정하는데 별도의 섹션을 따로 만들 수 있으며 #pragma comment 명령으로 링커에게 이 섹션의 특성을 지정할 수 있다. 공유 섹션을 만드려면 다음과 같이 한다.

 

#pragma data_seg("섹션명")

변수 선언

#pragma data_seg()

#pragma comment (linker, "/SECTION:섹션명,RWS")

 

섹션 이름은 임의의 문자열로 붙일 수 있되 대소문자는 구분하지 않으며 8문자 이하로 작성해야 한다. data_seg 지시자로 섹션 이름을 주면 이후 선언되는 변수들은 이 섹션에 저장되며 data_seg 지시자만 사용하면 디폴트 섹션으로 돌아간다. 그리고 #pargma comment로 이 섹션에 대해 읽기(R), 쓰기(W), 공유(S) 속성을 주도록 링커 옵션을 주었다. 공유 섹션에 선언되는 데이터는 반드시 초기값을 가져야 한다.

예제에서는 DLL의 인스턴스 핸들, 훅 핸들, 그리고 훅 서버의 윈도우 핸들을 공유 섹션에 선언하고 모두 NULL로 초기화했다. 이 변수들은 개별 프로세스와 연결되는 모든 DLL에 의해 공유되므로 한번 대입된 값은 어떤 프로세스의 주소 공간에서 실행되든 항상 동일한 값으로 참조할 수 있다.

DllMain에서는 DLL이 로드될 때 자신의 핸들을 저장해 두었는데 이 핸들은 전역 훅을 설치할 때 훅을 소유한 모듈이 누구인지를 지정하기 위해 사용된다. 훅 설치 함수인 InstallHook은 인수로 훅 서버의 윈도우 핸들을 전달받아 hWndBeeper 전역 변수에 저장했으며 DLL에 정의된 훅 프로시저 KeyHookProc 함수를 키보드 메시지에 대한 전역 훅으로 설치한다. 훅 서버에서 이 함수를 한번만 호출해 주면 이후 KeyHookProc 함수는 모든 응용 프로그램으로 전달되는 키보드 관련 메시지를 먼저 받을 수 있게 된다. UninstallHook 함수는 훅을 해제한다.

가장 중요한 KeyHookProc 함수는 키보드 메시지를 전달받았을 때 이 메시지를 훅 서버인 hWndBeeper에게 그대로 전달하는데 이때 WM_USER+1 사용자 정의 메시지를 사용했다. 키보드 메시지는 wParam, lParam에 모든 부가 정보를 다 실을 수 있기 때문에 이런 간단한 방법으로 훅 서버에게 정보를 전달할 수 있지만 좀 더 복잡한 메시지라면 WM_COPYDATA나 파일 맵핑 등의 더 복잡한 IPC 방법으로 통신해야 할 것이다. 훅 서버에게 메시지를 전달한 후 CallNextHookEx 함수를 호출하여 체인의 다음 훅 함수를 호출해 주어 결국은 목표 윈도우가 이 메시지를 받을 수 있도록 했다.

KeyHookProc은 키보드 메시지를 훅 서버 대신 받아주는 일만 할 뿐이며 가로챈 메시지를 실제로 사용하는 주체는 훅 서버이다. 결국 이 DLL은 메시지를 받을 프로세서의 주소 공간에 잠입해서 이 프로세서로 전달되는 메시지를 가로채 훅 서버에게 전달해 주는 일만 하는 심부름꾼에 불과하다. 훅 서버는 독립된 프로세스이기 때문에 다른 프로세스의 주소 공간을 들여다 볼 수 없으며 그래서 DLL이라는 간첩을 모든 프로세스의 주소 공간에 침투시키는 방법을 쓴다.

이 예제는 공유 데이터 처리를 위해 공유 섹션을 사용했는데 파일 맵핑으로도 데이터를 공유할 수 있다. 파일 맵핑은 프로세스간에 공유할 수 있는 메모리 영역이므로 이 영역에 전역 변수들을 저장하면 각 DLL 인스턴스끼리 동일한 값을 참조할 수 있다. 예제 소스에 파일 맵핑을 사용하는 코드도 작성되어 있으므로 참고하기 바란다.

다음은 가로챈 키보드 메시지를 처리하는 훅 서버의 코드를 보도록 하자. 훅 서버는 훅 드라이버(DLL)를 통해 시스템에 발생한 모든 메시지를 전달받는데 이 시점에서 어떤 작업이든지 할 수 있다. 이 예제의 경우 훅 DLL로부터 전달된 WM_USER+1 메시지가 작업을 할 시점이다. 자신이 설치해 놓은 훅 프로시저를 통해 다른 프로세스의 키 입력 시점을 정확하게 제공받는 것이다. 다음 훅 서버는 메시지 자체를 건드리지는 않으며 눌러진 키의 종류에 따라 적절한 효과음을 내기만 한다.

 

#include <mmsystem.h>

#include "resource.h"

#include "../KeyBeepDll/KeyBeepDll.h"

TCHAR Mes[]="시스템의 입력을 감시하며 키가 눌러질 때마다 소리를 냅니다.";

TCHAR Mes2[128];

 

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

   HDC hdc;

   PAINTSTRUCT ps;

   static count;

 

   switch(iMessage) {

   case WM_CREATE:

      InstallHook(hWnd);

      return 0;

   case WM_USER+1:

      wsprintf(Mes2,"입력된 :%d, lParam : %x ",wParam,lParam);

      InvalidateRect(hWnd,NULL,TRUE);

      if ((lParam & 0x80000000)==0) {

          if (wParam >= 'A' && wParam <= 'Z') {

             PlaySound(MAKEINTRESOURCE(IDR_CHARACTER), g_hInst, SND_RESOURCE | SND_ASYNC);

          }

          if (wParam == ' ') {

             PlaySound(MAKEINTRESOURCE(IDR_SPACE), g_hInst, SND_RESOURCE | SND_ASYNC);

          }

          if (wParam >= '0' && wParam <= '9') {

             PlaySound(MAKEINTRESOURCE(IDR_NUMBER), g_hInst, SND_RESOURCE | SND_ASYNC);

          }

          if (wParam >= VK_F1 && wParam <= VK_F24) {

             PlaySound(MAKEINTRESOURCE(IDR_FUNCTION), g_hInst, SND_RESOURCE | SND_ASYNC);

          }

          if (wParam >= VK_PRIOR && wParam <= VK_HELP) {

             PlaySound(MAKEINTRESOURCE(IDR_EDIT), g_hInst, SND_RESOURCE | SND_ASYNC);

          }

          if (wParam == VK_BACK || wParam == VK_TAB || wParam == VK_RETURN) {

             PlaySound(MAKEINTRESOURCE(IDR_BACKTAB), g_hInst, SND_RESOURCE | SND_ASYNC);

          }

          if ((wParam >= 186 && wParam <= 191) || (wParam >= 219 && wParam <= 222)) {

             PlaySound(MAKEINTRESOURCE(IDR_PUNC), g_hInst, SND_RESOURCE | SND_ASYNC);

          }

      }

      return 0;

   case WM_PAINT:

      hdc=BeginPaint(hWnd, &ps);

      TextOut(hdc,10,10,Mes,lstrlen(Mes));

      TextOut(hdc,10,30,Mes2,lstrlen(Mes2));

      EndPaint(hWnd, &ps);

      return 0;

   case WM_DESTROY:

      UninstallHook();

      PostQuitMessage(0);

      return 0;

   }

   return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

WM_CREATE에서 DLL의 훅 설치 함수를 호출하여 자신이 시작될 때 전역 훅을 설치했으며 WM_DESTROY에서 훅을 해제하도록 했다. 그래서 훅 서버가 실행중인 동안에는 전역 훅 프로시저가 지속적으로 시스템의 모든 키보드 관련 메시지를 감시하며 이때마다 훅 서버에게 메시지와 관련된 정보를 알려준다.

가로챈 메시지를 처리하는 대부분의 코드는 WM_USER+1에 있는데 화면에 눌러진 키의 정보를 문자열로 출력해 주고 키의 범위에 따라 적절한 효과음을 낸다. 효과음은 짧은 웨이브 파일을 사용자 정의 리소스로 미리 준비해 두었으며 웨이브 파일을 연주할 때는 PlaySound라는 함수를 사용한다. 직접 실행해 보면 이 프로세스가 실행중인 동안에는 메모장이나 워드 프로세서에서 키를 누를 때마다 타이프를 치는 듯한 효과음이 출력될 것이다.

포커스를 누가 가지고 있든간에 사용자가 키를 누르기만 하면 키보드 메시지가 발생하는데 이 메시지를 DLL의 훅 프로시저가 먼저 가로채서 훅 서버에게 전달해 주며 훅 서버는 입력된 키의 종류에 따라 그럴듯한 사운드를 출력하도록 되어 있기 때문이다. 훅 서버나 훅 드라이브가 가로챈 메시지를 조용히 다음 훅 체인으로 보내 주므로 다른 응용 프로그램의 동작은 방해하지 않도록 하였다. 만약 KeyHookProc에서 CallNextHookEx를 호출하지 않는다면 효과음만 나고 실제 키 입력 메시지가 목표 윈도우로 전달되지 않으므로 시스템의 키보드는 먹통이 되어 버릴 것이다.

예제를 직접 실행해 보면 키보드를 두드릴 때마다 찰칵 찰칵 소리가 나서 마치 타자기를 치는 듯한 색다른 기분이 든다. 코드를 짧게 만드느라 편의 기능은 넣지 않았는데 메인 윈도우를 조금 더 예쁘장하게 장식하고 여기에 효과음을 선택할 수 있는 기능이라든가 볼륨 조절 기능 등만 넣어도 깜찍한 악세사리로 쓸만한 프로그램이 된다.