안녕하십니까 이번 포스팅에서는 지뢰찾기핵의 코드를 살펴보겠습니다.

자 이제 코드를 하나씩 살펴봅시다. 앞 포스팅에서 설명한 부분은 따로 설명드리지 않겠습니다.

- #define FindLM 0
  #define Stoptime 1
  #define initTime 2
  #define PowerOverwhelming 3
  #define victory 4
  #define gameover 5

버튼의 ID들을 미리 상수로 정의합니다.

- LRESULT WINAPI WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
BOOL WINAPI EnumDesktopProc(LPTSTR lpszDesktop,LPARAM lParam);
BOOL WINAPI EnumWindowsProc(HWND hwnd,LPARAM lParam);

윈도우 프로시저 함수와 작업관리자를 만들때 쓰인 EnumDesktopProc함수와 EnumWindowsProc함수를 선언합니다. 이는 바탕화면에서 지뢰찾기 프로세스를 찾기 위해 사용되었습니다. 원리는 작업관리자에서 쓰인 것과 동일합니다.

- int Errorprocessing(void);
  void codeinjection(DWORD FA,int byte,DWORD FP,DWORD GetAccessAddr);
  int timecheck(void);

  DWORD* finelandmine(DWORD number);
  DWORD* stoptime(DWORD killtimer);
  DWORD* initializationTime(void);
  DWORD* startTime(DWORD setTimer);
  DWORD* poweroverwhelming(void);
  DWORD* Cancel_poweroverwhelming(void);
  DWORD* Victory(void);
  DWORD* Gameover(void);

앞에서 설명드린 함수들을 선언합니다.

- DWORD G_LandMinePID = NULL;
  HWND G_MainHWND,G_POW,G_ST,G_FLM;

지뢰찾기의 PID를 전역변수로 저장하기 위한 G_LandMinePID변수와 버튼들의 핸들을 전역변수로 저장하기 위한 변수들을 선언합니다.

WinMain함수는 간단하니 생략하겠습니다. 바로 윈도우 프로시저로 넘어갑시다.

- DWORD address;

DLL에 들어있는 함수(SetTimer,KillTimer)의 주소값을 저장하기 위해 선언하였습니다. 대부분의 프로그램 코드에서 사용자가 임의로 만든 함수가 아닌 DLL에서 로드하여 호출하는 함수는 주소값이 정해져있지 않기 때문에 미리 DLL에서 함수의 주소를 구하여 그 값을 인자로 넣어 호출하는 식으로 동작해야 합니다.

- TCHAR ButtonTEXT[100] = L"";

버튼의 텍스트를 가져와서 저장시키기 위한 변수를 선언합니다.

- switch (message)
    case WM_CREATE:
   G_MainHWND = hWnd;
   G_FLM = CreateWindow(L"button",L"FindLM",WS_CHILD | WS_VISIBLE,0,50,105,25,hWnd,(HMENU)FindLM,NULL,NULL);
   G_ST = CreateWindow(L"button",L"StopTime",WS_CHILD | WS_VISIBLE,0,75,105,25,hWnd,(HMENU)Stoptime,NULL,NULL);
   CreateWindow(L"button",L"initTime",WS_CHILD | WS_VISIBLE,0,100,105,25,hWnd,(HMENU)initTime,NULL,NULL);
   G_POW = CreateWindow(L"button",L"PowerOverwhelming",WS_CHILD | WS_VISIBLE,0,125,140,25,hWnd,(HMENU)PowerOverwhelming,NULL,NULL);
   CreateWindow(L"button",L"victory",WS_CHILD | WS_VISIBLE,0,150,105,25,hWnd,(HMENU)victory,NULL,NULL);
   CreateWindow(L"button",L"gameover",WS_CHILD | WS_VISIBLE,0,175,105,25,hWnd,(HMENU)gameover,NULL,NULL);

