Tutorial 24: Windows Hooks
이 번장에서는 윈도우 훅(Hook)에 대해서 설명한다. 윈도우 훅이란 매우 강력해서, 이로 인해, 다른 프로세스를 훅 할 수 있고 또한 행동을 변경할 수도 있다.
소스 | 리소스 | 인클루드 |
DLL 소스 | DLL 정의 파일 | 실행 결과 |
|
이번에 설명하는 「윈도우 훅」은 Windows에서 제공하는 가장 강력한 기능중의 하나이다. 이것을 사용하게 되면, 자신이 만든 프로그램의 프로세스, 또는, 다른 프로세스에서 발생한 이벤트를 훅(가로채기) 할 수 있다. 「훅 한다」 것은, 원하는 처리를 하려는 이벤트가 발생했을 때 언제든지, 지정한 훅 프로시저라 불리는 필터 함수(이벤트를 필터링 하기 위해 이렇게 불린다)를 Windows에서 호출 하게 할 수 있다. 훅에는 「로컬」과「리모트」의 2가지 종류가 있다.
- 로컬
자신의 프로세스에서 발생한 이벤트를 훅 한다- 리모트
다른 프로세스에서 발생한 이벤트를 훅 한다. 리모트에는 또다시 2가지의 종류가 있다
- thread 지정
다른 프로세스에서 지정한 thread에 발생한 이벤트를 훅 한다. 즉, 특정의 프로세스내의 특정의 thread에서 발생한 이벤트를 감시한다- 시스템 와이드
시스템의 전체 프로세스의 전체 thread에서 발생한 모든이벤트를 트랩 한다훅을 실행하게 되면, 시스템의 퍼포먼스에 영향을 주는 것을 잊지 말기 바란다. 「시스템 와이드」방식은 특히 이러한 성능 저하를 몸으로 느낄수 있을 정도이다. 모든 이벤트에 대해 필터 함수를 호출하게 되므로, 현저하게 퍼포먼스가 떯어지게 되는 것이다. 그래서, 시스템 와이드 훅을 사용한다면, 신중히 생각한 후 사용하기 바란다. 필요없는 메세지는 훅 하지 않게 할 필요가 있다.만약 필터함수에서 에러가 발생하면 해당 프로세스의 프로그램이 종료하게 될 것임을 기억하기 바란다. 이처럼 훅이란 칼날의 양면과 같은 존재임을 잊지 말기 바란다.
먼저 윈도우 훅을 사용하기 전에,어떻게 작동하는지를 이해해야 한다. 훅을 실행할때, 훅에 관한 정보가 포함된 데이터 구조를 메모리에 생성하고, 기존의 훅 리스트에 추가한다. 새롭게 추가된 훅은 기존의 훅의 이전에 추가되므로, 어떤 이벤트가 발생했을 때, 로컬 훅을 실행하게 되면, 그 프로세스의 필터 함수가 호출된다. 이것은 아무런 문제도 없다.하지만 문제는, 리모트 훅인 경우로서, 이 때는 시스템이 다른 프로세스의 주소공간에 훅 프로시저의 코드를 포함하는것이지만, 그 함수는 DLL에 존재해야 한다.
다만 여기에는, 2가지 예외가 있다. 저널 레코드와 저널 플레이백 훅이 그것이다. 이들 2개의 훅의 프로시저는 훅을 실행하는 thread에 반드시 존재해야만 한다. 이는, 두가지의 훅이 저레벨에 속하는 하드웨어 인터럽트 처리를 다루기 때문이다. 하드웨어 입력 이벤트는 순서대로 기록되고 또한 재생되어야 한다. 이들 2개의 훅코드가 DLL에 있다면, 입력 이벤트는 여러개의 thread에 분산되게 되고, 하드웨어 입력 이벤트의 순서를 알아내는 것은 불가능하게 된다. 이렇기 때문에, 이들 2개의 훅 프로시저는 단일 thread, 즉 훅을 실행하는 thread여야만 하는 것이다.
훅에는 다음과 같이 14가지 종류가 있다.
- WH_CALLWNDPROC
SendMessage 함수가 호출될때 호출된다- WH_CALLWNDPROCRET
SendMessage 함수에서 제어가 반환될 경우에 호출된다- WH_GETMESSAGE
GetMessage 함수나 PeekMessage 함수가 호출될때 호출된다- WH_KEYBOARD
GetMessage 함수, 혹은 PeekMessage 함수가 메시지큐로부터 WM_KEYUP나 WM_KEYDOWN를 얻게될때 호출된다- WH_MOUSE
GetMessage 함수, 혹은 PeekMessage 함수가 메시지큐로부터 마우스 메세지를 얻게될때 호출된다- WH_HARDWARE
GetMessage 함수, 혹은 PeekMessage 함수가 메시지큐와 연관되지 않은 하드웨어 메세지를 얻게될때 호출된다- WH_MSGFILTER
다이얼로그 박스, 메뉴, 스크롤바등이 메세지를 처리하려 할때 호출된다. 이 훅은 로컬로만 사용가능하며, 내부적으로 메시지루프를 수행하는 오브젝트인 경우에만 해당된다.- WH_SYSMSGFILTER
시스템 와이드인 점을 제외하면 WH_MSGFILTER와 같다- WH_JOURNALRECORD
Windows가 하드웨어 입력 큐로부터 메세지를 얻게될 때 호출된다- WH_JOURNALPLAYBACK
Windows가 시스템의 하드웨어 입력 큐로부터 메세지를 얻게될 때 호출된다- WH_SHELL
태스크바의 화면복구가 필요할때와 같은, 시스템쉘에 대해서 어떤 이벤트가 발생할 때 호출된다- WH_CBT
컴퓨터를 이용한 트레이닝(CBT)시에 사용한다- WH_FOREGROUNDIDLE
Windows가 내부적으로 사용한다. 일반적 으로는 거의 사용되지 않는다- WH_DEBUG
훅 프로시저를 디버그 할 때에 사용한다이번에는 훅의 설치와 제거 방법에 대한 설명을 한다. 훅을 설치하기 위해서는, SetWindowsHookEx 함수를 호출해야 한다.
SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD, ThreadID:DWORD
- HookType
위의 리스트에서 설명된 값. WH_MOUSE나 WH_KEYBOARD 중의 하나- pHookProc
지정한 훅의 메세지를 처리하기 위해 호출되는 훅 프로시저의 주소- hInstance
훅 프로시저가 들어있는 DLL의 인스턴스 핸들. 로컬 훅이면 이값은 NULL이여야 한다.- ThreadID
훅을 설치하고 싶은 thread ID. 이 파라미터에 의해서, 훅이 로컬인가 리모트인가를 구별한다. 이값이 NULL이면, Windows는 시스템 전체의 thread에 영향을 주는 시스템 와이드 리모트 훅이라고 가정한다. 자신의 프로세스 thread ID를 지정하게 되면, 로컬 훅이 된다. 만약 다른 프로세스의 thread ID를 지정하면, thread 특유의 리모트 훅이 된다.
이 룰에는 2가지 예외가 있다. WH_JOURNALRECORD와 WH_JOURNALPLAYBACK 은 항상, 로컬 시스템 와이드 훅으로서, DLL에 포함 되어 있을 필요는 없다. 그리고, WH_SYSMSGFILTER는 항상 리모트 와이드 훅이다. 이것은, ThreadID==0으로서 WH_MSGFILTER를 지정한 것과 같다.함수가 성공적으로 수행되면, eax 레지스터에 훅 핸들이 반환된다. 실패한다면 NULL이 설정된다. 나중에 사용하기 위해서 이 훅 핸들을 저장해 둘 필요가 있다.
훅을 해제하는데는 UnhookWindowsHookEx 함수를 호출한다. 이 함수는 훅이 설치된 훅의 핸들을 인수로 갖는다. 호출이 성공적으로 수행되면, eax 레지스터에 0 이외의 값이 반환되고,실패한다면 NULL이 설정된다.
이로써 훅의 설치와 제거하는 방법을 알았으므로, 이번에는 훅 프로시저에 대해 설명한다.
훅 프로시저는 설치한 훅의 타입에 관련된 이벤트가 발생하게 되면 매번 호출된다. 예를 들어, WH_MOUSE 훅을 설치하게되면, 마우스 이벤트가 발생할때마다 훅 프로시저가 호출된다. 훅 프로시저의 프로토타입은 다음과 같이 정의되어있고,설치한 한 훅의 타입과는 관계가없다.
HookProc proto nCode:DWORD, wParam:DWORD, lParam:DWORD
- nCode
훅 코드를 지정한다- wParam and lParam
이벤트에 대한 추가 정보를 지정한다HookProc라는 함수명은, 어떤 이름이라도 상관없다. wParam과 lParam, nCode의 해석은 설치한 훅의 타입에 따라서 달라지게 되며, 반환값도 마찬가지이다.
WH_CALLWNDPROC
- nCode
윈도우에 전송되는 메세지가 있다는 것을 의미하는 HC_ACTION 밖에 지정할 수 없다- wParam
0 이 아니라면, 전송되는 메세지.- lParam
CWPSTRUCT 구조체의 포인터- 반환값
사용하지 않는다. 0 이 반환된다.
WH_MOUSE
- nCode
HC_ACTION 혹은 HC_NOREMOVE- wParam
마우스 메세지- lParam
MOUSEHOOKSTRUCT 구조체의 포인터- 반환값
메세지가 처리되면 0. 메세지가 파괴되면 1.설치하고 싶은 훅의 반환값이나 인수의 의미는 Win32API 레퍼런스에 자세히 있으므로 참고하기 바란다.
여기의 훅 프로시저에는약간의 문제가 있다. 조금전에 설명했지만, 설치된 훅은, 가장 최근것이 리스트의 가장 처음에 배치된다는 것을 기억하기바란다. 이벤트가 발생했을 때, Windows는 리스트의 최상단의 훅 만을 호출한다. 그래서, 다음의 훅을 호출해줘야 하는 책임이 있다. 물론 다음의 훅을 부르지 않아도 되지만 호출하는것이 대부분일 것이다. CallNextHookEx 함수를 호출함으로써 다음 훅을 호출할 수 있다.
CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD, lParam:DWORD
- hHook
자신의 훅 핸들. 이 핸들을 사용해서, 훅 프로시저 리스트를 조사하고, 다음으로 호출되는 훅 프로시저를 검색한다.- nCode, wParam and lParam
Windows로부터 받은 값을, 그대로 CallNextHookEx 함수에 넘겨준다리모트 훅에 대해서는 알아야 할 중요한 것이 있다. 훅 프로시저는 다른 프로세스에 MAP된 DLL에 실행 코드가 있어야만 한다. Windows가 DLL을 다른 프로세스에 MAP할 때, data 섹션은 다른 프로세스로 MAP하지 않기 때문이다. 즉, 모든 프로세스는 실행 코드의 한개의 복사본을 공유하고 있지만, DLL의 data 섹션은 각각의 복사본을 가지게 된다!
반드시, DLL의 data 섹션변수에 어떤값을 저장했다면, DLL을 로드하고 있는 모든 프로세스에서 그 값을 공유하고 있다고 생각할 지 모르겠지만,실제로 그렇게는 되지 않는다. 하지만 보통, 각각의 프로세스가 DLL을 독자적으로 복사해서 사용하고 있다고 생각들게끔 작동하기 때문에, 이런 생각을 할 수도 있다.하지만, 윈도우 훅에서는 절대 그렇지 않다. 프로그래머는, DLL이 데이터를 포함하고 있고 모든 프로세스를 구별할 수 있도록 바라는 것이다.
이에 대한 해결 방법은, data 섹션을 공유하도록 표시해 두는 것이다. 링커에게 섹션속성을 지정하면 된다. MASM에서는 다음과 같이 한다.
/SECTION:<section name>, S초기화된 data 섹션명은 .data 이고, 초기화되지 않은 데이터는 .bss 이다. 예를 들어, 훅 프로시저가 포함된 DLL을 어셈블(assemble)할경우, DLL이 전체 프로세스에 공유되는 초기화 되지 않는 데이터를 유지할려면,
link /section:.bss, S /DLL /SUBSYSTEM:WINDOWS ..........위와 같이 실행한다. 「S 옵션」은 공유섹션을 의미한다.
|
이번에는 2가지의 코드가 있다. 한가지는 메인 프로그램으로 GUI 영역을, 다른 한가지는 DLL로서 훅의 설치 및 제거를 처리한다.
|
|
|
|
이번 예제에서는, 클래스명, 윈도우 핸들, 마우스 커서가 중복되어 있는 윈도우의 윈도우 프로시저 주소가 들어있는 3개의 에디트 컨트롤로 되어 있는 다이얼로그 박스가 출력된다. 그리고, 「Hook」과「Exit」의 2가지 버튼이 있다. Hook 버튼을 누르면, 프로그램은 마우스의 입력을 훅 하고, Hook의 문자열을 Unhook 으로 변경한다. 마우스 커서를 윈도우에 가져가면, 윈도우에 관한 정보가 메인 윈도우의 에디트 박스에 표시된다. 그리고, Unhook 버튼을 누르면, 프로그램은 마우스 훅을 제거한다.
메인 프로그램은 메인 윈도우로서 다이얼로그 박스를 사용하고 있고, 메인 윈도우와 훅 DLL의 사이에서 교환을 하기 위해서 사용하는 커스텀 메세지, WM_MOUSEHOOK을 정의하고 있다. 메인 프로그램이 이 메세지를 받게되면, wParam에는 마우스 커서가 위치하고 있는 윈도우의 핸들이 저장된다. 물론, 이것은 변경 할 수 있지만, 예제에서는 단순하게 하기 위해서 그렇게 사용했다. 메인 윈도우와 훅 DLL의 사이에서 일어나는 통신은 독자적인 방법을 사용해도 상관없다.
.if HookFlag==FALSE invoke InstallHook, hDlg .if eax! =NULL mov HookFlag, TRUE invoke SetDlgItemText, hDlg, IDC_HOOK, addr UnhookText .endif프로그램에서 훅 상태를 모니터링 하기 위해, HookFlag를 사용하고 있다. 훅이 설치되어있지 않으면, FALSE가 되고, 설치되어 있다면 TRUE가 된다.
유저가 훅 버튼을 누르면, 프로그램은 먼저 훅이 설치되어 있는지를 체크한다. 만약 설치되어 있지 않다면, 훅 DLL에 있는 InstallHook 함수를 호출해서 먼저 설치한다. 이 함수는 인수로 메인 다이얼로그의 핸들을 넘겨주므로, 훅 DLL은 WM_MOUSEHOOK 메세지를 이 메인 윈도우로 제대로 전송할 수 있다.
프로그램이 로드 되면, 훅 DLL도 로드 된다. 실제로, DLL은 프로그램이 메모리에 로드 된 직후 바로 로드 된다. DLL의 엔트리 포인트 함수는 메인 프로그램의 처음 명령어가 실행되기 전에 호출된다. 그렇기 때문에, 메인 프로그램이 실행될 때 DLL의 초기화는 이미 끝나 있게 된다. 덧붙여서, 훅 DLL의 엔트리 포인트 함수 코드는 다음과 같다.
.if reason==DLL_PROCESS_ATTACH push hInst pop hInstance .endif이 코드는 단순히, 훅 DLL의 인스턴스 핸들을, InstallHook 함수가 사용하는 hInstance라는 글로벌 변수에 저장하고 있다. DLL의 엔트리 포인트 함수는 DLL의 다른 함수부터 먼저 호출되므로, hInstance는 유일한 값이 된다.
코드에서는 hInstance를 .data 섹션에 둬서, 각각의 프로세스마다 따로 저장되는 값으로 하고 있다. 마우스 커서가 있는 윈도우에 위치할때, 훅 DLL은 프로세스에 매핑 된다. 여기에서,원래 그 훅 DLL이 로드 될 예정이었던 주소에 다른 DLL이 이미 로드 되어 있다고 가정하자. 그렇게 되면, 물론 그 훅 DLL은 다른 주소에 다시MAP되어 hInstance는 새롭게 로드 된 주소로 변경된다. 유저가 Unhook 버튼을 눌르면, SetWindowsHookEx 함수가 다시 호출된다. 하지만, 이 때, 인스턴스 핸들로 새로운 주소가 사용되긴 하지만, 인스턴스 주소는 의미가 없어진 것이다. 왜냐면,예제의 프로세스에서, 훅 DLL의 로드 주소는 변함이 없기 때문이다. 훅은 자신이 작성한 윈도우에서 발생한 마우스 이벤트만 훅 한다고 할 경우, 로컬 훅이 되어 버려서, 의미없는 것이 된다.
InstallHook proc hwnd:DWORD push hwnd pop hWnd invoke SetWindowsHookEx, WH_MOUSE, addr MouseProc, hInstance, NULL mov hHook, eax ret InstallHook endpInstallHook 함수는 아주 간단하다. 나중에 사용할 윈도우 핸들을 hWnd라는 글로벌 변수에 저장하고 있다. 그리고, 마우스 훅을 설치하기 위해, SetWindowsHookEx 함수를 호출하고 있다. SetWindowsHookEx 함수의 반환값은, UnhookWindowsHookEx 함수에서 사용하기 위해, hHook이란 글로벌 변수에 저장한다.
SetWindowsHookEx 함수를 호출한 후, 마우스 훅이 작동하게 된다. 시스템에 마우스 이벤트가 발생하면, 훅 프로시저의 MouseProc 함수가 호출된다.
MouseProc proc nCode:DWORD, wParam:DWORD, lParam:DWORD invoke CallNextHookEx, hHook, nCode, wParam, lParam mov edx, lParam assume edx:PTR MOUSEHOOKSTRUCT invoke WindowFromPoint,[edx]. pt.x,[edx]. pt.y invoke PostMessage, hWnd, WM_MOUSEHOOK, eax, 0 assume edx:nothing xor eax, eax ret MouseProc endp가장먼저 해야하는 것은, CallNextHookEx 함수를 호출해서 다른 훅에게 마우스 이벤트를 처리할 기회를 제공하는 것이다. 이 후, WindowFromPoint 함수를 호출해서 지정한 화면 좌표값에 위치하는 윈도우의 핸들을 구한다. 여기서, 현재의 마우스 좌표값도 넘겨진다. lParam에 의해 지정되고 있는 MOUSEHOOKSTRUCT 구조체에 있는 POINT 구조체를 사용하고 있다는 것을 주의하기 바란다. 그리고, 윈도우 핸들을 WM_MOUSEHOOK 메세지와 함께 PostMessage 함수를 호출하는 것으로써, 메인 프로그램에 전송하고 있다. 여기서 한가지 기억해 두어야 할 것이 있다. 그것은, 훅 프로시저에서 SendMessage 함수를 호출하면 안된다는 것이다.만약 호출하게 되면, 메세지 데드락 상태에 빠지게 될것이다.그러므로 반드시 PostMessage 함수를 사용한다.
MOUSEHOOKSTRUCT 구조체는 다음과 같이 되어 있다.
MOUSEHOOKSTRUCT STRUCT DWORD pt POINT <> hwnd DWORD ? wHitTestCode DWORD ? dwExtraInfo DWORD ? MOUSEHOOKSTRUCT ENDS
- pt
현재 마우스가 가리키고 있는 커서의 화면 좌표값- hwnd
마우스 메세지를 받는 윈도우의 핸들. 윈도우가 SetCapture 함수를 호출하면, 마우스 입력은 그 윈도우로 리다이렉트 된다. 이런이유로,이 멤버 변수는 사용하지 않지만, WindowFromPoint 함수를 호출하는 방법도 있다.- wHitTestCode
HITTEST의 값을 지정한다. HITTEST값은 현재의 마우스 위치에 대한 보다 자세한정보가 들어있고 마우스 커서가 윈도우의 어떤부분에 있는지를 알 수 있다.- dwExtraInfo
메세지에 정보를 추가하는데 사용하는 변수. 보통, mouse_event 함수에 의해서 설정되고 GetMessageExtraInfo 함수로 얻을 수 있다.메인 윈도우는 WM_MOUSEHOOK 메세지를 받게되면, wParam에 저장되어 있는 윈도우 핸들을 사용해서 윈도우에 대한 정보를 얻는다.
.elseif uMsg==WM_MOUSEHOOK invoke GetDlgItemText, hDlg, IDC_HANDLE, addr buffer1, 128 invoke wsprintf, addr buffer, addr template, wParam invoke lstrcmpi, addr buffer, addr buffer1 .if eax! =0 invoke SetDlgItemText, hDlg, IDC_HANDLE, addr buffer .endif invoke GetDlgItemText, hDlg, IDC_CLASSNAME, addr buffer1, 128 invoke GetClassName, wParam, addr buffer, 128 invoke lstrcmpi, addr buffer, addr buffer1 .if eax! =0 invoke SetDlgItemText, hDlg, IDC_CLASSNAME, addr buffer .endif invoke GetDlgItemText, hDlg, IDC_WNDPROC, addr buffer1, 128 invoke GetClassLong, wParam, GCL_WNDPROC invoke wsprintf, addr buffer, addr template, eax invoke lstrcmpi, addr buffer, addr buffer1 .if eax! =0 invoke SetDlgItemText, hDlg, IDC_WNDPROC, addr buffer .endif깜박거림을 막기위해, 에디트 컨트롤에 있는 문자열이 이미 존재하는지와 현재 마우스가 가리키고 있는 윈도우에 대한 정보의 문자열이 같은지를 체크해서, 만약 같다면 아무일도 하지 않는다.
GetClassName함수를 호출해서 클래스명을 알아내고, GetClassLong 함수를 GCL_WNDPROC로 지정해서 윈도우 프로시저의 주소를 얻고, 문자열을 형식에 맞게 다듬어서 에디트 컨트롤에 전송한다.
invoke UninstallHook invoke SetDlgItemText, hDlg, IDC_HOOK, addr HookText mov HookFlag, FALSE invoke SetDlgItemText, hDlg, IDC_CLASSNAME, NULL invoke SetDlgItemText, hDlg, IDC_HANDLE, NULL invoke SetDlgItemText, hDlg, IDC_WNDPROC, NULL유저가 Unhook 버튼을 누르면, 훅 DLL에 있는 UninstallHook 함수가 호출된다. UninstallHook 함수는 단지 UnhookWindowsHookEx 함수를 호출하는 것으로,버튼에 표시되는 문자열을 「Hook」로 변경하고, HookFlag를 FALSE로 설정하고, 에디트 컨트롤의 문자열을 클리어 한다.
Makefile의 링커 옵션으로 한가지 주의해서 봐야할 것이 있다.
Link /SECTION:.bss, S /DLL /DEF:$(NAME). def /SUBSYSTEM:WINDOWS이전프로세스가 훅 DLL의 같은 초기화 되지 않은 데이터를 사용하기 위해, 공유 섹션으로서 .bss 섹션을 지정하고 있다. 이 옵션이 없다면 훅 DLL은 정상적으로 작동하지 않을 것이다.