이번에는 바빌론 키젠미(Babylon Keygenme) 프로그램을 리버싱 해보도록 하겠습니다.


상당히 쉬운 편이며, 프로그램은 C언어로 만들어졌다고 합니다. 패킹은 되어있지 않은 상태이구요.


우선은 아래 파일을 다운로드 받아 Babylon Keygenme.exe를 실행시켜 보도록 합시다.


babylon-keygenme.zip


실행 파일을 실행시키시면 아래와 같은 화면이 보여질 것입니다.



우선은 아무렇게나 입력을 한 뒤에 결과를 확인해볼까요?



번역기로 저 문장을 해석해보니 잘못된 패스워드라고 합니다. 우선은 올리디버거를 열어 이 파일을 열어보도록 합시다. (필자는 이뮤니티 디버거를 사용했습니다. 올리 디버거를 쓰셔도 무방합니다.) 디버거로 파일을 열었으면 어셈블리 코드가 보이는 창에 우클릭을 하여 팝업 메뉴에서 Search for -> All referenced text strings를 선택합니다. 



위에 보시면 프로그램에서 참조되는 모든 문자열을 뽑아낸 것을 볼 수 있는데, 'Mauvais mot de passe!'는 패스워드가 틀렸을 경우에 나오는 문장이고, 'Yeah, c'est bon.'은 패스워드가 일치했을 경우 나오는 문장임을 추측할 수 있습니다. 우선은 처음으로 돌아가, 사용자에게 이름과 시리얼을 입력받는 부분을 살펴보도록 합시다. "[x] Name : "를 더블 클릭하여 이 문자열이 어디서 등장하는지 먼저 보도록 합시다.


00401455   . 68 AD124000    PUSH Babylon_.004012AD                   ; format = "%s"

0040145A   . E8 19040000    CALL <JMP.&msvcrt.scanf>                 ; scanf


위에서 보이는 scanf는 C언어의 입력 함수로써 format을 보시면 %s라고 되어있는데, 이는 문자열을 입력받는 다는 뜻입니다. 즉, 이름을 입력받고 나서 아래처럼 입력받은 문자열의 길이를 검사하고 있음을 알 수 있습니다.


0040146B   . 50             PUSH EAX                                 ; s

0040146C   . E8 FF030000    CALL <JMP.&msvcrt.strlen>                ; strlen

00401471   . 83C4 10        ADD ESP,10

00401474   . 8945 E8        MOV DWORD PTR SS:[EBP-18],EAX

00401477   . 837D E8 03     CMP DWORD PTR SS:[EBP-18],3

0040147B   . 7E 08          JLE SHORT Babylon_.00401485

0040147D   . 837D E8 0E     CMP DWORD PTR SS:[EBP-18],0E

00401481   . 7F 02          JG SHORT Babylon_.00401485

00401483   . EB 2B          JMP SHORT Babylon_.004014B0

00401485   > 83C4 F4        ADD ESP,-0C

00401488   . 68 B0124000    PUSH Babylon_.004012B0                   ; format = "Attention ! 3 < Nom < 15"

0040148D   . E8 EE030000    CALL <JMP.&msvcrt.printf>                ; printf


위를 천천히 살펴보시면 EAX 레지스터의 값을 스택에 올리고, 그 후에 strlen 함수를 호출합니다. 이는 strlen 함수를 호출한 뒤에 반환값이 EAX 레지스터에 담기기 때문에 나중에 EAX 레지스터의 원본 값을 복구하기 위함입니다. 그리고 ESP 레지스터에 10을 더해준 다음에 [EBP-18]에 EAX 레지스터의 값을 저장한다는 것을 알 수 있습니다. 그리고 [EBP-18]과 3을 비교하고 [EBP-18]이 3과 같거나 이하일 경우 JLE에 의해 401488로 점프되며, [EBP-18]과 0E(15)를 비교하고 나서 [EBP-18]이 15보다 크면 JG에 의해 401488로 점프됩니다. 만약 401488로 점프되지 않고, '3 < 이름의 길이 < 15'라면 4014B0으로 점프됩니다. 



