Try Attack/System Hacking[basic]

함수의 호출규약

D4tai1 2018. 5. 18.

1. 함수 호출 규약(Calling Convention)

▶ 파라미터를 전달하는 방법에 대한 약속을 정의한 것이다.


2. 32비트 모드

(1) __cdecl 방식(C declaration)

 [1] C와 C++(가변인자)의 기본 호출규약이다.

 [2] 인자(argument)는 스택을 사용해서 오른쪽에서 왼쪽 순서로 전달한다.

 [3] callee(호출자)가 스택포인터를 정리한다. (c언어에서는 내부적으로, assembly에서는 직접) 

 [4] 함수이름 앞에 _(언더바) 기호가 붙는다.


(2) __stdcall 방식(Standard Call)

 [1] Win32 API[Windows OS의 System call]의 기본 호출 규약이다.

 [2] __cdecl방식과 동일하게 인자(argument)는 스택을 사용해서 오른쪽에서 왼쪽 순서로 전달한다.

 [3] caller(피호출자)가 스택포인터를 정리(복원)한다.

 [4] 함수이름 앞에 _(언더바) 기호, 함수이름 끝에 @(at, 편하게 골뱅이) 기호가 붙는다.

 [5] 장점

  ▶ 함수의 독립성이 좋다.

  ▶ __cdecl방식보다 코드 양이 적다.(여러 곳에서 호출되더라도 스택정리 코드는 함수 내 1번만 존재)


(3) __fastcall 방식

 [1] 인텔 CPU에서만 사용이 가능하다.

 [2] 함수 호출 시 첫 번째 인자는 ecx에, 두 번째 인자는 edx에 저장한다.

 [3] 세 번째 인자부터는 오른쪽에서 왼쪽순서로 스택에 저장한다.

 [4] 함수이름 앞에 @(at) 기호가 붙는다.

 [5] 그 외는 __stdcall 방식과 동일하다.

 [6] 장점

  ▶ __stdcall과 동일하지만 인자가 2개 이하 시 레지스터를 이용하기 때문에 속도가 빠르다.


(4) __thiscall 방식

 [1] C++(가변인자를 사용하지 않는 함수)의 기본 호출 규약(컴파일할 때 가변인자는 __cdecl 방식으로 변경)이다.

 [2] 인자(argument)는 스택을 사용해서 오른쪽에서 왼쪽 순서로 전달한다.

 [3] caller(피호출자)가 스택포인터를 정리(복원)한다.

 [4] ecx에 클래스의 this포인터를 전달한다.

 [5] 직접적으로 호출규약을 사용할 수 없다.

 [6] 멤버함수는 __thiscall을 사용하지만 직접 지정해서 다른 호출규약 사용이 가능하다.

  다른 호출규약 사용 시 첫 번째 인자로 this포인터가 전달한다.


(5) 스택정리방식

 [1] __cdecl방식을 제외한 나머지 함수 호출 규약은 호출당한 쪽에서 스택을 정리할까? 

  ▶ __cdecl방식은 ret 시 [pop eip / jmp eip]와 같이 복귀주소를 꺼내고 리턴한다.

  ▶ 이 후 호출한 쪽에서 add 명령어를 이용해서 스택을 복원한다.

  ▶ 나머지 함수 호출 규약은 [ret 8] 시 [pop eip]와 같이 우선 복귀주소를 꺼낸다.

  ▶ 예를 [ret 8]로 들었기 때문에 8바이트이므로 pop을 2번하고 [jmp eip]로 복귀한다.

 

3. 64비트 모드

▶ 64비트는 함수 호출 규약을 하나만 사용한다.

▶ 즉, __fastcall을 업그레이드해서 사용한다.

▶ 같은 __fastcall이지만 파일 포맷에 따라 세부 규약은 다르다.


(1) Linux(ELF)

 [1] 인자가 정수일 때

  ▶ 함수 호출 시 rdi(1), rsi(2), rdx(3), rcx(4), r8(5), r9(6) 총 6개의 레지스터를 사용해서 순서대로 사용해서 인자를 전달한다.

  ▶ 인자가 7개 이상인 경우 스택을 이용해서 전달한다.

  ▶ 인자는 오른쪽에서 왼쪽 순서로 전달한다.

  + 예를 들면 7개부터는 오른쪽에서 왼쪽 순서대로 push하고, 6개째부터 r9(6), r8(5), rcx(4), rdx(3), rsi(2), rdi(1) 순서대로 인자를 전달한다.


 [2] 인자가 실수일 때

  ▶ 함수 호출 시 XMM0 ~ XMM7까지 총 8개의 레지스터를 순서대로 사용해서 인자를 전달한다.

  ▶ 인자가 9개 이상인 경우 스택을 이용해서 전달한다.

  ▶ 인자는 오른쪽에서 왼쪽 순서로 전달한다.

  + 예를 들면 9개부터는 오른쪽에서 왼쪽 순서대로 push하고, 8개째부터 XMM7(8) ~ XMM0(1) 순서대로 인자를 전달한다.


