7.미니 스파이

가장 기본적인 프로그래밍 도구인 스파이를 분석해 보고 있는데 사용 방법이 워낙 간단하고 직관적이기 때문에 Win32 API에 대한 개념이 있다면 이 프로그램을 이해하고 사용하는데는 별 어려움이 없을 것이다. 그런데 개발자 입장에서 저런 프로그램은 과연 어떻게 만들까 궁금한 생각이 들기도 할 것이다. 뭔가 대단해 보이고 어려워 보이지만 사실 스파이 수준의 유틸리티는 어렵지 않게 만들 수 있다.

스파이는 마이크로소프트사가 비주얼 스튜디오와 함께 배포하는 유틸리티이며 누구나 자유롭게 사용할 수 있을 뿐만 아니라 소스까지 공개되어 있다. MSDN의 예제중 sdk/sdktools 폴더를 보편 spy의 소스가 컴파일 가능한 상태로 제공되므로 스파이를 어떻게 만들었는지 궁금하다면 이 소스를 분석해 보기 바란다. 나는 솔직히 이 소스를 분석해 본 적은 없지만 대충 어떻게 만들었는지는 안봐도 알 수 있을 것 같다.

스파이의 가장 핵심적인 기능 두가지를 뽑는다면 커서 아래에 있는 윈도우의 속성을 조사해 주는 창 찾기 기능과 이 창으로 전달되는 메시지를 감시하는 메시지 뷰 기능을 꼽을 수 있다. 메시지 뷰는 디버깅에 아주 강력한 도구이며 창 찾기는 다른 프로그램의 구조를 살펴 보는 가장 기본적인 도구이다. 여기서는 스파이의 창 찾기 기능을 좀 더 쓰기 쉽게 만든 미니 스파이를 만들어 보도록 하자. 소스는 다음과 같다.

 

#include <windows.h>

#include "SHReg.h"

#include "resource.h"

 

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);

HINSTANCE g_hInst;

HWND hWndMain;

LPSTR lpszClass="MiniSpy";

 

#define CAPWIDTH 15

#define WIDTH 300

#define HEIGHT 100

#define KEY "Software\\MiyoungSoft\\MiniSpy\\"

 

char szAbout[]="각종 윈도우의 속성을 살펴보는 MiniSpy입니다\r\n"

   "마우스 커서를 원하는 윈도우위에 올려 두시면 커서 아래에 "

   "있는 윈도우의 속성들을 조사해서 보여줍니다.";

int bTop;

HWND hWndOld;

char str[1024];

HFONT font;

BOOL bCloseDown;

 

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

     ,LPSTR lpszCmdParam,int nCmdShow)

{

   HWND hWnd;

   MSG Message;

   WNDCLASS WndClass;

   g_hInst=hInstance;

  

   WndClass.cbClsExtra=0;

   WndClass.cbWndExtra=0;

   WndClass.hbrBackground=(HBRUSH)(COLOR_BTNFACE+1);

   WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);

   WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);

   WndClass.hInstance=hInstance;

   WndClass.lpfnWndProc=(WNDPROC)WndProc;

   WndClass.lpszClassName=lpszClass;

   WndClass.lpszMenuName=NULL;

   WndClass.style=CS_HREDRAW | CS_VREDRAW;

   RegisterClass(&WndClass);

 

   hWnd=CreateWindowEx(WS_EX_TOPMOST,lpszClass,lpszClass,WS_POPUP,

      CW_USEDEFAULT,CW_USEDEFAULT,WIDTH,HEIGHT,

      NULL,(HMENU)NULL,hInstance,NULL);

   ShowWindow(hWnd,nCmdShow);

   hWndMain=hWnd;

  

   while(GetMessage(&Message,0,0,0)) {

      TranslateMessage(&Message);

      DispatchMessage(&Message);

   }

   return Message.wParam;

}

 

// 윈도우 속성을 문자열 형태로 조립한다. WS_ 생략하였다.

#define TEST(n) (dwStyle & (1 << n))

void GetStyleString(DWORD dwStyle, char *sStyle)

{

   if (TEST(31)) strcpy(sStyle,"POPUP");

   else if (TEST(30)) strcpy(sStyle,"CHILD");

   else strcpy(sStyle,"OVERLAPPED");

 

   if (TEST(29)) strcat(sStyle," | MINIMIZE");

   if (TEST(28)) strcat(sStyle," | VISIBLE");

   if (TEST(27)) strcat(sStyle," | DISABLED");

   if (TEST(26)) strcat(sStyle," | CLIPSIBLINGS");

   if (TEST(25)) strcat(sStyle," | CLIPCHILDREN");

   if (TEST(24)) strcat(sStyle," | MAXIMIZE");

   if (TEST(23)) strcat(sStyle," | BORDER");

   if (TEST(22)) strcat(sStyle," | DLGFRAME");

   if (TEST(21)) strcat(sStyle," | VSCROLL");

   if (TEST(20)) strcat(sStyle," | HSCROLL");

   if (TEST(19)) strcat(sStyle," | SYSMENU");

   if (TEST(18)) strcat(sStyle," | THICKFRAME");

   if (TEST(17)) strcat(sStyle," | MINIMIZEBOX");

   if (TEST(16)) strcat(sStyle," | MAXIMIZEBOX");

}

 