직접 확인해보니 정말로 이름의 길이가 3자 이하이거나 15자 이상일 경우 저러한 문장이 출력되고 프로그램이 종료되어 버립니다. 그럼 이제 시리얼을 입력하는 부분을 보도록 하겠습니다. 입력되는 부분을 쭉 보시다 보면 반복되는 구간을 보실 수가 있습니다. 시리얼을 입력하는 부분 바로 뒤에 브레이크 포인트를 걸고 계속 F8로 실행해보시면 어느 부분에서 계속 반복하게 되는데, 이 부분을 잠깐 살펴보도록 하겠습니다. 우선은 이름이 su6net, 시리얼이 kneeng로 가정하고 설명을 드리도록 하겠습니다.


004014E0   > 8B45 E8        MOV EAX,DWORD PTR SS:[EBP-18]

[EBP-18]의 값을 EAX 레지스터에 저장한다.

-> [EBP-18]에 담긴 값은 이름의 길이입니다. 곧 EAX 레지스터에 닮기는 값은 6입니다.

004014E3   . 89C2           MOV EDX,EAX

EAX 레지스터의 값을 EDX 레지스터에 저장한다.

004014E5   . 8D0412         LEA EAX,DWORD PTR DS:[EDX+EDX]

[EDX+EDX]의 주소를 EAX 레지스터에 저장한다.

-> 여기서 EDX의 값이 6이므로, EDX+EDX는 12이며 이 주소를 EAX에 저장한다는 것입니다.

004014E8   . 3945 FC        CMP DWORD PTR SS:[EBP-4],EAX

EAX 레지스터와 [EBP-4]의 값을 비교한다.

-> [EBP-4]에는 처음에 0으로 초기화 되었으며, 이 분기문에서 반복문이 진행되냐 종료되냐를 결정합니다.

004014EB   . 7C 03          JL SHORT Babylon_.004014F0

위의 비교에서 [EBP-4]가 EAX보다 작을 경우 4014F0으로 점프한다.

004014ED   . EB 31          JMP SHORT Babylon_.00401520

401520으로 점프한다. 반복을 모두 끝마치고 반복문을 빠져나가는 부분이다.

004014EF     90             NOP

아무런 기능을 하지 않는다.

004014F0   > 8D85 A0FDFFFF  LEA EAX,DWORD PTR SS:[EBP-260]

[EBP-260]의 주소값을 EAX 레지스터에 저장한다.

004014F6   . 8B55 FC        MOV EDX,DWORD PTR SS:[EBP-4]

[EBP-4]의 값을 EDX 레지스터에 저장한다.

-> 처음의 [EBP-4] 값은 0이므로 EDX 레지스터에 0이란 값이 저장된다.

004014F9   . 8D8D E0FEFFFF  LEA ECX,DWORD PTR SS:[EBP-120]

[EBP-120]의 주소값을 ECX 레지스터에 저장한다.

-> [EBP-120]은 입력받은 이름이 들어가있는 변수의 주소다. 즉 이름 변수의 주소를 ECX 레지스터에 저장한다.

004014FF   . 8B5D F8        MOV EBX,DWORD PTR SS:[EBP-8]

[EBP-8]의 값을 EBX 레지스터에 저장한다.

처음에 [EBP-8]의 값은 0이므로 EBX에는 0이 저장된다.

00401502   . 8A0C0B         MOV CL,BYTE PTR DS:[EBX+ECX]

[EBX+ECX]의 값을 CL 레지스터에 저장한다.

-> ECX에는 이름의 주소가, EBX는 반복할때마다 1씩 증가한다. 처음에는 EBX 값이 0이므로 결과적으로는 ECX 레지스터에 담긴 값이 가리키는 문자의 아스키 코드를 CL 레지스터에 저장한다. 이름이 su6net 이므로, 처음에는 's'의 아스키 코드가 CL 레지스터에 들어가는 셈이다. 's'의 아스키 코드가 115이므로 CL엔 115(16진수로는 73)가 들어갈 것이다.

00401505   . 880C02         MOV BYTE PTR DS:[EDX+EAX],CL

CL의 값을 [EDX+EAX]에 저장한다.

00401508   . 8B45 FC        MOV EAX,DWORD PTR SS:[EBP-4]