(2) Windows(PE)

 [1] 인자가 정수일 때

  ▶ 함수 호출 시 rcx(1), rdx(2), r8(3), r9(4) 총 4개의 레지스터를 사용해서 순서대로 사용해서 인자를 전달한다.

  ▶ 인자가 5개 이상인 경우 스택을 이용해서 전달한다.

  ▶ 인자는 오른쪽에서 왼쪽 순서로 전달한다.

  + 예를 들어 5개부터는 오른쪽에서 왼쪽 순서대로 push하고, 4개째부터 r9(4), r8(3), rdx(2), rcx(1)순서대로 인자를 전달한다.


 [2] 인자가 실수일 때

  ▶ 함수 호출 시 XMM0 ~ XMM3까지 총 4개의 레지스터를 순서대로 사용해서 인자를 전달한다.

  ▶ 인자가 5개 이상인 경우 스택을 이용해서 전달한다.

  ▶ 인자는 오른쪽에서 왼쪽 순서로 전달한다.

  + 예를 들어 5개부터는 오른쪽에서 왼쪽 순서대로 push하고, 4개째부터 XMM3(4) ~ XMM0(1) 순서대로 인자를 전달한다.



(3) 공통

 [1] 스택정리

  ▶ __fastcall의 일종이기 때문에 호출당한 쪽에서 스택을 복원한다.

  ▶ 정수의 경우 [RDX(64) : RAX(64)]와 같이 반환(128비트)한다.

  ▶ 실수의 경우 [XMM1(128) : XMM0(128)]와 같이 반환(256비트)한다.

 [2] 반환값

  ▶ __fastcall 함수 호출 규약과 동일하게 스택 사용


(4) 스택사용방법

 [1] Linux(ELF)

  ▶ 위에서 사용한 __fastcall 방식과 동일하게 스택을 사용한다. 

 [2] Windows(PE)

  ▶ 인자가 정수든 실수든, 파라미터가 4개미만인 상태로 함수를 호출한다면?

   + 무조건 4개의 레지스터(RCX, RDX, R8, R9)를 위한 영역을 할당해야 한다.

 

(5) 시뮬레이션

 예를 들어 printf("%d, %d, %d, %d, %d, %d", a, b, c, d, e, f); 와 같은 구문이 있다고 가정하면..?


 [1] Linux(ELF)

  ▶ 인자의 순서대로

    push f

    mov r9, e

    mov r8, d

    mov rcx, c

    mov rdx, b

    mov rsi, c

    lea rdi, "%d, %d, %d, %d, %d, %d \0"

    call printf의 주소(push rip / jmp printf의 주소)


  ▶ 현재 상태를 그림으로 나타내면..?

  ▶ 부가설명을 한다면 1~6번째 인자는 레지스터에 고이 보관되어 있으며 7번째 인자만 스택에 저장하고 call을 한 상태이다.

  ▶ 이 부분이 우리가 일반적으로 알고있는 __fastcall 방식이며 리눅스는 위 그림과 동일하다.


 [2] Windows(PE)

  ▶ 인자의 순서대로

    push f

    push e

    push d

    mov r9, c

    mov r8, b

    mov rdx, c

    lea rcx, "%d, %d, %d, %d, %d, %d \0"

    call printf의 주소(push rip / jmp printf의 주소)


  와 같이 작성될 것이다.


  ▶ 현재 상태를 그림으로 나타내면..?

  ▶ 부가설명을 한다면 1~4번째 인자는 레지스터(RCX, RDX, R8, R9)에 고이 보관되어 있다.

  ▶ 그리고 5번째, 6번째, 7번째 인자만 스택에 저장하였다.

  ▶ 그러나 분명히 1~4번째 인자는 레지스터에 저장한다고 되어 있는데...

  ▶ 레지스터를 위한 공간을 만들어 놓은 후 call을 한 상태이다.

  ▶ 이 말이 [2]에서 설명한 내용이다.

  ▶ 즉, 인자가 타입과 개수에 상관없이 무조건 8바이트 * 4(레지스터 개수) 만큼은 확보 후 함수를 호출한다.


4. 컴파일러 설정[함수 호출 규약 사용방법]

