Try Attack/Reverse Engineering[basic]

[crackme5] abex' 5th crackme 풀이 및 복원

D4tai1 2018. 11. 16.

1. [abex' 5th crackme] 풀이시연

1) 실행파일 실행

 [1] 파일 열기

  ▶ 프로그램을 실행시키자 Serial 값을 입력하고 Check를 클릭할 수 있도록 만들어져 있다.

 


  ▶ Check를 클릭하자 입력한 시리얼 값이 올바르지 않다는 메세지를 출력하고 종료한다.


2) 개발언어 및 링커 확인


 [1] Pascal계열의 Delphi로 작성됨을 알 수 있다.

 [2] 32비트 실행파일임을 알 수 있다.

 [3] Terbo 링커를 사용하여 링킹한 것을 알 수 있다.

 [4] 헤더타입이 PE라고 적힌 것이 확인되어 Windows용 실행파일임을 알 수 있다.

 [5] ImageBase(기준주소)가 0x400000임을 알 수 있다 

 [6] EntryPoint(시작주소)가 기준주소로부터 0x1000 만큼 떨어진 곳임을 알 수 있다.

 [7] 기타 시그니쳐 및 옵션 세부사항은 직접 하나씩 확인해보면 된다.

 [8] Detect It Easy 1.01사용하였다.


3) 상상

 [1] 1)에서 실행해본 결과 특정파일 이름이 있는지 확인하는 조건문이 있을 것으로 추측된다.

 [2] 의사코드

시리얼 값 입력..

 

if (입력한 시리얼 값이 정상일 경우) {

//참인 경우? 실행할 내용

} else {

MessageBox(핸들번호, "The serial you entered is not correct!.", "Error!", MB_OK);

}

종료..

 


 

4) 풀이

 [1] Main함수 실행

  ▶ 먼저 시리얼 값이 실행파일 내 존재하는지 확인하기 위해 [search for - All referenced text strings]를 눌러 확인한다.

 

 [2] 문자열 검색

  ▶ [4562-ABEX, L2C-5781] 등의 값이 보이지만 분석하는 것이 목적이므로 "Yep, you entered a correct serial!" 주소로 이동해 보았다.

 

 [3] 분기문

  ▶ 위에서 조건문을 통해 이 메세지 박스 영역으로 온 것으로 보인다.

  ▶ [0x4010FC]의 compare문을 통해 EAX가 0이면 [0x401117]로 점프하는 것이 보인다.

 

 [4] 분기문 전

  ▶ [0x4010FC]주소 위에를 더 보면 lstrcmpiA( )함수와 lstrcatA( )함수가 보인다.

 

 [5] lstrcmpiA( ) 함수

lstrcmpiA( ) 함수의 원형


int lstrcmpiA(
  LPCSTR lpString1,
  LPCSTR lpString2
);

  ▶ lstrcmpiA( )함수는 lpString1과 lpString2를 비교하여 같으면 0을 반환한다.

 

 [6] lstrcatA( ) 함수

LPSTR lstrcatA( ) 함수의 원형
 
LPSTR lstrcatA(
  LPSTR  lpString1,
  LPCSTR lpString2
);

  ▶ lstrcatA( )함수는 lpString1문자열에 lpString2문자열을 추가해서 문자열의 주소를 반환한다.

  ▶ lstrcatA( )함수 위로 올라가보면 jmp문은 보이지 않고 GetDlgItemText( ) 함수와 GetVolumnInformation( ) 함수가 보인다.


 [7] GetDlgItemText( ) 함수

UINT GetDlgItemTextA( ) 함수의 원형


UINT GetDlgItemText (
  HWND hDlg,
  int nIDDlgItem,
  LPSTR lpString,
  int nMaxCount
);

   ▶ GetDlgItemTextA( )함수는 CHAR형 문자열을 복사하며 마지막 NULL을 빼고 복사한 문자열의 길이를 반환한다.

   ▶ 파라미터로는 핸들번호, 복사할 문자열의 ID, 복사 후 저장할 문자열의 주소, 복사할 최대 문자 수가 들어간다.

 

   ▶ 즉, 처음에 시리얼 값 입력한 것을 0x402324의 주소에 복사해 놓는다

 

 [8] GetVolumnInformation( ) 함수

BOOL GetVolumeInformationA( ) 함수의 원형


