본문 바로가기

Rerversing/Exercise reversing

[Reversing.kr] - Easy Keygen 문제 풀이

두 번째 포스팅 시작..

분석할 파일을 디버깅 하기 전에 같이 따라온 txt 파일을 보면 다음과 같은 메시지가 적혀져 있다.


ReversingKr KeygenMe

Find the Name when the Serial is 5B134977135E7D13


시리얼 값이 5B134977135E7D13 일 때의 이름을 찾아내라는 건데, 한 번 찾아보자.






[그림. 1]  실행 화면


이번 예제는 Win32 Console 프로그램군. 적으라는 Name 부분에는 이번에도 어김없이

개인로 적어본다. ㅎ

보자~~ 이름을 적고 시리얼 키를 텍스트 파일에서 알려준데로 해봤지만 냉정하게도 "wrong" 이라고 결과를 출력해주고 있다.

뭐 시리얼 키는 이미 지정된 것이 틀렸을리는 없겠고 저 키에 해당되는 이름이 별도로 있을 것이라 추정할 수 있겠다.

화면상으로도 힌트는 몇 가지 추정이 된다. 바로 프로그램을 실행하면 콘솔 창에 친절히 나타나준 Input Name, Input Serial, Wrong 이 세 글자 되시겠다.

이전에 1번 문제를 풀 때와 뭔가 좀 비슷한 스멜이 느껴지지 않나? 시작해보자.







[그림. 2] 파일 내역


PEiD로 파일의 기본 내역을 확인해보니 Entry Point 영역과 어떤 언어로 만들어졌는지를 알 수 있다.

뭐 Entry Point 부분은 이뮤니티 또는 올리 디버거에서 Ctrl + F9를 치면 바로 이동해주기는 하니 굳이 PEiD 로 볼 필요는 없지만 그냥 어떤 언어로 만들어졌나 싶어서 한 번 봐본다.

Microsoft Visual C++ 로 만들어진 것이 보인다. (갠적으로 Visual Basic 보단 Visual C++ 로 만들어진 파일들이 젤 좋다. ㅠㅠ)

메인 함수 찾기가 좀 더 편해지겠군. 훗~






[그림. 3] 스트링 내역


솔직히 메인 함수를 트레이싱 하면서 찾는 것 보다는 [그림. 1]에서 얻을 수 있었던 문자열 내역으로 바로 보는 것이 좋다.

디버거의 기능인 search for -> all referenced text strings 으로 보면 해당 샘플이 사용하는 문자열 내역이 바로 보인다.

(초급 단계이다 보니 텍스트의 암호화가 되어있지 않음이 얼마나 고마운가.. ㅎ)

역시나 Input Name, Input Serial, Wrong 의 아스키 코드가 보인다.

그 외에도 다른 단어들이 눈에 띄는데, 바로 Correct! 라는 이 단어... 아마 성공 시의 메시지가 아닐까?

왠지 첫 번째 문제와 비슷해 보인다. 자... 코드로 직접 이동을 해보자.






[그림. 4] 문자열 출력 영역


Correct 와 Wrong 문자열 출력 영역으로 접근하니 둘이 인접해 있으며 그 둘 위의 주소 00401118 영역을 보면 분기점인

JNZ 코드가 보여진다. 해당 분기문을 기점으로 Correct로 갈지 Wrong으로 갈지 나뉘는데, 그렇다면 JNZ 분기점 윗 부분에는

입력 값이 맞는지 아닌지를 비교하거나, 또는 뭔가를 하는 부분이 있을 것이다.





[그림. 5] 키 입력 구간


[그림. 4]에서 화면을 좀 더 위로 올리면 [그림. 5]와 같은 눈에 익은 코드들이 나타나게 된다.

0040102E 주소에서 Input Name: 문자열에 해당되는 아스키 코드를 PUSH 하고 그 아래 즈음에 CALL 하는 함수가 있는데,

