본문 바로가기

챌린지/CrackMe

Abex's 4th Crackme

이번에는 4번째 크랙미다. 사실 다른 사이트에 나와있는 풀이들도 그렇고 대부분 메모리 상에 남아있는 해답으로 푸는 방식이 대부분이지만 저렇게 몇시간동안(총 실행시간이니 22시간이나 붙잡고 있던 건 아닐것이다.. 아마도?) 붙잡고 있던 이유는 문제에서 다음과 같은 요구사항이 있었기 때문이다.

즉, 단순히 값을 찾는 게 아니라 키젠 프로그램을 만들어야 한다는 것이다. 분석해보면 알겠지만 이 크랙미는 매 시간마다 시리얼 키가 변하기 때문에 일일히 디버거를 켜서 값을 찾을수도 없는 노릇이고 결국 시리얼 키를 생성하는 키젠을 작성하는 것이 최종 해답이 될 것이다.

프로그램을 실행시켜보면 친숙한 비주얼 베이직 아이콘과 텍스트 필드, 버튼이 하나 주어진다. 일단 아무 시리얼이나 입력해보았지만 Registered 버튼은 활성화되지 않았다. 설명에서 키젠을 작성하라 했으니 키젠에서 생성된 올바른 키를 입력하면 그때 버튼이 활성화가 되리라고 추측할 수 있다.

다른 비주얼 베이직 애플리케이션과 마찬가지로 ThunRTMain에서 본격적으로 프로그램이 시작된다.

그렇게 실행하던 도중 msvbvm60.6600A4C2라는 함수를 호출하면서 폼 창이 뜨고 이벤트를 기다리게 된다. 여기서 어떤 문자열을 입력하던 간에 따로 중단점에 걸리지 않기 때문에 이전처럼 문자열을 탐색하는 방식을 시도해볼 수 있다.

프로그램 문자열을 찾아보자 다음과 같이 시리얼 키가 일치했을 때 나타날 것 같은 문자열들을 찾을 수 있었다.

이전과는 달리 해당 문자열 근처의 코드를 살펴봐도 어떤 값을 비교하거나 성공, 실패를 판단하는 분기를 찾을 수 없었다. 그렇다면 이는 Registered 버튼이 활성화된 후 버튼을 클릭했을 때 rtcMsgBox 함수를 활용하여 성공 문자열이 담긴 메시지 박스를 호출하는 역할을 하리라 생각해볼 수 있었다. 그렇기때문에 저 근처에 중단점을 걸어도 아직은 걸리지 않는데 그러면 어떻게 단서를 찾을 수 있을까? 여기서 사용할 수 있는 기능이 '모듈 간 호출(intermodular calls)' 탐색 기능이다.

코드에 마우스 오른쪽 클릭 후 다음을 찾기 - 현재 모듈 - 모듈간 호출을 눌러서 다음처럼 현재 모듈이 호출하는 다른 모듈의 함수들을 탐색할 수 있다. 올리 디버거에서는 영문으로 Search for - All intermodular calls로 되어있었기 때문에 x96dbg를 처음 사용할 때는 한글로 된 메뉴를 찾는데 헷갈릴 수 있다.

왼쪽의 중단점은 디버깅 중 걸어놓은 것으로 원래는 걸려있지 않다.

위처럼 여러 함수들이 나오는데 메인 함수격인 ThunRTMain, 두 번째 크랙미에서도 봤던 rtcVarBstrFromAnsi, 메시지 박스를 생성하는 듯한 rtcMsgBox, 변수를 메모리에서 해제하는 vbaFreeVar 등 여러 함수들 중 몇가지 눈에띄는 함수들이 있다.

  • rtcGetPresentDate

  • rtcGetHourOfDay

  • rtcGetYear

  • __vbaStrCmp

함수 이름으로 보면 현재 날짜를 구해서 시각과 연도를 가져오는 것이라 추측할 수 있는데 그렇다면 아래의 __vbaStrCmp 함수는 무엇일까? 이름으로 보면 C언어의 문자열 함수 strcmp() 처럼 문자열을 비교하는 것으로 추측할 수 있다. 확인하기 위해 해당 함수가 사용되는 코드를 확인해보자.

