2008-09-01

민성기님의 후킹 강좌(Delphi)

안녕하세요… 민성기 입니다.
화면 키보드를 만들면서 몸에 익힌 훅킹기법~ 남용하면 독이 되지만 잘 만 쓰면 정말 좋은 영약이 되는 것은 세상 다른 이치와 마찬가지겠죠…?? 훅~ 훅훅훅훅~~ (웃음소리 입니다~) 저역시 배우는데 깝깝했던 만큼, 가능하면 쉽게 설명하도록 노력하겠습니다.편의상 존칭은 생략합니다.
이 강좌는 다음과 같은 순서로 진행할 것입니다.
- 훅킹이란 무엇인가- 훅킹의 종류- 훅킹에 사용되는 함수 설명- 훅 프로시저- 훅 프로시저의 위치. 왜 시스템 훅킹은 DLL이어야 하는가.- 실전
{이 글은 Kent Reisdorph가 Delphi Developer's Journal에 기고한 글을 아주 많이 참조하여 작성한 것입니다.}
*** 훅킹이란 무엇인가~누구나 한번쯤, 델파이에 포함되어 있는 스파이 프로그램인 ‘윈 사이트’를 사용해 본 적이 있을 것이다. 이 스파이 프로그램들 처럼, 때때로 윈도우 시스템에서 어떤 일이 일어나고 있는지 알고 싶을 때가 있다. 어떤 어플리케이션이 시작될 때나 끝날 때, 또는 사용자가 쳐대는 마우스의 움직임이나 키보드의 내용 등등… 훅킹이란 윈도우를 괴롭히거나 다른 어플리케이션의 동작을 훔쳐보기 좋아하는 변태적인 프로그래머들을 위해서 윈도우가 마련해 놓은 꽤나 합법적인 통로이다.
이름에서 일 수 있듯이 ‘훅’은 피터팬에 나오는 ‘후크선장’이 오른손에 달고 다니던 엽기적인 갈구리~ 바로 그것이다. 따라서 훅킹은 순 우리말로 하면 ‘갈구리질’ 쯤이 된다. 이 갈구리를 터억~ 하니 메시지가 날아다니는 통로에 찍어놓고 지나가는 메시지를 협박하고 갈취해서 원하는 목적을 이루는 것~ 이것이 훅킹의 모든 것이다. 어떤가~ 듣는 순간, 찌리리~ ‘필’이 오지 않는가~??? ㈜ 물론 표준말은 ‘갈고리’ 입니다만… 왠지 어감이 약해서~ 이 강좌에서는 ‘갈구리’를 쓰겠습니다. 쿄호홋~

*** 훅킹의 종류~갈구리를 찍어두는 위치에 따라 훅은 크게 두가지로 나뉜다. 한 쓰레드만을 집중적으로 괴롭히는 훅킹을 ‘쓰레드 훅킹’, 통 크게 시스템 전체를 상대로 맞짱을 뜨는 훅킹을 ‘시스템 훅킹’이라고 한다. 괴롭히고 싶은 메시지에 따라 갈구리 또한 취사선택 해야 하는데… 이는 머리엔 십자, 배엔 일자 드라이버를 사용해야 하는 일반적인 게임의 법칙과 같다. 즉 윈도우에 관련된 메시지를 상대하기 위해서는 WH_CALLWNDPROC를, 키보드 메시지와 대적하기 위해서는 WH_KEYBOARD라는 갈구리를 사용해야 한다는 말이다. 또한 때와 장소를 잘 가려서 적절한 갈구리를 사용해야 하는데… WH_SYSMSGFILTER같은 무식한 갈구리를 보잘것없는 쓰레드를 상대로 휘두르려 한다면… 큰 형님인 윈도우가 공포의 퍼런화면을 보여주며 ‘고만 밥숟갈 놓지~’ 라며 협박을 하기 때문이다. 자, 이제 우리가 고를 수 있는 갈구리와 타격대상등을 적어둔 취급 설명서를 살펴보자.
**갈구리 취급 설명서* WH_CALLWNDPROC (대상 : 쓰레드 또는 시스템.)윈도우 관련 메시지들이 처리되기 전에 실컷 괴롭힐 수 있는 있는 갈구리.
* WH_CALLWNDPROCRET (대상 : 쓰레드 또는 시스템.)윈도우 관련 메시지들을 처리된 후 실컷 괴롭힐 수 있는 있는 갈구리.
* WH_CBT (대상 : 쓰레드 또는 시스템.)CBT어플리케이션에 대해 사용할 수 있는 놈이라고 하는데…쓰는놈도 잘 모름.
* WH_DEBUG (대상 : 쓰레드 또는 시스템.)다른 갈구리 사용을 디버그할 수 있는 갈구리
* WH_FOREGROUNDIDLE (대상 : 쓰레드 또는 시스템.)어플리케이션의 맨 앞의 윈도우가 아이들 상태일 때 맘껏 때릴 수 있는 갈구리.
* WH_GETMESSAGE (대상 : 쓰레드 또는 시스템.)메시지 큐로 들어오는 메시지들을 괴롭히는 갈구리.
* WH_JOURNALPLAYBACK (대상 : 시스템 만.)저널 레코드 훅에 의해 저장된 키보드나 마우스 이벤트를 재생시킬 수 있는 갈구리. 일반적으로 이 갈구리를 휘두르는 동안은 키보드나 마우스 사용이 정지된다.
* WH_JOURNALRECORD (대상 : 시스템 만.)모든 키보드와 마우스 동작을 괴롭히는 갈구리. 매크로 같은 녀석을 만들 때 아주 유용하다.
* WH_KEYBOARD (대상 : 쓰레드 또는 시스템.)WM_KEYDOWN, WM_KEYUP 메시지 전문 사냥꾼.
* WH_MOUSE (대상 : 쓰레드 또는 시스템.)마우스 메시지 전문 사냥꾼.
* WH_MOUSE_LL (대상 : 쓰레드 또는 시스템.)NT에서만 사용되는 저수준 마우스 갈구리.
* WH_MSGFILTER (대상 : 쓰레드 또는 시스템.)특정 어플리케이션에서 만들어낸 메뉴, 다이얼로그 박스, 스크롤 박스 같은 놈에서 발생하는 메시지를 전문적으로 괴롭힐 수 있는 갈구리.
* WH_SHELL (대상 : 쓰레드 또는 시스템.)윈도우 셀에 관계된 메시지를 전문으로 하는 갈구리.
* WH_SYSMSGFILTER (대상 : 시스템 만.)모든 어플리케이션이 만들어낸 메뉴, 다이얼로그 박스, 스크롤 박스 같은 놈에서 발생하는 메시지를 전문적으로 괴롭힐 수 있는 갈구리.
갈구리질을 잘 하기 위해서는 손에 딱 맞는 갈구리가 필요하다. 위의 취급 설명서가 때와 장소에 따라 적절히~ 갈구리를 선택할 수 있는 지혜에 도움이 되었으면 한다.
*** 윈도우의 훅 함수들~갈구리질을 위해 준비된 윈도우의 API는 세가지 이다. 갈구리를 찍는 녀석인 SetWindowsHookEx, 다 쓴 갈구리를 정리하는 UnHookWindowsHookEx, 마지막으로 단물 다 뽑아먹은 메시지를 기다리고 있는 다음 갈구리에게 넘겨주는 CallNextHookEx가 그것이다. 이 CallNextHookEx를 호출해야 하는 이유는 간단하다. 이미 알고 있겠지만, 윈도우는 나 혼자만 쓰는 것이 아니기 때문이다. 충분히 메시지를 괴롭혔다고 해도, 아직 뒤에는 이 메시지를 노리는 수많은 갈구리들이 있다는 것을 잊지 말자.
** SetWindowsHookEx~위에서도 설명 했지만, 이 함수는 메시지 통로에 갈구리를 찍기 위해 사용되는 녀석이다. 생긴 모양을 보자.
function SetWindowsHookEx( idHook: Integer; lpfn: TFNHookProc; hmod: HINST; dwThreadId: DWORD): HHOOK; stdcall;
아~ 정말 흉악하게 생겼다~.
첫번째 인자는 사용할 갈구리의 종류를 의미한다. (취급 설명서를 보자.)두번째 인자는 원하는 메시지가 왔을 때 호출될 ‘훅 프로시저’이다. 훅 프로시저에 대해서는 잠시 후에 살펴보자.세번째 인자는 훅 프로시저가 정의된 DLL이나 EXE의 인스탄스이고 마지막 인자는 괴롭힐 쓰레드의 아이디이다. 시스템 전체를 상대로 갈구리질을 하기 위해서는 이 마지막 인자에 0을, 특정 쓰레드를 괴롭히고 싶으면 원하는 쓰레드의 아이디를 넣어주면 된다. 갈구리가 정확히 찍히면, 이 함수는 훅의 핸들을 넘겨주게 된다. 실패하면 리턴값은 0이다. 이 훅의 핸들은 뒤에 갈구리를 해체할 때나 다음 갈구리를 호출할 때 사용되니 잘 보관해야 한다. 다음 예제는 현재 프로세스에 키보드 훅을 걸어주는 예제이다.
varHKbHook : HHOOK;
...
HKbHook := SetWindowsHookEx(WH_KEYBOARD, MyKBHook, HInstance, GetCurrentThreadId);
갈구리가 괴롭힐 쓰레드의 ID를 얻기 위해 GetCurrentThreadid를 사용했으며 결과값을 HKbHook에 저장해 둔 것에 유의하자.
** UnhookWindowsHookEx~갈구리를 제거하기 위해 사용하는 함수이다. 무사히 뽑히면 True값을, 그렇지 않으면 flase를 리턴한다. 대부분의 경우 갈구리를 뽑을 때 발생하는 에러는, 인자로 넘겨준 훅핸들이 이 함수의 맘에 들지 않을 때 발생한다.
Res := UnhookWindowsHookEx(HKbHook);
예제에서 보듯, 별다른 것이 없는 함수이다. 그저 정확한 훅 핸들을 넘겨주기 위해 훅 핸들 보관에 신경써야 한다는 것을 잊지 말자.
** CallNextHookEx~위에서도 잠깐 언급했지만, 우리가 괴롭히려는 메시지에 한이 맺힌 갈구리는 우리만 있는 것이 아니다. 따라서 적당히 손 본 메시지는 다음 갈구리들에게 넘겨주어야 한다. CallNextHookEx는 다음 갈구리에 메시지를 넘겨주기 위해 사용하는 함수이다. 여러 개의 훅이 ‘체인’으로 연결되어 있다고 이해하면 빠를 것이다.(갈구리에 체인까지~ 랄랄라~~) 실제로 우리가 설치한 훅은 윈도우가 다루는 훅 체인의 하나로서 동작하게 된다. 이 함수의 사용 예는 조금 뒤에 훅 프로시저에서 살펴보도록 하자.
*** 훅 프로시저~갈구리질의 가장 중요한 요소, 아니 갈구리질 그 자체를 뜻하는 녀석이다. (프로시저라고 불리지만, 이건 윈도우가 그렇게 부르기 때문이고~ 파스칼의 입장에서 볼 때는 함수 입니다.) 이 훅 프로시저는 우리가 설정한 훅이 발생할 때, 즉 괴롭히고자 하는 메시지가 발생할 때 호출된다. 키보드 훅을 예로 들면, 사용자가 키보드를 누르거나 뗄 때 실행된다. 윈도우는 갈구리의 형식에 따른 훅 프로시저를 정의해 놓았고, 위에서 언급한 키보드의 경우를 예로 들면 다음과 같다.
function MyKBHook(Code: Integer; wParam: WPARAM; lParam: LPARAM) : LongInt; stdcall;
실은, 나머지 갈구리질 함수 역시 모두 똑같이 생겼다. 다만 갈구리의 형식에 따라 넘어오는 wParam과 lParam이 달라질 뿐이다. 예를 들어, 현재 찍어놓은 갈구리가 WH_CALLWNDPROC 라면 wParam은 발생한 메시지를, lParam에는 메시지가 발생한 핸들 등 기타정보를 담은 구조체에 대한 포인터가 넘어온다. WH_KEYBOARD의 경우는 wParam에 가상 키값이, lParam에 키보드의 상태 등 기타 정보가 넘어오게 된다. 갈구리의 형식에 따른 훅 프로시저에 대해서는 SetWindowsHookEx()에 대한 Win32 API 도움말을 살펴보기 바란다. 이 훅프로시저를 작성할 때는 신경을 많이 써야 한다. 마우스 훅을 예로 들어보자. 설치해 놓은 마우스 훅 프로시저는 마우스 메시지가 발생할 때, 즉 마우스를 움직이기만 하면 호출된다. 만약 머리 터지도록 복잡한 계산을 훅 프로시저 내부에서 한다면… 또는 가뜩이나 느린 화면 그리기 같은 녀석을 여기서 처리 한다면… 뭐 윈도우가 괴롭다 괴롭다 못해 뻗어버릴 지도 모른다. 따라서 적당히~ 괴롭혀야 한다는 말씀~~ ^^; 훅 프로시저에서 메시지를 적당히 타작 했으면, CallNextHookEx()를 통해 다음 갈구리를 불러줘야 한다. 그렇게 하지 않으면 위에서 언급했던 훅체인은 깨져버리고, 메시지는 도망가 버리고 만다. 다음은 아무일도 하지 않는 훅 프로시저의 예이다.
function MyKBHook(Code: Integer; wParam: WPARAM; lParam: LPARAM) : LongInt; stdcall;beginResult := CallNextHookEx( HKbHook, Code, wParam, lParam);end;
CallNextHookEx의 첫번째 인자는 훅의 핸들이다. 나머지 인자들은 훅 프로시저로 넘어온 Code와 wParam, lParam을 채워준다. 또한 이 함수의 결과값을 훅 프로시저의 결과값으로 사용한 것에 주의하자.필자처럼 책상이 좁은 사람들은, 맘 먹고 책 한번 읽으려 해도 키보드가 걸리적 거려 힘이 든다. 이 경우, 시스템 전체에 키보드 훅을 걸어놓고 훅 프로시저에서 CallNextHookEx를 호출하지 않으면 키보드가 동작하지 않게 된다. 키보드에 락을 걸어두는 간단한 유틸리티를 만드는 것도 가능할 것이다.
*** 훅 프로시저의 위치. 왜 시스템 훅킹은 DLL이어야 하는가.~위에서 잠깐 언급 했지만, 훅을 설치할 때, 시스템 전체의 메시지를 괴롭히려면 SetWindowsHookEx의 마지막 인자에 0을, 특정 쓰레드만을 괴롭히려면 괴롭히고자 하는 쓰레드의 ID를 넣어준다.
<쓰레드 훅을 설치하는 예>HKbHook := SetWindowsHookEx( WH_KEYBOARD, MyKBHook, HInstance, GetCurrentThreadId);
<시스템 훅을 설치하는 예>HKbHook := SetWindowsHookEx( WH_KEYBOARD, MyKBHook, HInstance, 0);
쓰레드 훅에서의 훅 프로시저 위치는 별 문제가 되지 않는다. 훅의 핸들 등의 변수가 같은 어플리케이션 영역에 존재하기 때문이다. 그러나 시스템 훅에서는 이것이 큰 문제가 된다. 최초에 훅을 설치할 때는 상관 없지만, (실제로 설치는 된다.) 설치한 훅이 동작하려 할 때 훅 프로시저가 특정 어플리케이션의 영역에 있다면, 메시지가 발생한 어플리케이션에서 이 프로시저에 접근할 방법이 없기 때문이다. 때문에 시스템 훅에서는 훅 프로시저를 DLL에 집어 넣어야 한다. 고민해야 할 문제가 한가지 더 있다. DLL내에 전역변수를 선언해 갈구리를 찍을 때 훅의 핸들을 저장해 두고 훅 프로시저의 CallNextHookEx에서 사용한다고 하자. 이 경우 전역변수에는 어떤 값이 들어가 있을까…?? 최초에 SetWindowsHookEx에서 제아무리 기가막히게 훅의 핸들을 얻어다 넣어주었다 하더라도 각 어플리케이션에서 훅 프로시저가 실행될 때 읽어오게 되는 전역변수의 값은 언제나 0이다. 32비트에서는 각 어플리케이션에서 DLL이 호출될 때, 코드만 공유할 뿐이고 데이터는 각각 다른 프로세스 안에서 ‘보호’되고 있기 때문이다. 따라서 어플리케이션마다 따로 호출될 훅 프로시저에서 정확한 훅의 핸들값을 참조할 수 있도록 데이터 교환 방법에 대해 고민해야 한다.
이경우에 가장 좋은 해결방법은 ‘메모리 맵드 파일 아이오’를 이용하는 것이다. 공유할 파일 맵을 설정해 놓고 훅의 핸들이나 기타 필요한 정보들을 보관하는 것이다. 그러나, 이 개념까지 여기서 설명하기에는 조금 벅찬 듯 싶다. 메모리 맵드 파일 아이오에 대해서는 이 갈구리질 강좌가 끝난 후에 다시한번 고민해 보는 기회를 갖도록 하고, 일단 여기서는 임시적으로 VCL의 파일 스트림을 이용해 하드 디스크에 훅의 핸들을 저장하는 방법을 사용하도록 하겠다.

댓글 없음: