정현진Jeong Hyeonjin

정현진 — 보안 포트폴리오

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: 키 파일의 크기

  1. CreateFileA()로 abex.l2c라는 파일이 있는지 확인합니다. 결과는 EAX에 담깁니다.
  2. 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이었다가, 실행 후 값이 채워지는 것을 확인했습니다.

일련번호가 만들어지는 과정

  1. 볼륨 이름을 가져옵니다. 제 환경에서는 "OS"였습니다.
  2. lstrcatA()로 "OS"와 프로그램 안의 "4562-ABEX"를 이어 "OS4562-ABEX"를 만듭니다.
  3. 반복문이 앞 4글자의 값을 1씩 두 번 올려 "QU6762-ABEX"로 바꿉니다. (DEC DL이 0이 되면 ZF=1이 되어 반복이 끝납니다.)
  4. 다시 lstrcatA()로 "L2C-5781"과 이어 "L2C-5781QU6762-ABEX"를 만듭니다.
  5. lstrcmpiA()가 입력값과 비교해 같으면 EAX=0, 다르면 1을 돌려줍니다.

이 값을 입력하자 성공 메시지가 나왔습니다. 일련번호가 실행하는 컴퓨터의 볼륨 이름에 따라 달라지는 구조라서, 다른 PC에서는 답이 달라집니다.

정리

  • 호출 규약을 알면 스택에서 인자와 복귀 주소의 위치를 바로 짐작할 수 있습니다.
  • 문자열 검색이 안 되면 API 호출 목록에서 비교 함수를 찾는 방법이 있습니다.
  • 정답이 비교되는 순간의 인자 값을 보면 계산 과정을 다 따라가지 않아도 답을 알 수 있습니다.

0 백링크0 링크3 태그