kj0on

abex' crackme 1 상세분석 본문

Reversing/abex' crackme

abex' crackme 1 상세분석

kj0on 2025. 6. 17. 14:04
목차 접기

0. 실행환경

 

0-1. 운영체제 (2)

Window 11 Home
Window XP


0-2. 툴 (3)

x32dbg
Detect it easy

 

OllyDbg


1. 파일

abex' crackme1.exe
0.01MB
https://binvis.io/


2. 프로그램 동작

[이미지1] abex' crackme 1
[이미지2] abex' crackme 1

프로그램을 실행했을 때의 동작이다. 두 개의 팝업 창을 띄워주는 것을 볼 수 있다. 나타나는 메시지를 확인해 보면 문제에서 무엇을 요구하는지를 알 수 있다. 첫번째 팝업 메시지를 보면 프로그램을 실행했을 때 하드디스크가 CD롬으로 인식하기를 요구하고 있다. 이후 Error 팝업이 나타나는 것을 봐서 CD-ROM으로 인식이 되지 않았다는 것을 알 수 있다.


3. Detect It Easy

[이미지3] DIE 실행 결과

32비트 실행파일, 델파이로 작성된 프로그램인 것을 알 수 있다. 


4. 분석

 

4-1. EntryPoint

[이미지4] EntryPoint

x32dbg에서 프로그램을 실행했다. 코드의 길이가 매우 짧다.


4-2. MessageBoxA

[이미지5] MessageBoxA

첫번째 팝업을 띄워주는 부분이다. MessageBoxA 함수를 호출하기 전 4개의 인자를 스택에 push해주는 걸 알 수 있다.

 

int MessageBoxA(
  [in, optional] HWND   hWnd,
  [in, optional] LPCSTR lpText,
  [in, optional] LPCSTR lpCaption,
  [in]           UINT   uType
);