BOOL GetVolumeInformationA(
  LPCSTR  lpRootPathName,   
  LPSTR   lpVolumeNameBuffer,  
  DWORD   nVolumeNameSize,
  LPDWORD lpVolumeSerialNumber,
  LPDWORD lpMaximumComponentLength,
  LPDWORD lpFileSystemFlags,
  LPSTR   lpFileSystemNameBuffer,
  DWORD   nFileSystemNameSize
);

   ▶ GetVolumnInformation( ) 함수는 파라미터에 주소를 넣고 그 곳에 볼륨에 대한 정보를 얻어오는 함수이다.

   ▶ LPCSTR  lpRootPathName은 드라이브명에 대한 문자열 포인터를 의미한다.

    + lpRootPathName의 값이 NULL(0)일 경우 현재디렉터리의 최상위 디렉터리를 의미한다.

   ▶ LPSTR   lpVolumeNameBuffer는 볼륨명에 대한 문자열 포인터를 의미한다.

   ▶ DWORD   nVolumeNameSize는 볼륨명의 최대 길이를 의미한다.

   ▶ LPDWORD lpVolumeSerialNumber는 볼륨의 일련번호를 수신하는 변수의 포인터를 의미한다.

    + 볼륨의 일련번호는 cmd창에서 [vol c:]의 방식으로 입력하면 확인되는 것과 같다.

   ▶ LPDWORD lpMaximumComponentLength는 파일시스템이 지원하는 파일이름의 최대길이를 받는 변수의 포인터를 의미한다.

   ▶ LPDWORD lpFileSystemFlags는 파일시스템과 관련된 플래그를 수신하는 변수의 포인터를 의미한다.

   ▶ LPSTR   lpFileSystemNameBuffer는 파일시스템의 이름을 수신하는 문자열에 대한 포인터를 의미한다.

   ▶ DWORD   nFileSystemNameSize는 파일시스템이름의 최대길이를 의미한다.

   ▶ 0을 반환했다면 요청한 정보가 모두 검색되지 않았다는 의미이다.

 


   ▶ 즉, 현재 abex' 5th crackme가 있는 최상위 디렉토리의 볼륨명을 0x40225C에 저장하고, 볼륨의 일련번호를 0x402194에 저장한다.

   ▶ 파일시스템 이름을 저장할 문자열 포인터는 NULL로 부여했기 때문에 저장하지 않는다.

 

 [9] DialogBoxParamA( ) 함수

INT_PTR DialogBoxParamA( ) 함수의 원형


INT_PTR DialogBoxParamA(
  HINSTANCE hInstance,
  LPCSTR    lpTemplateName,
  HWND      hWndParent,
  DLGPROC   lpDialogFunc,
  LPARAM    dwInitParam
);

   ▶ DialogBoxParamA( ) 함수는 다이얼로그 박스를 실행시키는 함수이다.

   ▶ HINSTANCE hInstance는 커널이 WinMain( )함수에 부여한 값으로 응용프로그램의 인스턴스 값을 말한다.

    + 매개변수가 NULL일 경우 현재 실행파일의 인스턴스 값을 사용한다.

   ▶ LPCSTR    lpTemplateName은 생성할 대화상자의 ID 값을 말한다.

   ▶ HWND      hWndParent는 창의 핸들번호이다.

   ▶ DLGPROC   lpDialogFunc은 대화상자 함수에 대한 포인터로 대화상자에서 발생한 메세지를 처리하는 메세지처리함수이다.

   ▶ LPARAM    dwInitParam은 대화상자의 초기 모양을 설정하기 위한 값을 전달한다.



   ▶ 즉, 현재 응용프로그램의 인스턴스를 사용하고 ID는 1번을 사용하며, 메세지 발생시 0x401029로 이동한다.

   ▶ 초기화속성은 없다.


 [10] EndDialog( ) 함수

BOOL EndDialog(
  HWND    hDlg,
  INT_PTR nResult
);

   ▶ hDlg는 사용한 대화상자의 핸들값,  nResult는 대화상자 내 함수에서 응용프로그램에 반환할 값을 말한다.


   ▶ 사용한 대화상자를 반환(종료)한다. (두 번째 파라미터의 0은 대화상자를 끝낸다는 말이다.)


 [11] GetModuleHandleA()

HMODULE GetModuleHandleA( ) 함수의 원형