(1) GUI(Visual Studio)

 [1] 프로젝트속성 - C/C++ - 고급 - 호출규칙 - 원하는 것으로 변경


(2) CLI(cl.exe)

 [1] __cdecl = cl /Gd [소스파일.c]

 [2] __stdcall = cl /Gz [소스파일.c]

 [3] __fastcall = cl /Gr [소스파일.c]

  

  ▶ 시작 - V로 시작하는 것... Visual Studio 폴더 내 - 개발자 명령 프롬프트


  

  ▶ 위 그림과 같이 사용할 수 있다.

  ▶ 바이너리 분석 툴에 올려서 확인할 수 있다.


5. 시연

(1) __cdecl

▶ 컴파일 옵션 = gcc -m32 -fno-stack-protector -mpreferred-stack-boundary=2 -g -o call call.c

 [1] C 소스

  

   gcc는 함수 앞에 __attribute__((cdecl)) 과 같이 함수호출규약의 속성을 부여한다.

  ▶ Visual Studio는 함수 앞에 __cdecl 과 같이 함수호출규약의 이름을 적어주면 된다.


 [2] 디스어셈블한 코드

  

   하늘색부분은 변수에 값을 저장하는 부분이다.

   빨간색부분은 스택에 5개의 인자를 넣는 부분이다.

   노란색부분은 add함수가 종료되고 스택을 사용한만큼[20바이트(0x14)] 복원시켜준다.  


 [3] 함수 호출 전

  

  ▶ add함수를 호출하기 전 스택을 살펴보면 맨 아래 10(0xa)부터 5개의 인자가 저장되어 있는 것을 확인할 수 있다.


 [4] 사용자정의함수(add함수)

  

  ▶ 노란색부분을 보면 첫 번째 인자를 edx에 넣고, 두 번째 인자를 eax에 넣고 더한 후 eax를 반환한다.

  ▶ 빨간색부분을 보면 호출당한 쪽에서는 스택정리를 하지 않는 것을 알 수 있다.


(2) __stdcall

▶ 컴파일 옵션 = gcc -m32 -fno-stack-protector -mpreferred-stack-boundary=2 -g -o call call.c

 [1] C 소스

  

  ▶ __stdcall 함수호출규약 방식으로 작성하였다.


 [2] 디스어셈블한 코드

  

   하늘색부분은 변수에 값을 저장하는 부분이다.

   빨간색부분은 스택에 5개의 인자를 넣는 부분이다.

   노란색밑줄은 printf함수가 종료되고 스택을 사용한만큼[8바이트] 복원시켜준다. 

  ▶ add함수는 호출당한 쪽에서 정리하겠지만 printf()함수는 C의 기본 함수호출규약인 __cdecl을 사용하기 때문이다.


 [3] 함수 호출 전

  

  ▶ add함수를 호출하기 전 스택을 살펴보면 맨 아래 10(0xa)부터 5개의 인자가 저장되어 있는 것을 확인할 수 있다.


 [4] 사용자정의함수(add함수)

  

  ▶ 노란색부분을 보면 첫 번째 인자를 edx에 넣고, 두 번째 인자를 eax에 넣고 더한 후 eax를 반환한다.

  ▶ 빨간색부분을 보면 호출당한 쪽에서 [pop eip]이후 스택을 0x14 즉 20바이트(4바이트 인자 5개)만큼 돌려놓고 [jmp eip]한다.


(3) __fastcall

▶ 컴파일 옵션 = gcc -m32 -fno-stack-protector -mpreferred-stack-boundary=2 -g -o call call.c

 [1] C 소스

  

  ▶ __fastcall 함수호출규약 방식으로 작성하였다.


 [2] 디스어셈블한 코드

  

   빨간색부분은 스택에 5개의 인자를 넣는 부분이다.


 [3] 함수 호출 전

  

 ▶ add함수를 호출하기 전 스택을 살펴보면 맨 아래 77(0x4d)부터 3개의 인자가 저장되어 있는 것을 확인할 수 있다.

 ▶ 그렇다면 나머지 인자는..? __fastcall은 첫번 째 파라미터는 ecx에, 두 번째 파라미터는 edx에 저장된다.

 ▶ 이 그림 상단의 노란색부분에서 확인할 수 있다.


 [4] 사용자정의함수(add함수)

  

  ▶ 노란색부분을 보면 첫 번째 인자를 ecx에서 꺼내고 넣고, 두 번째 인자를 edx에서 꺼내서 지역변수에 저장한다.

  ▶ 빨간색부분을 보면 호출당한 쪽에서 [pop eip]이후 스택을 0xC 즉 12바이트(4바이트 인자 3개)만큼 돌려놓고 [jmp eip]한다.