__vbaStrCmp 함수를 호출하여 특정 동작을 수행한 후 00402313~0040231D까지 여러 연산을 수행한 후 0040232E에서 test di, di를 통해 edi의 하위 2바이트를 확인한 후 결과에 따라 00402371로 분기하거나 분기하지 않는다. 그렇다면 일단 __vbaStrCmp에 중단점을 걸고 텍스트 필드에 아무 값이나 입력해보면 어떻게 될까?

텍스트 필드에 숫자 1을 입력하자 중단점에 잘 걸리는 것을 볼 수 있었다. 그리고 친절하게도 ecx 레지스터에 "2181600" 이라는 문자열이 들어가 있다고 디버거에서 알려주고 있는데 보통 문자열 비교를 위한 함수는 비교 대상 문자열 두 개를 매개변수로 받는다는 것을 생각해보면 eax, ecx 레지스터에 비교대상 문자열이 들어가 있을 것이다.

그래서 해당 함수를 실행하기 전 EAX 레지스터가 가리키는 곳을 확인해보니 16진수로 31값, ASCII로 '1' 이 들어가 있었다. 텍스트 필드에 입력한 값과 동일한 것 같은데 좀 더 확실하게 확인하기 위해 텍스트 필드에 더 긴 문자열을 입력해보고 확인해았다.

1234를 입력한 결과 몇번 수행하다보니 디버거도 눈치챈 듯 문자열을 표시해주고 있다. 즉 eax 레지스터에는 우리가 입력한 문자열이 담긴 위치가, ecx 레지스터에는 비교 대상 문자열이 담긴 위치가 들어가 있다. 현재로서는 "1234"와 "2181600"이란 문자열이 일치하지 않기 때문에 현재 코드의 흐름이 실패의 결과일 것이다.

test di, di 결과 edi 레지스터는 0x00 값을 가지고 있기 때문에 ZF가 설정, 점프가 취해져 00402371로 이동하게 된다. 그렇다면 여기서 분기를 거꾸로 타면 어떻게 될까? je를 jne로 어셈블한 후 실행시켜보면 다음과 같이 버튼이 활성화된 것을 알 수 있다.

이를 활성화된 Registered 버튼을 클릭해보면 아까 본 성공 문자열이 메시지 박스로 호출되는 것을 확인할 수 있다.

또는 분기를 역전시키지 않고 문자열을 비교하는 함수에서 비교 대상이었던 "2181600" 문자열을 입력해도 성공 메시지를 볼 수 있다. 어쨌든 두 방법 모두 비교를 통한 분기 제어를 조작하는 방식으로 푸는 것인데 설명 파일에서 요구하던 것은 이 프로그램의 키젠을 작성하는 것이다. 즉 위에서도 말했지만 이렇게 디버거로 일일히 시리얼을 확인하는 방법은 정확한 해답이 아니라는 것이다.

 

그렇다면 어떻게 키젠을 작성할 수 있을까? 그러기 위해서는 어떻게 시리얼 키가 생성되는지를 확인해야 하며 이는 위의 모듈 간 호출 목록을 다시 보면 추측할 수 있다.

아까 확인했던 함수 중 신경쓰이는 함수가 세 가지 있다. 이 함수가 현재 시간과 년도를 구하는 것이라 추측할 수는 있는데 이를 어떻게 활용하는 것일까? 확인하기 위해 이 함수가 호출되는 코드로 이동해보았다.

맨 위쪽의 push ebp / mov ebp, esp로 함수 호출의 시작을 알리며 그 아래쪽에서 rtcGetPresentDate 함수가 참조된 것을 볼 수 있다. 이때 자세히 보면 00402123에서 실제로 함수를 호출하는 게 아니고 ebx 레지스터로 해당 함수가 위치한 주소를 저장하는 것이다. 즉 실제로 호출되는 부분을 살펴보려면 좀 더 아래쪽까지 살펴보아야 한다.

