| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- 실행파일
- Python
- RVA
- 코드엔진
- 리버싱
- x32
- x64
- Calling Convention
- 리치헤더
- Programmers
- Dos Stub
- 32bit
- 함수 호출 규약
- Image dos header
- Rich Header
- __fastcall
- image section header
- CodeEngn
- pe format
- 프로그래머스
- __cdecl
- 파이썬
- rev
- 크랙미
- Reversing
- ABI
- __stdcall
- stack frame
- crackme
- __vectorcall
- Today
- Total
kj0on
abex' crackme 1 상세분석 본문
0. 실행환경
0-1. 운영체제 (2)


0-2. 툴 (3)



1. 파일

2. 프로그램 동작


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

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

x32dbg에서 프로그램을 실행했다. 코드의 길이가 매우 짧다.
4-2. 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).

매개변수의 의미는 위와 같다.
| 매개변수 | 값 | 의미 |
| hWnd | 0x0 | 메시지 상자에 소유자 창이 없음 |
| IpText | 0x402012 | 표시할 메시지의 텍스트 데이터 주소는 0x402012 |
| IpCaption | 0x402000 | 대화 상자 제목의 텍스트 데이터 주소는 0x402000 |
| uType | 0x0 | 메시지 상자의 표시 단추의 플래그는 0x0 |
abex' crackme1.exe에서 MessageBoxA에 사용되는 인자의 값과 의미를 매칭하면 위와 같다.

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

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

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

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

반환값은 위와 같다. 확인버튼 하나만 있기 때문에 버튼을 누를 시 1이 반환된다.
4-3. 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).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

cmp eax, esi 명령어를 실행할 때 EAX의 값과 ESI값이 서로 같아지면서 ZF가 1로 세팅된다. 이후 성공으로 분기가 이루어진다.
6. ESI 값이 다르게 나타나는 이유
6-1. GetDriveTypeA 반환 값의 의미

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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가 남아있는지를 확인하는 작업을 거치고, 아니면 문맥을 이어서 진행한다.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

이후 Zwcontinue 함수가 호출된다.

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

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

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 |