[EBP-4]의 값을 EAX 레지스터에 저장한다.

-> 처음의 [EBP-4] 값은 0이므로 EAX 레지스터에 0이란 값이 저장된다.

0040150B   . 40             INC EAX

EAX 레지스터의 값을 1만큼 증가시킨다.

0040150C   . 8D95 A0FDFFFF  LEA EDX,DWORD PTR SS:[EBP-260]

[EBP-260]의 주소값을 EDX 레지스터에 저장한다.

00401512   . C60410 20      MOV BYTE PTR DS:[EAX+EDX],20

20이란 값을 [EAX+EDX]에 저장한다.

-> 0x20은 10진수로 32이며, 이는 공백의 아스키 코드이다.

00401516   . FF45 F8        INC DWORD PTR SS:[EBP-8]

[EBP-8]의 값을 1만큼 증가시킨다.

00401519   . 8345 FC 02     ADD DWORD PTR SS:[EBP-4],2

[EBP-4]의 값에 2를 더한다.

0040151D   .^EB C1          JMP SHORT Babylon_.004014E0

4014E0으로 점프한다.


위처럼 이름의 문자를 하나 달고 공백을 추가하고 이 과정을 반복하다 보면 [EBP-260]에 "s u 6 n e t "가 들어가게 됩니다. 그리고 쭉 내려가다보면 괴상한 특수문자들이 추가되며, 이 특수문자의 길이를 구하고 그 길이만큼 반복을 하는 반복문이 보이실 것입니다. 반복문을 살펴보자면,


00401531   . 50             PUSH EAX                                 ; s = "-[#]]=}&&&+(=$*,,)&.*/+++[][;/..?"

00401532   . E8 39030000    CALL <JMP.&msvcrt.strlen>                ; strlen


이렇게 괴상한 특수문자의 길이를 구하고 나서는 이 길이만큼 반복을 하게 되는데, 여기서 BL 레지스터에 괴상한 특수문자 하나가 넣어지고 INC 명령문에 의해 값이 증가되고, 길이를 비교하고 넣고 증가하고 이렇게 반복하다가 [EBP-160]의 특수문자 아스키코드에서 1씩 증가하여 ".\$^^>~''',)>%+--*'/+0,,,\^\<0//?"으로 바뀌게 됩니다. 그리고 그 아래에 또 반복문이 등장하는데, 이 반복문을 살펴보면 XOR 연산을 하는 부분이 있습니다.


00401581   . 50             PUSH EAX                                 ; s

00401582   . E8 E9020000    CALL <JMP.&msvcrt.strlen>                ; strlen

..

0040158C   . 3945 FC        CMP DWORD PTR SS:[EBP-4],EAX

..

004015C9   . 321C37         XOR BL,BYTE PTR DS:[EDI+ESI]

..

004015CF   . FF45 FC        INC DWORD PTR SS:[EBP-4]

004015D2   .^EB A4          JMP SHORT Babylon_.00401578


위 부분을 간략하게 보시면 공백이 들어간 이름 문자열의 길이만큼 반복을 하게 되는데, 이름의 문자와 괴상한 특수문자의 값이 각각 대응하여 XOR 연산을 합니다. 115(s) XOR 46(.), 32(공백) XOR \(92).. 이런식으로 계속 XOR 연산을 하다가 끝나게 되면 또다시 반복문이 등장하게 됩니다.


004015F0   > 837D FC 00     CMP DWORD PTR SS:[EBP-4],0

004015F4   . 7D 02          JGE SHORT Babylon_.004015F8

004015F6   . EB 20          JMP SHORT Babylon_.00401618

004015F8   > 8D85 A0FCFFFF  LEA EAX,DWORD PTR SS:[EBP-360]

004015FE   . 8B55 F4        MOV EDX,DWORD PTR SS:[EBP-C]

00401601   . 8D8D A0FEFFFF  LEA ECX,DWORD PTR SS:[EBP-160]

00401607   . 8B5D FC        MOV EBX,DWORD PTR SS:[EBP-4]

0040160A   . 8A0C0B         MOV CL,BYTE PTR DS:[EBX+ECX]