BOOL CALLBACK AboutDlgProc(HWND hDlg,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

   switch(iMessage)

   {

   case WM_INITDIALOG:

      return TRUE;

   case WM_COMMAND:

      switch (wParam)

      {

      case IDOK:

      case IDCANCEL:

          EndDialog(hDlg,0);

          return TRUE;

      }

      break;

   }

   return FALSE;

}

 

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

   HDC hdc;

   PAINTSTRUCT ps;

   POINT pt;

   HWND hWndPoint, hWndParent;

   char sCaption[256], sClass[256];

   RECT crt;

   UINT nHit;

   HFONT oldfont;

   HRGN hWndRgn;

   HMENU hMenu, hPopup;

   DWORD dwStyle;

   char sStyle[256];

 

   switch(iMessage) {

   case WM_CREATE:

      hWndRgn=CreateRoundRectRgn(0,0,WIDTH,HEIGHT,20,20);

      SetWindowRgn(hWnd,hWndRgn,FALSE);

      SetTimer(hWnd,1,500,NULL);

      font=CreateFont(12,0,0,0,0,0,0,0,HANGEUL_CHARSET,3,2,1,

          VARIABLE_PITCH | FF_MODERN,"굴림");

      // 최후 실행 위치로 이동한다.

      crt.left=SHRegReadInt(SHCU,KEY"Pos","X",0);

      crt.top=SHRegReadInt(SHCU,KEY"Pos","Y",0);

      bTop=SHRegReadInt(SHCU,KEY"Pos","Top",1);

      SetWindowPos(hWnd,(bTop ? HWND_TOPMOST:HWND_NOTOPMOST),

          crt.left,crt.top,0,0,SWP_NOSIZE);

      return 0;

   case WM_TIMER:

      GetCursorPos(&pt);

      hWndPoint=WindowFromPoint(pt);

      if (hWndPoint == hWndOld)

          return 0;

      hWndOld=hWndPoint;

      if (hWndPoint == NULL) {

          strcpy(str,"윈도우 없음");

      } else if (hWndPoint == hWnd) {

          strcpy(str,szAbout);

      } else {

         GetWindowText(hWndPoint,sCaption,256);

          GetClassName(hWndPoint,sClass,256);

          hWndParent=GetParent(hWndPoint);

          GetWindowRect(hWndPoint,&crt);

          dwStyle=GetWindowLong(hWndPoint,GWL_STYLE);

          GetStyleString(dwStyle,sStyle);

          wsprintf(str,

             "핸들 : %d(0x%x)\r\n"

             "클래스 : %s\r\n"

             "캡션 : %s\r\n"

             "부모 : %d(0x%x)\r\n"

             "영역 : (%d,%d) - (%d,%d), %d*%d\r\n"

             "스타일 : %X(%s)",

             hWndPoint,hWndPoint,sClass,sCaption,hWndParent,hWndParent,

             crt.left, crt.top, crt.right, crt.bottom, crt.right-crt.left,

             crt.bottom-crt.top,dwStyle,sStyle);

      }

      InvalidateRect(hWnd,NULL,TRUE);

      return 0;

   case WM_PAINT:

      hdc=BeginPaint(hWnd, &ps);

 

      RoundRect(hdc,0,0,WIDTH-1,HEIGHT-1,20,20);

      // 왼쪽에 타이틀 바를 그린다.

      GetClientRect(hWnd,&crt);

      crt.right=CAPWIDTH;

      FillRect(hdc,&crt,GetSysColorBrush(COLOR_ACTIVECAPTION));

 

      // 종료 버튼을 그린다.

      Rectangle(hdc,1,5,13,17);

      MoveToEx(hdc,4,8,NULL);LineTo(hdc,10,14);

      MoveToEx(hdc,10,8,NULL);LineTo(hdc,4,14);

 

      // 정보를 출력한다.

      GetClientRect(hWnd,&crt);

      crt.left+=CAPWIDTH+2;

      crt.top+=2;

      SetBkMode(hdc,TRANSPARENT);

      oldfont=(HFONT)SelectObject(hdc,font);

      DrawText(hdc,str,-1,&crt,DT_WORDBREAK);

      SelectObject(hdc,oldfont);

      EndPaint(hWnd, &ps);

      return 0;

   case WM_LBUTTONDOWN:

      pt.x=LOWORD(lParam);

      pt.y=HIWORD(lParam);

      if ((pt.x > 1) && (pt.x < 13) && (pt.y > 5) && (pt.y < 17)) {

          bCloseDown=TRUE;

      } else {

          bCloseDown=FALSE;

      }

      return 0;

   case WM_LBUTTONUP:

      pt.x=LOWORD(lParam);

      pt.y=HIWORD(lParam);

      if ((bCloseDown==TRUE) && (pt.x > 1) && (pt.x < 13)

          && (pt.y > 5) && (pt.y < 17)) {

          DestroyWindow(hWnd);

      }

      return 0;

   case WM_NCHITTEST:

      nHit=DefWindowProc(hWnd,iMessage,wParam,lParam);

      pt.x=LOWORD(lParam);

      pt.y=HIWORD(lParam);

      ScreenToClient(hWnd,&pt);

      if ((nHit==HTCLIENT) && (pt.x <= CAPWIDTH) && (pt.y > 15))

          nHit=HTCAPTION;

      return nHit;

   case WM_CONTEXTMENU:

      hMenu=LoadMenu(g_hInst, MAKEINTRESOURCE(IDR_MENU1));

      hPopup=GetSubMenu(hMenu, 0);

 

      if (bTop==TRUE)

          CheckMenuItem(hPopup, IDM_TOP, MF_BYCOMMAND | MF_CHECKED);

 

      TrackPopupMenu(hPopup, TPM_LEFTALIGN, LOWORD(lParam), HIWORD(lParam),

          0, hWnd, NULL);

      DestroyMenu(hMenu);

      return 0;

   case WM_COMMAND:

      switch(LOWORD(wParam)) {

      case IDM_EXIT:

          DestroyWindow(hWnd);

          break;

      case IDM_TOP:

          bTop=!bTop;

          SetWindowPos(hWnd,(bTop ? HWND_TOPMOST:HWND_NOTOPMOST)

             ,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE);

          break;

      case IDM_ABOUT:

          DialogBox(g_hInst,MAKEINTRESOURCE(IDD_DIALOG1),hWnd,AboutDlgProc);

          break;

      }

      return 0;

   case WM_DESTROY:

      DeleteObject(font);

      // 마지막 위치를 저장해둔다.

      GetWindowRect(hWnd,&crt);

      SHRegWriteInt(SHCU,KEY"Pos","X",crt.left);

      SHRegWriteInt(SHCU,KEY"Pos","Y",crt.top);

      SHRegWriteInt(SHCU,KEY"Pos","Top",bTop);

      PostQuitMessage(0);

      return 0;

   }

   return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