함수의 동작을 파악하기 위해 MSDN에서 MessageBoxA 함수를 확인했다(https://learn.microsoft.com/ko-kr/windows/win32/api/winuser/nf-winuser-messageboxa).

 

[이미지6] MessageBoxA 매개 변수 (https://learn.microsoft.com/ko-kr/windows/win32/api/winuser/nf-winuser-messageboxa)

매개변수의 의미는 위와 같다.

 

매개변수 의미
hWnd 0x0 메시지 상자에 소유자 창이 없음
IpText 0x402012 표시할 메시지의 텍스트 데이터 주소는 0x402012
IpCaption 0x402000 대화 상자 제목의 텍스트 데이터 주소는 0x402000
uType 0x0 메시지 상자의 표시 단추의 플래그는 0x0

abex' crackme1.exe에서 MessageBoxA에 사용되는 인자의 값과 의미를 매칭하면 위와 같다.

 

[이미지7] abex' crackme 1

hWnd가 0x0이기 때문에 메시지 상자에 소유자 창이 없다. 이는 부모 창의 핸들값이 없다는 것을 의미하고 첫 실행동작에서 알 수 있듯이 해당 메시지 창이 독립적으로 띄워진다는 의미다.

 

[이미지8] 0x402012 덤프

표시할 메시지의 텍스트 데이터는 0x402012에 있다. 해당 위치로 가보면 메시지 창의 메시지를 알 수 있다.

[이미지9] 0x402000 덤프

대화 상자 제목의 텍스트 데이터는 0x402000에 있다. 해당 위치로 가보면 대화 상자 제목의 메시지를 알 수 있다.

 

[이미지10] uType flag (https://learn.microsoft.com/ko-kr/windows/win32/api/winuser/nf-winuser-messageboxa)

uType값이 0x0이기 때문에 표시 단추 플래그는 위와 같다. 해당 값으로 인해 확인 버튼 하나만 나타난다.

 

[이미지11] MessageBoxA 반환 값 (https://learn.microsoft.com/ko-kr/windows/win32/api/winuser/nf-winuser-messageboxa)

반환값은 위와 같다. 확인버튼 하나만 있기 때문에 버튼을 누를 시 1이 반환된다.


4-3. GetDriveTypeA

[이미지12] GetDriveTypeA

다음으로 GetDriveTypeA함수가 호출된다. 인자로 0x402094에 있는 데이터("c:\\")가 인자로 사용된다.

 

UINT GetDriveTypeA(
  [in, optional] LPCSTR lpRootPathName
);

함수의 동작을 파악하기 위해 MSDN에서 GetDriveTypeA 함수를 확인했다(https://learn.microsoft.com/ko-kr/windows/win32/api/fileapi/nf-fileapi-getdrivetypea).

 

[이미지13] GetDriveTypeA 매개 변수 (https://learn.microsoft.com/ko-kr/windows/win32/api/fileapi/nf-fileapi-getdrivetypea)

드라이브의 루트 디렉터리로 "c:\\"값이 사용된다.

 

[이미지13] GetDriveTypeA 반환 값 (https://learn.microsoft.com/ko-kr/windows/win32/api/fileapi/nf-fileapi-getdrivetypea)

반환값은 위와 같다. "c:\\"이 인자로 사용되었기 때문에 반환값(EAX)은 3인것을 확인할 수 있다.


4-4. 연산 코드

[이미지14] 연산 코드

다음 동작으로 레지스터 값을 연산하는 코드를 볼 수 있다. ESI값을 3번 inc, EAX값을 2번 dec하고 cmp한다. jmp의 경우 다음코드로 무조건분기하기 때문에 문맥상 의미가 없는 코드다.


4-5. 조건부 분기

[이미지15] 연산 전 레지스터

연산을 하기 전 레지스터의 값은 위와 같다. 위의 연산을 통해 EAX는 0x1, ESI는 0x401003이 된다. cmp를 통해 두 값을 비교하므로 ZF의 값은 0이 된다.

 

[이미지16] 조건부 분기

조건부 분기를 통해 동작을 다르게 한다. 앞선 cmp의 명령어 실행 이후 ZF의 값에 따라 분기 여부가 결정된다. ZF가 0이기 때문에 분기가 이루어지지 않는다.

 

[이미지17] MessageBoxA

eax, esi가 다를경우 (ZF가 0으로 세팅된 경우) MessageBoxA를 실행할 때 실패에 해당하는 메시지를 출력한다. 이후 jmp를 통해 종료한다(ExitProcess 호출). 패치를 하지 않고 실행을 하면 해당 부분이 실행되고 나서 프로세스를 종료한다.

 

[이미지18] MessageBoxA

eax, esi가 같을경우 (ZF가 1으로 세팅된 경우) MessageBoxA를 실행할 때 성공에 해당하는 메시지를 출력한다. 이후 ExitProcess를 통해 종료한다.


5.풀이

 

5-1. ZF 수정

[이미지19] 조건부 분기

실패와 성공의 분기가 결정되는 지점에 BP를 걸어준다.

 

[이미지20] EFLAGS

해당 지점에서 ZF값을 1로 수정해준다.

 

[이미지21] MessageBoxA

이후 je 조건부 분기가 이루어 지면서 성공에 해당하는 메시지가 출력된다.


5-2. 무조건 분기

[이미지22] 패치

je 조건부 분기를 jmp 무조건 분기로 명령어를 변경해준다.

 

[이미지23] 무조건 분기

실패의 부분을 건너띄고 성공으로 분기가 이루어진다.


5-3. nop 패치

[이미지24] 패치

실패 메시지 출력 이후 ExitProcess로 jmp하는 부분을 nop패치해준다.

 

[이미지25] nop

실패메시지 출력 이후 프로세스 종료가 이루어지지 않기 때문에 성공메시지도 출력된다.


5-4. EAX 값 수정

[이미지26] GetDriveTypeA

GetDriveTypeA를 실행한 후 반환값은 0x3이고 이는 EAX에 담긴다.

 

[이미지27] 연산 코드

이후의 연산을 생각해서 EAX를 무슨값으로 수정할지 파악한다. cmp를 실행하기 바로 직전의 지점에서의 ESI, EAX의 값을 맞춰줘야 한다. ESI의 값은 3번 증가하기 때문에 0x401003이 된다. EAX의 값은 2번 감소하기 때문에 GetDriveTypeA를 수행하고 나서의 지점에서 EAX의 값은 0x401005가 되야한다.

 

[이미지28] GetDriveTypeA 호출 후 레지스터

GetDriveTypeA의 함수가 실행되고 나서 EAX의 값을 0x401005로 수정해준다.

 

[이미지29] 연산 코드 후 레지스터

cmp eax, esi 명령어를 실행할 때 EAX의 값과 ESI값이 서로 같아지면서 ZF가 1로 세팅된다. 이후 성공으로 분기가 이루어진다.


6. ESI 값이 다르게 나타나는 이유

 

6-1. GetDriveTypeA 반환 값의 의미

[이미지30] abex' crackme 1

프로그램 실행 시 나타나는 메시지는 "Make me think your HD is a CD-Rom"이다. 리버싱을 통해 HD를 CD-Rom으로 인식하게끔 하라고 요구 하고 있다.

 

[이미지31] GetDriveTypeA

CD-Rom인지 판별하는데 있어 가장 중요한 역할을 하는 함수는 GetDriveTypeA라고 할 수 있다. 문제에서 가장 핵심적인 함수다.

 

[이미지32] GetDriveTypeA

함수의 정의를 보면 그 이유를 알 수 있다. GetDriveTypeA 함수는 디스크 드라이브의 유형이 무엇인지 판별하는 함수다. 따라서 CD-ROM 또한 해당 함수로 판별할 수 있다.

 

[이미지33] GetDriveTypeA 반환 값 (https://learn.microsoft.com/ko-kr/windows/win32/api/fileapi/nf-fileapi-getdrivetypea)

GetDriveTypeA 함수는 인자로 루트 디렉터리를 사용한다. 즉 해당 루트 디렉터리의 드라이브 유형을 판별하는 함수라는 것이다. CD-ROM일 경우 0x05를 반환한다.

 

[이미지34] CD-ROM (https://en.wikipedia.org/wiki/CD-ROM)

다시 문제의 의도를 파악해 보면 요구하고 있는 것은 "Make me think your HD is a CD-Rom"이다. 따라서 "GetDriveTypeA의 반환값을 0x05로 만들어라"가 된다. 이때 인자의 값(루트 디렉터리)을 바꿔도 반환값은 변하지 않는다. 반환값을 변경하는 방법은 크게 두가지가 있다. 첫번째로 실제 CD-ROM의 루트 디렉터리를 입력하는 방법이 있다. 해당 방법은 물리적인 CD-ROM Drive가 있어야 한다. 두번째로 더 쉬운 방법이 있는데 GetDriveTypeA 함수 호출 후 반환값(EAX)을 0x05로 변경하는 방법이다.

 

[이미지35] abex' crackme 1

문제의 의도대로 GetDriveTypeA의 반환값(EAX)을 0x05로 변경하면 성공 메시지가 나와야 한다. 하지만 실패했다는 메시지가 출력된다.

 

[이미지36] GetDriveTypaA

GetDriveTypeA 함수 호출 이후 간단한 연산을 통해 EAX와 ESI를 비교한다. 반환값(EAX)을 0x05로 수정해도 실패메시지가 나오는 이유는 연산을 진행할 때 ESI의 처음 값이 0x00401000이기 때문이다.

 

[이미지37] abex' crackme 1

따라서 GetDriveTypeA의 반환값(EAX)을 0x05(CD-Rom)가 아닌 0x401005(???)로 수정해야 성공 메시지가 출력된다는 결론이난다. 하지만 해당 반환값은 문제의 의도를 완전히 벗어난다. GetDriveTypeA에서 0x401005의 값을 가지는 반환값은 없기 때문이다. 다른 풀이를 살펴봐도 반환값을 0x401005로 수정해서 성공메시지를 출력했다.

 

[이미지38] 리버싱 핵심 원리

하지만 책에서의 경우 연산을 진행할 때 ESI값은 0x00으로 나타난다. 이때는 GetDriveTypeA의 반환값(EAX)을 0x05(CD-Rom)으로 변경하면 성공메시지가 출력된다. 문제의 의도와 완벽히 부합하다.

 

[이미지39] 리버싱 입문

다른 책의 예제를 살펴봐도 ESI의 값은 0x00으로 나타난다. 왜 이런 차이가 발생하는지 여러 테스트를 통해 파악해 봤다.


6-2. 디버거 변경

[이미지40] OllyDbg

책의 예제에서 모두 OllyDbg를 사용하고 있기 때문에 디버거를 변경해 본다. 버전은 책과 동일하게 2.01이다.

 

[이미지41] OllyDbg 2.01

OllyDbg 2.01버전에서 확인해 본 결과 ESI의 값이 여전히 0x00401000으로 나타난다.

 

[이미지42] x32dbg

x32dbg에서 디버그 엔진 설정을 변경해도 여전히 0x00401000으로 나타난다.

 

[이미지43] x32dbg 레지스터

OllyDbg, x32dbg의 다른 버전에서도 역시 동일했다. 디버거, 설정, 버전을 변경해도 ESI의 값은 같았다. 따라서 디버거의 문제가 아니라고 판단했다.


6-3. Zwcontinue

[이미지44] EntryPoint

디버거의 문제는 아니기 때문에 그 다음으로 해야 할 것은 ESI 값의 의미와 이 값이 언제 설정되는지를 확인하는 것이다. ESI의 값은 0x00401000이다.  이 값은 EntryPoint에 해당하고, EP 진입 할 때 부터 해당 값이 설정 된다는 것을 확인했다.

 

[이미지45] EntryPoint 레지스터

EntryPoint에서 레지스터 값을 확인해 보면 위와 같다. 동일하게 EntryPoint의 값으로 초기화 된 부분이 보인다. 따라서 EntryPoint 접근 이전에 시스템 코드에서 0x00401000(EntryPoint)로 레지스터 값을 초기화 하는 코드가 있을 거라고 추측했다.

 

[이미지46] ZwContinue

건너서 자동진행(Ctrl+f7)로 진행한 결과 EntryPoint 진입 이전 ZwContinue 함수가 호출된다는 것을 알 수 있다.

 

[이미지47] ZwContinue

EntryPoint 진입 이전 ZwContinue에 BP를 걸어보면 ESI값은 0x00이다.

 

[이미지48] EntryPoint

ZwContinue함수를 호출 하는 순간 바로 EP로 진입한다. 이때 ESI를 포함한 일부 레지스터의 값이 0x00401000으로 초기화 된다. ZwContinue을 호출할 때 건너서 단계진행(f8)을 하면 바로 EP로 진입한다.

 

NTSYSAPI NTSTATUS NTAPI ZwContinue	(	
_In_ PCONTEXT 	Context,
_In_ BOOLEAN 	TestAlert 
)

MSDN에서 공개되지 않은 함수이기 때문에 ReactOS에서 함수 원형을 찾았다(https://doxygen.reactos.org/d0/ddc/ndk_2kefuncs_8h.html#a1d5b7bfe04e8dc8d6eb64bb86ef5b725).

 

인자 의미
Context 0x19FD24 현재 스레드의 CONTEXT 포인터
TestAlert 0x01 현재 스레드 객체의 경고상태

ZwContinue함수는 상태 값에 따라 스레드 큐에 APC가 남아있는지를 확인한 뒤 APC를 처리하고 문맥을 이어서 진행하는 함수다. 여기서 사용되는 상태값의 인자는 TestAlert, 문맥의 포인터는 Context이다. TestAlert의 값이 세팅되어 있다면 APC가 남아있는지를 확인하는 작업을 거치고, 아니면 문맥을 이어서 진행한다.

 

[이미지49] Process initialization flow chart (https://malwaretech.com/2024/02/bypassing-edrs-with-edr-preload.html)

EP이전의 실행 흐름을 살펴보면 ZwContinue 함수 이후 RtlUserThreadStart 함수가 호출된다. 전체적인 흐름을 파악했으니 ESI가 언제 0x00401000(EntryPoint)로 초기화 되는지를 파악한다.

 

[이미지50] CONTEXT(0x93FD24) 참조 덤프

CONTEXT 의 인자 포인터를 따라가 보면 위와 같은 값들이 나타난다.

 

[이미지51] CONTEXT (x86 32-bit) (https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-context-r2)

MSDN에서 CONTEXT 구조체를 살펴보면 EIP의 위치를 알 수 있다.

 

[이미지52] CONTEXT (x86 32-bit) (https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-context-r2)

FLOATING_SAVE_AREA를 포함해서 EIP의 오프셋 위치를 구한다.

 

[이미지53] FLOATING_SAVE_AREA (https://www.nirsoft.net/kernel_struct/vista/FLOATING_SAVE_AREA.html)

FLOATING_SAVE_AREA의 크기를 구해보면 4byte(ULONG) * 8 + 1byte(UCHAR) * 80 = 112byte

 

[이미지54] CONTEXT (x86 32-bit) (https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-context-r2)

EIP 이전의 크기를 구해보면 4byte(DWORD) * 18 + 112byte(FLOATING_SAVE_AREA) = 184byte

 

[이미지55] CONTEXT EIP

다시 CONTEXT 포인터로 와서 EIP의 위치를 구한다. 시작 지점으로 부터 184byte(0xB8)의 오프셋에 EIP가 위치해 있다. EIP의 값은 0x775F9540이므로 Zwcontinue 함수호출 이후 해당 EIP로 이동하게 된다.

 

[이미지56] RtlUserThreadStart

디스어셈블러에서 따라가기를 클릭해 보면 RtlUserThreadStart 함수가 호출 된다는 사실을 알 수 있다. 저기에 BP를 걸어버리면 KiUserExceptionDispatcher으로 커널에서 예외가 발생한다.

 

[이미지57] BaseThreadInitThunk

따라서 다음 호출되는 함수인 BaseThreadInitThunk에 BP를 걸어준다. 해당 BP에서는 KiUserExceptionDispatcher이 발생하지 않았다.

 

[이미지58] BaseThreadInitThunk

BP가 발생하는 지점을 살펴보면 call esi 부분에서 EP로 넘어간다.

 

[이미지59] BaseThreadInitThunk 호출 전 레지스터

BaseThreadInitThunk 호출 전 레지스터를 보면 ESI의 값이 0x766B7B90으로 나타난다.

 

[이미지60] BaseThreadInitThunk 호출 전 레지스터

이후 BaseThreadInitThunk를 호출하게 되면 ESI의 값이 0x00401000으로 초기화 된다.

 

[이미지61] Process initialization flow chart (https://malwaretech.com/2024/02/bypassing-edrs-with-edr-preload.html)

ESI의 값이 언제 초기화 되는지를 파악했다. BaseThreadInitThunk를 호출하고 EP로 진입하기 전 초기화가 이루어진다.

 

[이미지62] BaseThreadInitThunk

BaseThreadInitThunk 함수 내부로 들어가 보면 정확하게 저 지점에서 ESI 값이 초기화 된다. 이후 call esi로 EP에 진입한다.


6-4. Window XP

[이미지63] ntdll.7C93E8E6

운영체제 버전에 따라 값이 어떻게 나타나는지 알기 위해 XP에서 분석을 다시 진행했다. Window11에서와 달리 ESI의 값은 ntdll.7C93E8E6 함수를 호출할 때 초기화된다.

 

[이미지64] ntdll.7C93E8E6

함수 내부로 들어가 보면 pop esi 코드를 볼 수 있는데 정확히 저 부분에서 ESI값이 초기화 된다.

 

[이미지65] ntdll.7C93E8E6

여러번 해본 결과 해당 코드에서 EntryPoint에 접근했을 때의 ESI의 값이 결정된다. 프로그램을 재시작 할 때마다 값이 변하는 경우도 있었지만(이유는 알 수 없다) 대부분 0x00으로 나타난다.

 

[이미지66] ZwContinue

이후 Zwcontinue 함수가 호출된다.

 

[이미지67] sysenter

Zwcontinue 함수 내부로 들어가 보면 sysenter 코드가 실행된다. 내부적으로 시스템 코드가 실행 된 이후 EntryPoint에 접근한다.

 

[이미지68] EntryPoint

EntryPoint에서 레지스터를 확인해 보면 [이미지65]에서 pop esi를 한 값이 그대로 나타난다. 따라서 EntryPoint 접근 전 ESI값을 초기화 하는 루틴이 없다는걸 알 수 있다(Window11과의 차이점). XP에서 분석한 결과 ESI값이 0x00으로 나타나면서 책에서 본 값과 일치했다. 결론으로 ESI값이 달라지는 이유는 운영체제 버전이 다르기 때문이다. 더 정확히 말하자면 ESI를 초기화 하는 루틴이 존재하는지의 차이다.

 

[이미지63] 모듈간 호출

XP에서는 BaseThreadInitThunk 함수를 찾을 수 없다. 그렇기 때문에 EntryPoint에 접근하는 방식이 다르다고 할 수 있다. XP의 경우 sysenter을 하기 때문에 접근 과정을 정확하게 알 수는 없었지만 Window11의 경우 EntryPoint 접근 이전에 ESI에 EntryPoint의 값을 담아서 call esi를 하는 방식으로 이동한다. 버전에 따라 접근방식이 다르기 때문에 이런 차이가 발생하는 것이다.


 

'Reversing > abex' crackme' 카테고리의 다른 글

abex' crackme 5 상세분석  (3) 2025.06.23
abex' crackme 4 상세분석  (0) 2025.06.22
abex' crackme 3 상세분석  (1) 2025.06.21
abex' crackme 2 상세분석  (0) 2025.06.17