void __attribute__((cdecl)) callee(int a1, int a2){
}
void caller(){
callee(1,2);
}
; 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
리눅스는 SYSTEM V(SYSV) Application Binary Interface(ABI)를 기반으로 만들어졌음
SYSV에서 정의한 함수 호출 규약은 다음의 특징을 가짐
#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(); }
위의 코드를 gdb로 자세히 분석해봄
gdb로 sysv를 로드한 후 중단점을 설정하여 caller 함수까지 실행함
$ 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
...
context의 DISASM을 보면, caller+6부터 caller+33까지 6개의 인자를 각각의 레지스터에 설정하고 있고, caller+4에서는 7번째 인자인 7을 스택으로 전달함
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에서 반환되었을 때, 이 주소를 꺼내어 원래의 실행 흐름으로 돌아갈 수 있음
pwndbg> si
0x00005555555545fa in callee ()
...
pwndbg> x/4gx $rsp
0x7fffffffdf70: 0x0000555555554682 0x0000000000000007
0x7fffffffdf80: 0x00007fffffffdf90 0x0000555555554697
pwndbg> x/10i 0x0000555555554682 - 5
0x55555555467d <caller+43>: call 0x5555555545fa <callee>
0x555555554682 <caller+48>: add rsp,0x8
x/5i $rip 명령어로 callee 함수의 도입부를 살펴보면, 가장 먼저 push rbp를 통해 호출자의 rbp를 저장하고 있음
callee에서 반환될 때, sfp를 꺼내어 caller의 스택 프레임으로 돌아갈 수 있음
si로 push rbp를 실행하고, 스택을 확인해보면 rbp값인 0x00007fffffffdf80가 저장된 것을 확인할 수 있음
pwndbg> x/5i $rip
=> 0x5555555545fa <callee>: push rbp
0x5555555545fb <callee+1>: mov rbp,rsp
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
pwndbg> si
0x00005555555545fb in callee ()
...
pwndbg> x/4gx $rsp
0x7fffffffdf68: 0x00007fffffffdf80 0x0000555555554682
0x7fffffffdf78: 0x0000000000000007 0x00007fffffffdf90
pwndbg> print $rbp
$1 = (void *) 0x7fffffffdf80
이제 mov rbp, rsp로 rbp와 rsp가 같은 주소를 가리키게 됨
바로 다음에 rsp의 값을 빼게 되면, rbp와 rsp의 사이 공간을 새로운 스택 프레임으로 할당하는 것이지만, callee 함수는 지역 변수를 사용하지 않으므로, 새로운 스택 프레임을 만들지 않음
si로 실행하고, 레지스터를 보면 이 둘이 같은 주소를 가리키는 것을 확인할 수 있음
pwndbg> x/5i $rip
=> 0x5555555545fb <callee+1>: mov rbp,rsp
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
pwndbg> print $rbp
$2 = (void *) 0x7fffffffdf68
pwndbg> print $rsp
$3 = (void *) 0x7fffffffdf68
callee함수에서 선언한 지역 변수 ret은?
ret은 반환 값을 저장하는 용도 외로는 사용되지 않고 있으므로, gcc는 이런 변수에 대해 스택을 할당하지 않고, rax를 직접 사용함
덧셈 연산을 모두 마치고, 함수의 종결부에 도달하면 반환값을 rax에 옮김
pwndbg> b *callee+87
Breakpoint 3 at 0x555555554651
pwndbg> c
Continuing.
Breakpoint 3, 0x0000555555554651 in callee ()
...
pwndbg> x/5i $rip
=> 0x555555554645 <callee+75>: add rax,rdx
0x555555554648 <callee+78>: mov QWORD PTR [rbp-0x8],rax
0x55555555464c <callee+82>: mov rax,QWORD PTR [rbp-0x8]
0x555555554650 <callee+86>: pop rbp
0x555555554651 <callee+87>: ret
pwndbg> c
pwndbg> print $rax
$4 = 123456789123456816
반환은 저장해두었던 스택 프레임과 반환 주소를 꺼내면서 이루어짐.
여기서는 callee 함수가 스택 프레임을 만들지 않았기 때문에 pop rbp로 스택 프레임을 꺼낼 수 있지만, 일반적으로는 leave로 스택 프레임을 꺼냄
스택 프레임을 꺼낸 뒤에는 ret로 호출자로 복귀함
pwndbg> x/5i $rip
=> 0x555555554650 <callee+86>: pop rbp
0x555555554651 <callee+87>: ret
pwndbg> si
pwndbg> si
pwndbg> x/4i $rip
=> 0x555555554682 <caller+48>: add rsp,0x8
0x555555554686 <caller+52>: nop
0x555555554687 <caller+53>: leave
0x555555554688 <caller+54>: ret
pwndbg> print $rbp
$3 = (void *) 0x7fffffffdf80
pwndbg> print $rip
$4 = (void (*)()) 0x555555554682 <caller+48>