Post

Calling Convention x64

Calling Convention x86 설명에 이어서 Calling Convention x64를 설명하겠다.

x64

SYSV

리눅스는 SYSTEM V(SYSV) Application Binary Interface(ABI)를 기반으로 만들어졌다. SYSV ABI는 ELF 포맷, 링킹 방법, 함수 호출 규약 등의 내용을 담고 있다. SYSV에서 정의한 함수 호출 규약은 다음의 특징을 갖는다.

특징

  1. 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달한다. 더 많은 인자를 사용해야 할 때는 스택을 추가로 이용한다.
  2. Caller에서 인자 전달에 사용된 스택을 정리
  3. 함수의 반환 값은 RAX로 전달
1
2
3
4
5
6
7
8
9
10
11
12
// Name: sysv.c
// Compile: gcc -fno-asynchronous-unwind-tables  -masm=intel -fno-omit-frame-pointer -S sysv.c -fno-pic -O0
#include <stdio.h>
#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(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
callee:
        endbr64
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-24], rdi ; 첫번째 인자 저장
        mov     DWORD PTR [rbp-28], esi ; 두번째 인자 저장
        mov     DWORD PTR [rbp-32], edx ; 세번째 인자 저장
        mov     DWORD PTR [rbp-36], ecx ; 네번째 인자 저장
        mov     DWORD PTR [rbp-40], r8d ; 다섯번째 인자 저장
        mov     DWORD PTR [rbp-44], r9d ; 여섯번째 인자 저장
        ; 연산 과정 ~
        mov     eax, DWORD PTR [rbp-28]
        movsx   rdx, eax
        mov     rax, QWORD PTR [rbp-24]
        add     rdx, rax
        mov     eax, DWORD PTR [rbp-32]
        cdqe
        add     rdx, rax
        mov     eax, DWORD PTR [rbp-36]
        cdqe
        add     rdx, rax
        mov     eax, DWORD PTR [rbp-40]
        cdqe
        add     rdx, rax
        mov     eax, DWORD PTR [rbp-44]
        cdqe
        add     rdx, rax
        mov     eax, DWORD PTR [rbp+16]
        cdqe
        add     rax, rdx                
        ; ~ 연산 과정
        mov     QWORD PTR [rbp-8], rax  ; 최종 결과 값을 스택에 저장
        mov     rax, QWORD PTR [rbp-8]
        pop     rbp
        ret
        .size   callee, .-callee
        .globl  caller
        .type   caller, @function
caller:
        endbr64
        push    rbp
        mov     rbp, rsp
        push    7       ; 7 스택에 저장하여 callee의 인자로 전달
        mov     r9d, 6  ; 6 r9에 저장하여 callee의 인자로 전달
        mov     r8d, 5  ; 5 r8에 저장하여 callee의 인자로 전달
        mov     ecx, 4  ; 4 ecx(rcx) 저장하여 callee의 인자로 전달
        mov     edx, 3  ; 3 edx(rdx) 저장하여 callee의 인자로 전달
        mov     esi, 2  ; 2 esi(rdi) 저장하여 callee의 인자로 전달
        movabs  rax, 123456789123456789
        mov     rdi, rax ; 123456789123456789 rdi에 저장하여 callee의 인자로 전달
        call    callee
        add     rsp, 8  ; 스택 정리(push를 1번하였기 때문에, 8byte만큼 rsp가 증가)
        nop
        leave
        ret
        .size   caller, .-caller
        .globl  main
        .type   main, @function
main:
        endbr64
        push    rbp
        mov     rbp, rsp
        mov     eax, 0
        call    caller
        mov     eax, 0
        pop     rbp
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
        .section        .note.GNU-stack,"",@progbits
        .section        .note.gnu.property,"a"

분석

sysv.cgcc -fno-asynchronous-unwind-tables -masm=intel -fno-omit-frame-pointer -o sysv sysv.c -fno-pic -O0로 컴파일 해서 gdb를 이용하여 분석한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
gdb ./sysv

pwndbg> b *caller
Breakpoint 1 at 0x1185
pwndbg> r
Starting program: /home/br0nzu/Desktop/sysv
...
pwndbg> disass caller
Dump of assembler code for function caller:
=> 0x0000555555555185 <+0>:     endbr64 
   0x0000555555555189 <+4>:     push   rbp
   0x000055555555518a <+5>:     mov    rbp,rsp
   0x000055555555518d <+8>:     push   0x7
   0x000055555555518f <+10>:    mov    r9d,0x6
   0x0000555555555195 <+16>:    mov    r8d,0x5
   0x000055555555519b <+22>:    mov    ecx,0x4
   0x00005555555551a0 <+27>:    mov    edx,0x3
   0x00005555555551a5 <+32>:    mov    esi,0x2
   0x00005555555551aa <+37>:    movabs rax,0x1b69b4bacd05f15
   0x00005555555551b4 <+47>:    mov    rdi,rax
   0x00005555555551b7 <+50>:    call   0x555555555129 <callee>
   0x00005555555551bc <+55>:    add    rsp,0x8
   0x00005555555551c0 <+59>:    nop
   0x00005555555551c1 <+60>:    leave  
   0x00005555555551c2 <+61>:    ret
...
pwndbg> b *caller+50
Breakpoint 2 at 0x5555555551b7
pwndbg> c
Continuing.

caller+50callee()가 있기 때문에 callee()에 bp를 걸어서 함수 호출 규약이 어떻게 적용되고 있는지 확인한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
Breakpoint 2, 0x00005555555551b7 in caller ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────
*RAX  0x1b69b4bacd05f15
 RBX  0x0
*RCX  0x4
*RDX  0x3
*RDI  0x1b69b4bacd05f15
*RSI  0x2
*R8   0x5
*R9   0x6
...
*RSP  0x7fffffffdeb8 ◂— 0x7  

caller가 callee에게 넘겨줄 때 레지스터를 보면, callee(123456789123456789, 2, 3, 4, 5, 6, 7)에서 인자들이 순서대로 rdi, rsi, rdx, rcx, r8, r9 그리고 rsp에 저장되어 있는 모습을 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> b *callee+79
Breakpoint 3 at 0x555555555178
pwndbg> c
Continuing.

Breakpoint 3, 0x0000555555555178 in callee ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────
*RAX  0x7
 RBX  0x0
 RCX  0x4
*RDX  0x1b69b4bacd05f29
...

pwndbg> b *callee+91
Breakpoint 4 at 0x0000555555555184
pwndbg> c
Continuing.

Breakpoint 4, 0x0000555555555184 in callee ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
 RAX  0x1b69b4bacd05f30
...

si하고 callee() 내부를 살펴보면, 마지막 반환 부분(ret)에서 함수의 반환 값은 RAX로 전달되는 것을 볼 수 있다.

Ref

[1] 드림핵 시스템 해킹 강의 Stack Buffer Overflow