어떤 코드를 지뢰찾기에 삽입할 건지를 사용자로부터 입력받기 위해 각각의 버튼들을 생성합니다. 이 중 버튼의 핸들을 전역변수에 저장시키는 버튼이 있는데 이는 버튼의 텍스트를 읽어올 필요가 있기 때문입니다.

- case WM_COMMAND:
   switch(LOWORD(wParam))
     case FindLM:

FindLM버튼을 눌렀을 경우 즉 사용자로부터 지뢰를 보여달라는 명령을 받았을 경우 아래의 코드를 수행합니다.

- EnumDesktops(GetProcessWindowStation(),EnumDesktopProc,0);
       if(Errorprocessing())
             break;

EnumDesktops함수를 호출하여 바탕화면에 실행중인 지뢰찾기 프로그램의 PID를 구합니다. 이렇게 구한 PID는 G_LandMinePID에 저장되어지며 이 G_LandMinePID값에 따라 다음 코드인 Errorprocessing함수에 의해 에러처리를 해주게 됩니다.

- GetWindowText(G_FLM,ButtonTEXT,100);

xp버전의 지뢰찾기가 동작되고 있다는 것이 확인되면 먼저 FindLM버튼으로부터 텍스트를 가져와 ButtonTEXT에 저장합니다.

- if(lstrcmp(ButtonTEXT,L"FindLM") == 0)
      codeinjection((DWORD)finelandmine,34,0x0A,NULL);
      SetWindowText(G_FLM,L"Cancel");

가져온 버튼의 텍스트가 FindLM이면 codeinjection함수를 호출합니다. 인자는 어셈블리코드가 적힌 finelandmine함수의 주소값이며 크기는 34바이트, finelandmine의 인자값은 16진수 A, 그리고 특정주소의 보안을 얻을 필요는 없으므로 마지막 인자값은 NULL로 주었습니다. 여기서 34바이트라는 크기는 finelandmine함수에 적혀있는 어셈블리 코드의 총 크기를 의미하며 칼 같이 계산할 필요는 없이 그냥 크기값을 예측하여 무조건 많이 주면 됩니다. 어차피 RETN코드로 인해 추가로 쓰인 코드는 실행되지 않습니다. 그리고 0x0A라는 값은 finelandmine에 적혀있는 0x01002F80함수의 인자값으로 들어가게 되는데 이 값은 지뢰를 0x0A라는 비트맵으로 보여달라는 뜻입니다. xp지뢰찾기에서 0x0A비트맵은 다음 모양을 의미합니다.

코드 인젝션을 수행하고 나면 FindLM버튼의 텍스트를 Cancel로 바꿉니다.

- else
    codeinjection((DWORD)finelandmine,34,0x0F,NULL);
    SetWindowText(G_FLM,L"FindLM");

가져온 텍스트 값이 FindLM이 아닐 경우 즉 Cancel일 경우 codeinjection함수를 호출합니다. 다만 이번에는 함수의 파라미터 값이 0x0F인데 이는 지뢰를 0x0F라는 비트맵으로 보여달라는 뜻입니다. xp지뢰찾기에서 0x0F비트맵은 다음 모양을 의미합니다.

즉 이 뜻은 사용자에게 보여주는 지뢰를 다시 감추라는 뜻이 됩니다. 다른 말로 위의 지뢰를 보여달라는 기능을 취소하는 것이지요. 이렇게 기능을 취소시키고 나면 SetWindowText함수로 버튼의 텍스트값을 다시 FindLM으로 복구시킵니다.

- G_LandMinePID = 0;

코드 인젝션 기법이 완료되었으면 G_LandMinePID변수를 초기화시킵니다.

- case Stoptime:

Stoptime버튼을 눌렀을 경우 아래의 코드를 수행합니다. G_LandMinePID값을 구하고 에러처리를 하는 부분은 위에서 설명하였으니 생략하겠습니다.
     
