SYSV
리눅스는 SYSTEM V(SYSV) Application Binary Interface(ABI)를 기반으로 만들어졌다.
SYSV에서 정의한 함수 호출 규약은 3가지 특징을 가진다.
- 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달합니다. 더 많은 인자를 사용해야 할 때는 스택을 추가로 이용한다.
- Caller에서 인자 전달에 사용된 스택을 정리한다.
- 함수의 반환 값은 RAX로 전달한다.
// 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(); }
해당 코드는 sysv함수 호출 규약이다. sysv를 분석해보겠다.
1.인자전달
gdb로 sysv를 로드한 후 중단점을 설정한다. callee 함수를 호출하기 전까지 실행하고, 레지스터와 스택을 확인해본다. disass 명령어로 caller()의 디스어셈블된 코드를 보고 callee()를 호출하는 부분을 파악한 후 해당 부분에 중단점을 설정한다. c 명령어를 사용해서 프로그램 실행을 진행하면 callee() 를 호출하기 직전에 멈춥니다. 코드로 보면 아래의 순서다.
$ gdb -q sysv
pwndbg: loaded 139 pwndbg commands and 49 shell commands. Type pwndbg [--shell | --all] [filter] for a list.
pwndbg: created $rebase, $ida GDB functions (can be used with print/break)
Reading symbols from sysv...
...
pwndbg> b *caller
Breakpoint 1 at 0x1185
pwndbg> r
Starting program: /home/dreamhack/sysv
Breakpoint 1, 0x0000555555555185 in 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
...
pwndbg> disass caller
...
0x00005555555551b7 <+50>: call 0x555555555129 <callee>
...
pwndbg> b *caller+50
Breakpoint 2 at 0x5555555551b7
pwndbg> c
Continuing.
Breakpoint 2, 0x00005555555551b7 in caller ()
...
─────────────[ 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
2. 반환 주소 저장
si 명령어로 한 단계 더 실행시킵니다.
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
3. 스택 프레임 저장
x/5i $rip 명령어로 callee함수의 도입부(Prologue)를 살펴보면, 가장 먼저 push rbp를 통해 호출자(caller())의 rbp를 저장하고 있다. rbp가 스택프레임의 가장 낮은 주소를 가리키는 포인터이므로, 이를 Stack Frame Pointer (SFP)라고도 부릅른다. callee에서 반환될 때, SFP를 꺼내어 caller의 스택 프레임으로 돌아갈 수 있다.
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. 스택 프레임 할당
mov rbp, rsp로 rbp와 rsp가 같은 주소를 가리키게 한다. callee 함수는 지역 변수를 사용하지 않으므로, 새로운 스택 프레임을 만들지 않는다. si로 실행하고, 레지스터를 보면 이 둘이 같은 주소를 가리키는 것을 확인할 수 있습니다.
pwndbg> x/5i $rip
=> 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
pwndbg> print $rbp
$2 = (void *) 0x7fffffffe300
pwndbg> print $rsp
$3 = (void *) 0x7fffffffe2e8
pwndbg> si
pwndbg> print $rbp
$4 = (void *) 0x7fffffffe2e8
pwndbg> print $rsp
$5 = (void *) 0x7fffffffe2e8
5. 반환값 전달
덧셈 연산을 모두 마치고, 함수의 종결부(Epilogue)에 도달하면, 반환값을 rax에 옮긴다.
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
6 반환
반환은 저장해뒀던 스택 프레임과 반환 주소를 꺼내면서 이루어진다. 스택 프레임을 꺼낸 뒤에는, ret로 호출자로 복귀다.
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>
cdecl
스택을 통해 인자를 전달합니다. 또한, 인자를 전달하기 위해 사용한 스택을 호출자가 정리하는 특징이 있다. 스택을 통해 인자를 전달할 때는, 마지막 인자부터 첫 번째 인자까지 거꾸로 스택에 push한다.
퀴즈
A.push 0x3
이유:스택에 3개의 인수를 차례대로 push했기 때문이다.
Q2. 아래 코드를 컴파일 했을 때, 컴파일된 어셈블리 코드 중 (b)에 들어갈 것으로 가장 적절할 것을 고르시오.
A.push 0x2
이유:스택에 3개의 인수를 차례대로 push했기 때문이다.
Q3. 아래 코드를 컴파일 했을 때, 컴파일된 어셈블리 코드 중 (c)에 들어갈 것으로 가장 적절할 것을 고르시오.
A.push 0x1
이유:스택에 3개의 인수를 차례대로 push했기 때문이다.
A. add esp, 0xc
sum 함수를 호출한 뒤에 push된 인수들을 스택에서 제거한다. esp를 12바이트 증가시켜 푸시된 3개의 인수를 스택에서 제거한다.
Q5. SYSV를 적용하여 아래 코드를 컴파일 했을 때, 컴파일된 어셈블리 코드 중 (a)에 들어갈 것으로 가장 적절한 것을 고르시오.
A. mov edi, 0x3
인수 1을 rdi 레지스터에 저장한
Q6. SYSV를 적용하여 아래 코드를 컴파일 했을 때, 컴파일된 어셈블리 코드 중 (b)에 들어갈 것으로 가장 적절한 것을 고르시오.
A. mov esi, 0x2
인수 2를 rsi 레지스터에 저장한다.
Q7. SYSV를 적용하여 아래 코드를 컴파일 했을 때, 컴파일된 어셈블리 코드 중 (c)에 들어갈 것으로 가장 적절한 것을 고르시오.
A, mov edi, 0x1
세 번째 인수 3을 rdx 레지스터에 저장
Bomb
여기서 정수를 2개 입력받고, 첫번째 숫자가 7보다 클 시 폭발함을 알 수 있다. 값을 옮기는 부분이 사진에서는 잘렸지만 총 7개가 있다. 첫번째로 입력된 숫자에 따라 0에서 7사이인지를 고려하여 터지는지 안 터지는지가 결정된다. 첫 숫자를 2로 입력하면 이동하여 2c3이 된다. 16진수이므로 10진수로 바꾸면 707이 된다.