출처: http://cafe.naver.com/netpee.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=879
가. 훅이란
훅킹(Hooking)이란 이미 작성되어 있는 코드의 특정 지점을 가로채서 동작 방식에 변화를 주는 일체의 기술이다. 훅이란 낚시바늘같은 갈고리 모양을 가지는데 여기서는 코드의 중간 부분을 낚아채는 도구라는 뜻으로 사용된다. 대상 코드의 소스를 수정하지 않고 원하는 동작을 하도록 해야 하므로 기술적으로 어렵기도 하고 운영체제의 통상적인 실행 흐름을 조작해야 하므로 때로는 위험하기도 하다. 훅킹을 하는 방법에는 여러 가지가 있는데 과거 도스 시절에 흔히 사용하던 인터럽터 가로채기 기법이나 바로 앞에서 알아본 서브클래싱도 훅킹 기법의 하나라고 할 수 있다.
이외에도 미리 약속된 레지스트리 위치에 훅 DLL의 이름을 적어 주거나 BHO(Browser Helper Object)나 응용 프로그램 고유의 추가 DLL(Add in)을 등록하는 간단한 방법도 있고 PE 파일의 임포트 함수 테이블을 자신의 함수로 변경하기, CreateRemoteThread 함수로 다른 프로세스의 주소 공간에 DLL을 주입(Injection)하는 방법, 메모리의 표준 함수 주소를 덮어 쓰는 꽤 어려운 방법들도 있다. 이런 고급 훅킹 기술은 이 책의 범위를 벗어나므로 여기서는 소개만 하고 다루지는 않기로 한다. 이 절에서 알아볼 메시지 훅은 윈도우로 전달되는 메시지를 가로채는 기법으로 다양한 훅킹 방법중의 하나이다.
메시지 기반의 윈도우즈에서는 운영체제와 응용 프로그램, 또는 응용 프로그램 사이나 응용 프로그램 내부의 컨트롤끼리도 많은 메시지들을 주고 받는다. 훅(Hook)이란 메시지가 목표 윈도우로 전달되기 전에 메시지를 가로채는 특수한 프로시저이다. 오고 가는 메시지를 감시하기 위한 일종의 덫(Trap)인 셈인데 일단 응용 프로그램이 훅 프로시저를 설치하면 메시지가 윈도우로 보내지기 전에 훅 프로시저를 먼저 거친다. 서브클래스 프로시저와 마찬가지로 훅 프로시저에서는 메시지를 단순히 살펴보기만 할 수도 있고 메시지를 변경하거나 아예 없애버릴 수도 있다.
훅 프로시저가 어떤 메시지를 받을 것인가는 훅의 타입과 범위에 따라 달라진다. 훅 타입은 WH_로 시작되는 매크로 상수로 지정하는데 WH_MOUSE, WH_KEYBOARD 등 여러 가지가 있다. 이름으로 유추할 수 있듯이 WH_MOUSE 훅은 마우스 메시지를 가로채고 WH_KEYBOARD 훅은 키보드 메시지를 가로챈다. 이외에도 여러 가지 훅 타입이 정의되어 있는데 잠시 후 정리해 볼 것이다.
또한 훅은 메시지를 가로챌 범위에 따라 지역 훅(Thread Specific)과 시스템 전역 훅(System Wide)으로 나누어진다. 지역 훅은 특정 스레드에서 발생하는 메시지들만 전달받으며 전역 훅은 시스템의 모든 스레드에서 발생하는 메시지를 가로챌 수 있다. 응용 프로그램 자신의 메시지만 받고 싶을 때는 지역 훅을 사용하며 시스템에서 발생하는 모든 메시지를 받고자 할 때는 전역 훅을 설치해야 한다.
훅 프로시저는 응용 프로그램이 자신의 필요에 따라 언제든지 설치할 수 있기 때문에 하나의 훅 타입에 대해 여러 개의 훅 프로시저가 존재할 수도 있다. 그래서 운영체제는 설치된 훅 프로시저들을 훅 체인(Hook Chain)으로 관리한다. 훅 체인이란 훅 프로시저 함수들의 번지를 담고 있는 일종의 함수 포인터 배열이라고 할 수 있다. 응용 프로그램이 훅 프로시저를 설치하면 운영체제는 훅 체인의 선두에 이 프로시저를 등록한다. 훅 프로시저가 감시하는 메시지가 발생하면 운영체제는 훅 체인의 선두에 등록된 훅 프로시저에게 이 메시지를 전달하고 훅 프로시저는 체인을 따라 다음 훅 프로시저에게 메시지를 연속적으로 전달하며 종국에는 그 메시지를 받을 윈도우에게 전달된다. 물론 중간에서 메시지가 변형되거나 사라질 수도 있다.
훅 프로시저가 설치되어 있으면 시스템은 메시지가 발생할 때마다 훅 프로시저에게 메시지를 전달해 주어야 한다. 훅 체인에 여러 개의 훅 프로시저가 설치되어 있다면 메시지는 훅 체인 내의 모든 훅 프로시저를 거쳐야만 비로소 목표 윈도우로 전달될 수 있다. 그래서 훅은 시스템의 전반적인 속도를 눈에 띄게 느리게 만든다. 짧은 순간에 수백 개의 메시지가 처리되는데 이 메시지들이 훅 프로시저를 한 바퀴 돌아오려면 당연히 시간이 걸릴 수밖에 없다. 훅을 사용하는 디버거나 스파이 등의 툴을 띄워 놓으면 시스템이 느려지는 것을 직접 경험해 본 바 있을 것이다. 그래서 훅은 꼭 필요할 때만 설치해야 하며 사용이 끝난 후에는 곧바로 제거하는 것이 좋다.
나. 훅 프로시저
훅 체인에 등록되어 메시지를 감시하는 함수를 훅 프로시저(Hook Procedure)라고 한다. 훅 타입에 따라 훅 프로시저의 인수나 리턴값의 의미는 달라지지만 원형은 고정되어 있다. 다음은 WH_KEYBOARD 타입의 키보드 훅 프로시저인데 다른 타입의 훅 프로시저도 이름만 다르고 원형은 동일하다.
LRESULT CALLBACK KeyboardProc( int code, WPARAM wParam, LPARAM lParam);
훅 프로시저는 응용 프로그램이 제공하는 콜백 함수이므로 원형만 제대로 지킨다면 이름은 마음대로 정할 수 있다. 세 개의 인수를 가지는데 첫 번째 인수 code는 훅 프로시저에서 이 메시지를 어떻게 처리할 것인가를 알려주며 이 값이 음수이면 훅 프로시저는 이 메시지를 처리하지 말고 다음 훅 프로시저에게 메시지를 넘겨야 한다. wParam, lParam은 전달된 메시지에 대한 추가 정보들인데 실제 의미는 훅 타입에 따라 달라지므로 각 타입별로 레퍼런스를 참고해야 한다. 훅 프로시저를 설치할 때는 다음 함수를 사용한다.
HHOOK SetWindowsHookEx( int idHook, HOOKPROC lpfn, HINSTANCE hMod, DWORD dwThreadId );
첫 번째 인수 idHook은 설치하고자 하는 훅의 타입을 지정하며 WH_로 시작되는 매크로 상수중 하나를 써 주면 된다. lpfn 은 훅 프로시저의 번지이며 hMod는 훅 프로시저를 가진 인스턴스 핸들이다. dwThreadId는 훅 프로시저가 감시할 스레드의 ID이되 이 값이 0이면 시스템의 모든 스레드에서 발생하는 메시지가 훅 프로시저로 전달된다. 자신의 메시지를 훅킹할 때는 GetCurrentThreadId 함수로 현재 스레드의 ID를 넘겨주면 된다. 시스템의 모든 메시지를 감시하고자 한다거나 다른 프로그램의 메시지를 감시하고자 할 경우 lpfn은 반드시 분리된 DLL에 있어야 하며 이때 hMod는 이 DLL의 핸들이어야 한다. 다음은 지역 훅과 전역 훅을 설치하는 일반적인 방법이다.
지역 훅 : SetWindowsHookEx(idHook, lpfn, NULL, GetCurrentThreadId());
전역 훅 : SetWindowsHookEx(idHook, lpfn, hDll, 0);
SetWindowsHookEx 함수는 훅 프로시저를 설치한 후 HHOOK 타입의 훅 핸들을 리턴해 주는데 이 핸들은 해제를 위해 전역변수에 잘 보관해 두어야 한다. 만약 에러가 발생했다면 NULL을 리턴한다. 훅 프로시저를 해제하는 함수는 다음과 같다.
BOOL UnhookWindowsHookEx( HHOOK hhk );
해제하고자 하는 훅 핸들을 넘겨주기만 하면 된다. 훅을 설치한 프로그램은 종료되기 전에 반드시 훅 프로시저를 해제해 주어야 한다. 훅 프로시저가 설치되면 해당 타입의 메시지는 목표 윈도우로 보내지기 전에 훅 프로시저에게 먼저 전달되는데 훅 프로시저는 메시지를 살펴본 후 특별한 이유가 없으면 메시지를 훅 체인의 다음 훅 프로시저에게 전달해 주어야 한다. 이때는 다음 함수를 사용한다.
LRESULT CallNextHookEx( HHOOK hhk, int nCode, WPARAM wParam, LPARAM lParam );
hhk는 현재 처리하고 있는 훅의 핸들인데 SetWindowsHookEx 함수가 리턴한 값이다. 나머지 세 인수는 운영체제가 훅 프로시저에게 전달해준 인수들이다. 훅 체인에 포함된 훅 프로시저의 목록은 운영체제가 직접 관리하기 때문에 훅을 설치한 응용 프로그램은 다음 훅 프로시저의 번지를 따로 저장할 필요없이 이 함수만 호출해 주면 훅 체인을 따라 모든 훅 프로시저가 순서대로 호출되며 최종적으로 목표 윈도우로 메시지가 전달될 것이다.
훅 프로시저는 전달 받은 메시지를 다음 훅 프로시저에게 꼭 전달해 주어야 할 의무는 없으며 메시지를 아예 없애버리려면 전달하지 않아도 상관없으며 원하는 대로 변경할 수도 있다. 물론 이 때는 메시지를 없애버리거나 변경한 후의 효과에 대해 확실히 책임질 수 있어야 한다. 이 함수는 훅 체인에서 다음 훅 프로시저를 호출하고 훅 프로시저가 리턴하는 값을 다시 리턴해 주는데 현재의 훅 프로시저는 이 리턴값을 또 그대로 리턴해 주어야 한다. 그래서 훅 프로시저의 끝은 보통 return CallNextHookEx(...) 호출문이 온다.
다음은 윈도우즈가 제공하는 훅 타입들이다. 훅 타입에 따라 감시하고 취급할 수 있는 메시지의 종류가 달라진다.
훅 타입 |
설명 |
WH_CALLWNDPROC,
WH_CALLWNDPROCRET |
SendMessage 함수로 메시지를 보내기 전에
WH_CALLWNDPROC 훅 프로시저가 호출되며 윈도우 프로시저가 메시지를 처리한 후에 WH_CALLWNDPROCRET 훅 프로시저가 호출된다. WH_CALLWNDPROCRET 훅은 훅 프로시저에게 CWPRETSTRUCT 구조체를 전달하는데 이 구조체에는 메시지와 메시시를 처리한 리턴값을 담고 있다.
|
WH_CBT |
윈도우를 생성, 파괴, 활성화, 최대, 최소, 이동, 크기변경하기 전에, 시스템 명령을 처리하기 전에, 마우스나 키보드 메시지를 메시지 큐에서 제거하기 전에 이 훅 프로시저가 호출된다. 이 훅은 컴퓨터를 이용한 훈련 프로그램(Computer Based Training)에서 주로 사용된다.
|
WH_DEBUG |
다른 타입의 훅 프로시저를 호출하기 전에 이 타입의 훅 프로시저를 호출하며 다른 타입의 훅 프로시저 호출을 허가할 것인지를 결정한다.
|
WH_GETMESSAGE |
GetMessage나 PeekMessage 함수로 조사되는 메시지를 감시한다.
|
WH_JOURNALRECORD |
키보드나 마우스를 통해 입력되는 이벤트를 감시하고 기록한다. 기록된 이벤트는 WH_JOURNALPLAYBACK 훅에서 재생할 수 있다. 이 훅은 전역으로만 설치할 수 있으며 특정 스레드에만 설치할 수는 없다.
|
WH_JOURNALPLAYBACK |
시스템 메시지 큐에 메시지를 삽입할 수 있도록 한다. 이 훅에서 WH_JOURNALRECORD 훅에서 기록한 키보드 마우스 입력을 재생할 수 있다. 이 훅이 설치되어 있으면 마우스나 키보드 입력은 금지된다. 이 훅은 전역으로만 설치할 수 있으며 특정 스레드에만 설치할 수는 없다.
|
WH_KEYBOARD |
WM_KEYDOWN, WM_KEYUP 등의 키보드 메시지를 감시한다.
|
WH_MOUSE |
마우스 메시지를 감시한다.
|
WH_MSGFILTER,
WH_SYSMSGFILTER |
메뉴, 스크롤 바, 메시지 박스, 대화상자 등에 의해 처리되는 메시지와 사용자의 Alt+Tab키, Alt+Esc키 입력에 의한 포커스 이동을 감시한다. WH_MSGFILTER훅은 훅 프로시저를 설치한 프로그램에 대해서만 동작하며 WH_SYSMSGFILTER 훅은 모든 프로그램에 대해서 동작한다.
|
WH_SHELL |
쉘 프로그램이 활성화되거나 새로운 최상위 윈도우가 만들어지거나 파괴될 때 이 훅 프로시저가 호출된다.
|
WH_FOREGROUNDIDLE |
포그라운드 스레드가 한가해질 때 이 훅 프로시저가 호출된다. 아이들 시에 우선 순위가 낮은 작업을 하고 싶을 때 이 훅을 사용한다.
|
WH_KEYBOARD_LL |
스레드의 입력큐에 붙여지는 키보드 입력 메시지를 감시한다. WH_KEYBOARD보다 더 저수준의 메시지를 받을 수 있지만 NT 4.0 SP 3 이후에만 사용할 수 있다.
|
WH_MOUSE_LL |
스레드의 입력큐에 붙여지는 마우스 입력 메시지를 감시한다. |
각 훅 타입에 따라 사용되는 구조체나 리턴값, 훅 프로시저의 인수가 다르므로 상세한 정보는 레퍼런스를 참조하기 바란다.
다. 키보드 훅
훅 타입중에 개념적으로 가장 이해하기 쉽고 간단한 키보드 훅 프로시저를 작성해 보자. 다음 예제는 키보드 메시지를 훅킹하여 메시지에 대한 정보를 작업영역에 보여준다.
char Mes[]="키보드 훅 테스트 프로그램입니다.";
char Mes2[128];
char Mes3[128];
int Count=0;
HHOOK hKeyHook;
LRESULT CALLBACK KeyHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode>=0) {
InvalidateRect(hWndMain,NULL,TRUE);
if (wParam == VK_F2) {
wsprintf(Mes2,"F2는 입력이 금지된 키입니다.");
return 1;
} else {
wsprintf(Mes2,"nCode=%d, wParam=%u, lParam=%08x, Count=%d",
nCode, wParam, lParam,Count++);
}
}
return CallNextHookEx(hKeyHook,nCode,wParam,lParam);
}
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
switch(iMessage) {
case WM_CREATE:
hKeyHook=SetWindowsHookEx(WH_KEYBOARD,KeyHookProc,NULL,GetCurrentThreadId());
return 0;
case WM_KEYDOWN:
wsprintf(Mes3,"실제 받은 키 : %u",wParam);
InvalidateRect(hWnd,NULL,TRUE);
return 0;
case WM_PAINT:
hdc=BeginPaint(hWnd, &ps);
TextOut(hdc,100,10,Mes,strlen(Mes));
TextOut(hdc,100,30,Mes2,strlen(Mes2));
TextOut(hdc,100,50,Mes3,strlen(Mes3));
EndPaint(hWnd, &ps);
return 0;
case WM_DESTROY:
UnhookWindowsHookEx(hKeyHook);
PostQuitMessage(0);
return 0;
}
return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
실행 결과는 다음과 같다.
WM_CRAETE에서 SetWindowsHookEx 함수로 KeyHookProc 훅 프로시저를 WH_KEYBOARD 훅 타입의 훅 체인에 추가하였다. 현재 스레드의 ID를 주었으므로 자기 자신의 키보드 메시지만 감시하는 지역 훅이다. 사용자가 키보드를 누르거나 뗄 때 즉, WM_KEYDOWN, WM_KEYUP 메시지가 발생하면 KeyHookProc 훅 프로시저가 호출될 것이다. WM_DESTROY에서는 설치한 훅 프로시저를 해제하도록 했다.
훅 프로시저로 전달된 nCode 인수가 음수일 경우는 어떤 처리도 해서는 안 되며 곧바로 훅 체인의 다음 훅 프로시저로 메시지를 넘겨주어야 한다. 예제에서는 nCode가 0보다 크거나 같을 때만 메시지를 감시하도록 했다. 훅 프로시저는 전달된 메시지의 내용을 점검한 후 메시지를 변경하거나 없애버릴 수도 있고 아니면 그대로 체인의 다음 함수에게 전달할 수도 있다.
이 예제는 훅 프로시저가 특정 키입력을 거부할 수도 있다는 것을 보여 주기 위해 F2 키가 입력되었을 경우 0이 아닌 값(보통 1 또는 TRUE)을 리턴하여 이 키에 대한 입력 메시지가 다음 훅 프로시저로 전달되지 않도록 했다. 시스템은 훅 프로시저가 0이 아닌 값을 리턴하면 이 메시지를 중간에서 없애버리며 다음 훅 프로시저로 전달되지 않으므로 결국 목표 윈도우는 이 메시지를 받지 못하게 된다. F2가 아닌 경우는 어떤 키가 입력되었는지 wParam과 lParam에 대한 정보를 화면에 출력하도록 했다.
키보드 훅 프로시저는 WM_KEYDOWN, WM_KEYUP 메시지에 대해 호출되므로 키가 눌러지거나 떨어질 때 모두 호출된다. 만약 훅 프로시저내에서 키가 눌러진 것인지 떨어진 것인지를 구분하고 싶다면 lParam의 최상위 비트가 1인지 아닌지를 점검해 보면 된다. if ((lParam & 0x80000000)==0) 조건문이 참이면 WM_KEYDOWN 메시지가 전달된 것이고 거짓이면 WM_KEYUP이 전달된 것이다.
위 예제에서 훅 타입을 WH_MOUSE로 바꾸면 마우스 메시지를 훅킹할 것이다. 훅을 설치, 해제하는 방법은 동일하되 nCode 인수의 의미나 wParam, lParam으로 전달되는 추가 정보의 의미는 달라진다. 지역 훅은 사용 방법이 쉽고 별다른 제약 사항이나 주의 사항이 없어 위험하지도 않은 편이다.
라. 전역 훅
전역 훅은 시스템에서 발생하는 모든 메시지를 가로챌 수 있으므로 강력하고 활용성도 높다. 하지만 보호된 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를 호출하지 않는다면 효과음만 나고 실제 키 입력 메시지가 목표 윈도우로 전달되지 않으므로 시스템의 키보드는 먹통이 되어 버릴 것이다.
예제를 직접 실행해 보면 키보드를 두드릴 때마다 찰칵 찰칵 소리가 나서 마치 타자기를 치는 듯한 색다른 기분이 든다. 코드를 짧게 만드느라 편의 기능은 넣지 않았는데 메인 윈도우를 조금 더 예쁘장하게 장식하고 여기에 효과음을 선택할 수 있는 기능이라든가 볼륨 조절 기능 등만 넣어도 깜찍한 악세사리로 쓸만한 프로그램이 된다.
마. 훅의 활용
전역 훅 프로시저는 다른 프로세스의 메시지 흐름을 들여다 볼 수 있다는 점에서 활용성이 높다. 특정 윈도우로 입력되는 메시지의 흐름을 살펴보면 이 윈도우가 입력된 메시지에 대해 어떻게 반응할 것인가를 예측할 수 있으며 따라서 윈도우에게 사건이 발생하는 정확한 시점을 알 수 있다. 훅 프로시저는 메시지 흐름을 감시하고 있다가 관심있는 이벤트가 발생했을 때 원하는 어떤 조치를 취할 수 있을 것이다.
다음 예제는 메모장의 키 입력을 감시하는 훅 프로시저를 설치하고 사용자가 babo라는 키를 연속으로 입력하면 이 키 입력을 모두 취소하고 chunjae로 바꿔 버린다. babo라는 연속된 키입력 이벤트에 반응하여 메모장의 동작을 원하는 방식으로 제어할 수 있는 것이다. 훅 DLL은 앞서 작성했던 KeyBeepDll과 동일하므로 따로 살펴볼 필요가 없으며 훅 서버 프로그램이 키 입력을 감시하고 이벤트에 대응하는 방법만 보도록 하자.
#include "../HookNotePadDll/HookNotePadDll.h"
TCHAR Mes[]="메모장에서 BABO를 입력하면 chaunjae로 변경합니다.";
TCHAR szSrc[]="BABO";
TCHAR szDest[]="CHUNJAE";
int idx;
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
HWND hFGWnd;
TCHAR szClass[32];
int i;
switch(iMessage) {
case WM_CREATE:
InstallHook(hWnd);
return 0;
case WM_USER+1:
hFGWnd=GetForegroundWindow();
GetClassName(hFGWnd,szClass,32);
if (lstrcmpi(szClass,"NotePad")==0 && (lParam & 0x80000000)==0) {
if (wParam == (WPARAM)szSrc[idx]) {
idx++;
} else {
idx=0;
}
if (szSrc[idx]==0) {
for (i=0;i<lstrlen(szSrc);i++) {
keybd_event(VK_BACK,0,0,0);
keybd_event(VK_BACK,0,KEYEVENTF_KEYUP,0);
}
for (i=0;i<lstrlen(szDest);i++) {
keybd_event(szDest[i],0,0,0);
keybd_event(szDest[i],0,KEYEVENTF_KEYUP,0);
}
}
}
return 0;
case WM_PAINT:
hdc=BeginPaint(hWnd, &ps);
TextOut(hdc,10,10,Mes,lstrlen(Mes));
EndPaint(hWnd, &ps);
return 0;
case WM_DESTROY:
UninstallHook();
PostQuitMessage(0);
return 0;
}
return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
이 프로그램은 메모장의 동작에 대해서만 관여하므로 훅 프로시저로부터 WM_USER+1 메시지를 받았을 때 활성화된 윈도우가 메모장이 맞는지를 먼저 살펴 본다. 활성 윈도우의 윈도우 클래스명이 "NotePad"가 아니라면 메모장이 아니므로 아무런 동작도 하지 않으며 키가 떨어질 때도 역시 아무 동작도 하지 않는다. 오직 메모장에서 키를 누를 때(WM_KEYDOWN)만 동작하도록 했는데 이 조건을 변경하면 모든 프로그램에 대해 동작하도록 바꿀 수도 있다.
메모장에서 키가 눌러졌을 때, 즉 메모장 윈도우가 WM_KEYDOWN 메시지를 받았을 때 wParam이 babo의 연속인지 아닌지를 항상 감시하고 있다가 만약 babo가 연속으로 입력되면 4개의 BS키를 보내 babo를 지우고 chunjae키를 차례대로 누름으로써 babo를 chunjae로 바꾸어 준다. keybd_event 는 마치 사용자가 키보드를 누른 것처럼 키 이벤트를 발생시키는 함수이다.
이 예제는 연속된 키입력만 감시하기 때문에 한글, 대소문자 등은 구분하지 않으며 중간에 BS나 커서 이동키로 이동, 편집한 경우도 연속된 문자열로 인정하지 않는다. IME 상태나 대소문자 구분 등을 판별하고 BS, Del 등의 간단한 편집키를 처리한다면 좀 더 완벽한 동작을 할 수도 있다. 이런 방식으로 다른 프로그램의 키입력을 감시, 변경하면 백그라운드 맞춤법 검사기나 상용구 입력기 등을 만들 수 있을 것이다.
다음 예제는 지역 훅을 사용하여 메시지 박스를 부모 윈도우의 중앙에 출력한다. MessageBox 함수는 자신의 위치를 지정하는 플래그가 없으며 무조건 화면 중앙에 나타나도록 되어 있다. 이 함수는 호출하는 즉시 메시지 박스를 띄우고 확인 버튼을 누를 때까지 리턴하지 않기 때문에 호출원에서 윈도우의 위치를 옮길 수 있는 기회가 없다. 위치를 옮길 때는 MoveWindow나 SetWindowPos 함수를 사용한다는 것은 알고 있지만 이 함수를 호출할 마땅한 시점이 없는 것이다.
메시지 박스가 생성되는 시점, 그러니까 WM_CREATE 메시지를 받을 때 이 윈도우의 위치를 옮겨야 하는데 그 시점이 운영체제 내부에 있기 때문에 응용 프로그램이 자신의 코드를 실행할 기 회가 없는 것이다. 메시지 박스는 일종의 대화상자이고 이 대화상자의 윈도우 프로시저는 운영체제에 내장되어 있어 프로그래밍할 수 있는 대상이 아니다. 메시지 박스가 생성되는 시점을 구하기 위해 훅을 설치하고 윈도우가 생성될 때 보내지는 메시지를 가로채야 한다.
이때 사용하는 훅이 WH_CBT이다. CBT(Computer Based Training) 훅은 초보자들의 컴퓨터 조작 훈련을 위해 제공되는데 윈도우가 생성, 파괴, 이동 및 크기 변경시의 메시지를 감시하도록 한다. CBT 프로그램은 사용자에게 윈도우 조작 방법을 알려주고 실습을 유도하는데 이때 사용자들이 지시대로 윈도우의 생성, 이동, 종료 등을 제대로 하는지 감시하기 위해 WH_CBT 훅을 사용한다. 이 훅의 원래 목적은 사용자 교육용이지만 일반 응용 프로그램도 윈도우 관련 메시지를 가로채기 위해 이 훅을 사용할 수 있다.
이 훅을 사용하면 특정 윈도우가 생성되는 시점을 응용 프로그램이 알 수 있고 이때 원하는 처리, 예를 들어 위치를 옮기거나 크기를 바꾸거나 스타일을 변경할 수 있다. 메시지 박스처럼 사용자가 직접 만든 윈도우가 아닐지라도 말이다. 다음 예제는 WH_CBT훅과 서브클래싱을 사용하여 메시지 박스를 부모 윈도우의 중앙에 오도록 한다.
HHOOK hCbtHook;
void MoveToParentCenter(HWND hWnd)
{
RECT wrt,crt;
HWND hParent;
hParent=GetParent(hWnd);
if (IsIconic(hParent)) {
ShowWindow(hParent,SW_RESTORE);
}
GetWindowRect(hParent,&wrt);
GetWindowRect(hWnd,&crt);
SetWindowPos(hWnd,HWND_NOTOPMOST,wrt.left+(wrt.right-wrt.left)/2-(crt.right-crt.left)/2,
wrt.top+(wrt.bottom-wrt.top)/2-(crt.bottom-crt.top)/2,0,0,SWP_NOSIZE);
}
LRESULT CALLBACK NewWndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
WNDPROC OldProc;
OldProc=(WNDPROC)GetProp(hWnd,"OldProp");
switch(iMessage) {
case WM_CREATE:
MoveToParentCenter(hWnd);
break;
case WM_NCDESTROY:
SetWindowLong(hWnd,GWL_WNDPROC,(DWORD)OldProc);
RemoveProp(hWnd,"OldProp");
break;
}
return CallWindowProc(OldProc,hWnd,iMessage,wParam,lParam);
}
LRESULT CALLBACK CbtHookProc(int nCode,WPARAM wParam,LPARAM lParam)
{
CBT_CREATEWND *pCbt;
HWND hWnd;
TCHAR szClassName[32];
WNDPROC OldProc;
if (nCode == HCBT_CREATEWND) {
hWnd=(HWND)wParam;
pCbt=(CBT_CREATEWND *)lParam;
if (HIWORD(pCbt->lpcs->lpszClass)) {
lstrcpy(szClassName,pCbt->lpcs->lpszClass);
} else {
GlobalGetAtomName((ATOM)pCbt->lpcs->lpszClass,szClassName,32);
}
if (lstrcmpi(szClassName,"#32770")==0 && ((pCbt->lpcs->style & WS_CHILD)==0)) {
OldProc=(WNDPROC)GetWindowLong(hWnd,GWL_WNDPROC);
SetProp(hWnd,"OldProp",OldProc);
SetWindowLong(hWnd,GWL_WNDPROC,(DWORD)NewWndProc);
}
}
return CallNextHookEx(hCbtHook,nCode,wParam,lParam);
}
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
switch(iMessage) {
case WM_CREATE:
hCbtHook=SetWindowsHookEx(WH_CBT,CbtHookProc,NULL,GetCurrentThreadId());
return 0;
case WM_LBUTTONDOWN:
MessageBox(hWnd,"이 메시지 박스는 부모 윈도우의 중앙에 나타납니다","알림",MB_OK);
return 0;
case WM_PAINT:
hdc=BeginPaint(hWnd, &ps);
EndPaint(hWnd, &ps);
return 0;
case WM_DESTROY:
UnhookWindowsHookEx(hCbtHook);
PostQuitMessage(0);
return 0;
}
return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
WM_CRAETE에서 WH_CBT 훅을 설치하는데 메시지 박스는 같은 스레드내에서 생성되는 윈도우이므로 지역 훅을 설치하면 된다. WM_PAINT에서는 간단한 안내 메시지를 출력하고 WM_DESTROY에서는 훅을 제거했다. 마우스 왼쪽 버튼을 누르면 MessageBox 함수를 호출하여 메시지 박스를 띄운다. 별다른 처리를 하지 않는다면 메시지 박스는 항상 화면 중앙에 열리게 될 것이다.
CBT 훅 프로시저의 nCode로는 윈도우에 어떤 일이 발생했는지를 알려주는 다음과 같은 값이 전달되며 이때 wParam으로는 윈도우의 핸들이 전달되며 lParam으로는 메시지의 부가 정보가 전달된다. 다음 도표는 CBT 훅의 nCode값과 lParam 인수를 정리한 것인데 더 자세한 정보는 레퍼런스를 참고하기 바란다.
코드 |
설명 |
lParam |
HCBT_ACTIVATE |
윈도우 활성화 |
CBTACTIVATESTRUCT 구조체 |
HCBT_CREATEWND |
윈도우 생성 |
CBT_CREATEWND 구조체 |
HCBT_DESTROYWND |
윈도우 파괴 |
0 |
HCBT_MINMAX |
최소 또는 최대화 |
하위 워드에 현재 상태(SW_*) |
HCBT_MOVESIZE |
이동 또는 크기 변경 |
윈도우의 현재 위치값을 가지는 RECT 구조체 |
HCBT_SYSCOMMAND |
시스템 명령 실행 |
WM_SYSCOMMAND와 동일 |
리턴값으로는 해당 동작의 허가 여부를 리턴하는데 0을 리턴하면 동작을 허가하는 것이고 1을 리턴하면 금지하는 것이다. CBT 훅은 항상 해당 동작이 일어나기 전에 훅 프로시저에게 먼저 전달된다. 예를 들어 윈도우가 생성될 때 HCBT_CREATEWND 코드를 먼저 보낸 후 이 훅 프로시저가 0을 리턴하면 목표 윈도우로 WM_NCCREATE, WM_CREATE 메시지가 전달되며 윈도우가 파괴될 때도 WM_DESTROY 메시지를 보내기 전에 CBT훅의 HCBT_DESTROYWND 코드가 먼저 전달된다.
이 예제는 메시지 박스가 생성될 때 윈도우의 위치를 옮기고자 하므로 nCode가 HCBT_CREATEWND일 때 원하는 코드를 실행해야 한다. 이때 lParam으로는 다음과 같이 정의된 구조체의 포인터가 전달된다.
typedef struct {
LPCREATESTRUCT lpcs;
HWND hwndInsertAfter;
} CBT_CREATEWND, *LPCBT_CREATEWND;
lpcs는 CREATESTRUCT 구조체이며 hwndInsertAfter는 이 윈도우 바로 앞의 Z 순서를 가지는 윈도우 핸들이다. 훅 프로시저에서 이 구조체의 값을 직접 변경하면 윈도우의 위치나 크기, Z 순서를 바꿀 수 있다. lpcs->x, lpcs->y값을 조정하면 메시지 박스가 생성될 위치를 지정할 수 있는데 예제에서는 이 값을 직접 변경하지 않고 서브클래싱만 하고 있다. 왜냐하면 부모의 중앙 좌표를 구해야 하는데 윈도우가 생성되는 이 시점에는 아직 부모가 누구인지를 알 수 없기 때문이다. CBT훅은 동작이 일어나기 직전에 보내지므로 아직 이 윈도우는 만들어지지 않았으며 부모 자식 관계도 설정되어 있지 않다.
그래서 훅 프로시저는 서브클래싱만 해 놓고 서브클래스 프로시저의 WM_CREATE(또는 WM_INITDIALOG)에서 부모의 위치를 참조하여 부모의 중앙 위치로 가도록 했다. 여기서 WM_CREATE 메시지는 부모를 알 수 있는 최초의 시점이며 또한 이 윈도우가 보이기 전이므로 위치를 옮길 수 있는 최적의 위치에 해당된다. 원래 윈도우 프로시저의 번지는 별도의 전역 변수에 저장할 수도 있지만 윈도우 스스로 기억하도록 하기 위해 윈도우 프로퍼티를 사용했다. 이 윈도우는 파괴되기 직전에 윈도우 프로퍼티로부터 원래 윈도우 프로시저를 구해 자신의 서브클래싱을 직접 해제한다. 요약하자면 CBT 훅 프로시저는 윈도우가 생성되는 시점을 가로채서 서브클래싱만 하고 위치를 옮기는 작업은 서브클래스 프로시저가 하고 있는 셈이다.
훅 프로시저는 서브클래싱할 윈도우를 정확하게 선정해야 한다. CBT 훅은 모든 윈도우의 생성, 파괴, 이동 메시지를 받기 때문에 조건 점검을 정밀하게 하지 않으면 대화상자뿐만 아니라 대화상자안의 버튼이나 스태틱같은 컨트롤까지도 위치 이동의 대상이 되어 버리기 때문이다. 예제에서는 윈도우 클래스가 #32770, 즉 대화상자이고 차일드가 아닌지만으로 메시지 박스인지를 점검하고 있다. 윈도우가 좀 더 많은 프로젝트에서는 이보다 더 정밀한 조건 점검을 해야 할 것이다. 그렇지 않으면 원치않은 윈도우까지 위치가 이동되는 부작용이 발생한다.
예제를 실행하고 마우스 왼쪽 버튼을 눌러 보면 메시지 박스가 부모의 중앙에 열리게 될 것이다. 코드를 이해하는 것은 어렵지 않은데 그렇다면 메시지 박스의 위치를 옮기는데 왜 이렇게 복잡한 과정을 거쳐야 하는 것일까? 그 이유는 MessageBox라는 함수가 모달 대화상자를 열고 이 대화상자의 운용 일체를 관장하고 있기 때문이다. 이 함수를 호출하면 대화상자를 생성, 표시, 파괴하는 동작이 모두 이 함수내에서 일어나며 응용 프로그램이 메시지 박스에 대해 조작을 할 수 있는 기회가 없다. 그래서 훅을 설치하고 서브클래싱해서 생성 시점을 가로채는 복잡한 과정을 거쳐야 하는 것이다. 이 방법은 메시지 박스뿐만 아니라 공통 대화상자나 프로퍼티 시트 등의 컨트롤에도 동일하게 적용된다.