# 아주 간단한 Crack me 샘플 분석 (abex' crackme#2)
1. 우선 해당 크랙미 파일이 어떤 동작을 하는지 실행시켜 본다.
2. 파일을 실행시켜 실마리를 찾고, 디버깅을 할 때 참고한다.
3. 크랙미 파일에서 요구하는 부분을 만족 시키기 위한 패치를 하거나
혹은 디버깅을 통하여 필요한 정보를 확인한다.
[ 그림 1 ] abex' crackme#2 파일 실행
- 파일을 실행시켰더니, Name 그리고 Serial 이란 입력란이 있고, 3가지 버튼이 존재했다.
- 이번 크랙미 문제는 시리얼 번호를 알아내야하는 문제인것으로 예상된다.
- 임의의 입력값들을 주고 Check 버튼을 눌러보도록 하자.
[ 그림 2 ] 임의의 입력값을 주고 Check 버튼을 눌러본 경우
- Name의 값을 4글자 이상으로 수정한후 다시 Check 버튼을 눌렀더니, 잘못된 시리얼 값이라고 에러 메시지 박스가 떳다.
- 여기서 다시 정리를 해보면, 우리가 찾아야 할 것은 시리얼 값인데, 이 시리얼 값이 Name값과는 상관없는 고정된 하나의 값을 가지고 있거나
혹은 입력값으로 같이 받는 Name 값을 이용하여 유동적으로 변하는 값일 수 도 있을 것이다. 이제 디버깅을 시작해보도록 하자.
[ 그림 3 ] OllyDbg를 이용한 디버깅 (EP 코드)
- 크랙미 파일을 불러온 첫 화면이다. 주소 00401238이 Entry Point 이며, EP로부터 두세줄을 살펴보면 0040123D에서 CALL 명령어를 이용하여
MSVBVM60.ThunRTmain 이라는 것을 호출한다. 그 윗부분에서도 MSVBVM60 이라는 문자가 많이 보이는데 이것은 VB 전용 엔진
(The Thunder Runtime Engine)을 의미한다.
즉, 이 크랙미 파일은 VB 전용 엔진을 사용하는 Visual Basic으로 제작되었다는 것을 확인할 수 있다.
* 참고 : VB(Visual Basic) 파일은 MSVBVM60.dll(Microsoft Visual Basic Virtual Machine 6.0)이라는 VB 전용 엔진을 사용
한다. VB 파일은 컴파일 옵션에 따라서 N code와 P code로 컴파일이 가능하다. N code는 일반적인 디버거에서 해석 가능한
IA-32 Instruction을 사용하는 반면에 P code는 인터프리터(Interpreter) 언어 개념으로서 VB 엔진으로 가상 머신을 구현하여
자체적으로 해석 가능한 명령어(바이트코드)를 사용한다. 따라서 VB의 P code를 정확히 해석하기 위해선 VB 엔진을 분석하여
에뮬레이터를 구현해야한다.
- 이 크랙미 파일이 VB 파일이라는 것을 알게 되었다. VB는 주로 GUI 프로그래밍을 할 때 사용되며, IDE 인터페이스 자체도 GUI 프로그래밍에
최적화되어 있다.
즉 VB 프로그램은 Windows 운영체제의 Event Driven 방식으로 동작하기 때문에 main() 혹은 WinMain()에 사용자 코드(우리가
디버깅을 원하는 코드)가 존재하는 것이 아니라, 각 event handler에 사용자 코드가 존재한다. 따라서 event handler를 중점적으
로 하여 디버깅을 하면 될 것이다.
- 따라서, 위의 EP 코드는 VB 엔진의 메인 함수를 호출후에 돌아올 리턴 어드레스를 스택에 입력한 후 VB 엔진의 메인 함수를
호출한다. (ThunRTMain())
[ 그림 4 ] ThunRTMain() 함수 호출
- ThunRTMain() 함수를 호출한 모습이다. 메모리 주소를 살펴보면 완전히 달라진 것을 확인할 수 있다. (0040123D에서 733735A4)
이 주소는 MSVBVM60.dll 모듈의 주소 영역이다. 즉, 우리가 분석하는 프로그램의 코드가 아니라 VB 엔진의 코드라는 것이다.
따라서 이러한 코드들을 모두 분석할 필요가 없다.
[ 그림 5 ] 문자열 검색을 이용하여 코드 찾아가기
- 문자열 검색을 이용하여 핸들러 근처의 사용자 코드를 찾아야 한다. 우리가 Name 값과 Serial 값을 주고 Check를 눌렀을 때, 에러 메시지
박스의 타이틀 문자열인 "Wrong serial!" 이 있는 코드로 우선 이동하여 그 코드가 실행되기전의 조건 분기문을 찾아보도록 하자.
[ 그림 6 ] "Wrong serial!" 문자열이 있는 주소로 이동
- 지금 위치해 있는 주소는 "Wrong serial!" 문자열을 타이틀로 가진 메시지 박스에 관련된 코드 영역으로 추측된다.
- 따라서 이 코드가 나오기전에 시리얼이 맞는지, 틀린지 확인을 하는 조건 분기문이 있을 것으로 생각되며 따라서 그 조건 분기문을 찾아
보도록 하자.
[ 그림 7 ] 조건 분기문 발견
- [ 그림 6 ] 의 메모리 주소 영역에서 스크롤을 올려가며 코드를 살펴보니 분기문이 있는 메모리 주소를 찾을 수 있었다.
- 00403332 주소에 명령어 : JZ 00403408 (JZ 명령어는 연산 결과가 0 일경우, 즉 ZF(Zero Flag)=1로 세팅되면 00403408로 점프하라)
- 00403332 바로 직전의 0040332F 주소의 명령어를 살펴보면 TEST AX,AX 연산을 하는 것을 확인할 수 있다.
(TEST 명령어는 두 개의 오퍼랜드를 AND 연산하여 그 값이 0인지를 확인하기 위한 명령어이다. AX 값이 0이라면 AND 연산후에 0이 되고,
ZF 값은 1로 세팅된다. 만약 AX 값이 0이 아닌 값이라면 TEST AX, AX 를 실행한 후의 값은 AX 자기 자신이 나오는데 이 값은 0이아닌 쓸모없는
값이다. 즉, 앞에서 말했듯이 0인지 확인하는 명령어라고 생각하면 쉽다.)
- AX 값이 만약 0 이였다면 ZF=1로 세팅되고 조건 분기문이 참이되어 00403408 주소로 점프하게 될 것이다. 조건 분기문 아래의
코드들을 차례대로 살펴보면 조건 분기문이 참이되어 00403408로 점프한다는 것은 Serial 값이 틀린 경우라는 것을 확인할 수 있다.
(왜냐하면 맞췄을 경우 뜨는 메시지박스에 관련된 문자열 코드에 해당하는 부분들을 모두 뛰어 넘어 가는 것이기 때문이다)
- 이제 분기문 위의 코드들을 살펴보도록 하자.
[ 그림 8 ] 조건 분기문 이전의 코드 분석
- 위에서 확인했듯이 TEST AX, AX 에서 연산 결과가 0이되어 ZF=1이 되면, 실패인 경우로 가게 된다.
TEST 연산의 결과는 그 윗줄에서 호출되는 함수에 의해서 결정된다. (AX 값이 호출된 함수안에서 변경되기 때문에)
- [ 그림 7 ]에서 00403332 주소의 JZ 00403408 명령어는 결국 시리얼값이 맞는지 틀린지 확인한 직후의 조건 분기문이라고 했다.
그렇다면 그 직전에 호출되는 함수가 결국 사용자가 입력한 시리얼값과 프로그램의 시리얼값과 비교하는 함수이고, 직전의
2개의 PUSH는 함수에서 참조될 파라미터 값이라고 예상할 수 있을 것이다. (하나는 사용자가 입력한 시리얼값, 나머지 하나는
프로그램의 시리얼값)
- PUSH 되는 값들을 확인해보도록 하자.
[ 그림 9 ] PUSH되는 EDX와 EAX 값 확인
- PUSH 되는 값을 보니 메모리 EDX=0018F41C , EAX=0018F42C 라는 메모리 주소 값이 스택에 입력되는 것을 확인할 수 있다.
- 저 2개의 메모리 주소를 확인해보도록하자.
[ 그림 10 ] EDX 및 EAX가 가리키는 메모리 주소의 값을 확인
- Dump 창에서 해당 메모리 주소의 Hex 값을 확인해보니 각 각 16바이트 중 4바이트를 제외한 모든 값들이 같다는 것을 확인할 수 있었다.
* 참고 : VB의 문자열은 C++의 string 클래스와 마찬가지로 가변 길이 문자열 타입을 사용한다고 한다.
따라서 위의 [ 그림 10 ]에서 보는 바와 같이 바로 문자열이 나타나지 않고 16바이트 크기의 데이터가 나타난다.
(이것이 바로 VB에서 사용하는 문자열 객체이다.)
- 위에서 다른 4바이트 부분은 메모리 주소를 나타내는 것으로 추측된다.
(가변 길이 문자열 타입은 내부에 동적으로 할당한 실제 문자열 버퍼 주소를 가지고 있다.)
- Dump 창에서 Hex dump 형식이 아닌 Integer-Address with ASCⅡ dump 모드로 변경하여 보면 [ 그림 10 ] 의 두번째와 같이 볼 수 있다.
- 변경한 모드에서 서로 다른 4바이트의 값을 확인해봤더니 (74985000, 1CAC5000 : 리틀엔디언형식으로 저장된 주소값)
00509874 에는 UNICODE "B7A5B2AB"
0050AC1C 에는 UNICODE "12345678" 이라는 문자열이 저장되어 있는 것을 볼 수 있다.
0050AC1C 에 저장된 "12345678" 이란 값은 사용자가 입력한 임의의 시리얼값이라는 것을 확인할 수 있다.
즉, 00509874 에 저장된 "B7A5B2AB" 이란 값이 시리얼 값이라는 것이다.
- 결론적으로, Name이 "SANGDAE"라는 값을 주었을 때 시리얼값이 "B7A5B2AB"라는 것이다.
- 크랙미 파일을 다시 실행하여 "NAME에는 SANGDAE, Serial에는 B7A5B2AB 라는 값을 주고 Check 버튼을 눌러 확인해보도록 하자.
[ 그림 11 ] 디버깅으로 확인한 시리얼 값 입력 결과
- 위에서 확인한 시리얼 값을 입력한 결과, 크랙이 성공했다는 메시지 박스를 확인할 수 있었다.
[ 그림 12 ] 다른 임의의 NAME으로 시도했을 경우
- Name 값을 다르게 주고 위와 동일한 시리얼 값을 주었더니 시리얼 값이 다르다는 메시지 박스가 떳다.
즉, 사용자가 입력하는 Name의 값을 참조하여 시리얼을 생성한다는 것을 추측할 수 있었다.
- 그렇다면, 시리얼을 생성하는 부분을 다시 분석해보도록 하자.
[ 그림 13 ] Check Event Handler 시작 부분 찾아가기
- 위의 코드에서 Break Point 된 부분이 Check Event Handler의 시작 부분이다.
- 우리가 [ 그림 7 ]에서 발견한 조건 분기문은 여러가지 의미를 담고 있다. 그 조건 분기문 이후에 크랙 성공 여부가 나눠지는 메시지박스를
띄우므로 그 직전에 입력한 시리얼 값과 프로그램 시리얼 값을 비교하는 부분이 있을 것이고, 더 생각해보면 그 시리얼 값을 비교하기 위해
그 전 코드에 시리얼 값을 생성하는 코드 부분이 존재할 것이다.
- 즉, 이러한 작업들은 사용자가 Check 버튼을 입력했을 때 수행되는 작업들이다. 다시 말하면 그 조건 분기문을 기준으로 거슬러
올라간다면 Check Event Handler 시작 부분을 찾을 수 있다는 말이다.
(조건 분기문도 Check Event Handler의 일부분에 포함하므로)
- Check Event Handler 코드의 시작 부분을 찾았으므로, 이제 시리얼 값을 생성하는 코드부분을 찾아보도록 하자.
----------------------------------------------------------------------------------------------------------------------------------
* 참고
[ 참고 그림 ] VB 파일에서의 NOP 명령어
- VB 파일에서는 함수와 함수 사이에 NOP 명령어가 존재한다.
- NOP 명령어는 No Operation 을 의미하며, 즉 아무 동작을 하지 않는 명령어이다. (그냥 CPU 클럭만 소모된다)
----------------------------------------------------------------------------------------------------------------------------------
[ 그림 14 ] Name 문자열 읽어오는 코드 부분
- Check Event Serial 함수 시작 부분에서 순차적으로 내려오면서 Name 문자열을 읽어올것으로 예상되는 부분(CALL 명령어 위주로)들을
살펴보면 이러한 코드들이 있는 곳을 찾을 수 있다.
- 00402F8E 주소의 명령어는 LEA EDX, [EBP-88] (레지스터 EDX에 [EBP-88]의 주소값을 복사하는 것이다.)
즉, Name 문자열을 저장할 스트링 객체를 생성한다고 보면된다.)
- 그리고 5번째 줄인 00402F98 주소에서 사용자가 입력한 문자열을 읽어오는 것으로 추측된다.
- 함수를 호출한 후의 EDX가 가리키는 주소를 확인해보도록 하자.
[ 그림 15 ] 함수 호출 후의 [EBP-88] 값 확인
- 00402F98 주소에서 CALL 명령어 (함수호출) 이 후에 문자열을 읽어와 저장할것으로 예측한 주소를 확인해본 결과
사용자가 입력한 Name 값이 들어있는 것을 확인할 수 있었다.
- 두 번째 그림 : Stack 창에서 확인한 값
- 나머지 부분들을 계속 디버깅해보자.
[ 그림 16 ] 새로운 조건 분기문 발견 (Name 값이 4글자 미만인지 확인하는 부분)
- [ 그림 15 ] 에서 이어 뒷부분을 디버깅하다가 새로운 조건 분기문을 발견하였다.
- 빨간 코드영역 : 어떠한 값들(함수 파라미터 값으로 추측)을 스택에 PUSH하고 어떠한 함수를 호출한 후, TEST AX,AX를 실행하고 조건
분기문에 의해 두 가지 경우로 나누어진다.
- 노란 코드영역 : 이 노란 코드 영역을 해석하면 일단 00403026 주소에서 조건 분기문이 거짓이 될 때(ZF=0 일때) 실행되는 코드 영역이다.
코드를 살펴보니 "Error!" , "Please enter at lest 4 chars as name!" 이라는 유니코드를 확인할 수 있다.
이러한 정보들을 봤을 때, 이 노란 코드영역은 사용자가 Name의 값을 4글자 미만으로 줬을 때 진행되는 부분인것으로 추측할
수 있다.
- 우리가 처음에 실행 파일이 어떻게 동작하는지 확인하기 위해서 Name 값을 임의로 "ABC"로 주었을 때 떳던 메시지 박스에 관한
코드 부분인 것이다. ( [ 그림 2 ] 참고 )
- Name 값이 4글자 미만일 경우 다시 입력받는 곳으로 돌아가기 때문에 이 부분은 따로 디버깅을 할 필요는 없어보인다.
따라서, 00403026 주소에서 점프하는 004030F9 주소부터 디버깅을 다시 하면, 암호화 방법에 대해서 실마리를 찾을 수 있을 것이다.
[ 그림 17 ] Name 문자열에서 한 글자씩 읽어오는 함수 발견
- 사용자가 입력한 Name이 "SANGDAE" 였었다. 004031F0 주소에서 __vbaStrVarVal 함수를 호출하고 EAX의 값을 스택에 PUSH
하는데, 함수호출 직후의 변경된 EAX값을 참조하여 데이터를 확인했더니 Name의 값 "SANGDAE"에서 한문자를 가져온 "S"임을
확인할 수 있었다.
[ 그림 18 ] 004031F7 에서 rtcAnsiValueBstr 함수 호출 직후에 변경된 EAX 값 (유니코드 한문자를 아스키코드로 변환)
- [ 그림 17 ] 에서 __vbaStrVarVal 함수 호출 후 변경된 EAX 값(Name 값에서 유니코드 한문자를 가져온)을 파라미터로 rtcAnsiValueBstr 함수
를 호출한다.
- 호출 직후에 파라미터로 주었던 EAX 값이 변경되는데, 이 변경된 값을 살펴보면 "S"라는 유니코드를 아스키코드로 변경한 것을
확인할 수 있다.
- 즉, Name의 값에서 유니코드 한 글자를 가져와 아스키코드로 변경하는 작업을 두 개의 함수를 이용하여 한 것이다.
[ 그림 19 ] 그 이후의 작업들
( # 암호화시키는 키를 생성한후 위에서 변경해뒀던 아스키코드 값과 더한후 데이터형을 문자로 변환시킴 )
- 위에서 유니코드 한 글자를 가져와 아스키코드로 변경하는 작업을 한 후에 나머지 부분들을 디버깅해본 결과, 임의의 수를 생성하고
(64라는 값), 그 다음 그 값을 이용하여 앞에서 아스키 코드로 변경한 값을 더해준다. (암호화하는 과정) 그 후 다시 rtcHexVarFromVar 함수를
호출하여, 더했던 값을 문자형(유니코드)으로 변경시킨다.
- 정리를 다시 해보면,
암호화 과정은 아래의 순서와 같다. (4번 반복)
1. 주어진 Name 문자열을 앞에서부터 한 문자씩 읽어온다.
2. 읽어온 한 문자를 숫자(아스키 코드)로 변환시킨다.
3. 변환 시킨 숫자(아스키 코드)에 64를 더한다.
4. 더한 결과 값을 다시 문자형으로 변환시킨다.(데이터형을)
5. 변환된 문자를 연결시킨다.
이러한 암호화 과정을 사용자가 입력한 Name의 4글자만 가지고 작업을 반복한다.
( 사진에는 코드가 빠져 있는데, 사용자가 입력한 Name의 입력값이 4글자 미만인지 확인 한후, EBX 레지스터에 4라는 값을 주어, EBX 값만큼
루프를 돌린다. )
- 결국 "SANGDAE" 라는 Name 값을 주었을 경우에 시리얼 값 생성은 SANG 문자열을 가지고 생성한다는 말이다.
유니코드인 S 를 아스키코드인 53(Hex 값)으로 변경시키고, 64를 더한다.
0x53 + 0x64 = 0xB7, 결과 값을 다시 문자형으로 변경 -> "B7"
유니코드인 A 를 아스키코드인 41(Hex 값)으로 변경시키고, 64를 더한다.
0x41 + 0x64 = 0xA5, 결과 값을 다시 문자형으로 변경 -> "A5"
유니코드인 N 를 아스키코드인 4E(Hex 값)으로 변경시키고, 64를 더한다.
0x4E + 0x64 = 0xB2, 결과 값을 다시 문자형으로 변경 -> "B2"
유니코드인 G 를 아스키코드인 47(Hex 값)으로 변경시키고, 64를 더한다.
0x47 + 0x64 = 0xAB, 결과 값을 다시 문자형으로 변경 -> "AB"
변경된 문자들을 모두 연결시키면 "B7A5B2AB" 라는 시리얼이 생성된다.
# 암호화 방법을 모두 확인하였다.
그렇다면 마지막으로 [ 그림 12 ] 에서 실패했던 "YOKANG"이라는 Name의 시리얼값을 계산해서
입력해보자.
* YOKA 문자열을 이용하여 시리얼 값 계산
Y -> 0x59 + 0x64 = 0xBD -> "BD"
O -> 0x4F + 0x64 = 0xB3 -> "B3"
K -> 0x4B + 0x64 = 0xAF -> "AF"
A -> 0x41 + 0x64 = 0xA5 -> "A5"
Name 값이 "YOKANG" 일 때의 시리얼 값은 "BDB3AFA5" 이다.
[ 그림 20 ] 직접 구한 시리얼 값으로 확인
- 앞에서 분석한 암호화 방법을 이용하여 계산한 시리얼 값이 정확한 것을 확인할 수 있었다.
# 참고 도서 : 리버싱 핵심원리
'0x00 > 0x01 Reversing' 카테고리의 다른 글
- OllyDbg , IDA , GDB를 이용한 간단한 실행 파일 디버깅 (0) | 2014.02.25 |
---|---|
- 함수 호출 규약 (Calling Convention) (0) | 2014.02.24 |
- 아주 간단한 Crack me 샘플 분석 (abex' crackme#1) (0) | 2014.02.20 |
- PE File Format (0) | 2014.02.20 |
- 간단한 실행 파일 디버깅하기 (문자열패치) (0) | 2014.02.19 |