(4) __thiscall

▶ 컴파일 옵션 = g++ -m32 -fno-stack-protector -mpreferred-stack-boundary=2 -g -o call call.c

 [1] cpp 소스

  

  ▶ 위에서 사용한 프로그램과 내용은 동일하지만 클래스를 이용해서 객체를 생성 후 멤버함수를 사용한 c++소스이다.

  ▶ __thiscall 함수호출규약 방식으로 작성하였다.


 [2] 디스어셈블한 코드

  

  ▶ 하늘색부분은 변수에 값을 저장하는 부분이다.

  ▶ 빨간색부분은 스택 내 주소를 eax에 저장한다.

  ▶ 이 스택 내 주소에 객체포인터를 담기위해 push를 하고 생성자를 호출하여 객체를 생성한다.

  ▶ 노란색밑줄은 객체포인터를 ecx에 저장하고 나머지 인자는 전부 스택에 넣은 후 멤버함수를 호출한다. 


 [3] 함수 호출 전

  


 [4] 사용자정의함수(add함수)

  

  ▶ 객체포인터를 저장하고 인자 2개를 eax와 edx에 저장하고 더한다.

  ▶ 인자가 5개이기 때문에 호출당한 쪽이 ret 0x14(20)와 같이 20바이트만큼 스택을 복원한다.


(5) 64비트 모드 Linux(ELF)의 __fastcall

▶ 컴파일 옵션 = gcc -fno-stack-protector -mpreferred-stack-boundary=4 -g -o call call.c

 [1] C 소스

  

  ▶ 64비트 모드의 기본 호출규약을 사용하였다.


 [2] 디스어셈블한 코드

  

  ▶ 빨간색부분을 보면 3번째 파라미터인 77(0x4d)을 edx에 넣는다.

  ▶ 2번째 파라미터인 20(0x14)을 ecx에 넣고 노란색부분에서 esi에 넣는다.

  ▶ 1번째 파라미터인 10(0xa)을 eax에 넣고 노란색부분에서 edi에 넣는다.

  ▶ 4번째 파라미터를 xmm0에 넣고 main+72에서 xmm1에 넣는다.

  ▶ 5번째 파라미터를 xmm0에 넣는다.

  ▶ movaps는 레지스터가 한 번에 다룰 수 있는만큼 데이터를 복사하는 명령어이다.

  ▶ movss는 float형의 부동소수점을 복사하는 명령어이다.

  ▶ 하늘색부분을 보면 add함수 호출이 종료되고 결과값인 eax를 esi에 저장한다.

  ▶ esi는 printf()함수의 두 번째 인자로 들어간다.

  ▶ 하늘색부분을 보면 rdi에 printf()함수의 첫 번째 인자인 문자열의 주소를 넣는다.

  ▶ 64비트 모드는 printf()함수를 포함한 모든 함수가 __fastcall을 사용하기 때문이다. 


 [3] 함수 호출 전

  

  ▶ 첫 번째인자(RDI), 두 번째인자(RSI), 세 번째 인자(RDX)는 정상적으로 잘 들어간 것을 확인할 수 있다.


  

  ▶ 다섯 번째 인자인 xmm1과 네 번째 인자인 xmm0도 정상적으로 들어간 것을 확인할 수 있다.


 [4] 사용자정의함수(add함수)

  

  ▶ 하늘색부분을 보면 지역변수 5개를 초기화 후 스택을 사용하지 않았기 때문에 스택복원 없이 복귀한다.


(6) 64비트 모드 Windows(PE)의 __fastcall

 [1] C 소스

  

  ▶ 64비트 모드의 기본 호출규약을 사용하였다.


 [2] 디스어셈블한 코드

  

  ▶ 64비트 모드 Windows(PE)의 __fastcall의 내용이다.

  ▶ 정수인 첫 번째 파라미터는 ecx, 두 번째 파라미터는 edx, 세 번째 파라미터는 r8 레지스터에 각각 넣는다.

  ▶ 실수인 네 번째 파라미터는 xmm3, 다섯 번째 파라미터는 xmm0 레지스터에 각각 넣는다.

  ▶ 원래는 xmm0~xmm3까지 순서대로 들어가는 걸로 알고 있었는데 추후 확인을 해보아야 할 것 같다.



'Try Attack > System Hacking[basic]' 카테고리의 다른 글

gcc 메모리보호옵션  (0) 2018.05.18
gcc 사용방법  (0) 2018.05.18
gdb debugging  (0) 2018.05.17
gdbgui install  (0) 2018.05.17
gdb disassemble  (0) 2018.05.16

댓글