본문 바로가기

챌린지/CrackMe

Abex's 1st Crackme

리버싱 입문 도서에서 거의 빼놓지 않고 다루는 Abex의 첫번째 Crackme 파일이다.

파일은 https://crackmes.one/에서 간단한 가입 후 다운받을 수 있다.

크랙미의 목적은 이 파일이 하드 드라이브를 CD롬으로 인식하도록 조작하는 것이다.

성공한다면 "OK, I really think OK, I really think that your HD is a CD-ROM! :p"이라는 문자열을 출력하며

실패한다면 "NAH... This is not a CD-ROM drive!"라는 문자열을 출력하게 된다.

 

사실 문제를 풀어보기 전에 가장 좋은 습관은 그냥 실행시켜 보는 것이다.

아무런 조작을 하지 않은 채 실행시키면 처음에는 위와 같은 메시지박스가 뜨는 것을 볼 수 있다.

확인을 누르면 위와 같은 메시지박스가 뜬 후에 프로그램이 종료되는 것을 알 수 있다.

그렇다면 이 크랙미를 디버거로 열어보자.

x32dbg

디버깅을 시작하면 커서가 엔트리 포인트에 위치하게 되는데 고맙게도 프로그램의 전체적인 구조가 한눈에 들어오게 나와있다. F8을 눌러서 하나씩 실행시키다보면 MessageBoxA 함수를 호출하여 아까 처음에 본 메시지박스를 실행시키는 것을 알 수 있다.

문서에 따르면 MessageBoxA() 함수는 다음과 같은 4가지 매개변수를 받는다.

int MessageBox(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType
);

코드를 살펴보면 함수를 call하기전에 몇몇 데이터를 스택에 push하는 것을 알 수 있다. 이 0, abexcm1.402000, abexcm1.402012, 0 값은 각각 uType, lpCaption, lpText, hWnd에 해당할 것이다(순서는 함수호출규약 참조). 디버거에서 편히 두번째, 세번째 매개변수가 가리키는 곳이 어떤 값을 가지는지 알려주고 있지만 덤프에서 직접 확인해볼 수도 있다.

null-termination strings

아무튼 이 메시지박스가 호출된 후 그 다음에 호출되는 함수가 하나 더 있는데 GetDriveTypeA() 라는 함수다. 이는 무슨 함수일까? 직접 문서에서 확인해보거나 문제 설명파일에서 알려주는 것을 읽어보면 다음과 같다.

GetDriveType( lpRootPathName ); 

Parameter: lpRootPathName 
Points to a null-terminated string that specifies the root directory of the disk 
to return information about. If lpRootPathName is NULL, the function uses the root 
of the current directory.  

Return Value 
The return value specifies the type of drive. It can be one of the following values:  

Value Meaning 
0 The drive type cannot be determined. 
1 The root directory does not exist. 
2 The drive can be removed from the drive. 
3 The disk cannot be removed from the drive. 
4 The drive is a remote (network) drive. 
5 The drive is a CD-ROM drive. 
6 The drive is a RAM disk.

즉 매개변수로 받은 경로에 대하여 해당 경로가 위치하고 있는 드라이브가 어떤 종류인지 알려주는 함수다.

매개변수로 abexcm1.402094에 위치한 "C:\\" 문자열을 전달해주고 있다. 즉 컴퓨터의 C 드라이브가 어떤 매체에 존재하고 있는지를 확인하는 것이다. 대부분의 경우 C 드라이브는 SSD든 HDD든 디스크에 존재하기 때문에 그 결과는 CD-ROM이 아닐 것이다. 문제에서 원하는 것은 이 함수의 실행 결과가 5, 즉 "The drive is a CD-ROM drive."을 원하는 것이다. 그렇다면 이 함수를 실행시켜보면 어떻게 될까?

실행 결과는 EAX에 저장되어 위와 같이 3, 즉 "The disk cannot be removed from the drive."가 나타남을 알 수 있다. 당연히 사용중인 C 드라이브는 언마운트될 수 없는게 당연할 것이다. 그렇다면 결과는 어떻게 될까?

