콘텐츠로 이동

리버싱 핵심원리 1부 10장 정리

Note

본 게시글의 내용은 리버싱 핵심원리를 보며 복습 겸 정리하였습니다.

책의 내용과 일부 상이할 수 있고, 이해를 돕기 위해 강좌 형식의 말투와 적절한(?) 예시를 추가하였습니다.

본 게시글에서 사용되는 소스 코드와 파일, 프로그램은 리버싱 핵심원리에서 제공하는 파일과 언급되는 것들을 기반으로 하며, 일부 상이할 수 있습니다.

함수 호출 규약

함수 호출 규약Calling Convention은 함수가 호출되고 실행될 때 매개변수(인자)의 전달과 반환값의 처리가 어떻게 이루어지는에 대한 규칙을 말합니다.

매개변수(인자) 전달

스택을 이용한 전달레지스터를 이용한 전달이 가능합니다. 사용하는 인수가 적고 빠른 속도를 원한다면 레지스터 방식을, 가변 인수를 사용한다면 스택 방식을 이용합니다. 이는 사용하는 프로그래밍 언어와 컴파일러, 함수 호출 규약 종류에 따라 다르게 작동합니다.

대표적인 함수 호출 규약 (32Bit)

cdecl (C Declaration)

C 언어에서 주로 사용되기 때문에 cdecl이라 합니다. 스택의 정리를 호출자(Caller)에게 책임을 묻습니다. 즉, 해당 함수를 호출한 쪽에서 정리해야 한다는 것입니다.

cdecl
00401000  push ebp
00401001  mov ebp, esp
00401003  mov eax, dword ptr ss:[ebp + 8]
00401006  add eax, dword ptr ss:[ebp + c]
00401009  pop ebp
0040100A  ret
0040100B  int3
0040100C  int3
0040100D  int3
0040100E  int3
0040100F  int3
00401010  push ebp
00401011  mov ebp, esp
00401013  push 2
00401015  push 1
00401017  call 401000
0040101C  add esp, 8;(1)
0040101F  pop ebp
00401020  ret

  1. 함수를 호출한 쪽에서 스택을 정리하고 있다.

0x401013 ~ 0x40101C 영역의 코드를 보면 add() 함수의 매개변수를 역순으로 전달하고 있습니다. 그리고 add() 함수 호출 후 add esp, 8 명령을 통해 스택을 정리하고 있습니다. 이처럼 함수를 호출한 쪽에서 직접 정리하는 방식이 cdecl 방식입니다.

cdecl 방식은 다른 방식에 비해 가변 인수를 지원하는 데 최적화되어 있습니다. cdecl은 호출자가 직접 스택을 정리하는 특징 덕분에 다른 규약에 비해 가변 인수의 수를 제대로 알고 있어 이를 정확하게 처리할 수 있습니다. 호출된 함수는 전달받은 인수를 알아서 사용만하고 나머지는 호출자가 정리하면 되는거죠. 대표적으로 C 언어의 printf() 함수가 있습니다.

stdcall (Standard Call)

주로 Win32 API에서 사용되는 함수 호출 규약입니다. 스택의 정리를 피호출자(Callee)가 합니다. 즉, 함수를 호출당한(해당 함수) 쪽에서 정리합니다.

C / C++ 언어에서 stdcall 방식으로 컴파일하고 싶다면 __stdcall을 수식합니다.

stdcall
00401000  push ebp
00401001  mov ebp, esp
00401003  mov eax, dword ptr ss:[ebp + 8]
00401006  add eax, dword ptr ss:[ebp + c]
00401009  pop ebp
0040100A  ret 8 ;(1)
0040100D  int3
0040100E  int3
0040100F  int3
00401010  push ebp
00401011  mov ebp, esp
00401013  push 2
00401015  push 1
00401017  call 401000
0040101C  pop ebp
0040101D  ret
  1. 피호출자(호출된 함수)가 직접 스택을 정리한다.

stdcall의 스택 정리는 해당 함수의 마지막에 있는 ret 8 명령에 의해 이루어집니다. 즉, 값을 반환한 후 지정된 크기만큼 esp를 다시 증가시킵니다.

stdcall 방식은 cdecl 방식에 비해 작성되는 코드의 양이 적기 때문에 코드가 간단해진다는 장점이 있습니다. 단, 가변 인수의 경우 인수의 개수를 정확히 확인하기 매우 어렵기 때문에 가변 인수 처리에는 취약한 편입니다.

fastcall

fastcall은 함수 호출 시 레지스터를 우선으로 사용한 후 나머지는 스택을 이용하는 방식입니다. 속도와 성능을 챙기기 위해 사용하며 스택에 접근할 때 드는 오버헤드를 줄이기 위해서입니다. 성능면에서 유리하지만 가변 인수와 제한적인 레지스터 수 등으로 인해 잘 고려해서 사용해야 합니다.

함수의 인수 중 일부(보통 첫 번째와 두 번째)를 레지스터를 이용해 전달합니다. 주로 ecxedx 레지스터가 사용됩니다. 스택의 정리는 플랫폼에 따라 다른데 Windows의 경우 주로 피호출자가 정리합니다.

C / C++ 언어에서 __fastcall 키워드를 수식하여 사용할 수 있습니다.

fastcall
; MAIN 함수
mov ecx, 2
mov edx, 1
call add

; ADD 함수
push ebp
mov ebp, esp
mov eax, ecx
add eax, ecx
mov esp, ebp
pop ebp
ret

인수가 적은 경우(2개 이하) 스택을 사용하지 않고 레지스터를 사용하기 때문에 성능면에서 매우 유리합니다. 그 이상의 인수는(나머지) 스택을 이용해 전달하게 됩니다.

thiscall

thiscall은 C++ 언어의 멤버 함수 호출 시 사용되는 함수 호출 규약입니다. this 포인터를 지원하기 위해 만들어졌죠. 현재 객체의 포인터를 ecx 레지스터에 할당하고 이 레지스터를 통해 객체의 멤버 변수나 함수에 접근할 수 있습니다.

32Bit 플랫폼에서만 한정적으로 사용됩니다.

64Bit

64Bit 플랫폼에선 32Bit의 함수 호출 규약과는 다르게 fastcall을 기반으로 하도록 설계되었습니다. 최대한 레지스터를 사용하여 성능을 최적화합니다.

최대 8개의 레지스터를 사용해 인수를 전달할 수 있고 가변 인자를 지원하기 위해 cdecl처럼 호출자(Caller)에서 스택을 정리하도록 합니다. __stdcall을 수식할 순 있지만 컴파일러가 무시할 수 있습니다.