0040160D   . 880C02         MOV BYTE PTR DS:[EDX+EAX],CL

00401610   . FF45 F4        INC DWORD PTR SS:[EBP-C]

00401613   . FF4D FC        DEC DWORD PTR SS:[EBP-4]

00401616   .^EB D8          JMP SHORT Babylon_.004015F0

 

위에서 [EBP-4]는 XOR 연산된 문자열의 길이가 들어가며, 길이만큼 루프를 돌게 됩니다. 우선은 EAX 레지스터에 [EBP-360]의 주소값이 저장되고, [EBP-C]의 값이 EDX 레지스터에 저장됩니다. 그러고나서 [EBP-160]의 주소값이 ECX 레지스터에 저장되고, [EBP-4]의 값이 EBX 레지스터에 저장됩니다. 마지막으로는 CL 레지스터에 [EBX+EDX]의 값이 저장되고, CL 레지스터의 값을 [EDX+EAX]에 저장하며 [EBP-C]를 증가시키고 [EBP-4]를 감소시킵니다.

 

정리해보자면, 처음에 EBX+ECX가 가리키는 것이 EBX에는 XOR 연산된 문자열의 길이, ECX 레지스터에는 [EBP-160]의 주소값이 담기었는데 [EBP-160]의 주소값 + XOR 연산된 문자열의 길이는 [EBP-160]의 마지막 문자를 가리킬 것이고, 이 문자를 [EDX+EAX]에 놓게 되는데 EDX 레지스터에는 [EBP-C]의 값이, EAX 레지스터에는 [EBP-360]의 주소값이 담기므로 [EBP-360]의 주소값 + [EBP-C]일 것입니다. [EBP-4]는 점점 감소되며, [EBP-C]는 점점 증가하는데 이는 [EBP-360]에 [EBP-160]의 값이 거꾸로 뒤집혀서 저장이 되는것을 확인하실 수 있습니다. 
 

00401629   . 50             PUSH EAX                                 ; /s

0040162A   . E8 41020000    CALL <JMP.&msvcrt.strlen>                ; strlen

0040162F   . 83C4 10        ADD ESP,10

00401632   . 89C0           MOV EAX,EAX

00401634   . 3945 FC        CMP DWORD PTR SS:[EBP-4],EAX

00401637   . 72 07          JB SHORT Babylon_.00401640

00401639   . EB 45          JMP SHORT Babylon_.00401680

0040163B     90             NOP

0040163C     8D7426 00      LEA ESI,DWORD PTR DS:[ESI]

00401640   > 8D85 A0FBFFFF  LEA EAX,DWORD PTR SS:[EBP-460]

00401646   . 8B55 FC        MOV EDX,DWORD PTR SS:[EBP-4]

00401649   . 8D8D A0FCFFFF  LEA ECX,DWORD PTR SS:[EBP-360]

0040164F   . 8B5D F0        MOV EBX,DWORD PTR SS:[EBP-10]

00401652   . 8A0C0B         MOV CL,BYTE PTR DS:[EBX+ECX]

00401655   . 880C02         MOV BYTE PTR DS:[EDX+EAX],CL

00401658   . 8B45 FC        MOV EAX,DWORD PTR SS:[EBP-4]

0040165B   . 40             INC EAX

0040165C   . 8D95 A0FBFFFF  LEA EDX,DWORD PTR SS:[EBP-460]

00401662   . 8D8D A0FEFFFF  LEA ECX,DWORD PTR SS:[EBP-160]

00401668   . 8B5D EC        MOV EBX,DWORD PTR SS:[EBP-14]

0040166B   . 8A0C0B         MOV CL,BYTE PTR DS:[EBX+ECX]

0040166E   . 880C10         MOV BYTE PTR DS:[EAX+EDX],CL

00401671   . FF45 F0        INC DWORD PTR SS:[EBP-10]

00401674   . FF45 EC        INC DWORD PTR SS:[EBP-14]

00401677   . 8345 FC 02     ADD DWORD PTR SS:[EBP-4],2

0040167B   .^EB A3          JMP SHORT Babylon_.00401620

 