- GetWindowText(G_ST,ButtonTEXT,100);
     if(lstrcmp(ButtonTEXT,L"StopTime") == 0)

버튼의 텍스트가 StopTime일 경우 아래의 코드를 수행합니다.

- if(timecheck())
       MessageBox(hWnd,L"타이머가 동작중이 아닙니다.",L"Error",MB_OK);

timecheck함수를 호출하여 지뢰찾기의 타이머값을 체크합니다. 만약 참을 반환한다면 타이머의 값이 0이라는 뜻이므로 타이머가 동작중이 아님을 알리고 switch문을 빠져나갑니다.

- address = (DWORD)GetProcAddress(GetModuleHandle(L"user32.dll"),"KillTimer");

타이머가 동작중이라면 GetProcAddress함수를 호출하여 KillTimer함수의 주소값을 구해 address에 저장시킵니다. GetProcAddress함수를 살펴봅시다.

지정한 DLL에서 함수의 주소나 변수의 주소값을 구해주는 함수라고 되어있습니다. 인자들을 한 번 살펴봅시다.

* hModule

DLL 모듈의 핸들값을 인자로 받습니다. 보통 LoadLibrary함수나 GetModuleHandle함수를 사용하여 핸들값을 구하는데 두 함수의 차이점이 있다면 GetModuleHandle함수는 호출프로세스가 해당 DLL을 로드하고 있어야 함수가 성공한다는 점이고 LoadLibrary함수는 DLL의 경로를 인자값으로 주어야한다는 차이점이 있습니다. 기본적으로 비주얼 스튜디오로 만든 프로그램은 우리가 원하는 user32.dll을 로드하고 있으므로 그냥 GetModuleHandle함수를 사용하였습니다.

* lpProcName

모듈에서 찾을려는 함수나 변수명입다. 유니코드로 적어주시면 되겠습니다.

즉 이 코드가 호출되고 나면 KillTimer함수의 주소값이 address변수에 저장되어집니다.

- codeinjection((DWORD)stoptime,35,address,NULL);
  SetWindowText(G_ST,L"Cancel");

구한 주소값을 이용하여 codeinjection함수를 호출합니다. 이러면 stoptime함수의 인자값으로 address값(KillTimer 주소값)을 받게 됩니다. 코드를 삽입하고 나면 버튼의 텍스트를 Cancel로 바꿉니다.

- else
      address = (DWORD)GetProcAddress(GetModuleHandle(L"user32.dll"),"SetTimer");
      codeinjection((DWORD)startTime,35,address,NULL);
      SetWindowText(G_ST,L"StopTime");

버튼 값이 Cancel인 경우 즉 타이머가 KillTimer함수에 의해 파기되어 타이머가 더이상 동작되지 않는 경우 SetTimer함수의 주소값을 구해 startTime함수의 코드를 삽입합니다. 코드가 정상적으로 삽입이 되었다면 버튼의 텍스트를 StopTime으로 복구시킵니다.

모든 처리가 끝나면 G_LandMinePID변수값을 0으로 초기화합니다. 앞으로 이부분도 생략하도록 하겠습니다.

- case initTime:

   codeinjection((DWORD)initializationTime,40,NULL,NULL);

initTime버튼을 눌렀을 경우 initializationTime함수의 코드를 삽입합니다. 이 함수는 인자값이 없으므로 세번째 인자값을 NULL로 주었습니다.

- case PowerOverwhelming:

     GetWindowText(G_POW,ButtonTEXT,100);
     if(lstrcmp(ButtonTEXT,L"PowerOverwhelming") == 0)
           codeinjection((DWORD)poweroverwhelming,35,NULL,0x01003512);
           SetWindowText(G_POW,L"Cancel");

PowerOverwhelming버튼을 눌렀을 때 버튼의 텍스트 값이 PowerOverwhelming이면 poweroverwhelming함수의 코드를 삽입합니다. 이 때 네번째 인자값을 0x01003512로 주었는데 이는 poweroverwhelming함수가 값을 변경하는 0x01003591주소 값의 보안수치를 변경하기 위함입니다.

