본문 바로가기

챌린지/CrackMe

Abex's 2nd Crackme

2020.10.26 수정

- Abex's 4th Crackme를 풀면서 얻은 지식으로 키젠을 구현, 반영하였음.

 

Abex의 두 번째 크랙미다. 이번 파일은 Visual Basic으로 작성되어 있으며 다음과 같이 시리얼 코드를 확인하는 동작을 하고 있다.

설명은 간단하다. 그럼 프로그램을 실행시켜보자.

Check, About, Quit 버튼은 각각 시리얼 넘버 확인, 간단한 정보 출력, 프로그램 종료를 수행한다. 필드에 아무것도 입력하지 않고 Check 버튼을 눌러보면 어떻게 될까?

위와 같이 최소한 4글자를 입력하라는 경고가 나왔다. 그렇다면 4글자를 입력해보자.

위와 같이 올바른 시리얼을 입력하라는 문구가 나왔다. 그렇다면 어떻게 올바른 이름과 시리얼을 찾아낼 수 있을까? 우선 디버거로 이를 실행시켜보자.

위와 같이 엔트리 포인트에서 시작하자마자 ThunRTMain이라는 함수를 호출하는 것을 알 수 있다. 이 함수를 호출하면 위에서 봤던 프로그램 화면이 뜨게 된다. 그럼 이곳이 가리키는 곳은 어딜까? 이를 찾아들어가보면 다음과 같은 코드를 볼 수 있다.

여기서부터 쭉 읽어서 내려가다보면 msvbvm이라는 dll의 여러 함수들을 호출하는 것을 알 수 있다.

이 dll은 Visual Basic의 Virtual Machine으로 윈도우에서 비주얼 베이직 애플리케이션을 동작할 수 있도록 해주는 파일이다. 즉 자바의 JVM과 비슷하며 이 함수들은 Visual Basic GUI 폼을 띄우거나 하는 등 가상 머신의 기반 작업을 하고 있으리라 예상할 수 있다. 그렇다면 이들을 굳이 하나하나 읽기보다 필요한 부분으로 직접 찾아가는 방법이 효과적일 것이며 이를 위해 아까 에러 메시지에 나왔던 문자열을 탐색하는 방법을 생각해볼 수 있다.

x32dbg에서는 위와 같은 아이콘을 클릭하여 프로그램에서 사용하는 문자열을 찾을 수 있다. 찾은 문자열은 다음과 같다.

에러 메시지에서 나왔던 문자열들과 성공 시에 출력되리라 생각되는 문자열들이 존재하는 것을 볼 수 있다. 그렇다면 성공 문자열이 사용되는 위치로 먼저 탐색해보자.

00403372로 따라왔는데 더 아래에는 인증 실패 시의 문자열들도 나열되어 있으며 결정적으로 두 코드 모두 rtcMsgBox라는 함수를 호출하고 있다. 이는 비주얼 베이직에 내장된 함수로 윈도우 함수를 이용하여 메시지 박스를 띄우는 역할을 수행한다. 즉 이 부분 근처에서 시리얼 값을 파악하여 인증 실패, 성공 여부를 결정한다고 추측할 수 있다.

코드를 확인해 보면 00403332의 je 분기를 통해 성공, 실패로 갈리는 것 같다. test ax, ax 명령어를 통해 eax의 하위 비트를 비교하는 것 같은데 좀 더 확인해보기 위해 asdf같은 문자열을 적당히 이름에 넣고 Check 버튼을 눌러보면 다음과 같은 상태로 멈추게 된다.

즉 test ax, ax를 통해 EAX의 하위 4바이트, 0001이 AND연산되어 결과값이 1이 되어 ZF(Zero Flag)가 설정되지 않아 je에서 분기하지 않는다. 두 피연산자를 비교하는 cmp 명령어가 피연산자1에서 피연산자2를 빼면서 결과가 0이라면, 즉 피연산자가 동일하다면 ZF가 설정되어 je로 분기하는 것처럼 test도 동일하게 ZF 등의 플래그를 설정하여 분기에 영향을 준다. 그렇다면 이는 어디서 결정되는 걸까?

