Research / 리버싱 / 함수 호출 규약(cdecl·stdcall·fastcall)과 abex crackme 3~5
함수 호출 규약(cdecl·stdcall·fastcall)과 abex crackme 3~5
- 유형
- 연구 기록
- 분류
- 리버싱
- 발행
- 2025. 9. 4.
- 분량
- 2분
- 종류
- 연구 기록
인자를 누가 어떻게 넘기고 스택을 누가 정리하는지 정리한 뒤, 키 파일 검사, Visual Basic 문자열 비교, 볼륨 이름으로 만든 일련번호를 차례로 풀었습니다.
리버싱 입문서 3장을 공부하면서 정리했습니다. 앞부분은 함수 호출 규약, 뒷부분은 abex crackme 3·4·5번입니다.
함수 호출 규약
함수를 부를 때 인자를 어떻게 넘기고, 끝난 뒤 스택을 누가 정리하는지에 대한 약속입니다.
| 규약 | 인자 전달 | 스택 정리 |
|---|---|---|
| cdecl | 스택, 오른쪽 인자부터 | 호출한 쪽 |
| stdcall | 스택, 오른쪽 인자부터 | 호출된 함수 |
| fastcall | 앞의 두 인자는 ECX, EDX 레지스터, 나머지는 스택 | 호출된 함수 |
callee(1, 2)를 부르는 경우로 비교했습니다.
cdecl은 호출한 쪽이 ADD ESP, 8로 인자 2개(8바이트)를 직접 치웁니다.
PUSH 2
PUSH 1
CALL callee
ADD ESP, 8
stdcall은 호출된 함수가 RETN 8로 돌아가면서 치웁니다.
PUSH 2
PUSH 1
CALL callee
; [callee]
PUSH EBP
MOV EBP, ESP
...
POP EBP
RETN 8
fastcall은 인자를 레지스터로 넘기므로 PUSH가 줄어듭니다. 함수 안에서는 보통 그 값을 다시 스택의 지역 변수로 옮겨 놓고 씁니다.
MOV EDX, 2
MOV ECX, 1
CALL callee
인자를 오른쪽부터 넣는 이유는 스택이 LIFO라서, 그래야 함수 안에서 첫 번째 인자가 가장 가까운 위치(EBP+8)에 오기 때문입니다.
음수 표현
Windows는 음수를 2의 보수로 표현합니다. 모든 비트를 뒤집고(NOT) 1을 더합니다.
crackme 3: 키 파일의 크기
CreateFileA()로abex.l2c라는 파일이 있는지 확인합니다. 결과는 EAX에 담깁니다.GetFileSize()로 크기를 확인해,0x12(18바이트)인지 비교합니다.
처음에 "1234567890"(10바이트, 0x0A)을 넣은 파일로 확인했고, 18글자로 바꾸자 성공 메시지가 나왔습니다. 키 "내용"이 아니라 크기만 검사하는 구조였습니다.
crackme 4: Visual Basic 프로그램
Visual Basic 프로그램은 MSVBVM60.DLL의 API를 부르며 동작하는 이벤트 기반 프로그램입니다. 코드가 기계어가 아니라 중간 단계인 p-code로 저장돼 있어서 분석이 더 번거롭습니다.
이 문제는 맞는 일련번호를 넣어야 [Registered] 버튼이 활성화됩니다. 오류 메시지가 없어서 문자열 검색을 쓸 수 없었습니다. 대신 [All intermodular calls]에서 문자열 비교 함수 __vbaStrCmp를 찾았습니다.
- EAX, ECX를 인자로 비교하고 결과는 EAX에 담깁니다.
- 결과가 EDI로 옮겨지고,
TEST DI, DI→JZ로 분기합니다.
푼 방법 두 가지
- 플래그 수정:
TEST DI, DI직후 ZF를 1에서 0으로 바꾸자 버튼이 활성화됐습니다. - 비교 값 확인:
__vbaStrCmp에 인자가 들어가는0040230B에 브레이크포인트를 걸었습니다. EAX가 가리키는 곳에는 내가 넣은 "4"(0x34), ECX 쪽에는 "2166750"이 있었습니다. 이 값을 입력하자 성공했습니다.
crackme 5: 볼륨 이름으로 만든 일련번호
델파이로 만든 프로그램입니다.
GetVolumeInformation()
지정한 드라이브의 파일 시스템·볼륨 정보를 가져오는 함수입니다. MSDN의 _In_opt_는 선택적인 입력, _Out_opt_는 결과가 담길 선택적인 출력이라는 뜻입니다. 함수 실행 전에는 출력 변수가 0이었다가, 실행 후 값이 채워지는 것을 확인했습니다.
일련번호가 만들어지는 과정
- 볼륨 이름을 가져옵니다. 제 환경에서는 "OS"였습니다.
lstrcatA()로 "OS"와 프로그램 안의 "4562-ABEX"를 이어 "OS4562-ABEX"를 만듭니다.- 반복문이 앞 4글자의 값을 1씩 두 번 올려 "QU6762-ABEX"로 바꿉니다. (
DEC DL이 0이 되면 ZF=1이 되어 반복이 끝납니다.) - 다시
lstrcatA()로 "L2C-5781"과 이어 "L2C-5781QU6762-ABEX"를 만듭니다. lstrcmpiA()가 입력값과 비교해 같으면 EAX=0, 다르면 1을 돌려줍니다.
이 값을 입력하자 성공 메시지가 나왔습니다. 일련번호가 실행하는 컴퓨터의 볼륨 이름에 따라 달라지는 구조라서, 다른 PC에서는 답이 달라집니다.
정리
- 호출 규약을 알면 스택에서 인자와 복귀 주소의 위치를 바로 짐작할 수 있습니다.
- 문자열 검색이 안 되면 API 호출 목록에서 비교 함수를 찾는 방법이 있습니다.
- 정답이 비교되는 순간의 인자 값을 보면 계산 과정을 다 따라가지 않아도 답을 알 수 있습니다.