System Hacking/Basics

[시스템 해킹] 함수 호출 규약/ x86, x86-64

hanbunny 2025. 3. 11. 16:39

함수 호출 규약

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

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

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

 

함수 호출 규약 종류

컴파일러는 지원하는 호출 규약 중, CPU아키텍처에 적합한 것을 선택한다.

하지만 CPU의 아키텍처가 같아도, 컴파일러가 다르면 적용하는 호출 규약이 다를 수 있다.

 

ex) C언어를 컴파일 하는 경우에 윈도우에서는 MSVC를, 리눅스에서는 gcc를 사용.

여기서 MSVC는 MSx64호출 규약을 적용하고, gcc는 SYSTEM V호출 규약을 적용한다.

 

 

리눅스 gcc에서 x86 바이너리 컴파일

호출 규약 종류 : cdecl, stdcall, fastcall, thiscall

 

cdecl 분석 실습

// Name: cdecl.c
// Compile: gcc -fno-asynchronous-unwind-tables -nostdlib -masm=intel \
//          -fomit-frame-pointer -S cdecl.c -w -m32 -fno-pic -O0

void __attribute__((cdecl)) callee(int a1, int a2){ // cdecl로 호출
}

void caller(){
   callee(1, 2);
}

 

이 소스코드를 컴파일해 cdecl.s 생성 후 파일을 열어봄.

 

; Name: cdecl.s

.file "cdecl.c"
.intel_syntax noprefix
.text
.globl callee
.type callee, @function
callee:
nop
ret ; 스택을 정리하지 않고 리턴합니다.
.size callee, .-callee
.globl caller
.type caller, @function
caller:
push 2 ; 2를 스택에 저장하여 callee의 인자로 전달합니다.
push 1 ; 1를 스택에 저장하여 callee의 인자로 전달합니다.
call callee
add esp, 8 ; 스택을 정리합니다. (push를 2번하였기 때문에 8byte만큼 esp가 증가되어 있습니다.)
nop
ret
.size caller, .-caller
.ident "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0"
.section .note.GNU-stack,"",@progbits

 


리눅스 gcc에서 x86-64 바이너리 컴파일

호출 규약 종류 : System V(SYSV) AMD64 ABI의 Calling Convention, MS ABI의 Calling Convention

 

SYSV ABI는 ELF포맷, 링킹 방법, 함수 호출 규약등의 내용을 담고 있다.

$file /bin/ls 명령어를 쳐보면 SYSV 문자열이 포함된 걸 확인 가능하다.

 

SYSV 함수 호출 규약 특징

1. 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9 순서대로 저장해 전달. 더 많은 인자를 사용할땐 스택 추가 사용.

2. Caller에서 인자 전달에 사용된 스택을 정리.

3. 함수 반환 값은 RAX로 전달.

 

SYSV 분석 실습

// Name: sysv.c
// Compile: gcc -fno-asynchronous-unwind-tables  -masm=intel \
//         -fno-omit-frame-pointer -S sysv.c -fno-pic -O0

#define ull unsigned long long

ull callee(ull a1, int a2, int a3, int a4, int a5, int a6, int a7) {
  ull ret = a1 + a2 + a3 + a4 + a5 + a6 + a7;
  return ret;
}

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

int main() { caller(); }

파일 먼저 생성 후 컴파일을 했음.

 

 

1. Caller 함수까지 실행 후 분석 - 인자 전달

#gdb 실행 명령어 - caller함수까지 실행
$ gdb -q sysv
pwndbg: loaded 139 pwndbg commands and 49 shell commands. Type pwndbg [--shell | --all] [filter] for a list.
pwndbg: created $rebase, $ida GDB functions (can be used with print/break)
Reading symbols from sysv...
...
pwndbg> b *caller
Breakpoint 1 at 0x1185
pwndbg> r

 

 

#위에 코드 일부
void caller() { callee(123456789123456789, 2, 3, 4, 5, 6, 7); }

 

DISASM을 보면 caller+10부터 caller+37까지 6개의 인자를 각각 레지스터에 설정.

caller+8에선 7번째 인자인 7을 스택으로 전달한다.