이 부분도 반복문이 등장하고, 다른 반복문과 비슷합니다. 간략하게 보자면, [EBP-460]에 [EBP-360]과 [EBP-160]의 값을 번갈아가면서 저장하는데 이는 즉, [EBP-460][1]에는 [EBP-360][1]이, [EBP-460][2]에는 [EBP-160][1]이, [EBP-460][3]에는 [EBP-360][2]이, [EBP-460][4]에는 [EBP-160][2] 이런 식으로 들어간다는 것입니다. 이 부분 아래를 쭉 내리다 보면 반복문이 또다시 등장합니다. 

 

004016AC   . 803C02 1F      CMP BYTE PTR DS:[EDX+EAX],1F
004016B0   . 7E 11          JLE SHORT Babylon_.004016C3
...
004016BB   . 803C02 7A      CMP BYTE PTR DS:[EDX+EAX],7A
004016BF   . 7F 02          JG SHORT Babylon_.004016C3
004016C1   . EB 0D          JMP SHORT Babylon_.004016D0 

...

004016CC   . C60402 36      MOV BYTE PTR DS:[EDX+EAX],36

...

 

그 중에서도 위와 같은 어셈블리 코드를 보실 수 있는데, 이는 [EBP-460]의 문자열 중에서 헥스값이 z(7A)보다 크거나 1F 보다 작을 경우 모두 문자 6(36)으로 바꿉니다. 마지막으로는 아이디의 길이 만큼 [EBP-460]의 값을 앞에서 자르면 키가 나오게 되는데, 위의 과정을 차례차례 밟으면서 어떠한 값이 나오고 그 값이 정확한 시리얼인지 테스트를 한번 해보셔도 좋습니다. 우선은 이러한 과정을 모두 거쳤더니 1]66/Q라는 값이 나왔고, 이 값을 시리얼로 넣었더니 시리얼이 맞는 것을 확인하실 수 있습니다. 아래 C언어 코드에 대략적인 과정을 주석으로 끼워 넣었으니 참고하시면 좋을듯 합니다.

#define _CRT_SECURE_NO_WARNINGS
#include <STDIO.H>
#include <STRING.H>

int main(int argc, char **argv)
{
	char name[128], EBP260[128], EBP360[128], EBP460[256];
	char EBP160[35] = "-[#]]=}&&&+(=$*,,)&.*/+++[][;/..";
	int i, j, l;

	EBP160[32] = 0xA7; EBP160[33] = 0x30; EBP160[34] = 0;
	// 1. 아이디를 입력한다.
	printf("이름을 입력하세요: ");
	scanf("%s", name);
	// 2. EBP-160의 값을 증가시킨다.
	for (i = 0; i < 34; i++)
		EBP160[i] = EBP160[i] + 1;
	// 3. 이름 문자열 사이에 공백을 추가시킨다.
	l = strlen(name);
	for (i = 0, j = 0; j < l; i += 2, j++)
	{
		EBP260[i] = name[j];
		EBP260[i + 1] = 0x20;
	}
	EBP260[i] = '\0';
	// 4. [EBP-160]과 [EBP-260]의 XOR 연산
	l = strlen(EBP260);
	for (i = 0; i < l; i++)
		EBP160[i] ^= EBP260[i];
	// 5. [EBP-360]에 [EBP-160] 값이 거꾸로 된 값이 저장됨
	l = strlen(EBP160);
	for (i = 0; i < l; i++)
		EBP360[i] = EBP160[l - i - 1];
	EBP360[i] = '\0';
	// 6. [EBP-460]에 [EBP-360]과 [EBP-160] 값을 번갈아 가며 저장함
	for (i = 0, j = 0; j < l; i += 2, j++)
	{
		EBP460[i] = EBP360[j];
		EBP460[i + 1] = EBP160[j];
	}
	EBP460[j] = '\0';
	// 7. [EBP-460]의 값 중 7A보다 크거나 1F보다 작으면 6으로 바꾼다
	l = strlen(EBP460);
	for (i = 0; i < l; i++)
		if (EBP460[i] > 0x7A || 0x1F > EBP460[i])
			EBP460[i] = 0x36;
	EBP460[strlen(name)] = '\0';
	printf("시리얼: %s\n", EBP460);

	return 0;
}