0x01003512주소는 xp지뢰찾기의 함수주소를 의미하고 0x01003591값은 0x01003512함수 주소 내에 포함되어 있으므로 0x01003512 함수의 보안 수치를 통째로 변경하기 위해 값을 이렇게 주었습니다. 이 때 0x01003512 함수의 총 크기는 165바이트입니다.코드를 삽입하고 나면 버튼의 텍스트를 Cancel로 변경합니다.

- else
     codeinjection((DWORD)Cancel_poweroverwhelming,35,NULL,0x01003512);
     SetWindowText(G_POW,L"PowerOverwhelming");

    
버튼의 텍스트값이 Cancel라면 Cancel_poweroverwhelming함수의 코드를 삽입후 버튼의 텍스트를 PowerOverwhelming로 복구시킵니다.

- case victory:
     codeinjection((DWORD)Victory,40,NULL,NULL);

victory버튼을 눌렀을 경우 Victory코드를 삽입합니다.

- case gameover:
     codeinjection((DWORD)Gameover,40,NULL,NULL);

gameover버튼을 눌렀을 경우 Gameover코드를 삽입합니다.

중요한 부분은 다 설명을 마쳤습니다. 다음은 EnumDesktopProc를 살펴봅시다.

- HDESK desktop;

데스크탑의 핸들을 선언합니다.

 - desktop = OpenDesktop(lpszDesktop,0,NULL,DESKTOP_READOBJECTS);

   EnumDesktopWindows(desktop,EnumWindowsProc,NULL);

   CloseDesktop(desktop);

데스크탑을 열고 EnumDesktopWindows함수를 호출한 후 데스크탑의 핸들을 닫습니다. 이부분은 작업관리자 코드에서 설명을 하였으므로 그냥 넘어가겠습니다.

마지막으로 EnumWindowsProc함수를 살펴봅시다.

- TCHAR titleName[500] = L"";
  WINDOWINFO Wininfo;

윈도우의 타이틀을 저장시킬 변수와 윈도우의 정보를 저장시킬 WINDOWINFO 구조체 변수를 선언합니다.

- Wininfo.cbSize = sizeof(WINDOWINFO);

Wininfo.cbSize멤버의 값에 구조체 크기를 넣어줍니다.

-  if(GetWindowTextLength(hWnd) == 0)
    return TRUE;
   if(GetParent(hWnd) != NULL )
    return TRUE;
   if(IsWindowVisible(hWnd) == FALSE)
    return TRUE;

타이틀값이 0이거나 자식 윈도우거나 보이지 않는 윈도우라면 TRUE를 반환하여 다음 윈도우를 검사합니다.

- GetWindowText(hWnd,titleName,500);

윈도우의 타이틀명을 가져옵니다.

- if(lstrcmp(titleName,L"지뢰 찾기") == 0)
    GetWindowInfo(hWnd,&Wininfo);

타이틀 명이 지뢰 찾기 라면 그 핸들의 정보를 가져와 Wininfo구조체에 저장시킨 후 아래의 if문을 수행합니다.

- if(Wininfo.wCreatorVersion == 1024)
    GetWindowThreadProcessId(hWnd,&G_LandMinePID);
    return FALSE;

wCreatorVersion멤버의 값이 1024라면 그 핸들의 PID를 구해 G_LandMinePID에 저장시키고 FALSE를 리턴하여 윈도우 검사를 중지합니다. wCreatorVersion멤버는 해당 윈도우가 만들어졌을 때의 윈도우즈 버전 값을 저장합니다. 윈도우즈 xp라면 값은 1024가 저장되며 윈도우즈 7은 다른 값이 저장됩니다. xp와 7지뢰찾기를 구분할 수 있는 아주 간단한 방법입니다.

- else
   G_LandMinePID = -1;
   return TRUE;

