안녕하십니까 이번에는 level10을 한 번 풀어보겠습니다. 홈페이지로 들어가 문제를 한 번 봅시다.

시리얼이 WWWCCCJJJRRR일 때 이름값을 구하라고 되어있습니다. 이름은 4자리자로 하는군요. 프로그램을 다운받아 실행셔키봅시다.

이름과 시리얼이 매치가 안될 경우 실패문자열을 출력합니다. 올리디버거에서 문자열을 찾아봅시다.

CMP EAX,0C를 하여 EAX값이 0C가 아닐 경우 실패문자열이 출력되는것을 확인 할 수 있습니다. C가 10진수로 12이므로 아마 이부분은 시리얼의 자리수를 검사하는 부분인 것 같습니다. 계속 진행합시다.

00402092부분을 보시면 0022FEA3스택의 값과 0을 비교하는 것을 볼 수 있습니다. 만약 0022FEA3스택의 값이 0이라면 실패문자열이 출력되는 곳으로 점프하는군요. 0022FEA3스택에 하드웨어 브레이크를 건 후 다시 실행시켜봅시다. 

그러면 00401F8F에서 AL의 값을 0022FEA3에 대입하는 것을 확인할 수 있습니다. EAX레지스터는 보통 함수의 반환값을 저장하므로 위의 0040144C함수에서 값을 받을 확률이 큽니다. 0040144C함수를 살펴봅시다.

함수 내부로 가서 살펴보면 00401C80에서 22FD84스택의 값을 EAX에 옮기는 것을 확인할 수 있습니다. 22FD84스택에 하드웨어 브레이크를 걸고 재실행해봅시다.

그러면 00401BC2에서 22FD3C스택의 값을 EDX에 옮기고 그 EDX를 22FD84에 옮기는 것을 확인할 수 있습니다. 이번엔 22FD3C스택에 브레이크를 걸고 재실행해봅시다.

이번엔 00401B79에서 22FDDF스택의 값을 EAX에 옮기고 그 EAX를 22FD3C에 옮기는 것을 확인할 수 있습니다. 지겹지만 22FDDF에 브레이크를 걸고 또 재실행해봅시다.

그러면 00401B5C에서 CMP구문을 수행하고 그 결과에 따라 JLE명령어 수행. JLE가 수행되지 않으면 22FDDF에 0을 집어넣는 것을 확인할 수 있습니다. 즉 EAX가 5보다 클 경우 22FDDF에 0을 집어넣는 것을 볼 수 있으며 우리가 원하는 성공문자열을 출력하기 위해서는 22FDDF에 0이 들어가지 않아야 하는 것을 알 수 있습니다.

간단하게 00401B5C에서 EAX값이 5이하 여야 한다는 것이지요. EAX값이 5이하라면 JLE코드에서 점프를 하게 되므로 22FDDF에 0을 대입하는 코드를 생략할 수 있게 딥니다.

그렇다면 이 EAX는 어디서 오는 것일까요? 코드를 다시 한 번 분석해보니 다음과 같은 알고리즘을 찾을 수 있었습니다.

여기서 하나라도 CMP EAX,5에 의해 0022FDDF에 0이 넣어지면 실패하게 됩니다.

자 그러면 이제 이름으로 만든 22FDCC스택의 값과 시리얼로 만든 22FDD0스택의 값이 어떻게 만들어지는지 살펴볼 차레입니다. 분석을 한 결과 다음과 같은 알고리즘을알아내었습니다.

즉 CMP연산에 필요한 EAX와 EDX에서 EDX는 시리얼값을 토대로 만들어진 값이며 EAX는 이름값을 토대로 만들어진 값이라고 결론지을 수 있습니다. 이것을 코딩으로 한 번 구현해봅시다.

a배열에는 알파벳이 저장된 배열입니다.

i는 이름값을 차례대로 대입하기 위한 카운트이며 j는 EAX와 EDX값을 구하기 위한 카운트입니다.

for문이 총 5번 쓰였는데 첫번째 for문은 이름값을 차례대로 대입하기 위한 for문이고 그다음 실행하는 2개의 for문은 이름값을 토대로 만든 EDX값을 구하기 위해, 그리고 나머지 2개의 for문은 시리얼값을 토대로 만든 EAX값을 구하기 위해 사용하였습니다.

abs함수는 절대값을 구해주는 함수이고 이렇게 반환된 값이 5이하 일경우 해당하는 이름값을 출력하도록 하였습니다.

