개발자를위한윈도우후킹테크닉 SendMessage 후킹하기 지난시간에 WH_GETMESSAGE 훅을통해서메시큐에서메시지가처리되는과정을후킹해보았다. 이번시간에는 WH_CALLWNDPROC, WH_CALLWNDRETPROC 훅을통해서 SendMessage 의처리과정을후킹하는방법과다수의훅프로시저를설치한경우에동기화하는방법에대해서설명할것이다. 목차 목차...1 필자소개...1 연재가이드... 오류! 책갈피가정의되어있지않습니다. 연재순서... 오류! 책갈피가정의되어있지않습니다. 필자메모...2 Intro...2 SendMessage 는어떻게동작할까?...3 메시지처리순서...3 무한대기에빠지지않는법...4 WH_CALLWNDPROC 훅...5 WH_CALLWNDRETPROC 훅...6 다수의훅을설치해보자...7 동기화... 11 이벤트객체... 12 동기화프로토콜... 13 중복실행방지... 15 도전과제... 17 참고자료... 17 필자소개 신영진 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 의동작원리 필자메모 필자가처음 Visual C++ 을공부한것은 2000 년도였다. 이상엽님의기념비적인저서 Visual C++ 6.0 Bible 을사들고집에서공부하고있었다. 그러나사실그책은처음 Visual C++ 을접한독자가이해하기에너무도어려웠다. 필자가처음버튼의이벤트핸들러에다정확한코드를집어넣는데만도한참의시간의걸렸다. 아마선배한테물어봤으면 3 초만에배웠을것을물어보지않아서몇날몇일을고생한것이다. 독자여러분들은필자와똑같은실수를되풀이하지않기를바란다. 필자의메일함 (pop@jiniya.net) 은독자여러분의질문에답하기위해서 24 시간열려있다. 그러니강좌에서모르는부분이나의문점이생기면언제든지메일을통해서질문하도록하자. 또한블로그 (http://www.jiniya.net) 를통해서지면관계상못한이야기나설명이잘못된부분에대한내용을제공하고있으니그곳도참고하면공부에도움이될것같다. Intro Windows 의응용프로그램이가장많이사용하는함수하나를꼽으라면아마도 SendMessage 가될것이다. 왜냐하면대부분의 Windows 응용프로그램이 GUI 기반이고, GUI 프로그램의경우필연적으로메시지를기반으로동작하기때문이다. 또한 2/17 페이지
SendMessage 의경우동기화문제때문에 PostMessage 나다른함수보다많이사용된다. 이번시간에는이러한 SendMessage 의호출과정을후킹해보도록하자. SendMessage 는어떻게동작할까? SendMessage 의경우같은쓰레드의윈도우로메시지를보내는지아니면다른쓰레드에의해서생성된윈도우로메시지를보내는지에따라동작방식이조금다르다. 우선같은쓰레드의윈도우로메시지를보내는경우에는 SendMessage 는해당메시지프로시저를직접호출하고리턴값을반환한다. 이과정은직접함수를호출하는것과다를바가없다. 다른쓰레드에의해생성된윈도우로메시지를보내는경우에는메시지를받을윈도우를생성한쓰레드의메시지큐에메시지를추가하고해당큐의 QS_SENDMESSAGE 플래그를설정한다. 그리고는자신의응답메시지큐에응답이오기를기다린다. 만약이과정에서메시지를받는윈도우가무한루프나기타상황으로메시지를처리할수없는상황이라면 SendMessage 를호출한쪽의쓰레드도같이잠기게된다. 메시지처리순서 SendMessage 도단순히메시지큐에메시지를추가하는것이라면어떻게 PostMessage 보다먼저처리되는것일까? 이에대한해답은윈도우가메시지를검사하는알고리즘에있다. 쓰레드의메시지큐를조작하는함수로는 GetMessage 와 PeekMessage 가있다. 이함수들은메시지큐를무작위적으로검사하지않는다. 아래와같은순서에따라서메시지큐를검사한다. 1. QS_SENDMESSAGE 플래그가설정되어있으면해당메시지를적절한윈도우프로시저로보낸다. 이작업후에함수는리턴하지않고계속다음메시지를기다린다. 2. 포스트메시지큐에메시지가있으면인자로전달된 MSG 구조체에해당메시지정보를복사한후리턴한다. 3. QS_QUIT 플래그가설정되어있으면 WM_QUIT 메시지를리턴하고, QS_QUIT 플래그를제거한다. 4. 하드웨어입력큐에메시지 (WM_KEYDOWN, WM_LBUTTONDOWN, ) 가있다면 MSG 구조체에해당메시지정보를복사한후리턴한다. 5. QS_PAINT 플래그가설정되어있으면 WM_PAINT 메시지를리턴한다. 6. QS_TIMER 플래그가설정되어있으면 WM_TIMER 를리턴한다. 3/17 페이지
쉽게말하면 SendMessage 로전달된메시지, PostMessage 로전달된메시지, PostQuitMessage 로전달된메시지, 하드웨어입력메시지, WM_PAINT 메시지, WM_TIMER 메시지의순으로처리된다는것이다. 이를통해서 PostMessage 보다늦게메시지큐에포스트된 SendMessage 가왜먼저처리되는지를알수있다. WM_KEYDOWN, WM_CHAR, WM_KEYUP 의처리순서도위의알고리즘을통하면쉽게이해할수있다. 기본적으로사용자가키보드를눌렀다떼게되면 WM_KEYDOWN, WM_KEYUP 이하드웨어입력메시지큐에추가된다. WM_KEYDOWN 이 TranslateMessage 를통하면적절한형태의 WM_CHAR 메시지가 PostMesage 로큐에추가된다. 물론이과정에서 WM_CHAR 가 WM_KEYUP 보다늦게추가되었지만위의알고리즘의우선순위판정에의해서다음번처리되는메시지는 WM_CHAR 가되는것이다. 무한대기에빠지지않는법 SendMessage 를사용할경우메시지를받는대상윈도우의쓰레드가무한루프에빠진경우라면같이블록되는단점이있다. 이를사전에알아내거나차단하는방법과관련된함수들을간단하게살펴보도록하자. 함수의파라미터와리턴값에대한자세한설명은 MSDN 을참고하자. BOOL IsHungAppWindow(HWND hwnd); 위함수를사용하면 SendMessage 를보낼대상윈도우가현재메시지를처리할수있는지없는지를알수있다. TRUE 를리턴하는경우는해당윈도우가더이상메시지를처리할수없음을의미한다. 따라서이경우에 SendMessage 를호출하지않으면무한대기에빠지는문제를방지할수있다. 하지만이렇게하더라도 SendMessage 의메시지루프에서무한대기에빠지는경우는해결책이없다. LRESULT SendMessageTimeout( ); BOOL SendMessageCallback( ); 위두함수를사용하면어떠한상태의교착상황도피할수있다. SendMessageTimeout 의 경우는타임아웃시간을지정해서해당시간이지날경우자동으로리턴한다. 두번째 함수는호출하는즉시리턴하고, 메시지가완료된것은콜백을통해서알수있다. BOOL SendNotifyMessage( ); BOOL ReplyMessage(LRESULT lresult); BOOL InSendMessage(VOID); DWORD InSendMessageEx(LPVOID lpreserved); 4/17 페이지
위의함수들은자주사용하지는않지만알아두면유용한함수들이다. SendNotifyMessage 의경우는다른쓰레드로보낼경우바로리턴하고같은쓰레드에서생성한윈도우로보낼경우에는 SendMessage 와동일한역할을하는함수다. ReplyMessage 는메시지함수가처리가완료되지않은시점에서 SendMessage 의리턴값을전송하는역할을한다. InSendMessage 와 InSendMessageEx 함수는서로다른쓰레드간의메시지전송이면 TRUE 를같은쓰레드간의메시지전송이면 FALSE 를리턴하는함수다. WH_CALLWNDPROC 훅 WH_CALLWNDPROC 훅은 SendMessage 를호출하는시점을후킹한다. 즉, 이는 SendMessage 가메시지프로시저로전달되어서처리되기전시점을후킹하는것을의미한다. Spy++ 을보면 SendMessage 로전달된메시지에대해서 S,R 플래그가나오는것을볼수있다 ( 화면 1 참고 ). S 의경우 WH_CALLWNDPROC 을통해서 SendMessage 를호출하는시점에표시하는것이고, R 은다음장에소개되는 WH_CALLWNDRETPROC 을통해서 SendMessage 가리턴되는시점에표시하는것이다. 화면 1 Spy++ 을통해서 SendMessage 과정을모니터링한화면 LRESULT CALLBACK CallWndProc(int code, WPARAM wparam, LPARAM lparam); code [ 입력 ] code 값이 HC_ACTION 인경우훅프로시저를수행하고, 0 보다작은경우에는 훅프로시저를수행하지않고 CallNextHookEx 를호출한다음리턴해야한다. 5/17 페이지
wparam [ 입력 ] 메시지가현재쓰레드에의해서보내진것인지아닌지를나타낸다. 현재쓰레드에의해서보내진경우 0 이아닌값을가지고, 그렇지않은경우 0 으로설정된다. lparam [ 입력 ] SendMessage 와관련된정보를담고있는 CWPSTRUCT 구조체의포인터를담고있다. CWPSTRUCT 는아래와같은형태를하고있다. 구조체의필드별의미는 < 표 1> 을참고하자. typedef struct LPARAM lparam; WPARAM wparam; UINT message; HWND hwnd; CWPSTRUCT, *PCWPSTRUCT; 표 1 CWPSTRUCT 구조체필드별의미 필드명 hwnd message wparam lparam 의미메시지받을윈도우핸들메시지 ID 메시지 WPARAM 파라미터메시지 LPARAM 파라미터 리턴값 : code 가 0 보다작은경우에는 CallNextHookEx 의리턴값을그대로리턴해야 한다. 그렇지않은경우에도 CallNextHookEx 의리턴값을그대로사용하는것이좋다. CallNextHookEx 를통해서다음훅체인을호출하지않은경우엔 0 을리턴해야한다. WH_CALLWNDRETPROC 훅 WH_CALLWNDRETPROC 훅은 SendMessage 가처리되고리턴되는시점을후킹한다. 따라서메시지가처리되고난후어떤값이리턴되는지를알수있다. LRESULT CALLBACK CallWndRetProc(int code, WPARAM wparam, LPARAM lparam); code [ 입력 ] code 값이 HC_ACTION 인경우훅프로시저를수행하고, 0 보다작은경우에는훅프로시저를수행하지않고 CallNextHookEx 를호출한다음리턴해야한다. wparam [ 입력 ] 메시지가현재프로세스의해서보내진것인지아닌지를나타낸다. 현재프로세스에의해서보내진경우 0 이아닌값을가지고, 그렇지않은경우 0 으로설정된다. lparam [ 입력 ] SendMessage 의리턴정보를담고있는 CWPRETSTRUCT 구조체의포인터를담고있다. CWPRETSTRUCT 는아래와같은형태를하고있다. 구조체의필드별의미는 < 표 2> 를참고하자. 6/17 페이지
typedef struct LRESULT lresult; LPARAM lparam; WPARAM wparam; UINT message; HWND hwnd; CWPRETSTRUCT, *PCWPRETSTRUCT; 표 2 CWPRETSTRUCT 구조체필드별의미 필드명 hwnd message wparam lparam lresult 의미메시지받을윈도우핸들메시지 ID 메시지 WPARAM 파라미터메시지 LPARAM 파라미터 SendMessage 리턴값 WH_CALLWNDRETPROC 훅의리턴값은 WH_CALLWNDPROC 의리턴값과동일하다. 박스 1 메시지차단첫시간에작성한키보드후킹프로그램에서우리는후킹프로시저에서 1 을리턴함으로써키보드메시지가호출되지않도록할수있다고배웠다. 하지만그것은어디까지나 WH_KEYBOARD 에국한된이야기다. 이번시간에배운 WH_CALLWNDPROC, WH_CALLWNDRETPROC 모두 CallNextHookEx 를호출하지않는다고해서해당메시지호출이중지되지않는다. 단지 CallNextHookEx 를호출하지않게되면다음번훅프로시저가수행되지않을뿐이다. 따라서 WH_CALLWNDPROC 이나 WH_CALLWNDRETPROC 을이용해서는 SendMessage 의진행을모니터링할수는있지만작업을중간에수정할수는없다. 다수의훅을설치해보자 우리는이제껏하나의훅프로시저를설치하는프로그램만작성했었다. 하지만때로는하나의훅이아닌여러개의훅을동시에설치해야하는경우가있다. Spy++ 이그러한대표적인경우라할수있다. Spy++ 의경우여러개의윈도우의메시지전송과정을동시에모니터링할수있다. 이러한기능을제공하기위해서는여러개의훅을설치하고제거할수있도록만들필요가있다. 물론전역으로훅을설치할경우그러한고민을하지않아도된다. 하지만 WH_GETMESSAGE, WH_CALLWNDPROC, WH_CALLWNDPROCRET 등의훅을전역으로설치하면시스템이굉장히느려질수있다는점을기억해야한다. 7/17 페이지
여러개의훅을설치하고제거하는부분은추후에도사용하기편리하도록별도의 dll 로 제작했다. 앞으로훅에필요한유틸리티함수들은 hkutil.dll 에추가해서사용하도록하자. < 리스트 1> 에는훅의설치와제거에사용될구조체와 DllMain 부분이나와있다. HOOKINFO 구조체는훅정보를관리한다. 사실별도로관리할필요는없지만추후에필요할수도있기때문에저장해놓기로했다. 또한 RegisterHook 을통해서설치한훅핸들만추후에제거할수있도록하기위해서도어떤것들을설치되었는지는알고있어야한다. 설치된훅종류와해당훅을설치한쓰레드아이디가저장된다. 이렇게저장된정보는설치된훅핸들과연관지어서 g_hookmap 에저장된다. DllMain 은간단한두가지처리를해주고있다. 포로세스에로드될때에 DisableThreadLibraryCalls 를호출해서쓰레드에로드되는부분을호출하지않도록한다. 또한프로세스에서언로될때에 g_hookmap 에서제거되지않은훅핸들이있으면제거해준다. 리스트 1 훅정보구조체및 DllMain 코드 // 훅정보 typedef struct _HOOKINFO int hookid; // 훅종류 DWORD threadid; // 훅이설치된쓰레드 HOOKINFO, *PHOOKINFO; typedef std::map<hhook, HOOKINFO> HookMap; typedef std::map<hhook, HOOKINFO>::iterator HookMIt; // 훅정보를저장할맵 HookMap g_hookmap; BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID) try switch(reason) // 쓰레드에대한호출을하지않는다. case DLL_PROCESS_ATTACH: DisableThreadLibraryCalls(inst); break; // 종료시제거되지않고남아있는훅핸들을해제한다. case DLL_PROCESS_DETACH: 8/17 페이지
for(hookmit it=g_hookmap.begin(); it!=g_hookmap.end(); ++it) UnhookWindowsHookEx(it->first); break; except(exception_execute_handler) return FALSE; return TRUE; < 리스트 2> 는 RegisterHook 함수의코드를보여준다. 이함수는훅을등록하는역할을한다. 기본적으로 SetWindowsHookEx 와동일한순서의파라미터를가진다. 단지차이점이라면훅함수와모듈명대신에훅함수명과 dll 이름을파라미터로가진다는것이다. 간단하게 LoadLibrary 로 dll 을로드한후 GetProcAddress 로함수주소를구해서해당정보를토대로 SetWindowsHookEx 를호출하는간단한코드다. 성공적으로추가된경우에는해당정보를전역변수인 g_hookmap 에저장해서나중에제거할수있도록한다. 리스트 2 RegisterHook 코드 /*-- 훅을등록한다. 파라미터 hookid - [ 입력 ] 훅타입 (WH_KEYBOARD, WH_MOUSE,...) functionname - [ 입력 ] 훅프로시저이름 dllname - [ 입력 ] 훅프로시저를담고있는 dll 이름 threadid - [ 입력 ] 훅을설치할쓰레드 ID 리턴값 - 실패시 NULL, 성공시훅핸들 --*/ HHOOK WINAPI RegisterHook(int hookid, LPCTSTR functionname, LPCTSTR dllname, DWORD threadid) HINSTANCE dll = NULL; // dll 핸들 HOOKPROC func = NULL; // 훅프로시저함수포인터 HHOOK hook = NULL; // 훅핸들 try // dll 을로드한다. dll = LoadLibrary(dllName); if(!dll) 9/17 페이지
leave; // 훅프로시저를로드한다. func = (HOOKPROC) GetProcAddress(dll, functionname); if(!func) leave; // 훅을수행한다. hook = SetWindowsHookEx(hookId, func, dll, threadid); if(hook) // 성공한경우훅정보를맵에기록한다. HOOKINFO info; info.hookid = hookid; info.threadid = threadid; g_hookmap.insert(make_pair(hook, info)); finally // dll 을해제한다. if(dll) FreeLibrary(dll); // 훅핸들을반환한다. return hook; < 리스트 3> 은훅을제거하는역할을하는 UnregisterHook 의코드다. 이함수는위에서 RegisterHook 을통해서등록한훅핸들을제거하는역할을한다. 훅핸들이넘어오면 g_hookmap 에서해당훅정보를찾는다. 만약해당정보가없다면 RegisterHook 에의해서등록된훅핸들이아니기때문에 FALSE 를리턴한다. 맵에존재하는경우에는맵에서해당정보를삭제하고, UnhookWindowsHookEx 를호출해서훅핸들을제거한다. 리스트 3 UnregisterHook 코드 /*-- 등록된훅을해제한다. 파라미터 hook - [ 입력 ] 등록된훅핸들 리턴값 - 성공시 TRUE, 실패시 FALSE --*/ BOOL WINAPI UnregisterHook(HHOOK hook) 10/17 페이지
BOOL ret = FALSE; if(hook) // 맵에서훅정보를찾는다. HookMIt it = g_hookmap.find(hook); if(it!= g_hookmap.end()) // 존재하는경우해당정보를삭제하고, 훅을해제한다. g_hookmap.erase(it); ret = UnhookWindowsHookEx(hook); return ret; 동기화 지금까지 3 회의강좌에서는모두 SendMessage 계열의함수를사용해서동기화를수행했다. SendMessage 의경우부작용이별로없고달리신경쓰지않아도메시지처리가자동적으로직렬화되기때문에동기화에신경쓸일이없었다. 하지만 WM_COPYDATA 를통해서빈번하게많은양의데이터를전송하는것은성능이좋지않다. 이번강좌를바탕으로다음시간에제작하게될 Spy++ 의클론인 imspy 에서는이러한단점을개선하기위해서공유메모리를통해서정보를교환할것이다. 이렇게되면공유메모리는하나이고접근하는곳은여러군데가된다. imspy 프로그램자체는해당메모리를읽기위해서접근해야하고, 나머지훅프로시저는해당버퍼에쓰기위해서접근해야한다. 이과정에서는동기화를필수적으로해주어야한다. 그렇지않을경우에는버퍼의내용이엉망이될것이다. 버퍼의내용이엉망이되지않도록하기위해서는버퍼를읽고, 쓰는작업을동기화시켜주어야한다. 이러한작업에적합한객체로커널오브젝트의하나인이벤트객체가있다. 박스 2 배타적접근과직렬화컴퓨터용어를접하다보면무슨말인지가끔헷갈리는것들이나온다. 동기화에관한문서들을읽을때에항상등장하는단어인베타적접근과직렬화란말도그러한것중에하나다. 보통다음과같이사용된다. 자원에대해서베타적으로접근하도록해야한다. 자원에대한접근을직렬화시켜야한다. 11/17 페이지
결론적으로말하면이두가지말은동일한의미다. 한가지자원에동시에두군데이상에서접근하지못하도록하라는의미다. 베타적접근이란말은한쓰레드가자원에접근하고있을때다른쓰레드는접근하지못하도록해야한다는말이다. 직렬화란말은해당자원에접근하는놈들을동시에접근하지않고순서대로차례로접근하게하라는의미다. 이벤트객체 이벤트는가장원시적인커널오브젝트다. 하지만원시적이라고해서쉽다고이해해서는안된다. 아래에나오는함수들을읽어보고잘이해한다음한번씩멀티쓰레드프로그램을만들어서실습해보도록하자. 오토리셋과매뉴얼리셋이벤트의차이점은꼭확인해보도록하자. HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpeventattributes, BOOL bmanualreset, BOOL binitialstate, LPCTSTR lpname ); CreateEvent 함수는이벤트를생성하는역할을한다. lpeventattributes 에는생성될이벤트객체가가질보안속성을지정한다. bmanualreset 에는매뉴얼이벤트인경우 TRUE 를오토리셋이벤트인경우 FALSE 를지정하면된다. binitialstate 는이벤트의초기상태가시그널상태인지아닌지를지정한다. lpname 에는생성될이벤트객체가가질이름을지정한다. 성공적으로이벤트객체가생성된경우해당객체의핸들을그렇지않은경우에는 NULL 을리턴한다. 두번째인자가매뉴얼리셋과오토리셋이벤트를구분한다고설명했다. 그렇다면매뉴얼리셋과오토리셋이벤트의차이점은무엇일까? 보통의경우이둘의차이를 ResetEvent 의호출여부로알고있는경우가많다. 오토리셋이벤트는 ResetEvent 를호출하지않아도되고, 매뉴얼리셋이벤트는 ResetEvent 를호출해주어야한다는것이다. 물론앞의설명도맞긴하지만그것이두이벤트의근본적인차이는아니다. 오토리셋이벤트의경우해당이벤트를대기중인쓰레드중에임의의한쓰레드를깨운다. 이말은대기중인쓰레드가세개가있다고가정하면그중에한쓰레드를택해서깨운다는말이다. 어떤쓰레드가스케쥴될지는알수없다. 반면에매뉴얼리셋이벤트의경우에는대기중인쓰레드를모두깨운다. 앞의예와같이세개의쓰레드가대기중에있다면세 12/17 페이지
개의쓰레드를모두스케쥴한다는의미다. 따라서어떤이벤트를사용할지결정해야할 때에는어떤쓰레드가깨워져야하는지를보고판단해야한다. 모두다깨워야한다면 매뉴얼리셋이벤트를하나만깨워야한다면오토리셋이벤트를사용하면된다. HANDLE OpenEvent(DWORD dwdesiredaccess, BOOL binherithandle, LPCTSTR lpname); OpenEvent 함수는생성된이벤트객체를열때사용한다. dwdesiredaccess 에는 < 표 3> 에나온것과같은값을조합해서사용하면된다. binherithandle 은 CreateProcess 로생성되는프로세스에열린이벤트객체를상속시킬지를나타낸다. 보통의경우 FALSE 로지정하면된다. lpname 에는열려고하는이벤트객체의이름을지정한다. 표 3 dwdesiredaccess 에지정될수있는값설명 값 의미 DELETE 객체를삭제할수있음. READ_CONTROL 객체의보안속성을읽을수있음 해당이벤트를사용해동기화할수 SYNCHRONICE 있음 (WaitForSingleObject, WaitForMultipleObjects 등의 Wait 함수를사용할수있음을의미한다 ). WRITE_DAC 보안속성을기록할수있음. WRITE_OWNER 객체의소유자를변경할수있음. EVENT_ALL_ACCESS 객체에대한모든작업을할수있음. EVENT_MODIFY_STATE 이벤트상태를변경할수있음 (SetEvent, PulseEvent, ResetEvent 를사용할수있음을의미한다 ). BOOL SetEvent(HANDLE hevent); BOOL ResetEvent(HANDLE hevent); BOOL PulseEvent(HANDLE hevent); 위세함수는이벤트객체의상태를조작하는함수들이다. SetEvent 는이벤트객체를시그널상태로만들고, ResetEvent 는이벤트객체를넌시그널상태로만든다. PulseEvent 함수는이벤트객체를시그널상태로만들어서대기중인쓰레드를깨운후에다시해당이벤트를넌시그널상태로만든다. 동기화프로토콜 앞절에서우리는동기화의필요성과이벤트객체에대해서배웠다. 이제이벤트객체를사용해서실제로어떻게동기화를하는지살펴보도록하자. 우리는동기화를위해서두 13/17 페이지
개의이벤트객체를사용할것이다. 각각쓰기, 읽기를제어하는용도의이벤트객체다. 이벤트객체의오브젝트명과역할은 < 표 4> 를참고하자. 표 4 동기화에사용될이벤트객체 커널오브젝트명 의미 SSpy_Buffer_Ready 훅프로시저가버퍼에데이터를기록할수있다. SSpy_Data_Ready 클라이언트프로그램이버퍼의데이터를읽어갈수있다. < 그림 1> 에는훅프로시저와클라이언트프로그램이어떻게서로동기화하면서정보를주고받는지가나와있다. 왼쪽이후킹프로시저의순서도이고, 오른쪽이클라이언트프로그램의순서도다. 진행순서는오른쪽의클라이언트프로그램이먼저실행된다. 클라이언트프로그램의버퍼읽기쓰레드는적절한형태로두개의이벤트객체를생성한다음 SSpy_Buffer_Ready 이벤트를설정한다. 그리고 SSpy_Data_Ready 이벤트를기다린다. 다음훅프로시저가수행되면 SSpy_Buffer_Ready 이벤트가설정되어있기때문에통과해서데이터를버퍼에쓰고 SSpy_Data_Ready 이벤트를설정한다. 그러면이번에는클라이언트가깨어나고해당데이터를읽어가게된다. 데이터를모두읽어간후에클라이언트는다시 SSpy_Buffer_Ready 를설정해서다음번훅프로시저가버퍼에데이터를쓸수있도록해준다. 14/17 페이지
Sspy_Buffer_Ready 이벤트를열수있는가? SSpy_Buffer_Ready 생성 예 아니오 Sspy_Data_Ready 이벤트를열수있는가? SSpy_Data_Ready 생성 예 아니오 아니오 Sspy_Buffer_Ready 가시그널상태인가? Sspy_Buffer_Ready 셋 예 아니오 Buffer 에메시지데이터기록 SSpy_Data_Ready 가시그널상태인가? Sspy_Data_Ready 셋 예 Buffer 에메시지읽음 종료 그림 1 훅프로시저와클라이언트의동기화흐름도 중복실행방지 이제까지언급한동기화문제를해결하는방법에는한가지제약사항이있다. 그것은바로클라이언트가하나만존재해야한다는것이다. 만약클라이언트가두개이상이라면동기화과정에서문제가생길수있다. SSpy_Buffer_Ready 이벤트가순차적으로설정되면서두개의훅프로시저가동시에데이터에접근할수있으며, 자신이설치하지않은훅에대한메시지도받을수있다. 따라서반드시클라이언트는중복으로실행되는것을방지해서그러한일을사전에차단해야한다. 15/17 페이지
Windows 에서중복실행을방지하는방법에는여러가지방법이있다. FindWindow 를통해서자신이생성한윈도우의존재유무를검사하는방법도있고, 글로벌커널오브젝트를생성해서해당오브젝트가존재하는지를검사하는방법도있다. 커널오브젝트를사용하는방법이 < 리스트 4> 에나와있다. 리스트 4 커널오브젝트를사용해서중복실행을방지하는코드 class CSingleInstance private: HANDLE m_hglbmutex; // 전역뮤텍스핸들 public: CSingleInstance(LPCTSTR lpszmutexname="appsinginstcheck"); ~CSingleInstance(); ; BOOL IsExist(); // // 전역뮤텍스를생성해서중복실행체크 // // 파라미터 // lpszmutexname [in] 뮤텍스이름 CSingleInstance::CSingleInstance(LPCTSTR lpszmutexname) m_hglbmutex = CreateMutex(NULL, FALSE, lpmutexname); if(getlasterror() == ERROR_ALREADY_EXISTS) CloseHandle(m_hGlbMutex); m_hglbmutex = NULL; // 뮤텍스핸들해제 CSingleInstance::~CSingleInstance() if(m_hglbmutex) CloseHandle(m_hGlbMutex); // // 이미실행된프로그램이있는지검사 // // 파라미터없음 // // 리턴값 // 존재하는경우 TRUE 를, 처음실행인경우 FALSE 를리턴함 16/17 페이지
BOOL CSingleInstance::IsExist() return m_hglbmutex == NULL; 기본적인원리는간단하다. 네임드커널오브젝트의경우프로세스경계에서공유해서사용할수있다는점을이용한것이다. CreateMutex 함수로이미존재하는커널오브젝트를생성하도록한경우에는생성된커널오브젝트에대한핸들을리턴한다 (OpenMutex 가호출되는것과동일하다 ). 이경우에 GetLastError 를호출해보면 ERROR_ALREADY_EXSITS 가설정되어커널오브젝트가새로생성된것인지아닌지를판단할수있다. 즉, 애플리케이션시작부분에서커널오브젝트를생성하고, 종료시에커널오브젝트를해제한다면 ERROR_ALREDY_EXISTS 가설정된경우는이미응용프로그램이실행된경우고, 아니라면처음실행인경우라고생각할수있다. 프로그램에위의코드를추가한다음, 전역변수로 CSingleInstance 를하나생성하도록 하자. 그런후에프로그램의시작부분에서 IsExist 를통해서검사해서 TRUE 인경우에는 바로종료하도록하면중복실행이되지않는다. 도전과제 이번시간에는 WH_CALLWNDPROC, WH_CALLWNDPROCRET 훅의사용법과함께여러개의훅을설치한경우에동기화하는방법에대해서자세히살펴보았다. 이제까지배운지식을바탕으로다음시간에는 Spy++ 의클론인 imspy 를제작해볼것이다. 이번시간까지의지식을바탕으로자신만의 Spy++ 클론을미리제작해보도록하자. 참고자료 참고자료 1. Jeffrey Richter. <<Programming Applications for Microsoft Windows (4/E)>> Microsoft Press 참고자료 2. 김상형, <<Windows API 정복 >> 가남사참고자료 3. 김성우, << 해킹 / 파괴의광학 >> 와이미디어참고자료 4. 매뉴얼리셋이벤트와오토리셋이벤트의차이점 http://www.gosu.net/gosuweb/article-detail.aspx?articlecode=617 17/17 페이지