HMODULE GetModuleHandleA(
  LPCSTR lpModuleName
);

   ▶ exe나 dll, 즉 실행파일의 모듈 값을 가져오는 함수이다.

   ▶ lpModuleName 파라미터에 NULL을 넣으면 소스를 작성하고 있는 영역의 모듈 값을 가져온다.



   ▶ 함수 실행 후 eax에 모듈 값(0x400000[Image Base] = 현재 함수가 있는 시작영역)이 반환된다.




 

5) 풀이

 [1] 시작

   ▶ 먼저 첫 번째 파라미터에 NULL을 넣고 GetModuleHandleA( ) 함수를 실행하면 EAX에 현재 영역의 시작주소인 0x400000이 반환된다.


 [2] 대화상자 실행

   ▶ [1]에서 반환받은 0x400000(현재 영역의 시작주소)를 0x4023EC에 저장 후 첫 번째 파라미터에 넣는다.

   ▶ 두 번째 파라미터는 다이얼로그 박스의 식별번호를 1번으로 지정하기 위해 1을 넣는다.

   ▶ 세 번째 파라미터의 핸들번호는 NULL을 넣는다.

   ▶ 네 번째 파라미터의 0x401029는 대화상자에서 입력받은 메세지를 처리하는 곳의 주소를 넣는다.

   ▶ 다섯 번째 파라미터는 특별한 디자인을 설정하지 않도록 NULL을 넣는다.


 [3] 대화상자 입력

   ▶ 123456의 값을 넣고 체크를 누른다.


 [4] GetDlgItemTextA( ) 함수처리

   ▶ 첫 번째 파라미터로 [EBP+8]에 있는 Creakme5의 핸들번호를 넣는다. 

   ▶ 두 번째 파라미터는 복사할 문자열의 식별번호인 0x68을 넣는다.

   ▶ 세 번째 파라미터는 다이얼로그에서 입력한 문자열을 저장할 주소를 넣는다.

   ▶ 네 번째 파라미터는 복사할 문자열의 최대 길이인 0x25를 넣는다.

   ▶ 이 후 다이얼로그에서 입력한 문자를 0x402324의 주소에 저장 후 입력한 문자열의 길이를 반환한다.


 [5] 문자열의 길이 반환

 ▶ 대화상자에 입력한 123456의 길이인 6이 EAX에 반환된 것을 확인한다.


 [7] GetVolumeInformationA( ) 함수

   ▶ 첫 번째 파라미터는 현재 파일이 있는 곳의 최상위 디렉터리를 의미하는 NULL을 넣는다.

   ▶ 두 번째 파라미터는 볼륨명을 저장하기 위한 주소로 0x40225C를 넣는다.

   ▶ 세 번째 파라미터는 볼륨명의 최대 길이를 의미하며 0x32를 넣는다.

   ▶ 네 번째 파라미터는 볼륨의 일련번호를 저장하기 위한 주소로 0x402194를 넣는다.

   ▶ 다섯 번째 파라미터는 파일명의 최대 길이를 가지고 있는 주소로 0x402190을 넣으며 0x402190에는 255(0xFF)가 들어있다.

   ▶ 여섯 번째 파라미터는 플래그를 저장하는 주소로 0x4020C8을 넣는다

   ▶ 일곱 번째 파라미터는 파일시스템의 이름을 저장하는 주소를 넣지만 NULL을 넣는다.

   ▶ 여덟 번째 파라미터는 파일시스템이름의 최대사이즈를 넣지만 NULL을 넣는다.

  ... 기나긴 파라미터 push과정이 끝났다.


 [8] 문자열 합치기

   ▶ 첫 번째 파라미터에 0x40225C를 넣었고, 0x40225C는 [7]에서 볼륨명을 저장한 주소라고 확인했다.

   ▶ 두 번째 파라미터는 0x4023F3을 넣었고, 0x4023F3 주소에는 "4562-ABEX" 값이 들어있다.


 [9] [8]의 실행 결과

   ▶ 첫 번째 파라미터 주소의 문자열과 두 번째 파라미터 주소의 문자열을 붙여서 첫 번째 파라미터의 주소에 저장되었다.

   ▶ 여기서 드는 의문은 볼륨명에 아무런 값이 저장되지 않았을까..?


 [10] 연산

   ▶ 연산방법은 [9]에서 합한 문자열의 시작주소부터 4바이트의 값을 1씩 증가시킨다.

   ▶ 위 연산을 2번 반복한다.

   ▶ 결과적으로 "4562-ABEX"의 문자열이 "6784-ABEX"로 변경되었다.


 [11] 문자열 합치기

   ▶ 첫 번째 파라미터에 0x402000을 넣었고, 0x402000 주소에는 0으로 채워져 있다.

   ▶ 두 번째 파라미터는 0x4023FD를 넣었고, 0x4023FD 주소에는 "4562-ABEX" 값이 들어있다.


 [12] [11]의 실행 결과

 

   ▶ 첫 번째 파라미터 주소의 문자열과 두 번째 파라미터 주소의 문자열을 붙여서 첫 번째 파라미터의 주소에 저장되었다.


 [13] 문자열 합치기

   ▶ [8]에서 합치고 [10]에서 연산한 문자열과 [11]에서 만든 합해서 만든 문자열을 합한다.


 [14] [13]의 결과

   ▶ 첫 번째 파라미터 주소의 문자열과 두 번째 파라미터 주소의 문자열을 붙여서 첫 번째 파라미터의 주소에 저장되었다.


 [15] 문자열 비교

   ▶ [4]에서 0x402324의 주소에 대화상자에서 입력한 문자열을 저장했었다.

   ▶ 첫 번째 파라미터로 [13]에서 합해서 만든 문자열의 주소인 0x402000을 넣는다.

   ▶ 두 번째 파라미터로 [4]에서 입력한 문자의 주소인 0x402324를 넣는다.


 [16] 문자열 비교 결과

   ▶ EAX에 1이 반환된 것으로 보아 입력한 시리얼 값과 "L2C-57816784-ABEX"는 같지 않다는 것을 알 수 있다.


 [17] 비교에 따른 결과

   ▶ EAX가 0이 아니므로 0x401117로 이동하지 못하고 0x401101부터 시작하는 메세지 박스 출력 후 0x40112D부터 시작하는 EndDialog( ) 함수를 호출하고 대화상자를 닫고 종료한다. 


 [18] 의문점

   ▶ [9]에서 볼륨명이 왜 반환되지 않았을까?

   ▶ 직접 하드디스크 볼륨명을 확인해본 결과 "로컬 디스크"라고 적혀 있었다.

   ▶ 그러나 이 볼륨명은 볼륨명이 없을 때 표시되는 글자이다.


   ▶ 그래서 F2를 눌러 "test"라는 볼륨명을 직접 지정해 주었다.


 [19] 볼륨명 저장확인

   ▶ GetVolumeInformationA( ) 함수에 갔다오니 0x40225C의 주소에 "test"라는 볼륨명이 반환되었다.

   ▶ 그렇다면 시리얼 값은 고정적이지 않고 자신의 하드디스크의 볼륨명에 따라 변하는 것으로 보인다.


 [20] 문자열 합치기

   ▶ [19]에서 문자열을 합친 결과 0x40225C의 주소에는 "test4562-ABEX" 문자열이 저장되었다.

  

 [21] 연산

   ▶ 0x40225C에 있는 문자열 "test4562-ABEX"의 앞 4바이트의 값을 1씩 증가시킨다.

   ▶ 위 연산을 2번 반복한다.

   ▶ 0x40225C에 있는 문자열은 앞 4바이트인 "test"가 각각 2씩 더해져서 "vguv4562-ABEX"로 변경되었다.


 [22] 문자열 합하기

   ▶ 이미 instruction을 실행해서 주소의 값이 동일하게 변경되어 있어서 다음 줄에서 확인한다.

   ▶ 0x4010D9의 lstrcatA( )함수에서 0x402000주소의 값은 "L2C-5781"이다.

   ▶ 0x4010E8의 lstrcatA( )함수에서 0x402000주소의 값은 "L2C-5781vguv4562-ABEX"이다.

   ▶ 이유는 "L2C-5781" + "L2C-5781vguv4562-ABEX"를 했기 때문이다.


 [23] 확인한 시리얼 값 입력

   ▶ [22]의 결과로 얻은 시리얼 값을 넣는다.


 [24] 결과 확인


   ▶ 정상적인 시리얼 값이 입력되었음을 확인할 수 있다.

   ▶ 시리얼키 구하는 알고리즘은 아래와 같이 구할 수 있다.

     + ① [현재 파일이 위치하는 최상위디렉터리의 볼륨명] + "4562-ABEX"

     + ② [위에서 연산한 문자열]의 1~4번째 인덱스의 문자는 2씩 더해준다.

     + ③ "L2C-5781" + [위에서 연산한 문자열]


