본문 바로가기

리버싱 핵심원리

[리버싱 핵심원리] 8장 - abex' crackme #2

8.1 abex' crackme #2 실행

전형적인 crackme의 형태의 시리얼 키(serial key)를 알아내는 프로그램이다. Name과 serial값을 각각 입력받고 있다. 각각 값을 적절히 넣고 실행시켜보면, 아래와 같은 메시지 박스가 출력된다.

8.2 Visual Basic 파일 특징

디버깅 실습을 할 abex' 2nd crackme 파일은 Visual Basic으로 제작되었다. 먼저 Visual Basic 파일의 특징을 살펴보자.

1. VB 전용 엔진

VB 파일은 MSVBVM60.dll (Microsoft Visual Basic Virtual Machine 6.0)이라는 VB 전용 엔진을 사용한다. (The Thunder Runtime Engine이라는 이름으로도 불린다.) 

 

VB 엔진의 사용 예를 들어보자면, 메시지 박스를 출력하고 싶을 때 VB 소스코드에서 MsgBox() 함수를 사용한다. VB 컴파일러는 MSVBVM60.dll!rtcMsgBox() 함수가 호출되도록 만들고, 이 함수 내부에서 Win32 API인 user32.dll!MessageBoxW() 함수를 호출해주는 방식으로 동작한다. VB 소스코드에서 user32.dll!MessageBoxW() 함수를 직접 호출하는 것도 가능하다.

 

2. N(Native) code, P(Pseudo) code

컴파일 옵션에 따라 N code와 P code로 컴파일이 가능하다. N code는 일반적인 디버거에서 해석 가능한 IA-32 Instruction을 사용하는 반면에 P code는 인터프리터(Interpreter : 코드 번역과 실행이 동시에 이루어지는 언어) 언어 개념으로서 VB 엔진으로 가상 머신을 구현하여 자체적으로 해석 가능한 명령어(바이트 코드)를 사용한다. 따라서 VB의 P code를 정확히 해석하려면 VB 엔진을 분석하여 에뮬레이터를 구현해야 한다. (솔직히 무슨 말인지 잘 모르겠다....)

 

(에뮬레이터 : 다른 프로그램이나 장치를 모방하는 컴퓨터 프로그램 또는 전자기기로 하드웨어 기반일 수도, 소프트웨어 기반일 수도 있다. 지원되지 않는 하드웨어에서 소프트웨어를 실행하기 위한 목적으로 널리 사용된다.)

 

3. Event Handler

VB는 주로 GUI 프로그래밍을 할 때 사용되며, IDE 인터페이스 자체로 GUI 프로그래밍에 최적화되어 있다. VB 프로그램은 Windows 운영체제의 Event Driven 방식으로 동작하기 때문에 main() 혹은 WinMain()에 사용자 코드가 존재하는 것이 아니라, 각 event handler에 사용자 코드가 존재한다. 실습하는 파일에선 [check] 버튼 handler에 사용자 코드가 존재할 것이다. 

(Event Driven 방식 : 컴퓨터 프로그램 중에서 특히 이벤트에 반응해 동작을 변경하는 방식이다.)

(GUI : 그래픽 사용자 인터페이스, IDE : 통합 개발 환경)

 

4. undocumented 구조체

VB에서 사용되는 각종 정보들은 내부적으로 구조체 형식으로 파일에 저장된다. Microsoft에서는 이러한 구조체 정보를 정식으로 공개하지 않았다.

 

8.3 디버깅 실습

EP코드는 프로그램이 시작되면 VB 엔진의 메인 함수(ThunRTMain)를 호출한다. 

 

EP의 주소는 401238이다. VB 파일의 startup 코드는 401238, 40123D, 401232 이렇게 3줄이다. (코드의 흐름 순으로 작성했다.) 3줄의 내용은 다음과 같다.

(401238) : PUSH 명령에 의해 RT_MainStruct 구조체 주소(401E14)를 스택에 입력한다.

(40123D) : CALL 명령에 의해 401232 주소로 이동한다.

(401232) :  JMP 4010A0 명령어가 실행된다. 해당 명령어에 의해 ThunRTMain() 함수로 로 간다.

 

1. 간접호출

위에서 본 startup 코드는 한 가지 특이한 점이 있다. 바로 ThunRTMain() 함수를 직접 호출하는 방식이 아닌, 간접적으로 JMP 명령어를 통해 호출한다는 것이다. 해당 깁버은 VC++, VB 컴파일러에서 많이 사용하는 간접호출(Indirect Call) 기법이다.

 

2. RT_MainStruct 구조체

startup 코드에서 가장 처음으로 스택에 입력한 값은 401E14로 RT_MainStruct 구조체의 주소이다. Dump 창으로 401E14 주소로 가보면 아래와 같은 창을 볼 수 있다.

RT_MainStruct

