Background: Calling Convention
# 서론
1. 함수 호출 규약
- 함수 호출 규약: 함수의 호출 및 반환에 대한 약속
- 한 함수에서 다른 함수를 호출할 때, 프로그램 실행 흐름은 다른 함수로 이동함
- 호출한 함수가 반환하면, 다시 원래의 함수로 돌아와서 기존의 실행 흐름을 이어나감
- 함수를 호출할 때는 반환된 이후를 위해 호출자(Caller)의 상태(Stack Frame) 및 반환 주소(Return Address)를 지정해야 함
- 호출자는 피호출자(Callee)가 요구하는 인자를 전달해줘야 하며, 피호출자의 실행이 종료될 때는 반환 값을 전달받아야 함
2. 함수 호출 규약 종류
- 컴파일러는 지원하는 호출 규약 중, CPU 아키텍처에 적합한 것을 선택함
- x86(32bit) 아키텍처는 레지스터를 통해 피호출자의 인자를 전달하기에는 레지스터의 수가 적으므로, 스택으로 인자를 전달하는 규약을 사용
- x86-64 아키텍처에서는 레지스터가 많으므로 적은 수의 인자는 레지스터만 사용해서 인자를 전달하고, 인자가 너무 많을 때만 스택을 사용
- CPU의 아키텍처가 같아도, 컴파일러가 다르면 적용하는 호출 규약이 다를 수 있음
- C언어의 경우 컴파일 할 때 윈도우에서는 MSVC를, 리눅스에서는 gcc를 많이 사용함
- x86-64 아키텍처
- MSVC: MS x64 호출 규약 적용
- gcc: SYSTEM V 호출 규약 적용
# x86호출 규약: cdecl
- x86 아키텍처는 레지스터의 수가 적으므로, 스택을 통해 인자를 전달함
- 인자를 전달하기 위해 사용한 스택을 호출자가 정리하는 특징이 있음
- 스택을 통해 인자를 전달할 때는, 마지막 인자부터 첫 번째 인자가 거꾸로 스택에 push 됨
- Figure 1: 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 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
💡 컴파일의 정확한 의미
- 컴파일(Compile)의 정확한 의미는 어떤 언어로 작성된 소스 코드(Source code)를, 다른 언어의 목적 코드(Object code)로 번역하는 것
- 소스 코드를 어셈블리어로, 또는 소스 코드를 기계어로 번역하는 행위 모두 컴파일의 범주에 포함됨
- C언어를 실행 가능한 바이너리로 만드는 과정을 보통 전처리, 컴파일, 어셈블, 링크의 4단계로 구분하는데, 이를 합해서 '컴파일'이라고 부를 수 있는 것도 위와 같은 이유임
# x86-64 호출 규약: SYSV
1. SYSV
- 리눅스는 SYSTEM V(SYSV) Application Binary Interface(ABI)를 기반으로 만들어졌음
- SYSV ABI는 ELF 포맷, 링킹 방법, 함수 호출 규약 등의 내용을 담고 있음
- file 명령어를 이용하여 바이너리의 정보를 살펴보면, 아래와 같이 SYSV 문자열이 포함된 것을 확인할 수 있음
- SYSV에서 정의한 함수 호출 규약은 다음의 특징을 가짐
- 6개의 인자는 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달하고 더 많은 인자를 사용해야 할 때는 스택을 추가로 사용함
- Caller에서 인자 전달에 사용된 스택을 정리
- 함수의 반환 값은 RAX로 전달함
- Figure 2. sysv.c
// 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(); }
2. SYSV 상세 분석
- SYSV를 gdb로 분석해볼 것
gcc -fno-asynchronous-unwind-tables -masm=intel -fno-omit-frame-pointer -o sysv sysv.c -fno-pic -O0
2.1 인자 전달 🚚
- gdb로 sysv를 로드한 후 중단점을 설정하여 caller 함수까지 실행
- context의 DISASM을 보면, caller+6부터 caller+33까지 6개의 인자를 각각의 레지스터에 설정하고 있으며, caller+4에서는 7번째 인자인 7을 스택으로 전달하고 있음 callee 함수를 호출하기 전까지 실행하고, 레지스터와 스택을 확인함
- callee 함수를 호출하기 전까지 실행하고, 레지스터와 스택을 확인함
- 소스코드에서 callee(123456789123456789, 2, 3, 4, 5, 6, 7)로 함수를 호출했음
- 인자들이 순서대로 rdi, rsi, rcx, r8, r9 그리고 [rsp]에 설정되어 있음을 확인할 수 있음
2.2 반환 주소 저장 🏠
- si 명령어로 한 단계를 더 실행시킴
- call이 실행되고 스택을 확인해보면 0x555555554682가 반환 주소로 저장되어 있음
- gdb로 확인해보면 0x555555554682는 callee 호출 다음 명령어의 주소임
- callee에서 반환됐을 때, 이 주소를 꺼내어 원래의 실행 흐름으로 돌아갈 수 있음
2.3 스택 프레임 저장
- x/5i $rip 명령어로 callee 함수의 도입부(Prologue)를 살펴보면, 가장 먼저 push rbp를 통해 호출자의 rbp를 저장하고 있음
- rbp가 스택 프레임의 가장 낮은 주소를 가리키는 포인터이므로, 이를 Stack Frame Pointer(SFP)라고도 부름
- callee에서 반환될 때, SFP를 꺼내어 caller의 스택 프레임으로 돌아갈 수 있음
- si로 push rbp를 실행하고(callee+1에 breakpoint가 걸림), 스택을 확인해보면 rbp 값인 0x00007fffffffdf80 가 저장된 것을 확인할 수 있음
2.4 스택 프레임 할당
- mov rbp, rsp 명령어를 통해 rbp와 rsp가 같은 주소를 가리키게 함
- 바로 다음에 rsp의 값을 빼게 되면, rbp와 rsp 사이 공간을 새로운 스택 프레임으로 할당하는 것이지만, callee 함수는 지역 변수를 사용하지 않으므로, 새로운 스택 프레임은 만들지 않음
- si로 실행하고, 레지스터를 보면 이 둘이 같은 주소를 가리키는 것을 확인할 수 있음
💡 Callee함수에서 ret라는 지역 변수를 선언하지 않았나요?
- 코드를 보면, ret를 선언하기는 했으나, 반환 값을 저장하는 용도 외로는 사용하고 있지 않음
- gcc는 이런 변수에 대해 스택을 할당하지 않고, rax를 직접 사용함
int 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;
}
2.5 반환 값 전달
- 덧셈 연산을 모두 마치고, 함수의 종결부(Epilogue)에 도달하면, 반환 값을 rax에 옮김
- 반환 직전에 rax를 출력하면 전달한 7개 인자의 합인 123456789123456816을 확인할 수 있음
2.6 반환
- 반환은 저장해뒀던 스택 프레임과 반환 주소를 꺼내면서 이루어짐
- 여기서는 callee 함수가 스택 프레임을 만들지 않았기 때문에, pop rbp로 스택 프레임을 꺼낼 수 있지만, 일반적으로 leave로 스택 프레임을 꺼냄
- 스택 프레임을 꺼낸 뒤에는, ret을 통해 호출자로 복귀함
- 앞에서 저장했던 sfp로 rbp가, 반환 주소로 rip가 설정된 것을 확인할 수 있음
# 결론
- x86 함수 호출 규약
- x86-64 함수 호출 규약
# Quiz
1번)
2번)
3번)
4번)
5번)
6번)
7번)
'Hacking Tech > System Hacking' 카테고리의 다른 글
[Dreamhack] Mitigation: Stack Canary (0) | 2022.09.04 |
---|---|
[Dreamhack] Memory Corruption: Stack Buffer Overflow (0) | 2022.09.01 |
[Dreamhack] Exploit Tech: Shellcode (0) | 2022.04.03 |
[Dreamhack] Tool: pwntools (0) | 2022.03.28 |
[Dreamhack] Tool: gdb (0) | 2022.03.27 |