자 그러면 한 번 실행해봅시다. 제일 먼저 시리얼값인 W에 해당하는 첫자리 이름값을 구해보겠습니다.

여기서 출력된 숫자와 영문자 값이 시리얼 W에 해당하는 값. 즉 이름의 첫자리에 올 수 있는 값입니다.

이번에는 시리얼 C에 해당하는 값. 즉 이름의 두자리에 올 수 있는 값입니다.

이번에는 시리얼 J에 해당하는 값. 즉 이름의 세자리에 올 수 있는 값입니다.

마지막으로 시리얼 R에 해당하는 값. 즉 이름의 네자리에 올 수 있는 값입니다.

정리하면 다음과 같습니다.

보시다시피 올 수 있는 단어가 많으므로 이로 만들 수 있는 이름값도 굉장히 많습니다.

31A6, 61A6 38A6, 3DJG.... 이런식으로 말입니다.

그런데 문제에서 보면 순서상 가장 먼저 오는 문자열이 바로 정답문자열이라고 합니다. 그렇다면 가장 먼저 출력된 값인 3,1,A,6으로 만든 문자열이 정답이 되겠군요. 일단 확인해봅시다.

올바른 이름과 키값이라고 출력되는군요. 한번 인증해봅시다.

성공적으로 인증이 된 것을 확인 할 수 있습니다.

네 이것으로 문제풀이를 마치겠습니다.

'codeengn' 카테고리의 다른 글

codeengn - Advance RCE level 10  (0) 2017.07.19
codeengn - Advance RCE level 9  (0) 2017.07.15
codeengn - Advance RCE level 8  (0) 2017.07.13
codeengn - Advance RCE level 6  (0) 2016.08.07
codeengn - Advance RCE level 5  (0) 2016.08.07
codeengn - Advance RCE level 4  (0) 2016.08.07
Posted by englishmath

안녕하십니까 이번에는 level9를 한 번 풀어보겠습니다. 홈페이지로 들어가 문제를 한 번 봅시다.

패스워드를 찾으라고 되어있습니다. 파일을 다운받아 실행시켜봅시다.

이름과 패스워드를 입력하면 이름 혹은 패스워드가 틀렸다는 문구가 나옵니다. 올리디버거로 문자열을 찾아봅시다.

입력받을 때 출력된 문자열인 Username과 password가 보입니다. 따라 들어가봅시다.

00EC1000 함수를 호출하면 실패 문자열이 출력되는 것을 확인할 수 있네요. 따라 들어가봅시다.

계속 코드를 수행하다 보면 위의 TEST문과 CMP구문에 의해 실패 문자열이 출력되는 루프 부분으로 점프하는 것을 볼 수 있습니다. 즉 성공문자열을 출력하기 위한 루프로 가기 위해서는 위의 점프 명령어에 점프하지 않아야 한다는 것을 할 수 있습니다.

조건은 총 2개입니다.

TEST BL,BL -> BL의 값이 0이면 JZ로 인해 점프하게 됩니다. 즉 BL은 0이 아니어야 합니다.

CMP BYTE PTR SS:[ESP+0B], 0 -> ESP+0B주소값인 스택의 값이 0이면 JE에 의해 점프하게 됩니다. 즉 ESP+0B의 스택값은 0이 아니어야 합니다.

다시 한 번 코드를 살펴봅시다.

재시작하여 다시 코드를 수행하면 윗부분의 첫루프 부분에서 우리가 입력한 Name의 값이 첫번째자리랑 0x00(NULL값)을 비교하는 것을 알 수 있습니다. 코드를 계속 수행해봅시다.

코드를 계속 수행하면 JNE로 인해 SBB명령어 부문으로 점프하게 되고 SBB 명령어 부문에서 EAX를 0이 아닌 값으로 만듭니다.

그리고 TEST EAX,EAX 코드를 수행하여 그 ZF의 값에 따라 SETZ 명령어가 수행됩니다.

SETZ 명령어는 ZF의 값이 1일 때 해당 부분의 값을 1로 수정시켜주는 명령어입니다. 여기서는 BL의 값을 변경시키는데 이 때 이 BL의 값은 앞에서 보셧던 성공 문자열 출력 분기점에서 TEST 코드를 수행하는 레지스터입니다. 즉 이 코드에서 BL의 값이 1이 되어야 분기점의 TEST BL,BL 에서 TEST가 ZF를 0으로 반환하고 ZF가 0이므로 JZ명령어가 수행되지 않게 됩니다. 즉 성공부분으로 갈 수 있는 첫 부분입니다.