함수가 호출되면 콘솔 화면에 Input Name: 이라는 단어가 출력이 된다. 그렇다면 00401059 주소에서의 함수는 무엇일까?

C언어 기초에서 자주 나오는 문자 출력과 문자 입력 함수의 조합 같이 느껴지지 삘이~~

00401059 함수가 호출되기 전에 PUSH EAX, PUSH 0040805C 라는 코드가 보이는데 EAX에는 ESP+10에 해당되는

0018FE20 스텍 주소가 담겨져 있다. (참고로 스텍 주소는 디버거 환경마다 당연히 다르므로 절대적이지 않다.)

그 다음 %s 라는 아스키 코드가 PUSH 가 되는데, 이 조합을 코드를 C 언어로 구현한다면 scanf("%s", eax) 정도 되시겠다.

EAX 레지스터는 포인터로 사용이 된 것이며 ESP+10 해당되는 스텍 주소가 담겨있다.

함수를 호출하고 사용자 이름을 직접 적으면 그 입력 값이 EAX가 가리켰던 스텍 주소에 기록이 된다.






[그림. 6] 이름 길이 체크


이름을 입력하면 그 뒤에 나오는 코드이다. 그림에 보이는 모든 코드 중 일부를 간략히 설명하자면 EDI 레지스터에 이름이

입력된 스텍의 주소를 넣고, ECX를 0xFFFFFFFF 값과 OR 연산하여 -1 로 만들어 버린다. (카운팅을 하여 길이 체크에 사용 됨)

XOR EAX, EAX 명령어는 해당 레지스터의 값을 0으로 초기화 하는 아주 대표적이고 자주 쓰이는 명령어이다.

(참고로 MOV EAX, 0을 해도 되겠지만 XOR EAX, EAX 명령어의 길이가 더 짧기 때문에 이걸로 사용이 된다.)


위 코드에서 0040106E ~ 00401072 형식은 문자열의 길이를 체크하는데 자주 쓰이는 형식이다. (라고 어디선가 본 기억이 남)

0040106E 코드를 설명하자면 REPNE는 ZF 값이 0임과 동시에 ECX가 0보다 클 때만 동작을 반복한다.

반복마다 ECX는 1 감소.. (ZF나 ECX나 둘 중 하나라도 조건이 안 맞게 되면 반복을 중단하고 다음 코드로 넘어간다.)

SCAS는 EAX의 값을 EDI가 가리키는 BYTE 값과 비교하고 EDI를 1만큼 증가시킨다.

자 그렇다면 0040106E의 코드의 결과는 어떻게 될 것인가?


EDI 영역의 Hex 값은 내 PC 기준으로 아래처럼 나온다. 내가 입력한 이름이 저장되어있는 곳의 주소이다.


Addr        Hex dump                                   ASCII

18FE20  |  73  6C  61  79  73  00  00  00  00  |  slays....


SCAS 명령어를 통해 EAX의 0 값과 18FE20 주소의 알파벳 s에 해당되는 0x73을 같은지 아닌지 비교를 한다.

SCAS에서의 비교라는 건 혼자 삽질하며 반복 확인해보니 CMP 명령어가 두 값을 비교하는 행위와 같아 보인다.

즉 비교하는 두 값이 서로 같냐 아니냐를 판별하는 것... (한 번 판별하고 EDI 값을 1 증가시키는 걸 잊지 말자.)

0과 s를 비교하면 서로 다르기 때문에 ZF 값은 0이 될 것이고, ECX는 1 감소한 0xFFFFFFFE 이 될 것이다.

이번엔 0과 a를(EDI는 현재 1증가했으니 a 쪽을 가리킴) 비교하면 역시나 또 ZF는 0이 되고 ECX는 0xFFFFFFFD 가 되고,

반복하다 보면 NULL 값을 만날 때 까지 반복하겠지..

