공부/리버싱핵심원리

8장 abex' crackme #2 (abexcm2)

리버싱 핵심원리.

 

 

먼저 이 프로그램은 Visual Basic 으로 제작되었다.

VB는 MSVBVM60.dll 이라는 VB 전용 엔진을 사용한다. (Visual Basic 6.0이다. 뭐 이런건가?)

또한 이 엔진은 The Thunder Runtime Engine 이라고 불리운다.

 

메시지 박스를 출력하고 싶을 때 VB 코드에서 MsgBox()를 출력하는데, 이런 것들이 사용 예이다.

VB 컴파일러는 실제로 MSVBVM60.dll!rtc MsgBox()함수를 호출하게 하고,

이 함수 내부에서 Win32API인 user32.dll!MessageBoxW()함수를 호출해주는 방식으로 작동된다.

 

(MsgBox()함수는 VB에서 MessageBoxW()함수를 부르기 위한 함수인가 ?)

VB 소스코드에서 user32.dll!MessageBoxW()를 직접 호출하는 것도 가능하다.

 

1. 컴파일 옵션 N(Native) Code, P(Pseudo) Code

V는 컴파일 옵션에 따라서 N code와 P code로 컴파일이 가능하다.

N code는 일반적인 디버거에서 해석 가능한 IA-32 Instruction을 사용한다.

하지만 P code는 인터프리터 개념으로 VB 엔진으로 가상머신을 구현하여 해석 가능한 명령어 (바이트코드)를 사용하는 것이다.

 

VB의 P code를 정확히 해석하려면 VB엔진을 분석해 에뮬레이터를 구현해야 한다.

 

2. Event Handler

vb는 주로 GUI 프로그래밍에 사용됨.

VB 프로그램은 Windows 운영체제의 Event Driven 방식으로 동작하기에 main() 또는 WinMain()에 코드가 있는 것이 아니라, 각 Event Handler에 사용자 코드가 존재한다.

abexcm2에서 Check버튼을 누를 때 실행되는 것을 보면 Check버튼 핸들러에 사용자 코드가 존재할 것이다.

 

3. undocumented 구조체

VB에서 사용되는 각종 정보들 (Dialog, Control, Form, Module, Function 등)은 내부적으로 구조체 형식으로 파일에 저장된다.

문제는 마이크로소프트에서 이러한 구조체 정보를 정식적으로 공개하지 않았다. 그래서 VB파일 디버깅에는 약간의 어려움이 있다.

(그래서 undocumented구나.)

=====================================디버깅 시작=========================================================

1. Start Debugging

Entry Point로 가보자 x32dbg에서 F9를 누르면 EP로 간다.

으음 스택에 401E14를 집어넣는다. 이후 ThunRTMain 함수를 호출한다. (x32dbg에서 엔터를 안치고 올리디버거처럼 바로 주소를 알 수는 없는건가 ???)

 

더 자세히 알아보자. EP는 401238이다. 401238에서 PUSH 401E14에 의해 RT_MainStruct 구조체 주소를 스택에 입력한다.

그러니까 RT_MainStruct 구조체의 주소는 401E14라는 것이다. (VB는 항상 EP코드에서 저 구조체 주소를 스택에 입력하는건가 ?)

구글에 검색을 해보니 맞는 것 같다. 특정한 주소체를 Stack에 Push해주고 ThunRTMain 함수를 호출해준다.

 

우선 40123D 주소의 CALL 명령에 의해  401232주소로 이동된다. 또한 이곳에서 JMP 명령에 의해 VB 엔진의 메인함수인 ThunRTMain으로 이동하게된다. 그 위치는 004010A0인 것으로 보인다,

 

뭔가 이상하다. 그냥 40123D 부분에서 직접 ThunRTMain을 호출하면 되는 것 아닌가 ?? 왜 이렇게 호출하는 지 모르겠다.

 

2. 간접호출 (Indirect Call)

위에서 내가 말한 부분에 대한 설명인 것 같다.

다시 코드를 보자 40123D에서 401232를 호출하고 다시 4010A0으로 점프된다. 이런 방식을 간접호출 (Indirect Call) 기법이라고 한다.

 

3. RT_MainStruct 구조체

ThunRTMain() 함수의 파라미터인 RT_MainStruct 구조체를 주목해보자.

401E14가 구조체 주소이다.

MS에서 RT_MainStruct를 공개하지 않았다. 하지만 리버서들이 분석을 완료하고 인터넷에 공개하였다고 한다.

RT_MainStruct 구조체의 멤버는 또다른 구조체들의 주소라고 한다. 즉 VB 엔진은 파라미터로 넘어온 RT_MainStruct 구조체를 가지고 프로그램의 실행에 필요한 모든 정보를 얻는다. RT_MainStruct의 구조에 대해 공부해보고 싶은데 책에서는 자세한 설명은 생략한다고 한다.

나중에 따로 공부해봐야겠다.

 

4. ThunRTMain() 함수

733735A4에서 ThunRTMain() 함수가 시작된다. 메모리주소가 완전히 달라진다. 이 주소는 MSVBVM60.dll 모듈의 주소 영역이다.

