System Hacking - Stage 5-1 : Stack Buffer Overflow - Calling Convention

진주찬·2023년 5월 8일
0

System Hacking

목록 보기
4/6

1. 서론

함수 호출 규약

: 함수의 호출 및 반환에 대한 약속
함수를 호출할 때는 반환된 이후를 위해 호출자의 상태 및 반환주소를 저장해야 함
호출자는 피호출자가 요구하는 인자를 전달해줘야 하고 피호출자의 실행이 종료될 때는 반환 값을 전달받아야 함

함수 호출 규약의 종류

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

CPU의 아키텍처가 같아도, 컴파일러가 다르면 적용하는 호출 규약이 다를 수 있음
C언어 컴파일 할 때 윈도우 - MSVC, 리눅스 - gcc

다양한 함수 호출 규약

x86
-cdecl
-stdcall
-fastcall
-thiscall

x86-64
-System V AMD64 ABI의 Calling Convention
-MS ABI의 Calling Convention

2. x86호출 규약: cdecl

x86아키텍처는 레지스터의 수가 적으므로, 스택을 통해 인자를 전달
인자를 전달하기 위해 사용한 스택을 호출자가 정리함

3. x86-64호출 규약: SYSV

SYSV

SYSV ABI는 ELF 포맷, 링킹 방법, 함수 호출 규약 등의 내용을 담고 있음
flie 명령어를 이용하여 바이너리의 정보를 살펴보면, 아래와 같이 SYSV 문자열이 포함된 것을 확인 가능

$ file /bin/ls
/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV) ...

SYSV에서 정의한 함수 호출 규약의 특징
1) 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달, 더 많은 인자를 사용해야 할 때는 스택을 추가로 이용
2) Caller에서 인자 전달에 사용된 스택 정리
3) 함수의 반환 값은 RAX로 전달

SYSV 상세 분석

인자 전달

gdb로 sysv를 로드한 후 중단점을 설정하여 caller함수까지 실행하면
context의 DISASM을 보면, caller+6부터 caller+33까지 6개의 인자를 각각의 레지스터에 설정하고, caller+4에서는 7번째 인자인 7을 스택으로 전달

$ gdb -q sysv
pwndbg: loaded 187 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from sysv...(no debugging symbols found)...done.
pwndbg> b *caller
Breakpoint 1 at 0x652
pwndbg> r
Starting program: /home/dreamhack/sysv
Breakpoint 1, 0x0000555555554652 in caller ()
...
───────────────────────────────────[ DISASM ]───────────────────────────────────
 ► 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
...

callee함수를 호출하기 전까지 실행 후 레지스터와 스택을 확인해보면

pwndbg> b *caller+43
Breakpoint 2 at 0x55555555467d
pwndbg> c
Continuing.
Breakpoint 2, 0x000055555555467d in caller ()
...
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
 RAX  0x0
 RBX  0x0
*RCX  0x4
*RDX  0x3
*RDI  0x1b69b4bacd05f15 ;= 123456789123456789
*RSI  0x2
*R8   0x5
*R9   0x6
 R10  0x0
 R11  0x0
...
pwndbg> x/4gx $rsp
0x7fffffffdf78:	0x0000000000000007	0x00007fffffffdf90
0x7fffffffdf88:	0x0000555555554697	0x00005555555546a0

소스코드에서 "callee(123456789123456789, 2, 3, 4, 5, 6, 7)"로 함수를 호출했는데, 인자들이 순서대로 rdi, rsi, rdx, rcx, r8, r9 그리고 [rsp]에 설정되어 있는 것을 확인 가능

반환 주소 저장

si명령어로 한 단계 더 실행시, call 이 실행되고 스택을 확인해보면 0x555555554682가 반환 주소로 저장되어 있다.
gdb로 확인해보면 0x555555554682는 callee호출 다음 명령어의 주소이다.
callee에서 반환됐을 때, 이 주소를 꺼내어 원래의 실행 흐름으로 돌아갈 수 있다.

스택 프레임 저장

"x/5i $rip" 명령어로 callee함수의 도입부(Prologue)를 살펴보면, 가장 먼저 push rbp를 통해 호출자의 rbp를 저장한다.
rbp가 스택프레임의 가장 낮은 주소를 가리키는 포인터이므로, 이를 Stack Frame Pointer(SFP)라고도 부른다.
callee에서 반환될 때, SFP를 꺼내어 caller의 스택 프레임으로 돌아갈 수 있다.

si로 push rbp를 실행하고, 스택을 확인 시 rbp값인 0x00007fffffffdf80가 저장된 것을 확인 가능하다.

스택 프레임 할당

"mov rbp, rsp"로 rbp와 rsp가 같은 주소를 가리키게 한 후, rsp의 값을 빼게 되면 rbp와 rsp의 사이 공간을 새로운 스택 프레임으로 할당하지만, callee 함수는 지역 변수를 사용하지 않으므로, 새로운 스택 프레임을 만들지 않는다.

si로 실행하면 이 둘이 같은 주소를 가리키는 것을 확인할 수 있다.

반환값 전달

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

반환 : 저장해뒀던 스택 프레임과 반환 주소를 꺼내면서 이루어짐

callee 함수가 스택 프레임을 만들지 않았으므로, pop rbp로 스택 프레임을 꺼낼 수 있으나 일반적으로 "leave"로 스택 프레임을 꺼냅니다.

스택 프레임을 꺼낸 뒤에는, ret로 호출자로 복귀한다.
앞에서 저장해뒀던 sfp로 rbp가, 반환 주소로 rip가 설정된 것을 확인할 수 있다.

0개의 댓글