함수 호출 규약
(1) 함수 호출 규약이란?
함수 호출 규약은 함수의 호출 및 반환에 대한 약속이다.
✅ 함수 호출 과정
1. 한 함수에서 다른 함수 호출 시, 프로그램 실행 흐름은 다른 함수로 이동
2. 호출한 함수가 반환 시, 원래의 함수로 돌아와서 기존의 실행 흐름을 이어감
✅ 함수 호출의 특징
- 함수 호출 시, 반환된 이후를 위해 호출자의 상태, 반환 주소를 저장해야 함
- 호출자는 피호출자가 요구하는 인자를 전달해줘야 하며, 피호출자의 실행 종료 시 반환 값을 전달받아야 함
- 함수 호출 규약 적용은 컴파일러의 몫
(2) 함수 호출 규약의 종류
[ x86 ]
- cdecl
- stdcall
- fastcall
- thiscall
[ x86-64 ]
- System V AMD64 ABI의 Calling Convention
- MS ABI의 Calling Convention
x86-64 호출규약: SYSV
(1) SYSV
: 리눅스는 SYSTEM V (SYSV) Application Binary Interface (ABI)를 기반으로 만들어진 것으로, SYSV ABI는 ELF 포맷, 링킹 방법, 함수 호출 규약 등의 내용을 담고 있음.
✅ 함수 호출 규약의 특징
- 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달한다. 더 많은 인자를 사용해야 할 때는 스택을 추가로 이용한다
- Caller 에서 인자 전달에 사용된 스택을 정리한다
- 함수의 반환 값은 RAX로 전달한다
✅ 분석 진행할 sysv.c 코드
// Name: sysv.c // Compile: gcc -fno-asynchronous-unwind-tables -masm=intel -fno-omit-frame-pointer -o sysv 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(); }
💡 sysv.c 코드 컴파일 명령어 옵션 설명
1) -fno-asynchronous-unwind-tables : 디버거 혹은 가비지 콜렉터에서 사용할 수 있는 DWARF 2 형식의 해석 테이블(unwind table)을 만들지 않도록 함
2) -masm=intel : intel 방식
3) -nostdlib 옵션 : 링크 시에 표준 C 라이브러리를 사용하지 않음
4) -fno-omit-frame-pointer : 함수 호출을 하면, 함수에 전달되는 인자 및 함수 안에서 사용되는 지역 변수들을 위해 stack frame 이라는 것을 형성하게 되는데, 이때 스택의 특정 위치를 가리키게 되는 포인터가 frame pointer 이다. 이런 작업을 하지 않아도 될 함수들은 frame pointer 를 제거해서 거기에 필요한 연산을 줄이는 옵션이다.
5) -o sysv : -o [실행 할 파일명]
6) -O0 : 전혀 최적화 하지 말라는, default 옵션이다
출처 : https://dreamhack.io/forum/qna/3776/
(2) SYSV 상세분석 - 인자 전달
1. [ gdb -q sysv ] 로 sysv 디버깅 진행
2. [ b *claller ] 로 중단점 설정 후 caller 함수까지 실행 [→ r 명령어]
DISASM 부분을 확인해보면 caller+10부터 caller+37까지 총 6개의 인자를 각각의 레지스터에 설정하고 있으며, caller+8 부분에는 7번째 인자인 7을 스택으로 전달하고 있는 것을 확인할 수 있다.
disass 명령어로 caller()의 디스어셈블 된 코드 확인 후 callee()를 호출한 부분을 확인하고 이후 중단점을 설정할 수 있다.
disass 명령어 : 해당 소스코드를 어셈블리어로 변경한 결과
caller : 호출자 즉, 함수를 호출한 쪽
callee : 피호출자 즉, 호출을 당한 함수
참고자료 | 함수 호출 규약
c (continue) 명령어를 사용하여 이후 프로그램을 계속 실행하고 레지스터를 확인해보니 인자들이 RDI, RSI, RDX, RCX, R8, R9에 차례로 저장되어 있고, 스택으로 전달하고 있는 7번째 인자도 RSP에 설정되어 있는 것을 확인할 수 있었다.
(3) SYSV 상세 분석 - 반환 주소 저장 & 스택 프레임 저장
우선, si 명령어로 서브루틴의 내부로 들어갈 수 있다.
이후 [ x/4gx $rsp ] 명령어를 통해 반환 주소가 [ 0x00005555555551bc ] 으로 저장되어 있음을 확인할 수 있다. 이를 [ x/10i 0x00005555555551bc - 5] 명령어를 통해 반환 주소에서 5바이트 뺀 위치를 시작주소로 설정하여 해당 부분부터 10개의 연속된 기계어 명령어를 에섬블리 언어로 확인할 수 있다.
💡 메모리 검사 관련 명령어
- x : examine의 약자로 메모리의 내용을 검사하는 명령어
- /(숫자) : x 명령어 뒤에 나오는 숫자는 데이터 항목의 수를 의미
- g : 64비트 (8바이트) 크기의 값들을 의미
- x : 16진수 형식으로 출력하라는 의미
- i : 명령어를 디스어셈블하는 형식으로 출력하는 의미
- $rsp : 현재 스택 포인터의 값을 나타내는 레지스터
callee 함수의 도입부를 살펴보면, [ push rbp ] 를 통해 호출자 (caller)의 rbp를 저장하고 있다.▶ rbp : 스택프레임의 가장 낮은 주소를 가리키는 포인터로 Stack Frame Pointer (SFP)라고도 부름
si를 두번 실행해서 push rbp를 진행한 후 스택을 확인해보면 rbp 값으로 [ 0x7fffffffde30 ]이 된 것을 확인할 수 있다.
(4) SYSV 상세 분석 - 스택 프레임 할당 & 반환 값 전달
[ mov rbp, rsp ]로 rbp와 rsp가 같은 주소를 가리키게 한다. 이후 si로 실행하고 rbp와 rsp 레지스터를 확인해보면 같은 주소를 가리키고 있는 것을 확인할 수 있다.
덧셈 연산을 모두 마친 후 함수의 종결부에 도달하면 반환값을 rax에 옮길 수 있다.
반환 직전에 rax를 출력하면 전달한 7개 인자의 합인 123456789123456816을 확인할 수 있다.
(5) SYSV 상세 분석 - 반환
반환은 저장해둔 스택 프레임과 반환 주소를 꺼내면서 이루어질 수 있다.
여기서는 callee 함수가 스택프레임을 만들지 않았기 때문에 [ pop rbp ]로 스택프레임을 꺼낼 수 있지만, 일반적으로 leave로 스택프레임을 꺼낸다.
pop rbp 명령어를 실행하는 callee+90에 bp 즉, 종단점을 걸어두고 r 명령어로 실행한 이후 si 명령어를 두번 실행하면 pop rbp로 돌아갈 함수의 스택프레임을 다시 가져오고 그 이후에는 ret 로 호출자로 복귀한다.
이때, 앞에서 저장해둔 sfp로 rbp가, 반환주소로 rip가 설정된 것을 확인할 수 있다.
x86호출 규약: cdecl
x86아키텍처는 레지스터의 수가 적으므로, 스택을 통해 인자를 전달한다. 또한, 인자를 전달하기 위해 사용한 스택을 호출자가 정리하는 특징이 있다. 스택을 통해 인자를 전달할 때는, 마지막 인자부터 첫 번째 인자까지 거꾸로 스택에 push한다.
✅ cdecl 함수 호출 규약 관련 cdecl.c 코드
// 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
출처 : 드림핵 시스템해킹 로드맵 (https://dreamhack.io/lecture/roadmaps/2)
'Security > System Hacking' 카테고리의 다른 글
[Stack Buffer Overflow] Return Address Overwrite (3) | 2024.09.15 |
---|---|
Memory Corruption: Stack Buffer Overflow (0) | 2024.09.15 |
[Dreamhack] pwn-library write-up (0) | 2024.09.15 |
[dreamhack] fho write-up (0) | 2024.09.15 |
[Dreamhack] tcache_dup2 write-up (0) | 2024.09.09 |