2.Caller 함수 호출 전까지 실행 후 분석 - 인자 전달

#callee()호출 부분 파악 후 중단점 설정
pwndbg> disass caller
...
   0x00005555555551b7 <+50>:  call   0x555555555129 <callee>
...
pwndbg> b *caller+50
Breakpoint 2 at 0x5555555551b7
pwndbg> c
Continuing.

소스 코드에서 callee(123456789123456789, 2, 3, 4, 5, 6, 7)로 함수를 호출했는데,

인자들이 순서대로 rdi -> rsi -> rdx -> rcx -> r8 -> r9  -> [rsp]에 설정되어 있는 것을 확인.

 

아까 봤던 SYSV 함수 호출 규약 특징인 1. 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9 순서대로 저장해 전달. 더 많은 인자를 사용할땐 스택 추가 사용.을 확인해볼 수 있었음.

 

\

현재 스택 포인터 (RSP)에서 8바이트 단위로 4개의 값을 출력해보면 이렇게 나옴.

 

3. 반환 주소 저장

pwndbg> si
pwndbg> x/4gx $rsp
pwndbg> x/10i 0x0000555555554682 - 5

 

 

4. 스택 프레임 저장

pwndbg> x/9i $rip
pwndbg> si
pwndbg> si
pwndbg> x/4gx $rsp
pwndbg> print $rbp

callee함수의  도입부를 살펴보면, push rbp를 통해 호출자 caller()의 rbp를 저장한다.

si로 push rbp를 실행하고 스택을 확인해보면 rbp값인 0x7fffffffde50 이 저장된 거 확인 가능함.

 

5. 스택 프레임 할당

pwndbg> x/5i $rip
=> 0x55555555512e <callee+5>: mov    rbp,rsp
   0x555555555131 <callee+8>: mov    QWORD PTR [rbp-0x18],rdi
   0x555555555135 <callee+12>:  mov    DWORD PTR [rbp-0x1c],esi
   0x555555555138 <callee+15>:  mov    DWORD PTR [rbp-0x20],edx
   0x55555555513b <callee+18>:  mov    DWORD PTR [rbp-0x24],ecx

pwndbg> print $rbp
$2 = (void *) 0x7fffffffe300
pwndbg> print $rsp
$3 = (void *) 0x7fffffffe2e8

pwndbg> si

pwndbg> print $rbp
$4 = (void *) 0x7fffffffe2e8
pwndbg> print $rsp
$5 = (void *) 0x7fffffffe2e8

mov rbp, rsp로 rbp와 rsp가 같은 주소를 가리키게 한다. 

si로 실행하고 레지스터를 보면 둘이 같은 주소를 가리키고 있음.

 

6. 반환값 전달

pwndbg> b *callee+79
Breakpoint 3 at 0x555555555178
pwndbg> c

 

pwndbg> b *callee+91
Breakpoint 4 at 0x555555555184
pwndbg> c

덧셈 연산을 모두 마치고, 함수의 종결부에 도달하면, 반환값을 rax에 옮긴다. 반환 직전에 rax를 출력하면 전달한 7개 인자의 합인 123456789123456816을 확인할 수 있다.

 

pwndbg> print $rax
$1 = 123456789123456816

 

7. 반환

pwndbg> d
pwndbg> b *callee+90
Breakpoint 1 at 0x1183
pwndbg> r
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
 ► 0x555555555183 <callee+90>                     pop    rbp
   0x555555555184 <callee+91>                     ret
    ↓
...

pwndbg> si
pwndbg> si
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
   0x555555555183 <callee+90>                     pop    rbp
   0x555555555184 <callee+91>                     ret
    ↓
 ► 0x5555555551bc <caller+55>                     add    rsp, 8
   0x5555555551c0 <caller+59>                     nop
   0x5555555551c1 <caller+60>                     leave
   0x5555555551c2 <caller+61>                     ret
    ↓
...
pwndbg> print $rbp
$1 = (void *) 0x7fffffffe300
pwndbg> print $rip
$2 = (void (*)()) 0x5555555551bc <caller+55>