0040103D로 점프하지 않았다.

이후 je, Jump if Equal에 걸려서 실패로 분기가 틀어져서 결국 실패 문자열이 담긴 메시지 박스를 출력하고 프로그램을 종료하게 된다. 그렇다면 어떻게 성공 분기로 진행할 수 있을까? 가장 먼저 생각나는 것은 분기의 핵심인 이 je를 조작하는 것이다.

 

Jump if Equal이 있다면 Jump if Not Equal도 있을 것이다. 즉 jne 명령어로 어셈블하여 분기를 역전시켜 진행할 수 있다. 아예 jmp로 바꿔서 항상 점프하게 할 수도 있을 것이다. 어쨌든 한번 진행한 것은 되돌릴 수 없으니 디버거에서 프로그램을 다시 시작하고 엔트리 포인트까지 도달했을 때 아래와 같이 명령어를 바꿔주면 된다.

je -> jne

그러면 아래와 같이 프로그램이 성공 분기로 들어가서 결과적으로 성공 문자열을 볼 수 있다.

아니면 je의 조건이 되는 cmp 명령어를 조작해볼 수도 있다. je도 결국은 연산 결과에 따라 작동하는 것이기 때문에 어떤 연산이 이 je에서 점프하도록 만들었는지 살펴볼 필요가 있다. 위에서 말했지만 GetDriveTypeA() 함수의 반환값은 EAX에 저장되어 3이란 값을 가진다. 이는 추후 ESI 레지스터의 값과 비교되는데 이를 살펴보자.

inc, dec로 가감되는 레지스터들

함수호출 이후에 inc, dec, jmp 명령어들이 사용되는데 inc는 값을 증가, dec는 값을 감소, jmp는 해당 위치로 점프하는 명령어를 의미한다. 여기서 jmp abexcm1.401021 구문은 바로 다음 라인으로 점프하기 때문에 의미없는 부분이라 할 수 있다. 그렇게 inc가 세 번 적용된 esi 레지스터와 dec가 두 번 적용된 eax 레지스터는 어떤 값을 가지게 될까?

ESI 레지스터는 데이터 복사에 사용되는 레지스터라 했지만 여기서는 다른 값을 담고있다. 담겨있는 값을 보면 "abex' 1st crackme"라는 문자열의 주소지만 이는 지금 코드에선 별 의미가 없다. 중요한 것은 GetDriveTypeA() 함수의 반환값인 3이 1로 줄어들었는데 ESI는 00401003으로 증가하여 서로 다른 값이라는 것이다. 엄청난 차이가 나는 값이기 때문에 cmp eax, esi 명령어는 당연히 두 레지스터의 값이 일치하지 않는다는 결론을 내리게 될 것이다.

Zero Flag = 0(False)

비교 명령어 cmp는 첫번째 피연산자에서 두번째 피연산자를 빼서(subtract) 그 결과에 따라 플래그를 세팅한다. 만약 eax와 esi가 같은 값이라면 eax - esi = 0일 것이므로 연산 결과가 0임을 의미하는 Zero Flag, 즉 ZF가 1로 세워져야 할 것이다. 하지만 현재 00000001 - 00401003은 전혀 0이 아니기 때문에 Zero Flag는 세워져있지 않다. 그렇다면 우리가 직접 이 플래그를 반전시켜서 연산 결과가 0이라고 속이면 어떨까?

이미지엔 보이지 않지만 ZF는 1로 반전되었다.

이 경우에도 역시 성공 분기를 타는 것을 볼 수 있다. 또는 직접 플래그를 반전시킬 필요가 없이 cmp의 피연산자를 동일한 피연산자(cmp eax, eax 또는 cmp esi, esi 처럼)로 통일시키면 자기자신을 빼는 결과가 나오기 때문에 항상 0이 되어 성공 분기를 타게 할 수도 있다.

00401024: cmp eax, eax

 

'챌린지 > 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 2nd Crackme  (0) 2020.09.29