kj0on

abex' crackme 4 상세분석 본문

Reversing/abex' crackme

abex' crackme 4 상세분석

kj0on 2025. 6. 22. 16:09
목차 접기

0. 실행환경

 

0-1. 운영체제 (1)

Window 11 Home


0-2. 툴 (3)

x32dbg
Detect it easy
Cff Explorer


1. 파일

abex' crackme4.exe
0.02MB
https://binvis.io/

 


2. 프로그램 동작

[이미지1] abex' crackme 4

프로그램 실행 시 Serial을 입력하는 GUI를 볼 수 있다. 하지만 Registered 버튼이 동작하지 않기 때문에 다른 동작은 할 수 없다.


3. Detect It Easy

[이미지2] DIE 실행 결과

32비트 실행파일, Microsoft Visual Basic으로 작성된 프로그램임을 알 수 있다.


4. 분석


4-1. EntryPoint

[이미지3] EntryPoint

EntryPoint로 이동해 보면 abex' crackme 2와 마찬가지로 Visual Basic의 시작 부분인 ThunRTMain코드를 볼 수있다. 
 

[이미지4] RT_MainStruct

ThunRTMain의 인자를 살펴보면 RT_MainStrut를 확인할 수 있다. Visual Basic 엔진 모두를 분석하기에는 무리가 있기 때문에 분석이 필요한 지점으로 이동한다.


4-2. 문자열 참조

[이미지5] abex' crackme 3

프로그램 실행 시 힌트를 얻을 수 있는 문자열은 총 3가지이다.
 

[이미지6] 문자열 참조