만약 찾은 지뢰 찾기 핸들이 xp버전이 아니라면 G_LandMinePID에 -1을 저장시키고 다음 윈도우를 찾습니다. 만약 xp버전의 지뢰찾기를 못찾았다면 G_LandMinePID는 -1을 저장시킨채로 검사가 종료되게 됩니다.

- return TRUE;

return FALSE문을 만나지 못했다면 만날 때 까지 혹은 윈도우를 전부 검사할 때까지 반복합니다.

자 여기까지가 코드의 마지막입니다. 다만 비주얼 스튜디오로 이 프로그램을 코딩해 실행할려면 하나의 작업을 더해주어야 합니다. 그것은 비주얼 스튜디오에서 기본적으로 제공해주는 기능인 증분 링크 기능을 해제시켜주는 것이지요.

프로젝트 - 속성 - 링커 - 일반 - 증분 링크 사용 부분을 아니오로 바꿔줍시다.

이 증분 링크 라는 것은 링크 시간을 줄여주는 기능을 의미합니다. 다만 이 기능을 사용하며 링크를 하게 되면 프로그램의 크기가 커지며 무엇보다 프로그램에 점프 썽크가 포함되어 진다는 것입니다.

그렇다면 점프 썽크란 무엇일까요? 말로만 설명하기 뭐하니 직접 보여드리겠습니다.

위에 있는 프로그램은 비증분링크로 짜여진 프로그램입니다.

위에 있는 프로그램은 증분링크로 짜여진 프로그램입니다. 딱봐도 JMP코드가 들어가 있는 것을 볼 수 있지요? 이 JMP코드가 바로 점프 썽크입니다. 이 점프 썽크의 특징은 함수를 호출하게 될 때 바로 함수의 주소로 가는 것이 아니라 이 점프 썽크를 통해 함수의 주소로 가게 됩니다.

즉 시작주소값이 0x12345678인 함수를 호출한다면 바로 0x12345678로 가는 것이 아니라 JMP 12345678 코드가 적힌 부문으로 간 다음에 이 JMP에 의해 0x12345678로 가게 되는 것이지요.

그렇다면 문제가 뭘까요? 자 한번 지뢰찾기에 코드를 한번 삽입해보도록 하겠습니다.

먼저 비증분링크로 짜여진 프로그램으로 winmine의 가상메모리에 코드를 삽입해보겠습니다. finelandmine에 적힌 어셈블리코드가 들어가 있는 것을 볼 수 있습니다. 본 코드가 34바이트보다 작기 때문에 다소 쓸데없는 값이 추가로 들어가긴 했지만 RETN 문이 있으므로 별 상관이 없습니다.

자 그럼 이제 증분링크로 짜여진 프로그램으로 코드를 삽입해봅시다.

finelandmine함수의 주소값을 이용하여 코드를 작성하도록 했지만 보시다시피 점프썽크로 인해 finelandmine함수의 주소값이 실질적인 주소값이 아닌 JMP주소값이 되버렸습니다. 그래서 그 JMP 코드가 적힌 부분부터 34바이트 코드를 집어넣는 것을 볼 수 있습니다.

이래놓고 스레드를 실행시키면 지뢰찾기가 특이한 코드를 버텨내지 못하고 에러가 나게 되는 것이지요. 없는 주소로 점프하라하니 지뢰찾기가 버티겠습니까?

그래서 보통 비주얼 스튜디오에서는 함수의 주소값을 이용하지 않은 쉘코드를 사용하여 코드 인젝션 기법을 수행합니다. 이 방법은 증분링크에 구애받지 않기 때문이지요.

자 아무튼 설명도 끝났고 비증분링크로 프로그램을 코딩하는 것도 마쳤으면 이제 프로그램 결과를 보는 것만 남았군요.

다음 포스팅에선 지뢰찾기핵 결과를 포스팅하도록 하겠습니다.

Posted by englishmath