개발자를위한윈도우후킹테크닉 OutputDebugString 의동작원리 우리는지금까지후킹 DLL 을 OutputDebugString 과 DebugView 를이용해서동작내용을확인했다. 하지만 DebugView 가활성화된상태에서후킹 DLL 에서 OutputDebugString 을수행하면시스템이잠시동안멈추는현상이발생한다. 이번시간에는이러한 DebugView 의불편함을해소하기위해서커스텀디버깅뷰를제작하는방법에대해서배운다. 목차 목차...1 필자소개...1 연재가이드...1 연재순서...2 필자메모...2 Intro...2 OutputDebugString...3 DebugView...4 OutputDebugString 의동작원리...5 OutputDebugString 감시스레드...7 DbgLook... 12 도전과제... 15 참고자료... 15 필자소개 신영진 pop@jiniya.net, http://www.jiniya.net 시스템프로그래밍에관심이많으며다수의보안프로그램개발에참여했다. 현재데브피아 Visual C++ 섹션시삽을맡고있으며, Microsoft Visual C++ MVP 로활동하고있다. 최근에는 python 과 lua 같은스크립트언어를배우려고노력하고있다. 연재가이드 운영체제 : 윈도우 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 의동작원리 필자메모 모든일에재미란요소는굉장히중요하다. 몇시간동안한가지일에집중할수있는가장원초적인이유는아마도그일이재미있기때문일것이다. " 학문을아는자는이를좋아하는사람만못하고학문을좋아하는자는이를즐기는자만못하다." 라는공자님께서남기신명언도재미의중요성을강조한것이라할수있다. 개발도예외가아니다. 리누스토발즈의 < 리눅스, 그냥재미로 >, 존카맥을그린 < 둠 > 과같은책들을읽어본다면필자의생각에동의할것이다. 2007 년의목표로발전된개발자를그리고있는독자라면이러한프로그래밍의재미를찾는데주력하는것이좋을것같다. 프로그래밍을하면서컴퓨터랑채팅하고있다는생각을할정도면합격이아닐까? Intro 이제껏후킹 DLL 을디버깅하면서한번도 DebugView 의불편함을겪지않았다면반성해야할것같다. 디버깅중에 DebugView 때문에본의아니게커피타임을가져본독자라면아마도 DebugView 의대안프로그램을찾기도했을것이다. 이번시간에는그런독자들을위해서디버그메시지출력에사용되는 OutputDebugString 의동작원리를살펴보고 DebugView 처럼메시지를모니터링하는간단한프로그램을제작해본다. 2/15 페이지
OutputDebugString OutputDebugString 함수는디버그메시지를출력하는기능을한다. 활성화된디버거가있는경우엔그곳에디버그메시지를출력하고, 없는경우라면아무런일도하지않는다. OutputDebugString 의원형은아래와같다. lpdebugstring 으로전달된내용을디버거에출력해준다. void OutputDebugString(LPCTSTR lpdebugstring); OutputDebugString 을사용하는경우는대부분특정함수의실행흐름을추적하거나, 아니면특정순간의변수값을확인하고싶을때이다. OutputDebugString 은인자를하나만받기때문에매번문자열을조합해줘야하는불편함이있다. 또한별도의매크로로처리하지않으면릴리즈버전에서도디버그메시지를마구출력해버린다. 이런불편함을해결하기위해서우리는 XTRACE 라는매크로를제작해서사용했었다. < 리스트 1> 에는 XTRACE 매크로의소스가나와있다. XTRACE 의경우 printf 와같이가변인자를지원하고, 릴리즈버전에서는동작하지않도록제작되어있다. 릴리즈버전에서도디버그메시지를확인하기위해서는 FORCE_XTRACE 를선언해주면된다. 리스트 1 xtrace.h #if defined(_debug) defined(force_xtrace) #include <strsafe.h> #define XTRACE_BUF_SIZE 512 #define XTRACE _DbgPrintf inline void cdecl _DbgPrintf(LPCTSTR str,...) TCHAR buff[xtrace_buf_size]; va_list ap; va_start(ap, str); StringCbVPrintf(buff, sizeof buff, str, ap); va_end(ap); OutputDebugString(buff); #else #define XTRACE 1? (void) 0 : _DbgPrintf inline void _DbgPrintf(const char *str,...) #endif 3/15 페이지
DebugView 앞서 OutputDebugString 의경우활성화된디버거가있는경우에메시지를출력해준다고했다. 그러나디버그메시지를사용하는대부분의경우는디버깅작업이용의하지않은경우가많다. 이럴때디버거를동작시키지않고도디버그메시지를출력해주는유틸리티가 DebugView 다. 활성화된디버그가있으면 DebugView 에결과가나타나지않는다. < 화면 1> 에는 DebugView 의동작화면이나와있다. 화면 1 DebugView 동작화면 우리는지금까지주로후킹 DLL 의호출시점, 컨텍스트, 넘어온안자값을확인하기위해서디버그메시지를사용했었다. 그런데 DebugView 는이상하게자신이활성화된상태에서디버그메시지를출력하면시스템이멈추는현상이발생한다. 이문제를재연하는것은매우간단하다. 간단한마우스후킹 DLL 을작성한다음 WM_LBUTTONDOWN 에서디버그메시지를출력하도록만든다. 그리고 DebugView 를활성화시킨다음, 그위에서마우스왼쪽버튼을눌러보자. 아마도시스템이몇초간정지한듯이동작할것이다. 이러한문제때문에 DebugView 로후킹 DLL 을디버깅하는개발자들사이에는몇가지 불문율이있다. 절대로 DebugView 를디버깅도중에건드리지않거나, 후킹 DLL 4/15 페이지
컨텍스트의프로세스를구하는부분을넣어 dbgview.exe 면건너뛰도록제작하는것이다. 그러나이두가지모두근본적인해결책은될수없다. OutputDebugString 의동작원리 NT 계열의운영체제에서 OutputDebugString 은몇가지커널오브젝트를사용해서동작하기때문에디버그메시지를캡쳐하는것은무척간단하다. < 표 1> 에는 OutputDebugString 이사용하는커널오브젝트들의이름과역할이나와있다. 표 1 OutputDebugString 에사용되는커널오브젝트 이름 타입 역할 DBWinMutex 뮤텍스 OutputDebugString 중복실행보호 DBWIN_BUFFER 공유메모리 OutputDebugString 으로출력할문자열 DBWIN_BUFFER_READY 이벤트 DBWIN_BUFFER 에데이터를기록할수있음 DBWIN_DATA_READY 이벤트 DBWIN_BUFFER 에데이터가기록됐음 < 그림 1> 에는 OutputDebugString 과 DebugView 의동작순서도가나와있다. 왼쪽편은 OutputDebugString 의오른쪽편은 DebugView 의동작순서도다. OutputDebugString 부분을살펴보자. DBWIN_MUTEX 는 OutputDebugString 이동시에호출되는것을방지하는역할을한다. DBWIN_BUFFER 파일맵, DBWIN_BUFFER_READY, DBWIN_DATA_READY 이벤트는디버거쪽에서생성해두어야한다. OutputDebugString 이이러한객체를여는데실패하면아무일도하지않고리턴한다. 이후 DBWIN_BUFFER_READY 를대기해서버퍼에기록을해도되는지체크한다. 대기가성공적으로끝나면버퍼에메시지를기록하고, DBWIN_DATA_READY 를설정해서디버거에게디버그메시지가기록됐음을알려준다. DebugView 측은 OutputDebugString 이제대로동작하도록만들어주면된다. 시작할때에각종커널오브젝트를생성해준다. 그런다음 DBWIN_BUFFER_READY 를설정해서디버거가버퍼에기록할수있도록해준다. 이후 DBWIN_DATA_READY 이벤트를대기한다. 누군가 OutputDebugString 을호출해서해당이벤트가설정되면 DBWIN_BUFFER 의내용을화면에출력해주고, DBWIN_BUFFER_READY 이벤트를설정해서다음 OutputDebugString 이버퍼를쓸수있도록해준다. 5/15 페이지
그림 1 OutputDebugString 및 DebugView 순서도 우리가 Spy++ 의클론프로그램인 imspy 를제작할때살펴보았던것과같이이러한전역커널오브젝트를사용해서데이터를주고받는프로그램의경우에데이터를수신하는프로그램이여러개가되면엉뚱하게동작할수있다. 동기화가깨어지기때문이다. 따라서모니터링프로그램은반드시커널오브젝트가이미존재하는경우에는사용자에게이미다른디버거가존재함을알려주고모니터링을중지해야한다. < 리스트 2> 에는파일맵오브젝트인, DBWIN_BUFFER 에기록되는내용이무엇인지를담고있다. 파일맵오브젝트엔 pid 와출력할문자열이저장되어있다. msg 는 1 글자를저장할수있는배열이아니라, 가변길이를지원하기위해서저렇게사용한것이다. 이와관련된내용은 C-Faq 2.6 에자세히소개되어있다 (http://c-faq.com/struct/structhack.html 참고 ). 리스트 2 DBWIN_BUFFER 에기록된내용 6/15 페이지
// 파일맵오브젝트에기록된내용 typedef struct _DEBUGMMF DWORD pid; char msg[1]; DEBUGMMF, *PDEBUGMMF; 박스 1 9x 에서 OutputDebugString 메시지모니터링하기지금까지설명한내용은모두 NT 계열의운영체제국한된내용이다. 9x 계열의 OutputDebugstring 은커널객체를사용해서정보를전달하지않기때문에이러한방법으로메시지를모니터링할수없다. 하지만간단한방법으로동일하게디버그메시지를감시할수있는방법이있다. OutputDebugString 을직접구현하는방법이다. 참고자료에있는 dbwin32 소스를보면그런방법이사용되어있다. 물론이경우기존의프로그램을수정해야한다는불편함이있긴하다. OutputDebugString 감시스레드 이제모니터링프로그램의가장핵심적인부분은디버그메시지를감시하는스레드를살펴보자. < 리스트 3> 에감시스레드의코드가나와있다. < 그림 1> 에나와있는순서도와동일한절차로구현되었다. 리스트 3 OutputDebugString 감시스레드 UINT CWatchThread::Go() // 모든객체가접근할수있도록 NULL DACL 을만든다. CSecurityAttributes sa; CSecurityDesc sd; sd.setdacl(true); sa.set(sd); // DBWIN_BUFFER_READY 이벤트를생성한다. am::mate<handle> bufreadyevent( CreateEvent(&sa, FALSE, FALSE, "DBWIN_BUFFER_READY"), &CloseHandle ); if(bufreadyevent == NULL) bufreadyevent.dismiss_mate(); if(getlasterror() == ERROR_ALREADY_EXISTS) // DBWIN_DATA_READY 이벤트를생성한다. am::mate<handle> datareadyevent( 7/15 페이지
CreateEvent(&sa, FALSE, FALSE, "DBWIN_DATA_READY"), &CloseHandle ); if(datareadyevent == NULL) datareadyevent.dismiss_mate(); // DBWIN_BUFFER 파일맵오브젝트를생성한다. am::mate<handle> mmf( CreateFileMapping(INVALID_HANDLE_VALUE, &sa, PAGE_READWRITE, 0, 4096, "DBWIN_BUFFER"), &CloseHandle ); if(mmf == NULL) mmf.dismiss_mate(); // DBWIN_BUFFER 를사용할수있도록맵핑한다. am::mate<pvoid> sharemem( MapViewOfFile(mmf, FILE_MAP_READ, 0, 0, 512), &UnmapViewOfFile ); if(sharemem == NULL) sharemem.dismiss_mate(); PDEBUGMMF dmsg = (PDEBUGMMF)(PVOID) sharemem; HANDLE objs[2] = m_exitevent, datareadyevent ; DWORD obj; DEBUGMSG log; for(;;) // 버퍼에기록할수있음을알린다. SetEvent(bufReadyEvent); // 버퍼에데이터가기록될때까지대기한다. obj = WaitForMultipleObjects(2, objs, FALSE, INFINITE); // 종료이벤트인경우엔루프를탈출한다. if(obj == WAIT_OBJECT_0) break; // 로그기록이중지된경우엔루프를새로시작한다. if(ispaused()) continue; // 로그벡터에접근하기위해서뮤텍스를획득한다. 8/15 페이지
am::mate<dword> locker( WaitForSingleObject(GetMutex(), INFINITE), am::lambda::bind(&releasemutex, GetMutex()) ); if(locker!= WAIT_OBJECT_0 locker!= WAIT_ABANDONED) continue; // 로그정보를설정한다. log.msg = dmsg->msg; log.pid = dmsg->pid; log.t = time(null); // 로그벡터에추가한다. m_logs.push_back(log); // 메인윈도우에로그가추가되었음을알린다. AfxGetMainWnd()->PostMessage(WM_DEBUGMSGNOTIFY, 0, 0); 스레드의가장앞쪽에는 ATL 의 CSecurityAttributes, CSecurityDesc 를사용해서 NULL DACL 을생성하는부분이있다. 이코드를디버그버전으로수행하면 NULL DACL 생성오류가발생한다. 이오류는코드가잘못돼서나는것이아니라 NULL DACL 은보안상위험하기때문에개발자에게알려주기위해서오류를내는것이다. 디버깅하는데이부분이번거롭다고생각한다면 < 리스트 4> 에나타난것과같이 API 를사용해서생성하면된다. 이렇게 NULL DACL 을보안속성으로지정하는이유는모니터링프로그램의권한보다낮은권한으로동작하는프로그램에서도이객체들을원활하게열수있도록하기위해서다. 만약디버거가관리자권한으로객체를만들게되면, 사용자권한으로실행되는프로그램에서는해당객체가존재하더라도접근거부를받는다. 결국은디버그메시지를출력하지못하는것이다. 리스트 4 API 를사용해서 NULL DACL 을생성하는부분 security_attributes sa; security_descriptor sd; sa.nlength = sizeof(security_attributes); sa.binherithandle = TRUE; sa.lpsecuritydescriptor = &sd; if(!initializesecuritydescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) return; 9/15 페이지
if(!setsecuritydescriptordacl(&sd, TRUE, (PACL)NULL, FALSE)) return; 박스 2 mate 클래스예외에안전한코드를작성하기위해서가장중요한점은할당된자원을정확하게반환하는것이다. a, b, c, d 란자원을순차적으로할당하는함수를생각해보자. a 가할당된다음예외가발생했다면 a 만해제해야한다. c 까지할당된상황에서예외가발생했다면 a, b, c 를해제한다음함수가종료해야할것이다. 사용하는자원의개수가많아질수록코드의관리가힘들어진다. 이런경우에 C++ 의생성자와소멸자를사용하면손쉽게자원을관리할수있다. 생성자에서자원을할당하고, 소멸자에서해제하는것이다. mate 는생성자와소멸자를사용해서자원의관리를자동으로해주는템플릿클래스다. 데브피아의최재욱님께서작성하신것으로굉장히사용하기편리하고실용적이다. mate 클래스에대해서더자세한정보는 devpia 의강좌게시판 (http://www.devpia.com/forum/boardview.aspx?no=7528&forumname=vc_lec) 에나와있다. 박스 3 ACL 10/15 페이지
화면 2 특정폴더에지정된 ACL 을확인하는화면 ACL 은 Windows NT 계열의운영체제에서사용하는리소스의접근제어방법이다. 가장쉽게볼수있는곳은 NTFS 의파일이나폴더의권한을설정하는화면이다 (< 화면 2> 참고 ). 그런데이 ACL 을생성하고관리하는 API 는사용하는방법이굉장히어렵고복잡하다. 아래문서는 ACL 에관해서자세하게설명하고있는문서다. 아직 ACL 에대해서잘모르고있다면꼭읽어보도록하자. http://www.codeproject.com/win32/accessctrl1.asp http://www.codeproject.com/win32/accessctrl2.asp http://www.codeproject.com/csharp/accessctrl3.asp http://www.codeproject.com/win32/accessctrl4.asp 11/15 페이지
DbgLook 이번시간에샘플로제공할프로그램인 DbgLook 은앞서소개한원리에기초해서기본적인내용만구현한것이다. < 화면 3> 에실행화면이나와있다. 프로그램을실행하면바로모니터링을시작한다. 각종인터페이스관련코드는하나도없다. 따라서모니터링중지도, 리스트삭제도없다. 화면 3 DbgLook 동작화면 < 리스트 5> 에는내부적으로사용한구조체와정의부분이나와있다. 디버그메시지에 저장하는내용은 pid 와메시지문자열, 메시지가발생한시간이전부다. 리스트 5 디버그메시지를저장하는구조체 #include <vector> typedef struct _DEBUGMSG DWORD pid; CString msg; time_t t; DEBUGMSG, *PDEBUGMSG; typedef std::vector<debugmsg> DMVec; typedef std::vector<debugmsg>::iterator DMVIt; const DWORD WM_DEBUGMSGNOTIFY = WM_USER + 1394; 12/15 페이지
< 리스트 6> 에는앞서살펴보았던감시스레드에서메시지를전달한경우에수행되는 메시지핸들러가나와있다. 로그벡터에접근하기위한뮤텍스를획득하고로그벡터의 내용을리스트뷰에추가한다. 작업이완료되면로그벡터를삭제한다. 리스트 6 스레드에서보낸메시지에대한핸들러 LRESULT CMainFrame::OnDebugMsgNotify(WPARAM w, LPARAM l) // 로그벡터에접근하기위한뮤텍스를획득한다. HANDLE mutex = m_watchthread.getmutex(); am::mate<dword> locker( WaitForSingleObject(mutex, INFINITE), am::lambda::bind(&releasemutex, mutex) ); if(locker!= WAIT_OBJECT_0 locker!= WAIT_ABANDONED) locker.dismiss_mate(); // 로그벡터의내용을윈도우에추가시킨다. DMVIt it = m_watchthread.m_logs.begin(); DMVIt end = m_watchthread.m_logs.end(); for( ; it!= end; ++it) m_wndview.addmsg(*it); // 로그벡터의내용을지운다. m_watchthread.m_logs.clear(); 메시지핸들러에서 AddMsg 함수를호출한경우에수행되는부분이 < 리스트 7> 에나타나있다. 리스트컨트롤에메시지를출력하는단순한부분이다. 이경우에몇가지주의해야할점이있다. 에디터컨트롤을사용한다면단순히메시지를그대로출력하면되지만우리와같이리스트컨트롤을사용하는경우에는메시지를 \n 을기준으로분리해서출력해주는것이보기가좋다. 또한탭도적당한공백으로확장해서출력해주는것이좋다. 리스트 7 뷰에디버그정보를출력하는부분 void CChildView::AddMsg(DEBUGMSG &msg) int pos = 0; CString res; CString buf; // \n 기준으로분리한다. 13/15 페이지
while((res = msg.msg.tokenize("\r\n", pos))!= "") // 로그번호를출력한다. int cnt = m_lvcmsgs.getitemcount(); buf.format("%d", cnt+1); m_lvcmsgs.insertitem(lvif_text, cnt, buf, 0, 0, 0, 0); // 탭을공백으로확장시킨다. res.replace("\t", " "); // 정보를출력한다. CTime t(msg.t); buf.format("%d:%d:%d", t.gethour(), t.getminute(), t.getsecond()); m_lvcmsgs.setitemtext(cnt, 1, buf); buf.format("%d", msg.pid); m_lvcmsgs.setitemtext(cnt, 2, buf); m_lvcmsgs.setitemtext(cnt, 3, res); \n 으로토큰을분리할때한가지생각해야할점이있다. "\n\nabc" 와같은문자열을출력한경우다. 이경우텍스트에디터라면빈줄이두개삽입되고 abc 가나온다. 그러나 < 리스트 7> 의코드와같이토큰을분리하면빈줄없이그냥 abc 가삽입된다. strtok 나 Tokenize 와같은함수가구분자가연속으로있는경우그부분을하나의토큰으로분리해내지않기때문이다. 이경우빈줄을정확하게표시하기위해서는토큰분리함수를직접구현해야한다. 박스 4 릴리즈버전과디버그메시지필자는릴리즈버전에서는모든디버그메시지가제거되는것이바람직하다고생각한다. 왜냐하면디버그메시지출력은성능을저하시킬뿐아니라다른개발자가디버깅할때에는마치공해와도같은존재이기때문이다. 자신의디버그메시지하나를확인하려고기다리고있는데엉뚱한프로그램에서마구디버그메시지를출력한다고생각해보자. 아마도자신의메시지는찾기도힘들것이다. 디버그메시지를남겨두는입장은릴리즈버전에서문제가생긴컴퓨터의정보를손쉽게확인할수있다는것이다. 일반적인유저는디버그메시지뷰어를켜두지않을것이고그런의미에서성능저하도없다고생각한다. 하지만일반적인유저엔개발자도포함된다는사실을간과해서는안된다. 만약자신이후자의생각이더바람직하다고생각한다면 DLL 을사용해서문제가없는 컴퓨터엔피해가가지않도록하는것이좋겠다. 방법은간단하다. DLL 에디버그출력 14/15 페이지
함수를구현하는것이다. 함수이름을 MyOutputDebugString 이라고가정해보자. DLL 을두종류를만든다. 일반적인릴리즈버전의 MyOutputDebugString 은아무일도하지않는다. 다른버전은 MyOutputDebugString 함수가단순히 OutputDebugString 을호출하도록만든것이다. 특정컴퓨터에서문제가생긴다면, 후자의 DLL 을다운로드시켜서디버그메시지를수집하면된다. 도전과제 이번달의도전과제는우리가제작한 dbglook 을각자자신의입맛에맞게고쳐보는것이다. 사용해보면서불편함을느낀점들을한가지씩개선해보도록한다. 이것으로지난 9 개월간연재했던후킹강좌는모두마무리되었다. 여기가끝이라고생각해서는안된다. 끝은항상새로운시작의출발점일뿐이다. 메시지후킹은여러기법들중에서가장기초적인방법이다. 좀더발전된후킹방법이나주제에대해서공부하고싶다면참고자료에있는책들을읽어보도록하자. 참고자료 참고자료 1. Jeffrey Richter. <<Programming Applications for Microsoft Windows (4/E)>> Microsoft Press 참고자료 2. 김상형, <<Windows API 정복 >> 가남사참고자료 3. 김성우, << 해킹 / 파괴의광학 >> 와이미디어참고자료 4. Gary McGraw, Greg Hoglund << 소프트웨어보안 : 코드깨부수기 >> 정보문화사참고자료 5. Kris Kaspersky, Natalia Tarkova, Julie Laing, <<Hacker Disassembling Uncovered>> A-List Publishing 참고자료 6. Greg Hoglund, Jamie Butler, <<Rootkits: Subverting the Windows Kernel>> Addison-Wesley Professional 참고자료 7. OutputDebugString 의동작원리 http://www.unixwiz.net/techtips/outputdebugstring.html 참고자료 8. Dbwin32 소스코드 http://grantschenck.tripod.com/dbwinv2.htm 15/15 페이지