EP에서 문자열을 참조했을 때 나타나는 문자는 위와 같다. rtcMsgBox의 경우 문자열이 그대로 나타나지만 오브젝트에 표시된 문자열은 문자열 참조로는 알 수 없다(abex' crackme 2도 마찬가지).
 

[이미지7] Section Header

CFF Explorer에서 프로그램을 열면 해당 프로그램의 PE 구조를 확인할 수 있다. Section Header의 .text섹션에서 "Serial:"을 검색한 결과 프로그램 실행 시 나타나는 문자열을 확인할 수 있다.
 

[이미지8] ImagaBase

Optional Header에서 ImageBase(0x00400000)를 확인한다.
 

[이미지9] Section Header

Section Header의 Virtual Address(0x00001000)와 "&Registered"의 문자열 시작 위치(0x00000332)를 확인해 준다. 세 값을 더해주면 VA를 알 수 있다. VA의 값은 0x00401332이다.
 

[이미지10] 주소 참조

 x32dbg에서 해당 VA위치로 이동한다.
 

[이미지11] "&Registered" VA

위치로 이동해 보면 해당 값을 찾을 수 있다. 위와 같이 나타나는 이유는 x32dbg에서 해당 Hex값을 어셈블리 코드로 해석했기 때문이다.
 

[이미지12] 분석

해당 영역을 ASCII로 분석해준다.
 

[이미지13] 분석 크기

분석 할 크기를 입력해 준다, "&Registered"만 분석할거라 크기는 0xB로 했다.
 

[이미지14] "&Registered" VA

문자열이 ASCII로 해석되면서 "&Registered"가 보인다. 제일 앞의 '&'는 주소가 아니라 밑줄을 표현하는 문자인듯하다.
 

[이미지15] "PatchButton"

문자열을 수정해준다.
 

[이미지16] abex' crackme 4

이후 프로그램을 실행해 보면 문자열패치가 잘 된 것을 확인할 수 있다. 다른 문자열 부분도 동일하게 수정이 가능하다. 하지만 오브젝트에서 해당 문자열을 가져와서 표현되는 부분은 아직 알 수 없다. 해당 영역을 알기 위해서는 메인 윈도우(GUI)의 시작함수, 오브젝트 이벤트연결, 부착 등등의 코드를 분석해야 하는데 어셈블리 코드로 분석하기에는 상당히 어렵고 양이 많다.
 

[이미지17] Object Hex

그리고 가장 큰 문제는 오브젝트에서 사용하는 여러 포인터를 알기 위해서는 표현 방식(값이 무엇을 의미하는지)을 알아야 하는데 이거를 Hex값으로 봐야한다. 위의 이미지 처럼 값들이 나타나는데 해당 값이 어떻게 쓰이는지, 그리고 어떻게 해석되는지 등등을 파악해야 한다. abex' crackme2에서 오브젝트 부분은 상세분석을 못했는데 그 이유이기도 하다. 시간을 쏟아 부으면 분석이 가능하겠지만 엄청 오래 걸릴것으로 예상된다. 그래서 디컴파일로 코드를 보면 좀 더 의미 파악이 쉬워질거라 예상했지만....
 

[이미지17] VB Decompiler (https://www.vb-decompiler.org/products.htm)

가격이 상당하다.... 무료버전에서는 어셈코드만 볼 수 있다(x32dbg와 유사함). 가격을 확인하고 문자열 참조 말고 다른 방법으로 분석을 진행했다.


4-3. Break Point

[이미지18] 모듈 간 호출

EntryPoint에서 모듈 간 호출을 살펴보면 프로그램에서 사용하는 여러 함수들을 확인할 수 있다. 몇 가지 의미있는 함수들(vbaStrCmp, rtcMsgBox)이 보이지만 상세분석을 위해 특정 함수만 지정해서 분석을 진행하지 않았다.
 

[이미지19] 모듈 간 호출

EP를 제외하고 함수 전체에 BP를 걸어줘서 처음 지점 부터 하나씩 분석을 진행했다.


5. 전체 함수

 

5-1. rtcGetPresentDate


5-1-1. 코드

[이미지20] rtcGetPresentDate


5-1-2. 인자 (EDX)

[이미지21] EDX(0x19FAA0) 참조 덤프

EDX(0x19FAA0)을 참조해 보면 빈 공간이 나타난다(타입필드가 0x00).
 

[이미지22] 초기화

ESI는 0x00이므로(xor esi, esi) 해당 스택영역이 0x00으로 초기화된다.
 

[이미지23] 초기화

이후 call ebx를 통해 rtcGetPresentDate를 호출한다.


5-1-3. 동작 (return EDX)

[이미지21] EDX(0x19FAA0) 참조 덤프

 
호출 이후 EDX(0x19FAA0)을 보면 값이 변경된 것을 알 수 있다. 타입필드의 값 0x07은 VT_DATE를 의미하고 이는 데이터 필드의 값이 OLE Automation Date(8byte)으로 해석된다. (OLE Automation Date에 대한 자세한 설명은 https://kj0on.tistory.com/6 참고)

[이미지22] rtcGetPresentDate 호출 후 레지스터


5-1-4. 결과

rtcGetPresentDate(arg1); // arg1==EDX

OLE Automation Date의 결과를 arg1에 저장한다.


5-2. rtcGetHourOfDay


5-2-1.코드

[이미지23] rtcGetHourOfDay


5-2-2. 인자 (EAX, ECX)

[이미지24] EAX(0x19FAA0) 참조 덤프

EAX에는 앞서 rtcGetPresentDate에서 저장된 OLE Automation Date 값이 존재한다.

[이미지25] ECX(0x19FA90) 참조 덤프

ECX에는 빈 공간이 나타난다.


5-2-3. 동작 (return ECX)

[이미지26] ECX(0x19FA90) 참조 덤프

함수 호출 후 ECX를 살펴보면 0x0D의 값이 나타난다. 함수명이 rtcGetHourOfDay이라는 점, 0x0D는 10진수로 13이라는 점을 봤을 때 해당 값은 현재 시간을 의미한다는 것을 추측할 수 있다.
 

[이미지27] 날짜 및 시간 조정

날짜 및 시간조정을 통해 현재 시간을 가져오는 것이 맞는지 확인한다. 한가지 불명확한 점이 있는데 시간을 가져올 때 현재 시간을 가져오는지, rtcGetPresentDate를 호출했을 때의 저장된 시간을 가져오는지 알 수 없다. 따라서 rtcGetPresentDate을 호출하기 전 시간은 14:00로 설정한다. rtcGetPresentDate를 호출 하고  rtcGetHourOfDay를 호출하기 전에는 시간을 15:00으로 설정한다. rtcGetHourOfDay의 호출 결과 값이 0x0E로 나타나면 함수는 rtcGetPresentDate을 호출했을 때의  OLE Automation Date의 시간을 가져온다는 것을 의미한다. rtcGetHourOfDay의 호출 결과 값이 0x0F로 나타나면 함수는 현재 시간을 가져온다는 것을 의미한다.
 

[이미지26] ECX(0x19FA90) 참조 덤프

호출 후 값을 살펴보면 0x0E가 나타난다는 것을 알 수 있다. 따라서 rtcGetHourOfDay 함수는 rtcGetPresentDate을 호출했을 때의 OLE Automation Date의 시간을 가져오는 함수란 것을 알 수 있다.


5-2-4. 결과

rtcGetHourOfDay(arg1, arg2); // arg1==ECX, arg2==EAX

arg2에 있는 OLE Automation Date를 해석해서 시간에 해당되는 값을 arg1에 저장한다. 이때, 저장되는 값은 부동소수점으로 표현되지 않고 정수값으로 표현된다.


[이미지27] rtcGetPresentDate

이후의 코드를 살펴보면 rtcGetPresentDate 함수가 호출되는 것을 알 수 있다. 해당 함수는 위에서 분석했기 때문에 자세한 설명은 생략한다.
 

[이미지28] EDX(0x19FA60) 참조 덤프

함수 호출 후 현재 시간이 OLE Automation Date 형태로 저장된다.


5-3. rtcGetYear


5-3-1.코드

[이미지29] rtcGetYear


5-3-2. 인자 (EAX, ECX)

[이미지30] EAX(0x19FA60) 참조 덤프

EAX에는 rtcGetPresentDate 호출 결과가 저장되어 있다.
 

[이미지31] EAX(0x19FA60) 참조 덤프

ECX에는 빈 공간이 나타난다.


5-3-3. 동작 (return ECX)

[이미지32] ECX(0x19FA50) 참조 덤프

함수 호출 결과 ECX에 0x07E9값이 쓰여진다. 해당 값을 10진수로 바꿔보면 2025다. 함수명과 값으로 봤을 때 현재 년도의 값으로 추측된다.
 

[이미지32] ECX(0x19FA50) 참조 덤프

rtcGetHourOfDay에서와 마찬가지로 날짜 및 시간 조정을 통해 좀 더 명확하게 분석했다. 나타나는 값으로 해당 함수는 rtcGetPresentDate을 호출했을 때의 OLE Automation Date의 년도를 가져오는 함수란 것을 알 수 있다.


5-3-4. 결과

rtcGetYear(arg1, arg2); // arg1==ECX, arg2==EAX

arg2에 있는 OLE Automation Date를 해석해서 년도에 해당되는 값을 arg1에 저장한다. 이때, 저장되는 값은 부동소수점으로 표현되지 않고 정수값으로 표현된다.


5-4. __vbaVarMul


5-4-1.코드

[이미지33] __vbaVarMul


5-4-2. 인자 (EAX, ECX, EDX)

[이미지34] EAX(0x19FA30) 참조 덤프

EAX의 값을 살펴보면 0x05의 값이 저장되어있다.
 

[이미지35] mov 0x5

해당 값은 이전 코드에서 mov의 실행결과이다.

[이미지36] ECX(0x19FA80) 참조 덤프

ECX에는 빈 공간이 나타난다.

[이미지37] EDX(0x19FA90) 참조 덤프

EDX에는 0x0F의 값이 저장되어 있는데 이는 rtcGetHourOfDay의 호출 결과다.


5-4-3. 동작 (return ECX)

[이미지38] ECX(0x19FA80) 참조 덤프

함수 호출 후 값을 살펴보면 0x4B가 저장되어 있다. 해당 값은 0x0F * 0x05의 연산 결과다.


5-4-4. 결과

__vbaVarMul(arg1, arg2, arg3); // arg1==ECX, arg2==EAX, arg1==EDX

arg2와 arg3를 곱한 결과를 arg1에 저장한다.


5-5. __vbaVarAdd


5-5-1.코드

[이미지39] __vbaVarAdd


5-5-2. 인자 (EAX, EDX)

[이미지40] __vbaVarAdd

push eax를 2번사용하므로 첫번째 eax, 두번째 eax를 나눠서 살펴본다.
 

[이미지41] EAX(0x19FA80) 참조 덤프

첫번째 EAX 값에는  __vbaVarMul의 호출 결과가 저장되어 있다.
 

[이미지42] EDX(0x19FA20) 참조 덤프

EDX 값에는 0x03E8의 값이 저장되어 있다.
 

[이미지43] mov 0x03E8

해당 값은 이전 코드에서 mov의 실행결과이다.
 

[이미지44] EAX(0x19FA70) 참조 덤프

두번째 EAX 값에는 빈 공간이 나타난다.


5-5-3. 동작 (return EAX)

[이미지45] EAX(0x19FA70) 참조 덤프

호출 후 0x0433값이 쓰여진다. 해당 값은 0x03E8 + 0x4B의 결과다.


5-5-4. 결과

__vbaVarAdd(arg1, arg2, arg3); // arg1==EAX(s), arg2==EDX, arg3==EAX(f)

arg2와 arg3의 덧셈 결과를 arg1에 저장한다.


[이미지46] __vbaVarMul

이후의 코드를 살펴보면 __vbaVarMul 함수가 호출되는 것을 알 수 있다. 해당 함수는 위에서 분석했기 때문에 자세한 설명은 생략한다.
 

[이미지47] __vbaVarMul 인자

인자 값을 살펴보면 위와 같다.
 

[이미지48] EDX(0x19FA40) 참조 결과

EDX의 값을 살펴보면 0x0433 * 0x07E9의 결과 값이 저장된다.


5-6. __vbaStrVarMove


5-6-1.코드

[이미지49] __vbaStrVarMove


5-6-2. 인자 (EAX)

[이미지50] EAX(0x18FA40) 참조 덤프

EAX에는 __vbaVarMul 호출 결과값이 저장되어 있다.


5-6-3. 동작 (return pointer)

[이미지51] EAX(0x18FA40) 참조 덤프

함수 호출 결과 기존에 저장되어 있는 값이 지워진다.
 

[이미지52] __vbaStrVarMove 호출 후 레지스터

0x00629C6C의 값이 반환된다.
 

[이미지53] 0x00629C6C 참조 덤프

메모리 맵에서 따라가 본 결과 0x00629C6C는 Heap영역이다 따라서 동적 할당된 공간임을 의미한다. 해당 위치를 참조해보면 "2197125"의 문자열이 나타난다는 것을 알 수있다. 해당 값은 초기 EAX에 있는 __vbaVarMul 호출 결과값이다. 원래의 값은 0x218685이다. 해당 값을 10진수로 변환해 보면 2197125다. (해당 주소를 참조하지 못하는 경우도 있는데 자세한 설명은 https://kj0on.tistory.com/7 참고)


5-6-4. 결과

__vbaStrVarMove(arg1) // arg1==EAX

arg1의 값을 10진수로 변환한 뒤 동적할당한 공간에 문자열로 저장한다.


5-7. __vbaStrMove


5-7-1. 코드

[이미지54] __vbaStrMove


5-7-2. 인자(ECX, EDX)

[이미지56] EDX(0x629C6C) 참조 덤프

EDX는 0x00629C6C로 해당 주소를 참조해 보면 __vbaStrVarMove 함수 호출 결과가 저장되어 있다.

[이미지57] ECX(0x19FAB0) 참조 덤프

EDX에는 빈공간이 나타난다.


5-7-3. 동작 (return)

[이미지58] ECX(0x19FAB0) 참조 덤프

ECX에 문자열 "2197125"가 존재하는 동적할당된 공간의 주소(0x00629C6C)를 저장한다.


5-7-4. 결과

__vbaStrMove(arg1, arg2); // arg1==ECX, arg2==EDX

arg2의 값을 arg1로 이동한다.


5-8. __vbaStrCopy


5-8-1. 코드

[이미지59] __vbaStrCopy


5-8-2. 인자(ECX, EDX)

[이미지60] ECX(0x6284E4) 참조 덤프

ECX는 동적할당한 공간이다. 쓰레기 값이 존재한다.
 

[이미지61] EDX(0x629C6C) 참조 덤프

EDX에는 __vbaStrMove 호출 결과 값이 있다.


5-8-3. 동작 (return ECX)

[이미지62] ECX(0x6284E4) 참조 덤프

ECX에 새로 동적할당한 공간의 주소가 저장된다.
 

[이미지63] 0x629CA4 참조 덤프

해당 주소를 참조해 보면 EDX의 값이 그대로 복사된 것을 알 수있다.


5-8-4. 결과

__vbaStrCopy(arg1, arg2); // arg1==ECX, arg2==EDX

새로운 공간을 동적할당 후 arg1에 해당 주소를 저장한다. 그 후 해당 주소 영역에 arg2의 값을 저장한다.


5-9. __vbaFreeStr


5-9-1. 코드

[이미지64] __vbaFreeStr


5-9-2. 인자(ECX)

[이미지65] ECX(0x19FAB0) 참조 덤프

ECX에는 이전 __vbaStrMove의 호출 결과가 남아있다.


5-9-3. 동작 (return 0x0)

[이미지66] ECX(0x19FAB0) 참조 덤프

해당 영역을 지운다.


5-9-4. 결과

__vbaStrMove(arg1); // arg1==ECX

arg1의 자원을 해제한다(0x00).


[이미지67] __vbaFreeVarList

__vbaFreeVarList를 호출하기 전 인자를 중복해서 push하기 때문에 각각 push 하는 시점에서 값을 파악해 줬다.

[이미지68] rtcGetPresentDate

처음 지점에서 esi로 0x0으로 초기화 하는 값들을 기준으로 무슨 값들을 초기화 하고, 초기화 하지 않는지 구분해 봤다.
 

[이미지69] __vbaFreeVarList 호출 결과

__vbaFreeVarList을 호출한 결과는 위와 같다. 영역이 갖는 의미는 마지막으로 사용된 것을 기준으로 했다. 초기화하는 영역과 그 이유는 파악하지 못했다. 이후 남아있는 값들은 덮어씌워져서 사용되기 때문에 __vbaFreeVarList 함수의 의미는 파악하지 못했다.


[이미지68] abex' crackme 4

자원 해제 이후 코드를 쭉 진행하다 보면 Main GUI가 실행된다.
 

[이미지69] abex' crackme 4

이후 "Serial:" 에 값을 입력하는 순간 동작을 멈춘다.
 

[이미지70] __vbaObjSet

BP를 보면 __vbaObjSet과 __vbaHresultCheckobj 함수가 보인다. 해당 함수는 오브젝트 관련 함수와 오브젝트 에러처리 함수 이므로 자세한 분석은 이후에 진행했다.


5-10. __vbaStrCmp


5-10-1. 코드

[이미지71] __vbaStrCmp


5-10-2. 인자(EAX, ECX)

[이미지72] EAX(0x5ABCD4) 참조 덤프

EAX에는 사용자가 "Serial:"에 입력한 값이 들어가 있다. 해당 주소는 Heap 영역이다.

[이미지73] ECX(0x5A9CE4) 참조 덤프

ECX에는 이전 연산으로 생성된 값이 들어가 있다. 해당 주소는 Heap 영역이다.


5-10-3. 동작 (return)

[이미지74] __vbaStrMove 호출 후 레지스터

두 동적할당 영역에 있는 값을 비교해서 값을 반환한다. 값이 같지 않을 경우 0xFFFFFFFF를 반환한다.
 

[이미지75] __vbaStrMove 호출 후 레지스터

값이 같을 경우 0x0을 반환한다.


5-10-4. 결과

__vbaStrMove(arg1, arg2); // arg1==ECX, arg2==EAX

arg1과 arg2를 비교한다. 두 값이 같을 경우 0x0, 다를 경우 0xFFFFFFFF를 반환한다.


5-11. 전체 동작

[이미지76] 전체 동작


6. 버튼활성화 부분


6-1. VB.TextBox 오브젝트

[이미지77] __vbaObjSet

버튼 활성화 부분을 찾기 위해 "Serial:"에 값을 입력했을 때 BP가 발생하는 지점부터 다시 분석을 진행했다. 이전에 분석한 __vbaStrCmp 부분은 생략한다.
 

[이미지78] abex' crackme 4

"Serial:"에 "Findme"를 입력해서 해당 문자열이 나타난 위치를 파악할 수 있게 했다.

[이미지79] 0x2C22BF4 참조 덤프

__vbaObjSet을 호출하고 나면 0x2C22BF4 값이 반환된다. 해당 값은 오브젝트의 위치를 나타낸다. "Serial:"에 값을 입력할 때 마다 BP가 발생하는 지점에서 값을 확인해 보면 반환값은 변하지 않는것을 확인할 수 있다. 다만 프로그램 재시작 시 __vbaObjSet 반환값, 사용자입력값, 연산결과값의 위치는 변한다.
 

[이미지80] 0x2C22BF4 참조 덤프

오브젝트의 주소는 주로 0x02로 시작한다는 패턴을 파악했다. 해당 값들을 참조해서 의미있는 값을 찾아야 한다. 오브젝트의 경우 값의 패턴, ASCII 텍스트로만 분석해서 매우 주관적이다. 따라서 밑의 결과 또한 확실하지 않을 확률이 높다. 양이 너무 방대해서 상세분석에는 무리가 있었다.
 

[이미지81] 0xAB패딩

오브젝트의 끝을 나타내는 부분은 위와 같은 패턴으로 나타난다. 0xAB패딩으로 오브젝트의 끝을 파악했다.
 

[이미지82] 오브젝트 참조 결과

Hex값은 봐도 무슨 뜻인지 모르기 때문에 우선 ASCII문자가 나타나는 것들 위주로 파악했다. 이 중 버튼과 관련이 된 오브젝트는 없어보인다.
 

[이미지83] 3번째 오브젝트

VBDisabled라는 텍스트를 찾았다. 하지만 Disabled의 설정 값과 설정 값 위치는 모르기 때문에 Disabled 되는지 확인할 수는 없었다. 오브젝트 양이 방대해서 앞서 입력한 "Findme"를 참조하는 부분은 찾을 수 없었다.


6-2. VB.CommandButton 오브젝트

[이미지84] abex' crackme 4

버튼 활성화가 언제 이루어 지는지 파악하기 위해서 이번에는 앞서 연산된 값을 넣고 분석을 진행했다.
 

[이미지85] call dword ptr ds:[ecx+8C]

해당 함수를 호출하는 순간 버튼이 활성화 된다는 사실을 알 수 있었다. 한가지 확인해 봐야 할 점은 해당 함수를 호출 하기 전 __vbaObjSet에서 지정하는 오브젝트와 __vbaStrCmp를 호출하기 전 __vbaObjSet 오브젝트가 같은지 확인해야한다(프로그램을 재시작을 하지 않는 이상 오브젝트의 위치는 같다는 것은 확인했다). 만약 오브젝트가 같다면 위에서 VB.TextBox 오브젝트 분석결과가 틀렸다는 것을 의미한다. 이 경우 앞에서도 버튼 오브젝트가 있었는데 이 부분을 놓쳤다는 뜻이다. 반면 선택하는 오브젝트가 다를 경우 앞에서 분석한 오브젝트는 VB.TextBox가 맞으며 뒤에서 선택한 오브젝트는 VB.CommandButton일 가능성이 높다. 그렇기 때문에 앞의 분석에서 VB.CommandButton 부분을 찾을 수 없었던 것이다. 정리해서 최종적으로 파악해야 할 점은 "__vbaObjSet을 호출할 때 Main Object에서 VB.TextBox를 참조하는지, 아니면 바로 VB.TextBox를 특정하는지" 라는 것이다.
 

[이미지86] __vbaObjSet

__vbaStrCmp 이전 __vbaObjSet의 반환값은 0x02D32BF4이다.
 

[이미지87] __vbaObjSet

버튼을 활성화 하기 이전 __vbaObjSet의 반환값은 0x02D32904이다. 값이 달라지는 것으로 봐서 __vbaObjSet을 호출할 때 바로 오브젝트를 특정 하는것으로 보인다. 
 

[이미지88] 오브젝트 참조 결과

해당 오브젝트를 참조해서 위와 같은 방식으로 분석을 진행했다. 그 결과 VB.CommandButton과 관련된 문자열을 확인할 수 있었다.
 

[이미지89] 5번째 오브젝트

프로그램 실행 시 버튼에 나타나는 문자열이다.

[이미지90] 2번째 오브젝트

이전의 VB.TextBox와 달리 이번 오브젝트 에는 VB.CommandButton을 확인할 수 있다.
 

[이미지90] 4번째 오브젝트

VBDisabled 문자열을 확인할 수 있다. 하지만 여전히 설정값과 위치는 파악할 수 없다.


6-3. Enabled

[이미지91] call dword ptr ds:[ecx+8c]

버튼을 활성화 하는 함수에서 설정값을 불러오는 곳을 파악했다.
 

[이미지92] Enabled

코드를 쭉 진행하다 보면 Enabled를 불러오는 부분을 찾을 수 있다.
 

[이미지93] 레지스터

해당 부분을 참조해 본다.
 

[이미지94] 0x52951D90 참조 덤프

이후 해당 값을 0x46과 cmp한 뒤 조건부 분기한다.
 

[이미지95] 0x52951D90 참조 덤프

해당 주소는 프로그램을 재시작해도 동일하다. 0x09를 다른값으로 변경한 뒤 동작을 살펴봤다. 이때, "Serial:" 입력을 하지않고 처음 실행됐을 때의 상태만 봤다.
 

[이미지96] 'Enabled' cannot be set while loading or saving

0x00일 경우 "'Enabled' cannot be set while loading or saving"의 에러메시지를 출력한다.
 

[이미지97] CommandButton Disabled

0x09일 경우 버튼이 활성화 되지 않고 실행된다.
 

[이미지98] CommandButton Enabled

0x01, 0x02, 0x03, 0x04, 0x0B, 0x0E, 0x0F일 경우  버튼이 활성화 되고 실행된다.
 

[이미지99] CommandButton Invisibled

0x05, 0x06, 0x07, 0x08 일 경우  버튼이 보이지 않는다.
 

[이미지99] KiUserExceptionDispatcher

0x0C일 경우 KiUserExceptionDispatcher 예외처리가 발생하면서 프로그램이 동작하지 않는다.
 

[이미지100] Out of memory

0x0D일 경우 "Out of memory"의 에러메시지를 출력한다.
 

형식 코드 의미 실행 결과
0x00 VT_EMPTY Not specified. 'Enabled' cannot be set while loading or saving
0x01 VT_NULL Null. CommandButton Enabled
0x02 VT_I2 A 2-byte integer. CommandButton Enabled
0x03 VT_I4 A 4-byte integer. CommandButton Enabled
0x04 VT_R4 A 4-byte real. CommandButton Enabled
0x05 VT_R8 An 8-byte real. CommandButton Invisibled
0x06 VT_CY Currency. CommandButton Invisibled
0x07 VT_DATE A date. CommandButton Invisibled
0x08 VT_BSTR A string. CommandButton Invisibled
0x09 VT_DISPATCH An IDispatch pointer. CommandButton Disabled
0x0A VT_ERROR An SCODE value. CommandButton Enabled
0x0B VT_BOOL A Boolean value. True is -1 and false is 0. CommandButton Enabled
0x0C VT_VARIANT A variant pointer. KiUserExceptionDispatcher
0x0D VT_UNKNOWN An IUnknown pointer. Out of memory
0x0E VT_DECIMAL A 16-byte fixed-pointer value. CommandButton Enabled
0x0F     CommandButton Enabled

나타나는 오류를 종합해 봤을 때 해당 값은 VARIANT.vt 로서 속성 데이터의 형식으로 보인다.


6-4. EnableWindow

[이미지101] call dword ptr ds:[ecx+8C]

버튼이 활성화 되는 함수로 다시 돌아와서 동작을 살펴본다.
 

[이미지102] EnableWindow

정확한 Serial 값을 입력했을 경우 call dword ptr ds:[ecx+8C] 내부 코드를 따라가다 보면 최종적으로 EnableWindow 함수를 통해 버튼이 활성화 된다.
 

[이미지103] EnableWindow

함수 호출 이전의 코드를 살펴보면 총 4가지의 조건부 분기를 거쳐 버튼 활성화가 결정된다.
 

[이미지104] EnableWindow

우선 첫번째 분기가 어떻게 발생하는지 파악했다.
 

[이미지105] EnableWindow

ZF는 해당 부분에서 설정한다. ds:[ebx+3C]와 0x00을 cmp해주기 때문에 ds:[ebx+3C]에 어떤 값이 있는지 알아내야한다. 버튼이 활성화 될 때와 비활성화 될 때의 동작을 비교해서 분석을 진행했다.
 

[이미지106] ebx+3C 참조 덤프

프로그램 시작 시 해당 위치의 값을 살펴보면 0x000000이다. 따라서 위의 cmp에서 ZF가 1로 세팅되고 조건부 분기가 발생하면서 EnableWindow에 도달하지 못한다.
 

[이미지107] ebx+3C 참조 덤프

정확한 시리얼 값을 입력했을 때, 해당 코드에서 값을 살펴보면 0x1F0A8A이다. 따라서 위의 cmp에서 ZF가 0으로 세팅되고 조건부 분기가 발생하지 않으면서 다음 조건부 분기 코드로 넘어간다. 이후 프로그램 실행 후 해당값이 언제 쓰여지는지 확인했다.
 

[이미지108] msvbvm60.5296894E

해당 함수를 호출할 때 값이 변경되는 것을 확인했다. 함수 내부로 들어가서 좀 더 정확한 위치를 특정했다. 함수를 호출했을 때의 시점은 abex' crackme 4의 GUI가 나타나기 전이다.
 

[이미지109] CreateWindowExA

함수 내부 코드에서 CreateWindowExA를 호출했을 때 ds:[ebx+3C]에 값이 저장된다.
 

HWND CreateWindowExA(
  [in]           DWORD     dwExStyle,
  [in, optional] LPCSTR    lpClassName,
  [in, optional] LPCSTR    lpWindowName,
  [in]           DWORD     dwStyle,
  [in]           int       X,
  [in]           int       Y,
  [in]           int       nWidth,
  [in]           int       nHeight,
  [in, optional] HWND      hWndParent,
  [in, optional] HMENU     hMenu,
  [in, optional] HINSTANCE hInstance,
  [in, optional] LPVOID    lpParam
);

CreateWindowExA의 함수원형은 위와 같다.
 

인자 의미
dwExStyle 0x04 자식이 마우스 버튼 등을 받을 때 부모에게 WM_PARENTNOTIFY 를 보내지 않음
lpClassName 0xC2E5 VB6 런타임이 RegisterClassA로 등록해 둔 커맨드 버튼 전용 클래스
lpWindowName 0x02C32A28 버튼에 표시될 문자열 포인터 "&Registered"
dwStyle 0x4C012000 스타일 플래그
X 0X08 버튼 X 좌표
Y 0X40 버튼 Y 좌표
nWidth 0X109 버튼 너비
nHeight 0X19 버튼 높이
hWndParent 0X2D0AB2 부모 핸들 값
hMenu 0X01 컨트롤 식별자
hInstance 0x52950000 모듈 핸들
lpParam 0x00 추가 데이터 없음

인자값과 의미를 매칭하면 위와 같다(https://learn.microsoft.com/ko-kr/windows/win32/api/winuser/nf-winuser-createwindowexa). 따라서 해당 CreateWindowExA는 VB.CommandButton을 생성하는 함수다.
 

[이미지110] CreateWindowExA 호출 후 레지스터

함수 호출 후 레지스터의 값은 위와 같다. 반환값은 0x0400AEE로 버튼 창에 대한 핸들이다.
 

[이미지111] ebx+3C 참조 덤프

호출 이후 ds:[ebx+3C] 값을 살펴보면 CreateWindowExA의 반환값이 들어가 있다. 따라서 해당 값은 생성된 VB.CommandButton의 핸들값이다.
 

[이미지104] EnableWindow

따라서 첫번째 분기의 의미를 해석해 보면 "VB.CommandButtonn이 생성됐는가?"이다.

[이미지105] ebx+3C 참조 덤프

ds:[ebx+3C]에 0이 아닌 값을 넣어서 "VB.CommandButtonn이 생성됨"의 상태로 변경해준다. 이러면 첫번째 분기가 발생하지 않으면서 다음 코드가 실행된다.
 

[이미지106] abex' crackme 4

특이한 점이 있는데 첫번째 분기만 우회해도 Registered 버튼이 활성 상태로 나타난다. 이유는 정확하게 파악하지 못했다. EnableWindow에 BP를 걸어줬는데도 해당 함수를 거치지 않고 버튼이 활성화 됐다. 첫번째 분기에서 활성상태가 결정되기 때문에 나머지 분기는 분석하지 않았다.


6-5. dwStyle

[이미지107] CreateWindowExA 호출 전 스택

CreateWindowExA 호출 전 스택을 살펴보면 dwStyle의 값으로 0x4C012000이 인자로 사용된다는 것을 알 수 있다.
 

상수 이름 의미
0x40000000L WS_CHILD 창이 자식 창입니다. 이 스타일이 있는 창에는 메뉴 모음이 있을 수 없습니다. 이 스타일은 WS_POPUP 스타일과 함께 사용할 수 없습니다.
0x08000000L WS_DISABLED 창은 처음에 사용하지 않도록 설정되었습니다. 사용하지 않도록 설정된 창은 사용자로부터 입력을 받을 수 없습니다. 창을 만든 후 이를 변경하려면 EnableWindow 함수를 사용합니다.
0x04000000L WS_CLIPSIBLINGS 자식 창을 서로 상대적으로 클리핑합니다. 즉, 특정 자식 창이 WM_PAINT 메시지를 받으면 WS_CLIPSIBLINGS 스타일은 업데이트할 자식 창의 영역에서 겹치는 다른 모든 자식 창을 클립합니다. WS_CLIPSIBLINGS 지정하지 않고 자식 창이 겹치는 경우 자식 창의 클라이언트 영역 내에서 그릴 때 인접한 자식 창의 클라이언트 영역 내에서 그릴 수 있습니다.
0x00010000L WS_TABSTOP 창은 사용자가 TAB 키를 누를 때 키보드 포커스를 받을 수 있는 컨트롤입니다. TAB 키를 누르면 키보드 포커스가 WS_TABSTOP 스타일로 다음 컨트롤로 바뀝니다.
이 스타일을 켜고 끄면 대화 상자 탐색을 변경할 수 있습니다. 창을 만든 후 이 스타일을 변경하려면 SetWindowLong 함수를 사용합니다. 사용자가 만든 창 및 모덜리스 대화 상자가 탭 정지에서 작동하려면 메시지 루프를 변경하여 IsDialogMessage 함수를 호출합니다.
0x00002000L BS_MULTILINE 텍스트 문자열이 너무 길어 단추 사각형의 한 줄에 다 들어가지 않는 경우 단추 텍스트를 여러 줄로 래핑합니다.

조합을 살펴보면 위와 같다. 해당 값들은 MDSN을 참고했다(https://learn.microsoft.com/ko-kr/windows/win32/winmsg/window-styles). 0x00002000L의 값은 해당 MSDN에 나오지 않아서 GPT로 값을 찾았다(https://learn.microsoft.com/en-us/windows/win32/controls/button-styles?utm_source=chatgpt.com).
 

[이미지108] WS_DISABLED

따라서 가장 처음 버튼의 활성화 상태에 관여하는 값은 해당 WS_DISABLED 플래그 값이다.
 

[이미지109] CreateWindowExA 호출 전 스택

0x4C012000 − 0x08000000 = 0x44012000로 WS_DISABLED 플래그가 적용되지 않게 한다.
 

[이미지110] abex' crackme 4

처음 버튼이 생성될 때 WS_DISABLED가 적용되지 않기 때문에 버튼이 활성화 된다.


7. 풀이


7-1. ZF 수정

[이미지111] __vbaStrCmp

__vbaStrCmp 이후 버튼의 활성화 여부에 관여하는 조건부 분기문은 위와 같다. __vbaStrCmp에 BP를 걸고 아무 값을 입력해 해당 지점에서 분기가 발생하지 않도록 한다.
 

[이미지112] abex' crackme 4

정확한 값을 입력하지 않아도 버튼이 활성화 된다.


7-2. Serial 값 입력

[이미지113] __vbaStrCmp

__vbaStrCmp 부분에서 인자 값을 확인해 보면 무슨 값을 비교하는지 파악할 수 있다.
 

[이미지114] EAX(0x5EBDAC) 참조 덤프

EAX의 경우 사용자 Serial 입력 값이다.
 

[이미지115] ECX(0x5E9DBC) 참조 덤프

ECX의 경우 Serial 값이다.
 

[이미지116] abex' crackme 4

Serial 값을 확인 한 뒤 입력창에 넣으면 버튼이 활성화 된다.


7-3. Serial 생성 루틴 파악

[이미지117] 전체 동작

앞서 전체 루틴을 분석한 결과는 위와 같다.
 

[이미지118] Serial 생성 수식

해당 분석 결과를 토대로 수식을 알아낼 수 있다. Serial을 생성하는 식은 위와 같다. 해당 식에서 사용한 Hour, Year은 abex' crackme 4를 실행했을 때의 '시'와 '년도'다.
 

from datetime import datetime

now = datetime.now()
current_hour = now.hour
current_year = now.year
result = (current_hour * 5 + 1000) * current_year

print("Serial :", result)

파이썬으로 간단하게 작성했다.
 

[이미지118] 파이썬 실행 결과

한가지 주의 할 점은 코드를 실행할 때와 abex' crackme 4를 실행했을 때의 '시'와 '년도'가 일치해야 한다.
 

[이미지119] abex' crackme 4

Serial 값을 확인 한 뒤 입력창에 넣으면 버튼이 활성화 된다.


7-4. 버튼 활성화


7-4-1. Enabled 형식 코드 값 수정

[이미지120] 0x52951D90 참조 덤프

0x52951D90으로 이동 후 해당 값을 수정한다.


7-4-2. dwStyle 플래그 값 수정

[이미지121] CreateWindowExA 호출 전 스택

WS_DISABLED 플래그가 적용되지 않게 한다.


7-4-3. VB.CommandButtonn 생성 상태로 변경

[이미지122] ebx+3C 참조 덤프

ds:[ebx+3C]에 0이 아닌 값을 넣어서 "VB.CommandButtonn이 생성됨"의 상태로 변경해준다.
 

[이미지123] abex' crackme 4

세가지 방법 중 하나를 적용하면 버튼이 활성화 된다.


 
 

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

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