보다시피 별로 길지도 않고 중간 중간에 주석까지 있으므로 따로 분석해 볼 필요까지는 없을 것 같다. 이 예제의 핵심 함수는 WM_TIMER에 있는 WindowFromPoint인데 이 함수는 지정한 좌표에 있는 윈도우의 핸들을 구해 준다. 윈도우 핸들만 구하면 좌표 조사나 속성 조사는 식은 죽먹기이므로 나머지 작업은 이 핸들로부터 구한 정보를 잘 가공하여 읽기 쉽게 출력해 주는 것 뿐이다. 윈도우 핸들과 클래스 이름을 조사하고 정수형으로 되어 있는 스타일의 실제 의미를 문자열로 풀어서 출력하였다.

윈도우 모양이 정 사각형이 아닌 둥근 사각형으로 되어 있고 가급적이면 화면 영역을 작게 차지하도록 하기 위해 타이틀 바를 왼쪽 변에 조그맣게 별도로 만들어 두었다. 이런 기교에 관심이 있다면 이 예제가 도움이 될 것이다. 둥근 사각형 형태의 윈도우를 만드는 핵심 함수는 SetWindowRgn이며 커스텀 타이틀 바를 처리하는 핵심 코드는 WM_NCHITTEST에 있으므로 분석해 보기 바란다. 실행중의 모습은 다음과 같다.

타이틀 바 위쪽에 닫기 버튼이 있으며 팝업 메뉴에서 '항상 위' 옵션을 선택할 수 있다. 별도의 명령없이 실행시켜 놓고 커서만 가져 가면 해당 윈도우의 정보를 간단하게 살펴볼 수 있다. 특히 메뉴나 시스템 모달같이 스파이로 도저히 찍어볼 수 없는 것까지도 볼 수 있다는 것이 장점이다. 이 예제의 소스는 http://www.winapi.co.kr/book/download/MiniSpySrc.zip에 있다.