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

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