계속 다음 코드로 넘어가봅시다.

SETZ명령어 바로 아래의 코드를 보시면 CMP EAX,DWORD PTR DS:[ECX]부분이 보입니다. 이 때 비교하는 값은 우리가 입력한 패스워드(EAX)와 0088228F를 비교하는 것을 볼 수 있습니다. 그리고 이로 인한 ZF가 설정되어 밑의 SETE BYTE PTR SS:[ESP+0F]에서 값이 입력되는 것을 알 수 있습니다.

SETE 명령어는 ZF가 0일 때 해당 주소의 값을 1로 수정시켜주는 0이며 이 때 수정시키는 주소값인 BYTE PTR SS:[ESP+0F]는 우리가 성공문자열로 가기 위한 분기점에 존재하는 BYTE PTR SS:[ESP+0B]의 값이랑 동일한 것을 알 수 있습니다.

즉 CMP EAX,DWORD PTR DS:[ECX]부분에서 두 값이 서로 같아야 ZF가 0을 반환하고 이 ZF에 의해 SETE코드에서 BYTE PTR SS:[ESP+0F]의 값을 1로 수정하여 성공분기점에서 JE명령어를 수행하지 않게 할 수 있습니다.

최종적으로 정리를 하면 다음과 같습니다.

1. 우리가 입력한 이름의 첫자리 값이 NULL이어야 한다.

2. 우리가 입력한 패스워드 값이 16진수 0088228F이어야 한다.

다만 패스워드를 입력받을 때에는 10진수로 받으므로 우리는 16진수값인 0088228F를 10진수로 바꿔줄 필요가 있습니다. 10진수로 바꾸면 값은 8921743가 됩니다.

즉 이 8921743값이 패스워드라고 볼 수 있겠습니다. 이름 같은 경우에는 NULL을 입력해주면 될 것 같은데 NULL값을 입력할 방법이 없어 시연은 생략하겠습니다.

이 패스워드를 인증해봅시다.

정답이 맞는 것 같군요.

추가로 재미있는 부분이 하나 있다면 코드에 이름과 DonaldDuck를 비교하는 부분이 나오는데 이름을 DonaldDuck으로 주면 "니가 그것(DonaldDuck)을 이름으로 느꼈다는 걸 못믿겠어" 라는 문자열이 추가로 출력됩니다. 일종의 낚시라고 볼 수도 있겠네요.

네 이것으로 문제풀이를 마치겠습니다.

'codeengn' 카테고리의 다른 글

codeengn - Advance RCE level 10  (0) 2017.07.19
codeengn - Advance RCE level 9  (0) 2017.07.15
codeengn - Advance RCE level 8  (0) 2017.07.13
codeengn - Advance RCE level 6  (0) 2016.08.07
codeengn - Advance RCE level 5  (0) 2016.08.07
codeengn - Advance RCE level 4  (0) 2016.08.07
Posted by englishmath

안녕하십니까. 오랜만에 코드엔진 문제를 포스팅하겠습니다. 이번에 포스팅 할 문제는 level8이며 level7은 C#의 이해도가 제법 필요한 문제라 생략하였습니다.

두자리인 Name값을 구하여 MD5로 변환시킨 값이 정답이라고 하는군요. 프로그램을 다운받아 실행시켜봅시다.

어디서 많이 본 그림이군요. 실행시켜봅시다.

이름은 두자리에 키값은 5D88-53B4-52A87D27-1D0D-5B09이라고 했으므로 한 번 Check it버튼을 눌러봅시다.

Please Enter More Chars...글자가 뜨는군요. 올리디버거로 살펴봅시다.

Search for의 All referenced strings를 눌러줍시다.

아까 본 문자열이 있는 것을 볼 수 있습니다. 그쪽으로 진입해봅시다.

보시면 문자열 위의 코드부분에 CMP EAX,3 구문이 있고 바로 밑에 있는 JGE명령어가 있는 것을 보아 EAX가 3이상일 경우 JGE에 의해 점프하는 것을 확인할 수 있습니다. 그리고 이 때 EAX반환값은 2인걸로 보아 아무래도 입력한 이름의 자리수를 EAX에 저장시킨다고 추측할 수 있습니다.