옆에 달린 주석은 디버깅 과정에서 작성한 것으로 원래는 적혀있지 않다.

실제로 rtcGetPresentDate 함수가 호출되는 부분은 00402156으로 00402123에서 ebx 레지스터에 저장된 rtcGetPresentDate 함수의 주소를 호출함으로써 실행된다. 그런데 그 아래쪽을 보면 rtcGetHourOfDay, 그리고 위 사진에서는 안보이지만 rtcGetYear 함수가 호출되고 있다.

그리고 __vbaVarMul, __vbaVarAdd 같은 함수들도 호출되고 마지막에는 __vbaStr류 함수들도 호출되고 있다. 대체 무슨 일이 일어나고 있는 것일까? 일단 코드를 하나하나 실행시켜보면서 실제로 어디에서 어떤 함수가 호출되는지 정리해보면 다음과 같은 순서가 나온다.

  • 0x00402156: rtcGetPresentDate 호출

  • 0x00402160: rtcGetHourOfDay 호출

  • 0x0040218F: rtcGetPresentDate 호출

  • 0x00402199: rtcGetYear 호출

  • 0x004021B4: __vbaVarMul 호출

  • 0x004021C2: __vbaVarAdd 호출

  • 0x004021D4: __vbaVarMul 호출

  • 0x004021D7: __vbaStrVarMove 호출(이 함수 이후에 시리얼 키가 생성되는 것을 확인할 수 있었다)

순서를 보면 시각, 연도를 구하기 전에 rtcGetPresentDate가 호출되는 것을 볼 수 있다. 그렇다면 rtcGetPresentDate로 어떤 DateTime 같은 객체를 구해서 거기에서 현재 시각(rtcGetHourOfDay)을 구하고 다시 해당 객체를 구해서 현재 년도(rtcGetYear)를 구하는 게 아닐까? 라고 생각해볼 수 있을 것이다. 왜 같은 함수를 두 번 호출하는지에 대해서는 아마도 다음과 같이 사용했던 게 아닐까 추측할 뿐이다.

var hour = DateTime.Hour;
var year = DateTime.Year;
// or
var hour = getDateTime().getHour();
var year = getDateTime().getYear();

이렇게 현재 시각과 연도를 구했으면 이걸 어디다 쓸 수 있을까? 이는 __vbaVarMul, __vbaVarAdd, __vbaVarMul 함수에 사용되리라 추측할 수 있다. 현재 시각 또는 연도값을 곱하고, 더하고, 곱해서 얻은 정수값을 문자열로 변환하여 시리얼 키를 만드는 것인데 그렇다면 어떤 값을 얼마나 곱하고, 더하고, 곱하는 것일까? 이를 확인하기 위해서 해당 함수들이 호출될 때 스택에 뭐가 들어가는지를 확인해보자.

첫번째 연산인 __vbaVarMul 함수의 경우 edx, eax, ecx 레지스터를 스택에 넣고 호출하는데 각 레지스터에는 lea 명령어를 통해 특정 주소가 복사된다. 해당 주소가 가리키는 곳을 덤프로 따라가보면 다음과 같다.

  • edx = ebp-38 = 0x0019FA8C: 0x000A0002

  • eax = ebp-98 = 0x0019FA2C: 0x00000002

  • ecx = ebp-48 = 0x0019FA7C: 0x00000000

언뜻 보기에는 무슨 값을 나타내는지 알 수가 없다. 이후에 나오는 __vbaVarAdd, __vbaVarMul 함수 호출의 경우도 이와 비슷하게 무슨 값을 나타내는지 알기가 어려운데 여기서 꽤나 오랜시간동안 고민했던 결과 알게 된 것은 일반적으로 C언어에서 사용하던 int 같은 자료형을 생각하면 안된다는 것이었다. 비주얼 베이직은 'Dim newVar' 처럼 특별한 자료형 없이 변수를 선언하기 때문에 기존의 사고방식에서 벗어날 필요가 있었다. 그렇기 때문에 상단에서 지역변수를 선언하는 블록을 살펴보기로 했다.

