개발자를위한윈도우후킹테크닉 Spy++ 클론 imspy 제작하기 지난시간까지우리는 WH_GETMESSAGE, WH_CALLWNDPROC, WH_CALLWNDPROCRET 을 사용하는이론적인방법과동기화방법에대해서살펴보았다. 이번시간에는세가지훅을 사용해서 Spy++ 과같이메시지처리과정을보여주는프로그램을작성할것이다. 목차 목차...1 필자소개...1 연재가이드...1 연재순서...2 필자메모...2 Intro...2 공용자료들...4 훅프로시저...6 메시지관리... 10 IPC 쓰레드... 12 윈도우찾기... 15 도전과제... 18 진짜 Spy++ 을제작하고싶은분들을위한팁... 18 참고자료... 19 필자소개 신영진 pop@jiniya.net 부산대학교정보, 컴퓨터공학부 4 학년에재학중이다. 모자란학점을다채워서졸업하는것이꿈이되버린소박한괴짜프로그래머. 병역특례기간을포함해서최근까지다수의보안프로그램개발에참여했으며, 최근에는모짜르트에심취해있다. 연재가이드 운영체제 : 윈도우 2000/XP 개발도구 : 마이크로소프트비주얼스튜디오 2003 기초지식 : C/C++, Win32 프로그래밍
응용분야 : 메시지모니터링프로그램 연재순서 2006. 05 키보드모니터링프로그램만들기 2006. 06 마우스훅을통한화면캡쳐프로그램제작 2006. 07 메시지훅이용한 Spy++ 흉내내기 2006. 08 SendMessage 후킹하기 2006. 09 Spy++ 클론 imspy 제작하기 2006. 10 저널훅을사용한매크로제작 2006. 11 WH_SHELL 훅을사용해다른프로세스윈도우서브클래싱하기 2006. 12 WH_DEBUG 훅을이용한훅탐지방법 2007. 01 OutputDebugString 의동작원리 필자메모 훌륭한책은마음의양식이되기도하고한사람의삶을바꾸어놓기도한다. 물론기술서적중에이러한책은잘없지만, 저자의깊은이해와설명에감탄이나오는책은종종만날수있다. Windows 프로그래밍관련책중에위와같은느낌을받은책을한권만꼽으라면주저없이 Jeffrey Richter 의 Programming Applications for Microsoft Windows(4/e) 를선택할것이다. 이책은 Windows 시스템프로그래밍의거의대부분의영역을커버하고있으며, Windows 시스템의동작원리에대해서도자세하게설명하고있다. 아직까지책을읽어보지않았다면꼭읽어보길권하고싶다. 필자가이책을처음접한건 2003 년이었다. 하지만지금까지도이책을볼때면눈물을 흘린다. 이책을읽고난후에는 Windows 시스템에대해서좀더깊이있는이해를할수 있게될것이다. Intro 이번시간에우리는지금까지공부했던세가지훅 (WH_GETMESSAGE, WH_CALLWNDPROC, WH_CALLWNDPROCRET) 를사용해서윈도우로전송되는메시지를모니터링하는프로그램을작성해볼것이다. 이프로그램이하는일은 Spy++ 과유사하다. Spy++ 의기능 2/19 페이지
중일부를구현한것이라생각하면편하다. 우리가제작할 imspy 를간단히살펴보도록 하자. 화면 1 imspy 에서윈도우목록을표시한화면 < 화면 1> 은 imspy 에서윈도우목록을표시한화면이다. 기본적으로 Spy++ 과동일하게 트리뷰로윈도우의계층구조를표시해주며, 오른쪽에는선택된윈도우에대한일부 속성을구해서표시해준다. 화면 2 imspy 에서메시지옵션을설정하는화면 3/19 페이지
감시메뉴에서로그메시지를선택하면 < 화면 2> 와같은대화상자가표시된다. 왼쪽에는모니터링할메시지목록이트리형태로나타난다. 오른쪽에는창찾기도구와선택한창에대한속성이나타난다. 창찾기도구는 Spy++ 과마찬가지로드래그를통해서윈도우를찾는기능을한다. 화면 3 imspy 에서메시지후킹을통해서메시지를확인하는화면 < 화면 3> 은 imspy 를사용해서키보드메시지를모니터링하고있는화면이다. 종류는 Spy++ 과마찬가지로 P, S, R 이출력된다. RETURN 부분에는 SendMessage 로전달된메시지가리턴될때의결과값이출력된다. 공용자료들 imspy 전역에서공통으로사용되는자료형이 < 리스트 1> 에나와있다. 주석에개략적인설명이나와있다. IMSPYMSGDATA 구조체는파일매핑오브젝트에저장되는내용이다. type 은메시지가발생된상황을나타낸다. 우리는세개의훅을설치해서메시지정보를받기때문에총세가지종류가나올수있다. MSGT_POST, MSGT_SEND, MSGT_SENDRET 가그것이다. result 는 type 이 MSGT_SENDRET 인경우에만의미를가진다. 리스트 1 공용자료들 const UINT MSGT_POST = 1; // WH_GETMESSAGE 4/19 페이지
const UINT MSGT_SEND const UINT MSGT_SENDRET = 2; // WH_CALLWNDPROC = 4; // WH_CALLWNDRETPROC typedef struct _IMSPYMSGDATA UINT type; // 메시지종류 HWND hwnd; UINT message; WPARAM wparam; LPARAM lparam; LRESULT result; BYTE param[2048]; // 메시지발생윈도우 // 메시지번호 // WPARAM // LPARAM // SendMessage 리턴값 // 메시지관련정보 IMSPYMSGDATA, *PIMSPYMSGDATA; #ifndef _T #define _T TEXT #endif LPCTSTR const IMSPY_MUTEXNAME = _T("imspy_mutex"); LPCTSTR const IMSPY_BUFFER_EVENTNAME = _T("imspy_bufferReady"); LPCTSTR const IMSPY_DATA_EVENTNAME = _T("imspy_dataReady"); LPCTSTR const IMSPY_FILEMAPNAME = _T("imspy_filemap"); LPCTSTR const IMSPYHK_DLLNAME = _T("imspyhk.dll"); param 은메시지별로추가적인정보를담는역할을한다. 메시지로전달되는 WPARAM 이나 LPARAM 에포인터가포함된경우해당포인터에대한정보는컨텍스트가변경되면알수없다. 이러한정보를훅프로시저내에서복사하는데사용한다. 이러한메시지의대표적인예로 WM_WINDOWPOSCHANGED 가있다. 이메시지의경우 LPARAM 으로 WINDOWPOS 구조체의포인터가넘어온다. 이경우에 WINDOWPOS 구조체를훅프로시저에서복사해두지않는다면나중에해당내용을참조할수가없다. 다음으로나오는문자열들은전역커널오브젝트의이름을나타낸다. 각오브젝트가하는 일은 < 표 1> 에나타나있다. IMSPYHK_DLLNAME 은 imspy 에서사용할후킹 DLL 모듈의 이름을나타낸다. 표 1 커널오브젝트역할 오브젝트이름 IMSPY_MUTEXNAME IMSPY_BUFFER_EVENTNAME 역할 imspy 의중복실행을방지하는역할을한다. 또한훅프로시저에서무한대기를방지하는용도로사용된다. 파일맵오브젝트에훅프로시저가접근해도됨을알리는이벤트다. 5/19 페이지
IMSPY_DATA_EVENTNAME 파일맵오브젝트에훅프로시저가데이터기록을 완료했음을알리는이벤트다. IMSPY_FILEMAPNAME 프로세스간통신을위해사용되는파일맵오브젝트다. 훅프로시저 세가지훅에대한훅프로시저코드가 < 리스트 2> 에나타나있다. GetMessageProc 은 WH_GETMESSAGE 훅에대한, CallWndProc 은 WH_CALLWNDPROC 에대한, CallWndRetProc 은 WH_CALLWNDPROCRET 에대한훅프로시저다. 세개의훅프로시저모두두개의헬퍼함수를사용해서작업을수행한다. 메시지정보를수집하는 FillMsgData 함수와, 수집된메시지를응용프로그램에게전달하는 NotifyMsgData 가그것이다. 리스트 2 훅프로시저코드 LRESULT CALLBACK GetMessageProc(int code, WPARAM w, LPARAM l) if(code == HC_ACTION && w == PM_REMOVE) PMSG msg = (PMSG) l; IMSPYMSGDATA data; FillMsgData(&data, MSGT_POST, msg->hwnd, msg->message, msg->wparam, msg->lparam, 0 ); NotifyMsgData(&data); return CallNextHookEx(NULL, code, w, l); LRESULT CALLBACK CallWndProc(int code, WPARAM w, LPARAM l) LRESULT ret = CallNextHookEx(NULL, code, w, l); if(code == HC_ACTION) PCWPSTRUCT cwp = (PCWPSTRUCT) l; IMSPYMSGDATA data; FillMsgData(&data, MSGT_SEND, cwp->hwnd, cwp->message, 6/19 페이지
cwp->wparam, cwp->lparam, 0 ); NotifyMsgData(&data); return ret; LRESULT CALLBACK CallWndRetProc(int code, WPARAM w, LPARAM l) LRESULT ret = CallNextHookEx(NULL, code, w, l); if(code == HC_ACTION) PCWPRETSTRUCT cwpr = (PCWPRETSTRUCT) l; IMSPYMSGDATA data; FillMsgData( &data, MSGT_SENDRET, cwpr->hwnd, cwpr->message, cwpr->wparam, cwpr->lparam, cwpr->lresult ); NotifyMsgData(&data); return ret; FillMsgData 함수가하는일은인자로넘어온정보를이용해서 PIMSPYMSGDATA 의각필드를설정하는것이다 (< 리스트 3> 참고 ). 필드설정이끝나고나면 param 데이터를복사하는부분이나온다. 현재함수에는두가지메시지에대한 param 정보설정코드만나타나있다. 실제 Spy++ 과같이전체메시지에대한구조체정보를표시하기위해서는이부분에포인터가전달되는모든메시지에대한처리루틴을넣어야한다. 리스트 3 FillMsgData 코드 void FillMsgData( PIMSPYMSGDATA data, UINT type, HWND hwnd, UINT msg, WPARAM w, LPARAM l, LRESULT result ) ASSERT(data!= NULL); 7/19 페이지
data->type = type; data->hwnd = hwnd; data->message = msg; data->wparam = w; data->lparam = l; data->result = result; switch(msg) case WM_WINDOWPOSCHANGED: CopyMemory(data->param, (PWINDOWPOS) l, sizeof(windowpos)); break; case WM_NEXTMENU: CopyMemory(data->param, (PMDINEXTMENU) l, sizeof(mdinextmenu)); break; < 리스트 4> 는 FillMsgData 함수를통해서수집된정보를실제로응용프로그램으로전달되는부분이다. NotifyMsgData 는일단버퍼에기록을허용하는이벤트를대기한다. 해당이벤트가발생하면 FillMsgData 에서지역변수에수집된메시지를파일맵오브젝트로복사를한다. 그리고데이터기록이완료되었다는이벤트를전달한다. 여기서굳이지역변수를사용해서메시지를수집한주된이유는훅프로시저가지나치게오랜시간동안잠기지않도록하기위해서다. ( 물론지금은 FillMsgData 가굉장히간단하기때문에 CopyMemory 하는것이더오래걸릴수도있다.) SafeWaitForSingleObject 는 Wait 함수에의해서훅프로시저가무한대기에빠지는현상을방지하기위해서사용된것이다. 메인프로그램이강제종료되면대기중인이벤트에대한처리를마무리할수없다. 만약운이나쁘게아주좋지않은상황에메인프로그램이강제종료된다면훅루틴은메인프로그램이다시기동하기전까지잠기게된다. 이러한상황을방지하기위해서메인프로그램이동작중인지를체크해서없는경우에는대기하지않고빠져나가도록처리한것이다. 이와같은방법을사용함으로써우리는최악의상황에도훅프로시저는 500ms 이하의대기만수행한다는보장을할수있다. 리스트 4 메시지정보를응용프로그램으로전달하는함수들 DWORD WINAPI CheckMutex(LPCTSTR mutexname) HANDLE h = OpenMutex(SYNCHRONIZE, FALSE, mutexname); if(h) CloseHandle(h); return TRUE; 8/19 페이지
return FALSE; DWORD WINAPI SafeWaitForSingleObject(HANDLE h, LPCTSTR mutexname) DWORD r = WAIT_FAILED; while(checkmutex(mutexname)) r = WaitForSingleObject(h, 500); if(r!= WAIT_TIMEOUT) break; return r; void NotifyMsgData(PIMSPYMSGDATA data) if(safewaitforsingleobject(g_bufferreadyevent, IMSPY_MUTEXNAME) == WAIT_OBJECT_0) CopyMemory(g_msgData, data, sizeof(*data)); SetEvent(g_dataReadyEvent); 박스 1 TerminateProcess 프로세스의강제종료는후킹프로그램에게는재앙과도같은일이다. 작업관리자에서프로세스를강제종료시키면 TerminateProcess 라는 API 가호출된다. 이 API 가호출되면운영체제는해당프로세스를강제로메모리에서내리고자원을반환한다. 하지만이과정에서해당프로그램에게는어떠한통지도가지않는다. 이경우에혼자동작하는프로그램의경우에는큰문제가없다. 할당된메모리와커널오브젝트의경우운영체제에서알아서해제해주기때문이다. 하지만여러프로세스에걸쳐서동기화해서움직이는후킹프로그램의경우큰문제가발생할수있다. 왜냐하면동기화에사용된오브젝트를해당프로그램에서적법한절차를거치지않고파기시켰기때문에다른프로세스에서해당동기화오브젝트에접근할수없는일이발생하기때문이다. 만약해당프로그램이전체프로세스를후킹하고있었다면최악의경우에는컴퓨터를껐다켜야하는상황이발생할수도있다. 따라서후킹프로그램을작성할때에는항상 TerminateProcess 가발생하더라도안정적으로동작하도록하는데많은신경을써야한다. 9/19 페이지
메시지관리 Windows 에는수천종류의메시지가있다. 이렇게많은메시지를분류해서관리하는작업은상당히힘든일이다. 실제로 imspy 에는모든종류의메시지가들어있진않다. 100 여가지의메시지에대한정보만추가되어있다. 메시지정보를저장하는구조체는 < 리스트 5> 에나타나있다. 메시지정보를저장하는구조체와해당메시지가포함될카테고리를저장할구조체가있다. 두구조체를사용해서프로그램에서처리할메시지목록을 < 리스트 6> 과같이구조체배열을사용해서저장한다. 리스트 5 메시지정보구조체 // 메시지정보구조체 typedef struct _MSGDATA UINT category; // 카테고리 UINT id; // 메시지번호 LPCTSTR name; // 메시지이름 MSGDATA, *PMSGDATA; // 카테고리정보구조체 typedef struct _MSGCATDATA UINT category; // 카테고리 LPCTSTR name; // 카테고리명 MSGCATDATA, *PMSGCATDATA; 리스트 6 메시지정의부분 const UINT MSGCT_KEYBOARD = 1; // 키보드메시지 const UINT MSGCT_MOUSE = 2; // 마우스메시지 const UINT MSGCT_CLIPBOARD = 3; // 클립보드메시지 const UINT MSGCT_DLG = 4; // 다이알로그메시지 //... 중략 #define DEFMSG(category, id, name) category, id, name #define DEFMSG_KEY(id, name) DEFMSG(MSGCT_KEYBOARD, id, name) #define DEFMSG_MOUSE(id, name) DEFMSG(MSGCT_MOUSE, id, name) #define DEFMSG_CLIP(id, name) DEFMSG(MSGCT_CLIPBOARD, id, name) //... 중략 const MSGCATDATA CMsgData::m_msgCatData[] = MSGCT_KEYBOARD, _T(" 키보드 "), MSGCT_MOUSE, _T(" 마우스 "), MSGCT_CLIPBOARD, _T(" 클립보드 "), 10/19 페이지
//... 중략 const MSGDATA CMsgData::m_msgData[] = DEFMSG_KEY(WM_KEYDOWN, _T("WM_KEYDOWN")), DEFMSG_KEY(WM_KEYUP, _T("WM_KEYUP")), DEFMSG_KEY(WM_CHAR, _T("WM_CHAR")), DEFMSG_KEY(WM_DEADCHAR, _T("WM_DEADCHAR")), DEFMSG_KEY(WM_SYSKEYDOWN, _T("WM_SYSKEYDOWN")), DEFMSG_KEY(WM_SYSKEYUP, _T("WM_SYSKEYUP")), DEFMSG_KEY(WM_SYSCHAR, _T("WM_SYSCHAR")), DEFMSG_KEY(WM_SYSDEADCHAR, _T("WM_SYSDEADCHAR")), DEFMSG_KEY(WM_UNICHAR, _T("WM_UNICHAR")), DEFMSG_MOUSE(WM_MOUSEMOVE, _T("WM_MOUSEMOVE")), DEFMSG_MOUSE(WM_LBUTTONDOWN, _T("WM_LBUTTONDOWN")), DEFMSG_MOUSE(WM_LBUTTONUP, _T("WM_LBUTTONUP")), DEFMSG_MOUSE(WM_LBUTTONDBLCLK, _T("WM_LBUTTONDBLCLK")), //... 중략 메시지를실제사용자가알아보기쉽게문자열로변환해주는작업은 < 리스트 7> 에나타난자료구조를통해서수행된다. FDECODEMSG 는메시지를문자열로변환해주는함수포인터다. 각각의메시지는하나이상의변환함수를가진다. MSGDECODER 를보면변환함수가벡터로구성된것을볼수있다. MsgIndexMap 은메시지번호와거기에따른정적배열의인덱스넘버와변환함수를저장한다. 즉, 메시지번호가날라오면단번에인덱스번호와, 변환함수목록을알수있는것이다. MsgIndexMap 은프로그램이로딩될때생성된다. 다음으로중요한자료구조는 MsgSet 이다. 각각의메시지모니터링윈도우는서로다른메시지를처리하게된다. 각각의윈도우는자신이처리해야하는메시지를 MsgSet 에저장해서각자담아둔다. 메시지가발생하면모든윈도우로해당정보를전송하고, 해당윈도우는자신의 MsgSet 에메시지번호가존재하는경우에만해당내용을처리해서추가한다. 리스트 7 메시지디코딩정보를저장할구조체 // 메시지디코딩함수 typedef BOOL (CALLBACK *FDECODEMSG)(LPTSTR buf, UINT size, IMSPYMSGDATA &data); // 메시지디코딩함수목록을저장할벡터 typedef std::vector<fdecodemsg> DecodeFuncVec; typedef std::vector<fdecodemsg>::iterator DecodeFuncVIt; typedef std::vector<fdecodemsg>::reverse_iterator DecodeFuncVRIt; // 메시지디코더정보 11/19 페이지
typedef struct _MSGDECODER UINT no; DecodeFuncVec fn; MSGDECODER, *PMSGDECODER; typedef std::set<uint> MsgSet; typedef std::set<uint>::iterator MsgSIt; typedef std::map<uint, MSGDECODER> MsgIndexMap; typedef std::map<uint, MSGDECODER>::iterator MsgIndexMIt; 위에서소개한모든데이터를저장하고있는클래스가 CMsgData 이다 (< 리스트 8> 참고 ). 메시지목록을정적배열로가지고있다. 해당메시지목록에대한 MsgIndexMap 은생성자에서초기화한다. 가장기본적인메시지디코딩함수인 Default 를포함하고있다. Get 을통해서메시지번호에대한메시지정보구조체에접근할수있고, Decode 를통해서수신된메시지에대한정보를생성할수있다. 리스트 8 CMsgData 클래스 class CMsgData private: static const MSGDATA m_msgdata[]; static const MSGCATDATA m_msgcatdata[]; MsgIndexMap m_index; protected: CMsgData(); static BOOL CALLBACK Default(LPTSTR buf, UINT size, IMSPYMSGDATA &data); public: const PMSGDATA Get(UINT msg); BOOL Enum(CMsgDataCallback &cb); BOOL Enum(CMsgCatDataCallback &cb); BOOL Decode(LPTSTR buf, UINT size, IMSPYMSGDATA &data); BOOL AddDecoder(UINT msg, FDECODEMSG fn); BOOL DeleteDecoder(UINT msg, FDECODEMSG fn); ; friend CMsgData &MsgData(); IPC 쓰레드 < 리스트 9> 에훅 DLL 과통신하는 IPC 쓰레드의코드가나와있다. 쓰레드는굉장히간단하다. 시작과동시에 bufferready 이벤트를설정해서다른 DLL 들이버퍼에접근할수있도록만든다. 그리고 dataready 이벤트가설정되면공유메모리의내용을지역버퍼로 12/19 페이지
복사한후큐에추가하고메인윈도우에메시지가발생했음을알려준다. m_exitevent 는 쓰레드내부적으로종료체크를하기위해서사용된다. 리스트 9 훅프로시와통신하는쓰레드코드 void CCommThread::Go() //... 중략 IMSPYMSGDATA msg; HANDLE events[2] = m_exitevent, dataready ; HANDLE lock[2] = m_exitevent, m_lockmutex ; DWORD s = 0; for(;;) SetEvent(bufferReady); s = WaitForMultipleObjects(2, events, FALSE, INFINITE); if(s == WAIT_OBJECT_0) break; if(s!= WAIT_OBJECT_0 + 1) continue; CopyMemory(&msg, data, sizeof(imspymsgdata)); s = WaitForMultipleObjects(2, lock, FALSE, INFINITE); if(s == WAIT_OBJECT_0) break; if(s == WAIT_OBJECT_0 + 1 s == WAIT_ABANDONED) CMutexLocker locker(m_lockmutex); m_msgs.push(msg); PostMessage(m_notifyWnd, WM_MSGFIRE, 0, 0); 위에서전달한 WM_MSGFIRE 의메시지핸들러가 < 리스트 10> 에나와있다. 메시지핸들러에서는쓰레드큐에서메시지데이터를하나꺼내서 MDI 자식들을순회하면서메시지를추가해주는일을한다. m_lockmutex 는쓰레드의큐에접근을동기화시키기위한뮤텍스다. 리스트 10 메시지처리함수 LRESULT CMainFrame::OnMsgFire(WPARAM w, LPARAM l) 13/19 페이지
CWnd *wnd; CMsgListFrm *p; IMSPYMSGDATA data; for(;;) // 버퍼에서메시지하나를빼낸다. CMutexLocker locker(m_commthread->m_lockmutex); if(m_commthread->m_msgs.empty()) break; data = m_commthread->m_msgs.front(); m_commthread->m_msgs.pop(); // 메시지목록윈도우에메시지를추가한다. for(wnd = m_mdiclient.getwindow(gw_child); wnd!= NULL; wnd = wnd->getwindow(gw_hwndnext)) if(wnd->iskindof(runtime_class(cmsglistfrm))) p = (CMsgListFrm *) wnd; p->addmessge(data); return 0; 실제로메시지를리스트에추가하는부분은 < 리스트 11> 에나타나있다. m_hookenabled 는현재훅이활성화되어있는지를나타난다. 로그중지메뉴를선택하면 m_hookenabled 가 FALSE 로설정된다. m_watchwnd 는후킹대상윈도우를나타낸다. m_watchmsgs 는모니터링중인메시지목록을나타낸다. 리스트 11 리스트에메시지를추가하는부분 void CMsgListFrm::AddMessge(IMSPYMSGDATA &data) if( m_hookenabled && data.hwnd == m_watchwnd && m_watchmsgs.find(data.message)!= m_watchmsgs.end()) LPCTSTR type[] = _T(""), _T("P"), _T("S"), _T("R") ; CString buf; int cnt = m_lvcmsgs.getitemcount(); 14/19 페이지
buf.format(_t("%04d"), cnt+1); m_lvcmsgs.insertitem(cnt, buf); m_lvcmsgs.setitemtext(cnt, 1, type[data.type]); buf.format(_t("%08x"), data.hwnd); m_lvcmsgs.setitemtext(cnt, 2, buf); //... 중략 윈도우찾기 많은사람들에게 Spy++ 하면가장먼저떠오르는기능은아마도마우스로커서를드래그하면서윈도우를찾는기능일것이다. 이기능의경우눈에보이는윈도우를바로찾아준다는점에서굉장히유용한기능이다. 이기능은 imspy 에서는메시지옵션을설정하는대화상자에구현되어있다. < 화면 2> 에서창찾기도구에나타난아이콘을드래그하면 Spy++ 과같이창이찾아지는것을확인할수있다. 마우스포인터위의윈도우를찾는함수로 WindowFromPoint, ChildWindowFromPoint, RealChildWindowFromPoint 등의함수가있다. 하지만 ChildWindowFromPoint 의경우그룹상자위에놓여진 Static 컨트롤등을정확하게찾아내지못한다는단점이있고, RealChildWindowFromPoint 는그러한문제가해결되었으나, 95 에서는지원하지않고마우스위치에있는가장작은자식윈도우를정확하게찾아내진못한다는단점이있다. < 리스트 12> 에는이러한문제를해결한코드가나와있다. 리스트 12 마우스포인터위의윈도우를찾는코드 HWND FindSmallestChildWindowFromPoint(HWND hwnd, CPoint &pt) HWND child = GetWindow(hwnd, GW_CHILD); HWND result = NULL; if(child) HWND h; CRect rc; CRect tmp; BOOL issmall; // 자식을조사한다. for(child = GetWindow(hwnd, GW_CHILD); child!= NULL; child = GetWindow(child, GW_HWNDNEXT)) 15/19 페이지
// 자기보다더깊은곳의자식을먼저찾는다. h = FindSmallestChildWindowFromPoint(child, pt); // 찾았으면리턴한다. if(h) return h; GetWindowRect(child, &tmp); // 현재윈도우영역에마우스포인터가포함되고 // 처음이거나이전에찾은윈도우보다작은경우 issmall = tmp.width() <= rc.width() && tmp.height() <= rc.height(); if(tmp.ptinrect(pt) && (!result issmall)) result = child; rc = tmp; return result; HWND FindSmallestWindowFromPoint(CPoint &pt) HWND hwnd = WindowFromPoint(pt); if(hwnd) HWND child = FindSmallestChildWindowFromPoint(hwnd, pt); if(child) return child; return hwnd; 기본적인아이디어는 WindowFromPoint 를사용해서큰윈도우를찾은다음, 그아래자식들을기준으로깊이우선탐색 (depth-first search) 를하는것이다. 이렇게할경우가장깊은자식에게우선순위가간다. 같은깊이의자식들중에는마우스포인터를포함하는가장작은영역을가진윈도우를찾도록되어있다. 그럼이제마우스트래킹을하면서윈도우를찾는방법을알아보자. 마우스트래킹을하기위해서는네개의메시지핸들러를작성해주어야한다. WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_LBUTTONUP, WM_CAPTURECHANGED 가그것이다. WM_LBUTTONDOWN 이발생하면 SetCapture 를통해서마우스를캡쳐하고트래킹을시작하면된다 (< 리스트 13> 참고 ). WM_MOUSEMOVE 에서는트래킹이시작되었다면마우스아래에있는윈도우를찾아서표시하는일을한다 (< 리스트 15> 참고 ). 16/19 페이지
WM_LBUTTONUP 과 WM_CAPTURECHANGED 에서는 ReleaseCapture 를통해서마우스 캡쳐를해제하고트래킹을중지하면된다 (< 리스트 14> 참고 ). 리스트 13 마우스트래킹시작함수 void CMsgOptionDlg::StartFindWindow() m_finding = TRUE; // 트래킹중인지를나타내는멤버변수 m_watchwnd = 0; // 최종적으로발견한윈도우핸들 ChangeFinderIcon(); // 파인더아이콘변경 m_prevcursor = SetCursor(m_finderCursor); // 커서변경 //ShowWindow(SW_HIDE); SetCapture(); 리스트 14 마우스트래킹중지함수 void CMsgOptionDlg::StopFindWindow() m_finding = FALSE; ChangeFinderIcon(); SetCursor(m_prevCursor); //ShowWindow(SW_SHOW); ReleaseCapture(); DrawBorder(); 리스트 15 마우스포인터에있는윈도우를찾아서표시하는함수 void CMsgOptionDlg::Find() CPoint pt; GetCursorPos(&pt); HWND hwnd = FindSmallestWindowFromPoint(pt); if(hwnd && hwnd!= m_watchwnd) DrawBorder(); // 이전에그려진경계를지운다 m_watchwnd = hwnd; UpdateWindowInfo(); DrawBorder(); // 새롭게찾은윈도우의경계를그린다. 17/19 페이지
도전과제 필자가처음에 imspy 를제작할때에윈도우목록필터링기능과메시지디코딩기능을넣으려했었으나시간관계로기능을추가하지못했다. 이번달도전과제는이기능을추가하는것으로해보자. 윈도우목록필터링기능은많은윈도우목록중에자신이원하는특정윈도우만보여주는기능을하는것이다. 예를들면화면에보이는창만출력한다거나아니면특정프로세스의창만출력하는기능등을생각해볼수있다. 이경우한가지조심해야할것은자신은화면에나타나지않는창이라고무조건제거해서는안된다는점이다. 자식이화면에표시되는창이라면그부모가화면에나타나지않아도표시해주어야지정확한계층구조를확인할수있다는점이다. 메시지디코딩기능은메시지디코딩함수를추가해서설명부분에디코딩한결과를출력하는것이다. Spy++ 은메시지가출력될때에 WPARAM, LPARAM 이런식으로표시하지않고각메시지에맞게항목을보기좋게출력해주는것을볼수있다. 이러한기능을하도록구현해보자. 설명부분에추가하면될것이다. 진짜 Spy++ 을제작하고싶은분들을위한팁 이번달의 imspy 샘플은진짜 Spy++ 의클론을구현하기위한좋은파일럿프로그램이될수있다. 하지만이번달의우리가제작한 imspy 는지역훅을사용했기때문에기능구현에한계를가지고있다. Spy++ 의전체기능을작성해야한다면전역훅을사용하는것이편리하다. 지난시간까지우리는전역훅의경우시스템성능이느려질수있고, 위험하기때문에사용하지않는것이좋다고했었다. 이말은실제로맞는말이다. 하지만항상트레이드오프는있는법이다. 느려지고위험하지만그것보다얻는이득이크다면설치해서사용해볼수있다는점이다. 전역훅을사용할경우엔어떤기능을구현하기쉬울까? 첫째는윈도우속성구하는기능이다. 윈도우속성중일부항목은해당윈도우를생성한프로세스의컨텍스트에서만구할수있다. 따라서모든윈도우의속성을구하기위해서는모든윈도우의프로세스컨텍스트에서실행이되어야하고이는곧전역적으로훅이설치되어야함을의미한다. 실제로 Spy++ 은전역훅을설치하고해당윈도우로 WM_NULL 메시지가포스팅되면해당윈도우의속성을구해서공유섹션부분에기록한다. 18/19 페이지
두번째기능은여러윈도우를동시에모니터링하는기능이다. Spy++ 의메시지옵션부분을보면부모윈도우, 소유자윈도우, 같은프로세스윈도우등을같이모니터링할수있는기능이있다. 이러한기능의경우우리와같이지역훅을사용할때에는매우구현이까다롭게된다. 왜냐하면이러한윈도우들이같은쓰레드에존재하라는법이없기때문이다. 물론이두가지를모두전역훅을설치하지않고할수있다. 하지만그렇게할경우엔배보다배꼽이더커지는격이될수있음을기억하자. 참고자료 참고자료 1. Jeffrey Richter. <<Programming Applications for Microsoft Windows (4/E)>> Microsoft Press 참고자료 2. 김상형, <<Windows API 정복 >> 가남사참고자료 3. 김성우, << 해킹 / 파괴의광학 >> 와이미디어참고자료 4. Spy++ 과같이윈도우를찾아내는방법 http://www.codeproject.com/dialog/windowfinder.asp 19/19 페이지