코드를 조금 올려보면 위과 같이 반복문으로 의심되는 블록이 있다. 00403197에서 eax가 0이 될 때까지 코드를 진행하고 004032A0에서 다시 00403197로 점프하여 플래그에 따라 점프하여 004032A5, 즉 004032A0의 점프 명령어 다음으로 이동하여 아래쪽에서 성공, 실패 결과를 출력하는 방식으로 동작한다. 그렇다면 이 반복에서는 무슨 일이 일어나고 있는 것일까? 이 코드 블록에서 호출되는 함수들은 다음과 같다.

  • __vbaI4Var

  • __vbaFreeVar

  • __vbaStrVarVal

  • rtcAnsiValueBstr

  • __vbaFreeStr

  • __vbaVarAdd

  • __rtcHexVarFromVar

  • __vbaVarCat

  • __vbaVarForNext

이 중 마지막 함수 '__vbaVarForNext'의 반환값을 살펴볼 필요가 있다. 이 함수가 호출된 후에 00403197로 점프하여 'test eax, eax'를 통해 이 반복되는 블록을 벗어날지 말지 결정되기 때문이다. 지금 예시로 'asdf'를 Name 필드에 입력했을 때는 4번 위 과정을 반복한 후에 0이 EAX 레지스터에 저장되었다. 조금 더 길게 'asdfqwer'를 Name 필드에 입력했을때는 어떨까? 의외로 동일하게 4번 반복한 후에 실패 메시지 박스가 출력되었다. 즉 어찌됐든 4번 반복한 후에는 '__vbaVarForNext'에서 0을 반환하여 'test eax, eax'가 ZF를 설정, 반복을 탈출하게 되는 방식이었다.

 

그렇다면 이 4번의 루프 속에서 다른 함수들이 어떤 동작을 하는지 살펴볼 필요가 있다. 함수 이름으로 추측해보자면 '__vbaFreeVar'는 할당한 변수를 메모리에서 해제하는 듯 한데 이는 매 반복마다 호출되고 있다. 이것은 아마 반복문 내에서 지역변수를 선언후 루프 종료 시 해제하는 다음과 같은 코드가 작성되었다고 추측할 수 있다.

while(condition) {
    int j;
    ....
}

그 다음으로 '__vbaStrVarVal' 함수를 호출하는 부분을 살펴보자.

eax, ecx를 push하고 함수를 호출하여 얻은 반환값을 다시 push하고 있다. 이 함수를 호출한 후 얻은 반환값은 무엇일까? 이는 해당 레지스터의 주소를 덤프에서 따라가보면 알 수 있다.

스택에 'a'를 push하고 있다는 것을 알 수 있다. 아까 Name 필드에 입력한 문자열이 'asdf'였는데 그것과 연관이 있을까? 확인을 위해 한번 더 루프를 돌려보았다.

이 외에도 몇번 더 돌려봤을때 a, s, d, f 이 4글자가 순서대로 스택에 push되었다. 즉 Name 필드의 문자열의 첫 4글자를 가져와서 어떤 작업을 수행한다고 생각할 수 있는 것이다. 바로 아래에 있는 'rtcAnsiValueBstr' 함수도 살펴보자.

해당 함수는 'asdf'의 첫 글자 'a'를 아스키 코드값으로 EAX 레지스터에 반환하였다. 더 아래쪽으로 내려가서 '__vbaVarAdd' 함수까지 실행시켜보자.

함수를 호출한 후 EDX 레지스터에 어떤 값이 들어왔다. 언뜻 보기에는 대문자 'A'같지만 이는 확장 아스키 코드에 포함된 라틴어 문자로 Name 필드에 입력했던 'asdf'와 특별한 상관은 없다. 처음 'a'때는 C5값을 반환했으니 계속 진행해보면서 어떤 값을 반환하는지 확인해보면 다음과 같다.

각 문자마다 C5, D7, C8, CA를 반환하는 것을 알 수 있다. 이 값들을 가지고 아래쪽의 'rtcHexVarFromVar' 함수에서는 무슨 일을 하는지 알아보기 위해 맨 마지막 루프의 'd' 문자의 경우에 대하여 이 함수를 실행한 결과를 확인해보았다.

먼저 이 함수에서는 EDX, EAX 레지스터 값을 push하여 매개변수로 받는 것 같다. 이때의 EDX, EAX 레지스터는 위와 같이 0019F280, 0019F238 값을 가지고 있다. 그렇다면 이 함수가 실행된 후 이 레지스터가 기억하고 있던 위치에는 어떤 변화가 생기는지 관찰해보았다. 그 결과 0019F238 근처인 0019F240에 006B507C값이 저장되었다. 이 위치로 이동해보니 아스키로 문자 "C", "A"가 저장된것을 볼 수 있었다. 즉 이는 '__vbaVarAdd'에서 16진수로 변환한 값(CA)을 문자열("CA")로 변환하는 역할을 수행하는 함수라 볼 수 있다. 그렇다면 함수 이름으로 추측해보건대 이 다음에 호출되는 '__vbaVarCat' 함수는 이 문자열들을 이어붙이는 역할을 하지 않을까? 라고 생각해볼 수 있겠다.