스택에서 ebp-18부터 ebp-A8까지 16바이트씩 esi 레지스터의 값(명령어 실행 기준 0x00)으로 초기화시켜주고 있다. 즉 이 부분을 시리얼 키를 생성하는 함수 내에서 사용하고 있는 지역 변수들이 저장되는 공간으로 볼 수 있을 것이다. 그렇다면 어느 공간을 어떻게 사용하는 걸까? 이를 확인해보기 위해 위처럼 함수 호출 전 스택에 저장되는 공간을 비교해보았다.

이를 확인하기 위해 사용했던 방법은 스택 부분을 함수 호출 전후로 비교하여 감시하는 방법이었다. 분명 함수 실행의 결과가 어떤 방식으로든 스택에 저장될 것이라 생각하고 해당 함수가 실행된 후에 변화가 일어난 부분이 이 함수가 사용하는 부분이라 추측한 것이다. 초행길이라 익숙하지 않아서 이렇게 일일히 감시한 것이지만 함수 호출 전 스택에 넣어지는 공간을 확인함으로써 이 공간이 어떤 함수에 사용되는지 확인할 수도 있을 것이다. 어쨌든 결과는 다음과 같이 정리할 수 있었다.

살펴보다가 발견한 것은 ebp-98, ebp-A8 공간에는 정수 5와 1000이 저장된다는 것을 발견할 수 있었다.

정수를 저장한 후에도 eax 레지스터의 값을 근처에 저장하는데 두 정수 모두 근처에 동일한 값을 가진 eax 레지스터를 저장하고 있었다. 정수 5를 저장하는 곳의 경우 ebp-98 ~ ebp-88 사이의 공간에 저장되었고 정수 1000(0x3E8)을 저장하는 곳의 경우 ebp-A8 ~ ebp-98 사이의 공간에 저장되었는데 이 정수값 뿐 아니라 eax 레지스터의 값(2)도 각각 공간에 저장되는것으로 보아 이는 어떤 연산에 사용될 수 있는 유니크한 값이 아니라 비주얼 베이직 자료형에 필요한 어떤 필드일 것이라 추측하였다.

 

그렇다면 ebp-18부터 ebp-A8까지 10개의 16바이트짜리 공간에는 실제 데이터 뿐 아니라 기타 데이터도 같이 저장되는 일종의 구조체들이 저장되어 있을 것이며 그렇다면 __vbaVarMul, __vbaVarAdd 같은 함수가 사용하는 공간의 '구조체' 내부에 있는 값을 살펴봄으로써 어떤 값이 피연산자가 되는지 알아낼 수 있을 것이라고 생각하였다. 그래서 첫번째로 호출된 0x004021B4의 __vbaVarMul을 살펴보았다.

이 함수에서는 edx, eax, ecx 레지스터를 스택에 넣고 있는데 이 값들은 lea 명령어로 ebp-38, ebp-98, ebp-48 주소 값이 복사된다. 위에서 파악한 공간의 용도에 비교해보면 각 레지스터는 다음과 같은 값을 가리킨다.

  • edx = ebp-38 = ebp-38 ~ ebp-28: rtcGetHourOfDay

  • eax = ebp-98 = ebp-98 ~ ebp-88: 5

  • ecx = ebp-48 = ebp-48 ~ ebp-38: (vbavarMul)1st

eax 레지스터같은 경우 ebp-98은 0x00402179에서 eax 레지스터에 의해 저장된 2 값을 가리키고 있어 이게 무슨 값을 의미하는지 헷갈릴 수 있다. 그러나 지금은 어떤 구조체를 가리키고 있다고 생각하고 해당 구간 내 ebp-90에 저장된 정수값 5를 사용한다고 판단했다. ecx 레지스터에는 __vbaVarMul 함수호출전후로 값이 변한다는 것 말고는 어떤 의미있는 값을 찾을 수 없었는데 함수 이름으로 보아 어떤 값을 곱하는 기능을 수행하기 때문에 다른 피연산자 두개를 곱한 값을 이곳에 저장하리라 추측하고 저렇게 명명했다.

 

