- Published on
[Dreamhack] 시스템 해킹 로드맵 부수기 - 2
- Authors
- Name
- Yeowoong Park
- @ZEROCOKE_2162
오늘은 Stack Buffer Overflow 만 하다 갈거임. 그렇게 아십셔. 일단 호출 규약부터 시작!
BG - Calling Covention
종류
x86
- cdecl -> linux
- stdcall
- fastcall -> 윈도 x86
- thiscall
x86-64
- SYSTEM V AMD64 ABI Calling Convention -> 고냥 바로 fastcall (Linux)
- 줄여서, SYSV 라고 함.
- MS ABI Calling Convention -> 윈도 x64에서 사용하는 Calling Convention
- 지금부터 할건 리눅스 기반 Calling Convention임.
x86-64(x64)
SYSV Calling Convention
// 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(); }
위와 같은 코드를 컴파일 후 디버거를 붙여보면
|──────────────────────[ DISASM / x86-64 / set emulate on]──────────────────────|
||► 0x555555555185 <caller> endbr64|
||0x555555555189 <caller+4> push rbp|
||0x55555555518a <caller+5> mov rbp, rsp|
||0x55555555518d <caller+8> push 7|
||0x55555555518f <caller+10> mov r9d, 6|
||0x555555555195 <caller+16> mov r8d, 5|
||0x55555555519b <caller+22> mov ecx, 4|
||0x5555555551a0 <caller+27> mov edx, 3|
||0x5555555551a5 <caller+32> mov esi, 2|
||0x5555555551aa <caller+37> movabs rax, 0x1b69b4bacd05f15|
||0x5555555551b4 <caller+47> mov rdi, rax|
||0x5555555551b7 <caller+50> call 0x555555555129 <callee>|
||0x5555555551bc <caller+55> add rsp,0x8|
위에 보다시피 10 ~ 37번까진 각 레지스터로 인자값을 넣는것을 볼 수 있다. 8번부터는 레지스터가 아닌 스택에 인자를 저장하는 것을 볼 수 있다.
이제 callee 함수 호출 직전으로 가보면,
|─────────────[ REGISTERS / show-flags off / show-compact-regs off]─────────────|
||RAX 0x1b69b4bacd05f15|
||RBX 0x0|
||RCX 0x4|
||RDX 0x3|
||RDI 0x1b69b4bacd05f15|
||RSI 0x2|
||R8 0x5|
||R9 0x6|
||R10 0x7ffff7fc3908 ◂— 0xd00120000000e|
||R11 0x7ffff7fde680 (_dl_audit_preinit) ◂— endbr64|
||...|
|||
||pwndbg> x/4gx $rsp|
||0x7fffffffe2f8: 0x0000000000000007 0x00007fffffffe310|
||0x7fffffffe308: 0x00005555555551d5 0x0000000000000001|
인자들이 rdi, rsi, rdx, rcx, r8, r9 그리고 rsp(스택)에 저장되어 있는것을 알 수 있다. 이게 바로 인자가 들어가는 순서다.
이번엔 call 이 실행된 뒤를 보겠다.
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
보면 rsp에 0x0000555555554682가 저장되어 있다. 저 값이 바로 call 다음 주소이다. call이 끝나면 기존 실행 흐름으로 돌아오기 위해 스택에 주소를 저장하게 된다.
이번엔 callee 함수의 프롤로그를 보겠다.
pwndbg> x/9i $rip
=> 0x555555555129 <callee>: endbr64
0x55555555512d <callee+4>: push rbp
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
0x55555555513e <callee+21>: mov DWORD PTR [rbp-0x28],r8d
0x555555555142 <callee+25>: mov DWORD PTR [rbp-0x2c],r9d
pwndbg> si
pwndbg> si
0x000055555555512e in callee ()
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
0x555555555129 <callee> endbr64
0x55555555512d <callee+4> push rbp
► 0x55555555512e <callee+5> mov rbp, rsp
0x555555555131 <callee+8> mov qword ptr [rbp - 0x18], rdi
...
pwndbg> x/4gx $rsp
0x7fffffffe2e8: 0x00007fffffffe300 0x00005555555551bc
0x7fffffffe2f8: 0x0000000000000007 0x00007fffffffe310
pwndbg> print $rbp
$1 = (void ) 0x7fffffffe300
4번을 보면 push rbp
를 하는데 이걸 SFP 라고 부른다. 나중에 callee 함수에서 return 될 때, pop 해서 스택프레임으로 돌아갈 때 사용한다.
이번엔 에필로그를 보자.
pwndbg> b callee+79
Breakpoint 3 at 0x555555555178
pwndbg> c
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
► 0x555555555178 <callee+79> add rax, rdx
0x55555555517b <callee+82> mov qword ptr [rbp - 8], rax
0x55555555517f <callee+86> mov rax, qword ptr [rbp - 8]
0x555555555183 <callee+90> pop rbp
0x555555555184 <callee+91> ret
pwndbg> b callee+91
Breakpoint 4 at 0x555555555184
pwndbg> c
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
0x555555555178 <callee+79> add rax, rdx
0x55555555517b <callee+82> mov qword ptr [rbp - 8], rax
0x55555555517f <callee+86> mov rax, qword ptr [rbp - 8]
0x555555555183 <callee+90> pop rbp
► 0x555555555184 <callee+91> ret <0x5555555551bc; caller+55>
↓
...
pwndbg> print $rax
$1 = 123456789123456816
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>
반환값을 rax에 옮기고, 최종적으로 스택프레임으로 가서 return address 로 반환값을 가지고 return 한다. 여기서 callee 함수에선 스택프레임을 만들지 않아서 pop rbp
를 했지만, 대부분 leave로 처리한다.
x86
- cdecl Calling Convention
// 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은 레지스터가 아닌 스택에 인자 값을 저장함. 위에 callee 함수를 호출하면서 1, 2를 인자 값을 줌. 스택을 통해 인자 값을 줄 땐, 마지막 인자부터 첫 번째 인자까지 거꾸로 스택에 push 함.
push 2 # 2번째 인자 값
push 1 # 1번째 인자 값
call callee # 함수 호출
add esp, 8 # 스택 정리
nop
ret