RT_Mainstruct 구조체의 멤버는 또 다른 구조체의 주소들이다. 즉 VB 엔진은 파라미터로 넘어온 RT_MainStruct 구조체를 가지고 프로그램의 실행에 필요한 모든 정보를 얻는다는 것을 알 수 있다.

 

3. ThunRTMain() 함수

위 코드는 ThunRTMain()의 시작 부분이다. 앞서 보았던 주소들과 큰 차이가 있다는 걸 알 수 있다. 이 주소는 MSVBVM60.dll 모듈의 주소 영역으로, 우리가 분석하는 프로그램의 코드가 아니라 VB 엔진의 코드이다. 

 

8.4 crackme 분석

우리가 코드를 패치하기 위해선, 해당 코드가 어디에 위치해 있는지를 알아야 한다. 맨 처음 파일을 실행시켜 보았던 문자열들을 단서로 찾아본다.

1. 문자열 검색

문자열 검색 기능을 사용해 맨 처음 보았던 문자열들을 찾아본다. 조금 내리다보면, 맨 처음 보았던 문자열을 찾을 수 있고 해당 문자열을 더블클릭해 주소로 간다.

메시지 박스의 타이틀("Wrong serial!"), 내용("Nope, this serial is wrong!") 그리고 실제 메시지 박스 함수 호출 코드(맨 아래 4034A6)까지 나타난 것을 볼 수 있다. 파일에서 갖고 있는 key의 값과 사용자가 입력한 key의 값을 비교해 키가 같은지 다른지에 따라 코드가 갈라지는 형태로 진행될 것이라고 유추해볼 수 있다. 문자열 검색 기능을 통해  "Congratulations!"라는 문자열을 볼 수 있었기 때문이다. 현재 화면에서 스크롤을 위로 조금 더 올리면 아래와 같이 성공 문자열을 볼 수 있다. 또한, 입력된 값과 serial 값을 비교하는 함수도 볼 수 있다.

조건분기 명령어

403329주소에서  __vbaVarTsEq() 함수를 호출해 리턴 값(AX)를 비교(TEST : 40332F)한 후 403332의 조건분기 명령어에 의해 참, 거짓 코드로 분기하게 된다.

 

+) TEST : 논리 비교 / 두 operand 중에 하나가 0이면 AND 연산결과는 ZF=1로 세팅된다.

    ( TEST 명령어는 두 값이 같은 지를 확인하는 것이 아니라, 0인지 아닌지를 확인하는 용도로 사용된다. 두 값을 비교하          는 명령어는 주로 CMP를 사용한다.)

+) JE : 조건 분기 / ZF = 1이면 점프

 

2. 문자열 주소 찾기

위 사진에서  __vbaVarTsEq() 함수가 문자열 비교 함수라면 그 위에 있는 2개의 PUSH 명령어는 비교 함수의 파라미터, 즉 비교 문자열이 될 것이다. 

00403321		LEA EDX, DWORD PTR SS:[EBP-44]
00403324		LEA EAX, DWORD PTR SS:[EBP-34]
00403327		PUSH EDX
00403328		PUSH EAX
00403329		CALL DWORD PTR DS:[&MSVBVM60.__

PUSH 명령어 위에 존재하는 2개의 명령어는 무엇일까?

먼저 SS, [EBP-44]와 같은 명령어의 의미를 알아보자면, SS는 Stack Segment이고 EBP는 Base Point Register이다. 즉 SS:[EBP-44]가 의미하는 것은 스택 내의 주소를 말하는 것이다. 그리고 이것이 바로 함수에서 선언된 로컬 객체의 주소이다. 이 상태에서 스택을 보면 아래와 같다. (박스 표시된 곳이 해당되는 곳이다.)

PUSH EDX가 먼저 실행되기 때문에 스택에 아래쪽에 있는 것이 EDX에 입력된 값이다.스택에 저장된 메모리 주소를 따라가보면 아래와 같다.

VB의 문자열은 C++의 string 클래스와 마찬가지로 가변 길이 문자열 타입을 사용한다.따라서 위 사진과 같이 문자열이 바로 나타나는 것이 아니라 16byte 크기의 데이터가 나타난다. (이것이 바로 VB에서 사용하는 문자열 객체이다.) 

이렇게 입력된 값들은 초록 박스 안에 있는 값을 제외하곤 모두 동일하다는 것을 알 수 있다. 마치 메모리 주소처럼 보이기도 한다. (가변 길이 문자열 타입은 내부에 동적으로 할당한 실제 문자열 버퍼 주소를 가지고 있기 때문)

 

+)가변 길이 문자열은 고정 길이 문자열과 다르게 지정된 크기 보다 적은 크기의 값이 들어오면 남은 공간에 공백을 넣지 않는다. 가변 길이는 공간 절약면에서 효율적이다.

 