즉, slays 단어 끝의 NULL 값 까지 비교를 하고 나서야 ZF가 1이 되며 빠져나오고 ECX는 0xFFFFFFF9가 나올 것이다.

반복 명령문을 빠져나온 뒤에는 NOT ECX 라는 코드가 보인다. 0xFFFFFFF9 을 NOT 연산하면 0x06이 나오며

그 다음 코드인 DEC ECX 코드로 인해 ECX 값은 최종적으로 0x05가 될 것이다.

0x05 는 slays 라고 내가 입력한 값의 자릿수와 동일하지 않은가? 이런 방식으로 입력한 스트링의 길이를 알아내는 것이다.

자주 쓰인다고 하니 알아두도록... ㅋ


00401073 주소의 TEST ECX, ECX 와 00401075의 JLE 명령어를 통해서 ECX가 0 이하면 점프 시켜버리고 0보다 크면 점프 안하고 계속 진행하겠다는 것이다.

근데 입력을 받는 함수 특성 상 뭔가라도 입력해야 넘어가므로 JLE 분기문 땜에 점프할 일은 아마? 없을 것이다. -_-






[그림. 7] 이름 값 변환 구간


자.. 뭔가 조건부 루프문이 등장했다. 루프문 사이에 함수 호출이 하나 보이는데, 한 번 살펴보자.

0040107E 주소의 코드에서 BYTE PTR SS: [ESP+ESI+C] 이 코드는 앞서 언급하지 않았지만 0x10 값이 들어가 있다.

사진에는 안 보이지만 00401038 ~ 00401042 코드를 보면 사이좋게 BYTE 단위로 0x10, 0x20, 0x30을 각각 스텍에 넣어줬는데

그 구간이 이번 [그림. 7]의 40107E 코드에서 [ESP+ESI+C] 부분으로서 ECX에 셋트 되면서 루프 연산 내에서 각각 모두 이용된다.

00401083의 [ESP+EBP+10] 이 가리키는 위치에는 사용자가 입력한 이름, 즉 slays 라는 단어가 존재하는 영역이다.

이 영역의 문자열을 BYTE 단위로 EDX 레지스터에 넣는 것이다. (첫 시작은 s 부터이니 0x73 값이 EDX에 저장 된다.)

그 후에 XOR ECX, EDX 연산으로 ECX 값이 0x63으로 바뀌게 된다. (입력한 이름에 따라 값은 달라지니 절대값으로 착각은 금물..)

그리고 4개의 값이 PUSH가 되는데 PUSH 된 것들은 다음과 같다.


0040108E    PUSH ECX    <= 이 값은 이전의 XOR ECX, EDX 를 시킨 후의 값을 PUSH하는 것이다.

0040108F    PUSH EAX    <= 이 값은 특정 값이 저장 될 Buffer 영역이다.

00401094    PUSH 00408054    <= 이 값은 아스키 코드로서 %s%02X 으로 표현되는 코드를 PUSH 한다.

00401099    PUSH ECX    <= 이 값은 위의 ECX와는 다르다. 00401090에서 ECX에 다른 값이 들어간 후의 코드이다.

    Buffer 영역으로서 인자 값으로 PUSH 된 EAX가 가리키는 주소와 동일한 주소를 갖고 있다.


그 후 Call 을 하는데, 이 것을 C언어로 작성한다면 대략 sprintf(buf, "%s%02X", buf, 연산된 특정 값) 이 될 것이다.






[그림. 8] 16진수 자체를 문자열로 변환


첫 번째로 연산(XOR ECX, EDX 부분)되었던 특정값이란 16진수 0x63이었는데 해당 함수는 그 63을 자체를 문자열로 변환시키고 그 문자열에 해당되는 16진수 값을 다시 Buf에 입력시킨다.

즉 Buf에는 0x63이 그대로 저장되는 것이 아닌 63의 6과 3이 각각 문자열로 변환되어 저장 된 것이다.