6) 원본소스 가복원

<<Event_start>>

eax =KERNEL32.GetModuleHandleA(0);


USER32.DialogBoxParamA(eax, 1, 0, 0x401029, 0);


<<Event_check>>

eax = USER32.GetDlgItemTextA(hWnd, 0x68, 0x402324, 0x25);


KERNEL32.GetVolumnInformationA(NULL, 0x40225C, 0x32, 0x402194, 0x402190, 0x4020C8, NULL, NULL);


KERNEL32.lstrcatA(0x40225C, 0x4023F3);


for (int i = 2; i >0; i--) {

[0x40225C]++;

[0x40225D]++;

[0x40225E]++;

[0x40225F]++;

}


KERNEL32.lstrcatA(0x402000, 0x4023FD);


KERNEL32.lstrcatA(0x402000, 0x40225C);


eax = KERNEL32.lstrcmpiA(0x402000, 0x402324);


if (eax == 0) {

USER32.MessageBoxA(hWnd, "Yep, you entered a correct serial!", "Well Done!", MB_OK);

}

else {

USER32.MessageBoxA(hWnd, "The serial you entered is not correct!", "Error!", MB_OK);

}


<<Event_OK>>

USER32.EndDialog(hWnd, 0);


KERNEL32.ExitProcess(0);