메모리(덤프) 창에서 마우스 우측 메뉴의 'Long - Address with ASCII dump' 명령을 선택하면 메모리 창의 보기 형식을 마치 스택 창처럼 변경할 수 있다. 또한 문자열 주소인 경우 해당 문자열을 표시해준다는 특징이 있다.

위 사진을 보면 EDX에 serial 값을 가리키는 주소가, EAX에 사용자가 입력한 값을 가리키는 주소가 입력된 것을 알 수 있다. 여기서 각 주소 부분(00564DD4, 00564B64)을 찾아가 보면 실제 문자열을 확인할 수 있다.

(위 사진에서 초록 박스에 들어갔던 부분이 각 주소 부분에 해당된다!)

 

crackme 파일을 다시 실행시켜 알아낸 serial 값을 입력하면 아래와 같이 성공 메시지 박스를 얻을 수 있다!

serial을 찾았기 때문에 crack을 성공했다고 할 수 있지만, 여기서 Name의 값을 기존과 다르게 입력하고 앞에서 구한 serial 값을 입력하면 실패 메시지 박스가 출력된다. 이는 serial 값은 내부에 고정된(미리 저장된) 값이 아니라는 것을 알 수 있고 Name을 통해서 serial 값을 만들어낸다는 것을 유추할 수 있다.

 

3. Serial 생성 알고리즘 - 함수 생성 찾기

앞에서 보았던 조건 분기 코드는 어떤 함수에 속해있다. 그 함수는 [check] 버튼의 event handler일 것이다. [check] 버튼을 눌렀을 때 위 함수가 호출되었으며 성공/실패 메시지 박스를 출력하는 사용자 코드를 포함하고 있기 때문이다.

조건분기 명령어 코드가 있던 위치에서 스크롤을 위로 계속 올리면 위 사진과 같은 코드를 볼 수 있다.(PUSH EBP와 MOV EBP, ESP는 함수의 시작과 함께 스택 프레임을 만드는 단계에 해당하기 때문에 해당 함수의 시작으로 볼 수 있다!)

해당 부분에 BP를 걸고 디버깅을 시작한다.

4. Name 문자열 읽는 코드

VB 엔진 API를 이용해 사용자가 입력한 문자열을 가져올 것으로 예상되기 때문에 CALL 명령어 위주로 디버깅을 한다. 디버깅을 하면 4번째 CALL 명령어를 만날 수 있다. 아래 사진은 CALL 명령어를 수행해 메모리에 사용자가 입력한 문자열이 입력된 것을 볼 수 있다.

5. 암호화 루프

디버깅을 이어가면 위와 같이 반복문을 만나게 된다. 해당 반복문의 동작 원리를 간단히 설명하면 함수들을 통해 linked list에서 next pointer를 이용해 다음 원소를 참조하듯이 문자열 객체에서 한 글자씩 참조하도록 한다. EBX값(4 = loop count) 만큼 반복문을 수행한다. (입력한 문자열에서 앞 4문자만 사용한다!)

6. 암호화 방법

위 사진까지 디버깅을 진행하고 스택과 메모리를 보면 아래와 같다.

스택
메모리 주소

이어서 아래 함수를 실행시키면 ECX 레지스터가 가리키는 버퍼에 암호화된 값이 저장된다.

CALL DWORD RTP DS : [&MSVBVM60.__vbaVaradd]

==> 암호화 : 52(첫 번째 문자의 ASCII 값) + 64(암호화 키) = B6

암호화 함수가 실행된 순간의 스택의 모습이다.

숫자 B6

이후 숫자 B6을 문자(유니코드) 'B6'으로 변환하는 함수를 실행한 직후 EAX의 값을 보면 "B6" 문자열이 생성된 것을 볼 수 있다.

문자열 B6

마지막으로, 생성된 문자열을 하나의 문자열로 붙이는 단계가 있다. 해당 과정은 아래와 같다.

위 사진의 맨 마지막 코드가 문자열을 이어 붙이는 함수이다. 따라서 해당 위치에 BP를 걸고 계속 실행[F9]을 하면 메모리에 serial 코드가 완성되는 모습을 볼 수 있다. (4번만 실행시키면 완성된다.)

문자열 이어붙이는 중
serial 완성

 

암호화 방법을 다시 정리해보자면 다음과 같다.

1. 주어진 Name 문자열을 앞에서부터 한 문자씩 읽는다.(총 4회)

2. 문자를 숫자(ASCII)로 변환한다.

3. 변환된 숫자에 64를 더한다.

4. 숫자를 다시 문자로 변환한다.

5. 변환된 문자를 하나의 문자열로 연결시킨다.

 

 

출처 : 리버싱 핵심원리(이승원, 인사이트)

 

\\ 해당 부분은 처음 공부할 때 어려워서 3주만에 다시 보는데.... 3번정도 보니까 조금 이해가 간다..... 화이팅.....