[그림. 8]을 보면 ASCII 영역에 문자열 63이 보이고 그에 해당되는 16진수 값이 0x36, 0x33 으로 저장이 되어있다.

[그림. 7]에서의 이름 값 변환 루프는 이런 식으로 하여 문자열 길이만큼 루프가 돌고, 0x10, 0x20, 0x30의 값도 순차적으로 번갈아가면서 XOR ECX, EDX의 연산에 사용되어 차곡차곡 Buf에 쌓이게 된다.






[그림. 9] 이름 값 최종 변환


slays 문자열을 0x10, 0x20, 0x30과 문자열 길이만큼 루프 돌리면서 XOR 연산된 값이 최종적으로 저장된 Buffer의 상태이다.

루프문을 벗어나면 시리얼 키를 입력하라는 부분이 나오는데 텍스트 파일에서 말한 5B134977135E7D13 를 적으면 된다.






[그림. 10] 키 값 비교 루프문


루프문 진입 전에 박스 친 두 코드를 보자, ESI 와 EAX에 각각 이름 값을 변환시킨 값이 저장된 버퍼와 시리얼 키 값을 넣은 버퍼의 주소를 담고 있다.

이제 그 아래에 루프를 열심히 돌면서 각각의 값을 하나씩 비교하고 다를 경우에는 [그림. 10]의 맨 아래 쪽에 보이는 JNZ 분기문을 통해 Wrong을 출력하는 함수로 빠질지 아니면 Correct를 출력하는 함수로 빠질지가 결정이 된다.


루프문 비교 방식은 Easy CrackMe 때랑 방식이 크게 차이나지 않기 때문에 대략적으로 설명하겠다.

입력한 시리얼 키의 첫 번째 자리와 사용자 이름을 연산시켜서 변환된 버퍼의 값을 하나씩 비교하면서 같으면 패스~

다르면 Wrong 메세지로 빠이빠이 시켜버리는 구성이다.

즉 비교시켜야 할 문자열이 모두 시리얼 키와 똑같은지를 보는 것이니깐 EDI가 가리키는 버퍼의 값을 EAX가 가리키는 시리얼 값과 동일하게 미리 변경을 시켜둔 후에 루프문에 수행시키면 Correct의 출력을 맞이할 것이다.






[그림. 11] 이름 값을 시리얼 값과 동일하게 변경


내가 입력한 slays 라는 이름은 634C516953 이라는 아스키 코드로 변환이 되었는데, 시리얼 값으로 입력된 아스키 코드 길이에 한참 못 미친다. -_- Hex Dump 에서 그냥 개의치 말고 시리얼 값과 똑같이 변경시켜주자. 길이가 총 16바이트구만...

자.. 여기서 생각을 해보자. 앞서서 내가 입력한 slays 라는 5글자를 0x10, 0x20, 0x30 과 XOR 연산을 순차적으로 뺑뺑이 돌면서 값을 변환시켰다.

그렇다면 시리얼 값을 반대로 0x10, 0x20, 0x30과 XOR 연산 뺑뺑이를 시켜주면? 그 것이 사용자 이름으로 다시 되지 않겠는가?

이해 안 되시는 분들은 피연산자를 0x01로 두고 0x02와 XOR 연산을 시켜보자.

연산 후의 결과 값을 다시 0x02로 XOR 연산을 하면 어떻게 될까?


자.. 위의 원리대로 재연산을 해보면 해당 시리얼 키의 문자열을 16진수 아스키 코드라 생각하고 XOR 연산을 하면...


ex.1) 0x5B ^ 0x10

ex.2) 0x13 ^ 0x20.....


이렇게 연산시켜서 나온 16진수 값에 해당되는 아스키 코드의  8자리 문자열 값이 정답이 될 것이다.

에고! 아스키 2개를 한 자리의 16진수로 표기되게 코드 짜다가.. 변환 방법 떠오르질 않아서 수동으로 일일히.. ㅠㅠ