그래서 이전처럼 실행시켜보고 매개변수로 넘겨진 주소를 관찰해보았더니 EAX가 가리키던 0019F238 주변에 "C5D7C8CA"라는, 위에서 Name 필드에 입력한 "asdf"에 대하여 각 문자의 16진수 값이 문자열로 저장되어 있었다. 이쯤되면 슬슬 이게 어떤 문자열인지 감이 올것이다. 바로 시리얼 넘버다. 그렇다면 이를 확인하는 로직은 어디 있을까? 일단 반복 블록을 탈출하고 아래로 내려가보자.

아래로 내려가면 성공, 실패 메시지 박스를 띄우는 분기 바로 위에서 '__vbaVarTstEq' 함수를 호출하고 있다. 많이 축약되어 있지만 대충 예상해보면 "Variable Test Equal"이라 생각된다. 즉 두 값이 동일한지 비교하여 결과에 따라 값을 반환하여 다음 명령어(사진에서는 가려서 안보이지만) 'test ax, ax'를 통해 성공, 실패 분기(je)를 타게 된다.

그러면 이전처럼 edx, eax 레지스터에 위치한 곳에 무슨 데이터가 있는지 확인해보자. 이상하게 eax 레지스터에는 아무런 값도 없고 edx 레지스터 근처에는 "C5D7C8CA"라는, 위에서 확인한 시리얼로 추측되는 값이 존재하고 있다. 프로그램의 입력 폼과 특성을 생각해보면 입력된 시리얼과 생성된 시리얼을 비교해야 할 테니 EAX 레지스터의 위치에는 사용자가 입력한 시리얼 값이 들어가야 할 것이다. 현재는 아무런 값도 입력하지 않았기 때문에 비어있다고 추측할 수 있는데 그렇다면 직접 값을 넣어서 확인해볼 수 있을 것이다.

확인결과 그렇다는 것을 알 수 있었다. 즉 Serial 폼에서 입력값을 가져와서 생성된 시리얼과 비교하여 일치하면 성공 메시지 박스를, 일치하지 않으면 실패 메시지 박스를 출력하게 될 것이다. 틀린 시리얼을 입력했을 경우 해당 함수는 0을 반환한다. 이 경우 'test ax, ax'에 의해 ZF가 설정되어 실패 메시지 박스를 출력하는 00403408로 점프하게 된다. 그렇다면 Name에 "asdf", Serial에 "C5D7C8CA"를 입력해보자.

올바른 시리얼을 입력했을 경우 반환값으로 FFFFFFFF가 EAX에 전달되어 test 구문에서 AND 연산해도 ZF가 설정되지 않는다. 그렇기 때문에 성공 메시지 박스로 분기하여 위와 같이 크랙에 성공한 것을 알 수 있었다.

 

아쉬운 점은 Visual Basic의 각 함수들이 어떤 역할을 하는지 제대로 파악하지 못해서 많이 헤맸다는 점이다. 또한 디버깅에 익숙하지 않아 명령어가 헷갈리거나 어딜 참조해야하는지도 헷갈리는 경우가 많았는데 기초지식을 탄탄하게 하고 다시 공부해야겠다.

 

[참고 | https://gutte.tistory.com/24]

[참고 | https://www.tutorialspoint.com/assembly_programming/assembly_conditions.htm]

[참고 | https://blog.naver.com/sh0w_line/220651897109]

[참고 | https://tnmsoft.tistory.com/118]

 

==================================

 

확인 결과 __vbaVarAdd로 각 문자의 아스키 값에 더해지는 값은 0x64, 즉 100으로 찾아낼 수 있었다. 역시 함수 호출전후로 바뀌는 스택값, 또는 매개변수로 전달되는 레지스터가 가리키는 부분을 살펴본 결과 확인할 수 있었다. 이를 바탕으로 키젠을 작성해서 테스트해본 결과 잘 수행되었다.

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

Abex's 5th Crackme  (0) 2020.10.26
Abex's 4th Crackme  (0) 2020.10.25
Abex's 3rd Crackme  (0) 2020.10.23
Abex's 1st Crackme  (0) 2020.08.29