7) C언어 코드로 복구

 [1] dialog.rc


 [2] resource.h

#define IDD_DIALOG1                     101
#define IDC_EDIT1                       1001


 [3] C소스파일.c

#include<stdio.h>
#include<Windows.h>
#include "resource.h"

char buffer[100];

BOOL CALLBACK dlg_proc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam) {
	int eax;

	switch (msg) {
	case WM_COMMAND:
		switch (LOWORD(wParam)) {
		case IDOK:	//IDOK는 확인을 눌렀을 때
			eax = GetDlgItemTextA(hdlg, IDC_EDIT1, buffer, 0x25);
			//IDC_EDIT1은 시리얼 키를 입력한 공간의 식별번호이다.
			printf("입력한 문자열 길이   : %d \n", eax);
			EndDialog(hdlg, 0);
			break;
		}
		break;
	}
}

int main() {
	int eax;
	char first_buffer[] = "4562-ABEX";
	char second_buffer[] = "L2C-5781";
	char save[100];
	save[0] = NULL;

	char VolumeNameBuffer[100];
	char pVolumeSerialNumber[100];
	int pMaxFilenameLength;
	int pFileSystemFlags;

	eax = GetModuleHandleA(NULL);

	DialogBoxParamA(eax, MAKEINTRESOURCE(IDD_DIALOG1), 0, dlg_proc, 0);
	//MAKEINTRESOURCE(IDD_DIALOG1)는 불러올 대화상자의 식별번호이다.
	
	GetVolumeInformationA(NULL, VolumeNameBuffer, 0x32, pVolumeSerialNumber, &pMaxFilenameLength, &pFileSystemFlags, NULL, NULL);

	lstrcatA(VolumeNameBuffer, first_buffer);
	printf("문자열 더한 결과 1   : %s \n", first_buffer);
	
	for (int i = 2; i > 0; i--) {
		(*(first_buffer))++;
		(*(first_buffer + 1))++;
		(*(first_buffer + 2))++;
		(*(first_buffer + 3))++;
	}
	printf("반복문 이후 결과     : %s \n", first_buffer);
	
	lstrcatA(save, second_buffer);
	printf("문자열 더한 결과 2   : %s \n", save);

	lstrcatA(save, first_buffer);
	printf("문자열 더한 최종결과 : %s \n", save);
	
	eax = lstrcmpiA(save, buffer);
	printf("최종 결과와 비교할 대화상자에서 입력한 문자열 : %s \n", buffer);

	if (eax == 0) {
		MessageBoxA(NULL, "Yep, you entered a correct serial!", "Well Done!", MB_OK);
	}
	else {
		MessageBoxA(NULL, "The serial you entered is not correct!", "Error!", MB_OK);
	}

	return 0;
}


 [4] 복구한 C언어 코드로 실행

   ▶ 대화상자에 시리얼 키 입력


   ▶ 일치할 경우 위와 같이 출력되며 소스복원에 어느정도 성공했음을 알 수 있다.

   ▶ printf문은 시각적으로 보여주기 위해 추가하였으므로 주석처리해도 무방하다.



댓글