다음에 호출되는 0x004021C2의 __vbaVarAdd도 비슷하게 살펴보면 다음과 같은 정보를 얻을 수 있었다.

  • eax = 0x0019FA7C = ebp-48 ~ ebp-38: (vbaVarMul)1st

  • edx = ebp-A8 = ebp-A8 ~ ebp-98: 1000

  • eax = ebp-58 = ebp-58 ~ ebp-48: (vbaVarAdd)

lea 명령어로 새로운 값으로 덮어씌워지기 전 eax 레지스터에는 __vbaVarMul 함수에서 ecx 레지스터로 전달됐던 위치가 담겨 있었다. 덮어씌워진 후 eax 레지스터는 비슷하게 어떤 의미있는 값을 찾을 수 없었으며 마찬가지로 __vbaVarAdd 함수가 피연산자 두 개를 더한 값이 저장되는 위치라 추측하고 저렇게 명명하였다. 그럼 이제 마지막으로 다시 한번 호출되는 __vbaVarMul 함수도 비슷하게 파악할 수 있을 것이다.

  • eax = 0x0019FA6C = ebp-58 ~ ebp-48: (vbaVarAdd)

  • ecx = ebp-78 = ebp-78 ~ ebp-68: rtcGetYear

  • edx = ebp-88 = ebp-88 ~ ebp-78: (vbaVarMul)2nd

eax 레지스터에 저장된 __vbaVarAdd 함수의 결과값과 ecx 레지스터에 저장된 rtcGetYear로 얻은 현재 년도를 곱해서 edx 레지스터가 가리키는 공간에 저장한다. 그렇다면 이 세 번의 함수 호출을 통해 다음과 같은 계산이 수행된다고 추측할 수 있겠다.

                   __vbaVarMul     __vbaVarAdd   __vbaVarMul         

( ( ( rtcGetHourOfDay * 5) + 1000 ) * rtcGetYear

그렇다면 실제로 현재 시각과 년도를 기준으로 계산된 값을 텍스트 필드에 넣어보자(2020년 10월 25일 오후 9시 23분 기준 2232100).

왼쪽 키젠은 위의 공식을 기반으로 간단히 만든 C# 윈폼 애플리케이션이다.

올바른 시리얼을 입력하여 성공한 것을 알 수 있다. 물론 아까처럼 단순히 분기문을 역전시키면 틀린 시리얼을 입력해도 성공 메시지 박스를 얻을 수 있겠지만 말했듯이 키젠을 작성해야 하는 이 크랙미의 경우 그런 풀이방법은 의미가 없다고 생각한다.

 

 

사실 이번 크랙미를 풀면서 온갖 곳을 뒤져보고 문서를 읽어봐도 대부분의 사람들이 생성된 문자열을 스택에서 읽어서 프로그램에 입력하는 식으로 풀이했고 외국에서 풀이를 찾아도 SmartCheck인가 하는 비주얼 베이직 분석 프로그램을 사용하여 저 시리얼 키를 생성하는 로직을 찾아냈기 때문에 그냥 x32dbg 리버싱만으로는 알아낼 수 없는 내 한계인가.. 라고 생각하여 그냥 포기할까 수없이 고민했었다. 하지만 함수 실행 전후의 스택을 비교하고 어떤 구조체 안에 실제 데이터가 담겨 있으리라는 추측을 통해 결국 이렇게 해결할 수 있어서 매우 감격스럽다. 디버거 기준 1일(24시간)을 넘겼기 때문에, 물론 중간중간 딴짓도 해서 그렇지만, 이 풀이는 잊을 수 없는 경험이 될 것 같다.

'챌린지 > CrackMe' 카테고리의 다른 글

Abex's 5th Crackme  (0) 2020.10.26
Abex's 3rd Crackme  (0) 2020.10.23
Abex's 2nd Crackme  (0) 2020.09.29
Abex's 1st Crackme  (0) 2020.08.29