신기히다. 이 함수를 우리가 분석해야 하는 지 의문이 드는데, 책에서는 현재는 이 방대한 코드를 분석할 필요는 없다고 한다.

아마 문제해결하고는 거리가 약간 있는 부분인 것 같다.

 

5. CrackMe 분석

우리가 보고 싶은 '패치해야 할 코드'를 찾아야 한다. 고로 단서를 찾아야 한다.

이 경고메시지와 문자열이 단서가 될 것이다.

 

첫 번째 방법으로 문자열을 검색하는 것이다.

문자열 검색 기능을 이용해보자.

뭐가 정말 많다.

Wrong serial! 문자열을 확인할 수 있다. 더블클릭해서 들어가보자.

메시지 박스의 타이틀 ("Wrong Serial")과 내용 ("Nope, this serial is wrong!") 그리고 메시지박스(rtcMsgBox)를 호출하는 것이 보인다.

 

추측을 해보자. Name을 입력했으면 그에 맞는 Serial도 생겼거나 존재할 것이다. 그리고 내가 입력한 문자열과 비교를 하는 코드가 존재할 것이다. 혹시나 하는 마음에 위로 올려보자.

 

두둥등장ㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇ

자세히 보자. __vbaVarTstEq 함수를 호출하고 있다. 구글링을 해보니 __vbaVarTstEq함수는 EDX, EAX를 스택에 push해서 파라미터로 쓴다고 한다. 저 함수가 무슨 함수인지 정확하게는 모르겠는데 비교를 하는 것이라고는 확인할 수 잇으며, 그 리턴값이 AX(EAX하위)에 저장된다고 생각할 수 있다. 그리고 그 AX에 대하여 TEST 연산을 진행한다.

 

TEST 는 두 연산자에 대하여 AND 연산을 진행한다고 보면된다. 근데 operand는 변경되지 않는다.

and 연산 특성상 하나라도 0이 있으면 무조건 0을 출력한다. 그러므로 연산의 결과가 0이 되고 ZF는 1로 설정된다.

결론적으로 오퍼랜드 중에 하나라도 0이면 그 TEST연산은 ZF를 1로 설정해주는 결과를 낳는다.

 

그럼 그 밑에서 JE 조건 분기를 확인할 수 있는데, ZF가 1이면 403408로 JUMP 하라는 명령이다.

JE (JUMP IF EQUAL)

 

아 나 스스로 풀 때는 완전히 뻘짓을 했구나... 왜 문자열 찾기나 이런 시도를 하지 않았을까

바보같다

우선 __vbaVaarTstEq() 함수를 호출하기 전 EDX, EAX를 비교해보자.

Name에 abcd를, Serial에 efgh를 입력해보자.

 

EAX는 19F298을, EDX는 19F288을 가지고 있다. 그리고 그 두값을 인자로 쓰기 위해 스택에 PUSH 한다.

그럼 EAX와 EDX의 그 값들은 어떻게 들어왔을까

이 과정을 통해 들어왔다. 403321주소에서 DWORD PTR SS:[EBP-44]를 잘 이해해보자.

우선 SS는 Stack Segment이다. 그리고 EBP는 Base Pointer이다. 아. 그니까 SS:[EBP-44]는 스택 주소를 가리키나보다.

그렇다. 실제로 [EBP-44]의 주소는 19F288이다. 완전 신기하다. 그럼 이 값들을 스택메모리에서 확인해보자.

확인을 해보면 16바이트 크기의 데이터가 나타나는데, 이것이 바로 VB에서 사용하는 문자열 객체이다.

문제는 다른 부분이 보이는데, 19F288에서는 7C 77 4C 00 이렇게 있는 부분이 19F298에서는 44 77 4C 00 이렇게 있다.

근데 이것이 메모리 주소처럼 보인다고 한다. (난 아직 멀었구나. ...)

 

19F288 : 7C 77 4C 00

19F298 : 44 77 4C 00

근데 intel x86 cpu는 리틀 엔디언 방식을 채택한다. 그러므로 

 

가변길이 문자열 타입은 내부에 동적으로 할당한 실제 문자열 버퍼 주소를 가지고 있다고 한다. ㅜㅜㅜㅜㅜㅜ 그럼 그 주소를 어떻게 알아낼까 ?

 

OllyDbg 같은 경우는, Long - Address with ASCII dump로 가주면 된다. 근데 난 x32dbg이다. 고로 모른다. 구글링 해도 되는데

걍 바이트 오더링을 이용해서 변환해보자,

 

7C 77 4C 00 -> 004C777C

44 77 4C 00 -> 004C7744 이다. 이 주소의 값을 확인해보자.

4C777C에 C5C6C7C8 이라는 문자열이 들어가졌고

4C7744에 efgh 라는 문자열이 들어가졌다. 

 

EAX -> 19F298 -> 44774C(4C7744) -> efgh

EDX -> 19F288 -> 7C774C(4C777C) -> C5C6C7C8

 

임을 확인할 수 있다.

 

결론적으로 NAME에 abcd를 입력한다면, SERIAL에는 C5C6C7C8을 입력해줘야 한다.

