STUDY/Pwnable

함수 호출 규약

Dalseung 2022. 10. 21. 21:15

개요

Dreamhack 함수 호출 규약에 대해서 공부한 것을 정리한 포스트이다.

 

함수 호출 규약이란?

함수 호출 규약은 함수의 호출 및 반환에 대한 약속

한 함수에서 다른 함수를 호출할 때, 프로그램의 실행 흐름은 다른 함수로 이동하고, 호출한 함수가 반환하면, 다시 원래의 함수로 돌아와서 기존의 실행 흐름을 이어나간다.

그러므로 함수를 호출할 때는 반환된 이후를 위해 호출자(Caller)의 상태(Stack frame) 및 반환 주소(Return Address)를 저장해야 한다.

또한, 호출자는 피호출자(Callee)가 요구하는 인자를 전달해줘야 하며, 피호출자의 실행이 종료될 때는 반환 값을 전달받아야 한다.

x86(32bit) Architecture 특징

레지스터를 통해 피호출자의 인자를 전달하기에는 레지스터 수가 적어서 스택으로 인자를 전달한다.

또한 인자를 전달하기 위해 사용한 스택을 호출자가 정리하는 특징이 있다.

스택을 통해 인자를 전달할 때는, 마지막 인자부터 첫 번째 인자까지 거꾸로 스택에 push한다.

 
함수 호출 규약
  • cdecl호출 규약: 리눅스 gcc에서 x86 바이너리를 컴파일할 때 사용한다.
  • stdcall
  • fastcall
  • thiscall

 

예시

void caller(){
   callee(1, 2);
}
caller:
    push    2 ; 2를 스택에 저장하여 callee의 인자로 전달합니다.
    push    1 ; 1를 스택에 저장하여 callee의 인자로 전달합니다.
    call    callee
    add    esp, 8 ; 스택을 정리합니다. (push를 2번하였기 때문에 8byte만큼 esp가 빠져있으므로 8을 더해서 스택 정리.)

x86-64 Architecture 특징

레지스터가 많아 적은 수의 인자는 레지스터만 사용해서 인자를 전달하고, 인자가 너무 많을 떄만 스택을 사용한다.
6개의 인자를 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달합니다. 더 많은 인자를 사용해야 할 때는 스택을 추가로 사용
Caller에서 인자 전달에 사용된 스택을 정리합니다.
함수의 반환 값은 RAX로 전달합니다.

함수 호출 규약
  • SYSTEM V호출 규약: 리눅스 gcc에서 x86-64 바이너리를 컴파일할 때 사용한다.
  • MS ABI의 Calling Convention

예시

void caller() { callee(123456789123456789, 2, 3, 4, 5, 6, 7); }

int main() { 
  caller(); 
  }
 ► 0x555555554652 <caller>       push   rbp
   0x555555554653 <caller+1>     mov    rbp, rsp
   0x555555554656 <caller+4>     push   7
   0x555555554658 <caller+6>     mov    r9d, 6
   0x55555555465e <caller+12>    mov    r8d, 5
   0x555555554664 <caller+18>    mov    ecx, 4
   0x555555554669 <caller+23>    mov    edx, 3
   0x55555555466e <caller+28>    mov    esi, 2
   0x555555554673 <caller+33>    movabs rdi, 0x1b69b4bacd05f15
   0x55555555467d <caller+43>    call   callee <callee>
   0x555555554682 <caller+48>    add    rsp, 8 ;스택을 정리한다.(push를 1번하였기 때문에 8byte만큼 rsp가 빠져 있으므로 8을 더해서 스택 정리)

   ; 이 사이에 원래의 실행 흐름이었던 주소 0x555555554682 <caller+48>이 RET으로서 스택에 쌓임

   0x5555555545fa <callee>:         push   rbp     ; 이전 스택의 rbp 주소가 저장 = SFP(StackFramePointer)
   0x5555555545fb <callee+1>:     mov    rbp,rsp ; rbp를 rsp가 있는 곳까지 내린다. 바로 다음에 rsp의 값을 빼서, rbp와 rsp의 사이 공간을 새로운 스택 프레임으로 할당할 수 있지만, callee 함수는 지역 변수를 사용하지 않으므로, 새로운 스택 프레임을 만들지 않는다.
   0x5555555545fe <callee+4>:     mov    QWORD PTR [rbp-0x18],rdi
   0x555555554602 <callee+8>:     mov    DWORD PTR [rbp-0x1c],esi
   0x555555554605 <callee+11>:     mov    DWORD PTR [rbp-0x20],edx
   0x555555554608 <callee+14>:     mov    DWORD PTR [rbp-0x24],ecx
                                ...
   0x555555554645 <callee+75>:    add    rax,rdx
   0x555555554648 <callee+78>:    mov    QWORD PTR [rbp-0x8],rax
   0x55555555464c <callee+82>:    mov    rax,QWORD PTR [rbp-0x8];함수의 종결부(Epilogue)에 도달하면, 반환값을 rax에 옮김.
   0x555555554650 <callee+86>:    pop    rbp ;callee 함수가 스택 프레임을 만들지 않았기 때문에, pop rbp로 스택 프레임을 꺼낼 수 있지만, 일반적으로 leave로 스택 프레임을 꺼낸다.
   0x555555554651 <callee+87>:    ret ;스택 프레임을 꺼낸 뒤에는, ret로 호출자로 복귀합니다. sfp가 rbp로, 반환 주소로 rip가 설정

leave = mov rsp rbp, pop rbp
ret = pop rip, jmp rip

결론

x86

image

x86-64bit

image

'STUDY > Pwnable' 카테고리의 다른 글

[Dreamhack] Shell_basic  (2) 2022.10.21
pwntools  (0) 2022.10.11
어셈블리어 기초  (0) 2020.01.15
환경변수 이용하는 법  (0) 2020.01.02
메모리 조작하기2  (0) 2019.12.24