문제에서 Name은 두자리수라고 하였는데 프로그램은 3자리 이상만 값을 받으므로 프로그램의 코드값을 수정해줍시다.

덤프에서 해당 코드가 적혀있는 0045BB24로 이동한 다음 값을 Go To의 Executable file버튼을 눌러줍시다.

기존의 명령인 CMP EAX,3 을 뜻하는 83 F8 03 부분을 83 F8 02로 수정하여 CMP EAX,2 로 코드를 변경한 후 파일을 저장합니다.

저장한 파일을 다시 열면 코드가 변경된 것을 볼 수 있으며 두자리 수를 입력하면 정상적으로 시리얼 생성 부문으로 갈 수 있게 됩니다. 물론 JGE를 JMP로 바꾸는 방법도 괜찮은 방법입니다.

코드를 수행하면 CALL 0045B850코드에 의해 시리얼이 생성된 것을 볼 수 있습니다. 내부로 들어가 봅시다.

내부로 들어가 코드를 수행하다 보면 첫번째 반복 부분에서 우리가 입력한 Name값을 이용하여 ESI의 값을 만드는 것을 볼 수 있습니다.

성공적으로 반복문이 끝나면 ESI값은 2D7B9F20가 되는데 이 ESI값의 상위16비트(2D7B)가 시리얼 값의 첫번째 4자리값이 되는것을 알 수 있습니다.

자 그럼 이제 정리해봅시다.

대충 정리하면 이런식으로 시리얼의 첫 4자리 값을 구할 수 있을 것 같습니다. 코딩으로 한 번 구현해봅시다.

arr배열에는 이름값이 들어가게 됩니다. 첫자리는 arr[0], 두번째 자리는 arr[1]에 들어가게 됩니다.

그다음 for문을 이용하여 첫자리수를 0x30부터 0x7A까지 반복하도록 코딩합니다.

0x30은 아스키코드 값으로 0을 의미하며 0x7A는 아스키코드 값으로 z를 의미합니다.

즉 0부터 소문자 z까지를 전부 반복하기 위해 이렇게 작성하였습니다.

두번째 for문도 마찬가지이며 이런식으로 for문이 진행되게 되면

0 0 -> 0,1 -> 0,2 .......... 0,z

1,0 -> 1,1 -> 1,2 ........... 1,z

최종적으론 z,z까지 반복하게 됩니다.

이렇게 for문을 만들어 name값을 전부 주게 만든다면 이제 그다음은 for문을 이용하여 어셈블리어를 연산하도록 해주면 끝입니다. 다만 코드를 보시면 연산for문을 시작하기 전에 EDX를 0으로 주는 부분이 있는데 이는 코드의 add ESI,EDX 에서 첫자리수를 연산할 때 EDX값이 0이기 때문에 이렇게 구현하였습니다. 두번째 자리 수를 연산할 때에는 EDX값이 첫자리 수를 연산하고 남은 값이 저장되어 있어 일부러 초기화를 하지 않았습니다. 실제로 올리디버거에서 본 코드로 이런식으로 연산합니다.

그리고 마지막으로 연산한 ESI값은 result에 저장시키고 이 result의 상위16비트 값이 문제에 적힌 시리얼의 첫 4자리 값인 5D88일 경우 이 때의 name값들을 알기 위해 16진수로 출력시키도록 하였습니다. 자 그럼 이제 한 번 실행시켜봅시다.

딱 하나가 나온 것을 볼 수 있습니다. name의 첫번째 자리값은 0x43이고 두번째 자리값은 0x36일 때 5D88이 나오는군요.

0x43을 아스키코드로 바꾸면 C, 0x36을 아스키코드로 바꾸면 6입니다. 이 값을 한번 프로그램에 입력해봅시다.

성공했다는 박스가 나타났습니다. 이 값을 MD5해쉬화 합시다.

나온 값을 인증해봅시다.

네 이것으로 문제풀이를 마치겠습니다.

'codeengn' 카테고리의 다른 글

codeengn - Advance RCE level 10  (0) 2017.07.19
codeengn - Advance RCE level 9  (0) 2017.07.15
codeengn - Advance RCE level 8  (0) 2017.07.13
codeengn - Advance RCE level 6  (0) 2016.08.07
codeengn - Advance RCE level 5  (0) 2016.08.07
codeengn - Advance RCE level 4  (0) 2016.08.07
Posted by englishmath

안녕하십니까 이번 포스팅에서는 직접 만든 지뢰찾기핵 프로그램을 이용하여 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