이건 이 프로그램 분석을 마치고 한번 시도해보자.

여러번 시도해보니 Name과 Serial은 대응하는 값을 가지는 것을 확인할 수 있다. Name에 따라 Serial을 다르게 만드는 것 같다.

이제 Serial 생성 알고리즘을 찾으러 가보자.

 

6. Serial 생성 알고리즘 분석

이 상태에서 Check버튼을 누른다면 그 때 Name을 가지고 Seiral을 만든 다음 JE를 통과하여 결과를 알려줄 것이다.

그러므로 우리가 분석한 JE 분기문에서 위로 올려주면 스택프레임을 형성하는 곳이 있을 것이라고 어느정도 추측은 할 수 있겠다.

(Name에 글자가 입력될 때 마다 그에 맞는 Serial이 생성되는 경우라면 매우 어려워지겠다. 물론 그 때는 그 때에 맞는 함수가 존재하겠지만...)

 

좀 더 자세히 말하자면 Check 버튼의 Event Handler에서 Serial을 생성하거나 그 함수를 호출할 것이다.

 

이렇게 Stack Frame이 형성되어있는 것을 볼 수 있다. ( VB는 함수와 함수 사이에 저렇게 NOP가 존재한다고 한다.)

402ED0에 BP를 걸고 분석을 해보자. Name과 Serial 에 abcd, efgh를 다시 입력해보자.

 

Serial 생성 방법에 대해 생각해보자. 간단히 생각해보면 Name의 문자열을 함수로 읽고 그 밑에서 루프를 돌며 문자를 암호화한다. 정도로 생각할 수 있겠다.

 

그럼 문자열을 읽어들이는 함수가 있어야 한다. 그러므로 CALL 명령을 위주로 생각해보자.

 

첫 번째 CALL 명령이다. BASIC_CLASS_AddRef 를 호출하는 것을 볼 수 있다. -> 이게 뭔지 모르겠다ㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏ

우선 msvbvm60.dll 에 포함된건 알 수 있다.

 

친구 생일이니까 밥 먹고 다시 오자. 밥 다먹고 놀고 왔다.

 

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

다시 다시 생각을 해보자.

 

첫번째 CALL 에서는 ECX+4 주소를 CALL 한다. ECX Register는 사실 Counter Register인데, 지금은 별 의미 없고 주소를 저장하기 위한 용도로 사용되는 것 같다. 아직 데이터 세그먼트를 잘 알지는 못하지만 뭐 그렇다.

 

그럼 ECX+4가 가리키는 73376C28 부분을 봐보자. eax에 [esp+4]주소의 값을 4바이트만큼 옮겨준다.

다시 eax에 [eax+8] 주소가 가리키는 값을 옮기려나 ?

 

한번 F7을 통해 들어가보자.

 

이게 그 함수이다. RET가 오기 전에 CALL을 한번 더 해주는 것을 볼 수 있다.

MOV EAX, DWORD PTR SS:[ESP+4]를 하게 된다면, EAX에 4C0410이 들어가져야 한다.

딩동댕댕댕 아직 어려운걸 한건 아니지만 너무 행복하다 맞춰서 ㅎㅎㅎㅎㅎㅎㅎㅎ

 

그다음 명령어는 MOV EAX, DWORD PTR DS:[EAX+8]이다.

이게 문제다.

데이터 세그먼트 영역에서 값을 불러와야 하므로 그 값은

2C 04 4C 00인데, 이건 바이트오더링이 적용되어있는 상태이고 원래 값을 찾아주어야 한다.

아마 004C042C 즉, 4C042C가 들어갈 것 같다.

맞다 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

 

근데 BASIC_CLASS_AddRef 를 분석하면 할 수록 뭔가 쓸 데 없다는 생각이 든다. (문제에서 원하는 결과에서 멀어지는 느낌)

 

나가서 다음 CALL까지 가보자.

 

두 번째 CALL은 CALL DWORD PTR DS:[EDX + 308]이다.

아 실수로 f9를 눌러버렸다. 다들 조심하자.

이런식으로 계속 분석을 진행해보자.

이 코드가 눈에 들어온다. 정말?

사실 난 책에서 이 코드가 문자열을 읽는 부분이라고 한 근거도 이해가 가지 않는다.

먼저, EDX에 SS:[EBP-88]의 주소를 넣어준다. 그리고 스택에 넣는다. (아마 파라미터로 쓰기 위해서 일것이다.)

[EBP-88] 메모리 주소는 19F244이다. 그럼 EDX에 19F244가 들어가고, 이후 스택에 PUSH한다. ESI도 PUSH 한다.

이후 CALL DWORD PTR DS:[ECX + A0] 명령을 실행해보자.

값이 이렇게 바뀐것을 확인할 수 있다. 

진짜 신기하다.....

 

이렇게 name을 찾았다. 이제 이 name을 가지고 어떠한 과정을 거친 값이 Serial이 될 것이다.

402FB6의 MOV EAX, DWORD PTR SS:[EBP-88]에 의해 EAX에는 4C7744가 들어가진다. 즉, "abcd"가 들어가진다.

EAX에만 초점을 두고 한줄한줄 디버깅을 해보자.