← 返回文章列表ROPGOD JOURNAL

二进制 PWN 中最基础的调用约定知识

PWN1232 字

二进制 PWN 中最基础的调用约定知识

二进制中最常用的调用约定知识,做题时真正会用到的地方:函数传参、返回值、栈布局、ROP 构造、ret2libc、syscall 参数。

二进制中最常用的调用约定知识,做题时真正会用到的地方:函数传参、返回值、栈布局、ROP 构造、ret2libc、syscall 参数。

一、什么是调用约定

调用约定就是规定:函数参数放在哪里返回值放在哪里谁负责清理栈哪些寄存器调用前后必须保持不变函数调用时栈怎么变化 在 PWN 里,调用约定直接影响你怎么构造:ret2libc/ROP chain/ret2syscall/ret2csu/SROP/栈迁移/格式化字符串利用/函数参数劫持


二、x86 32 位调用约定

32 位 Linux PWN 里最常见的是 cdecl

1. cdecl 基本规则

参数从右往左压栈。

例如:

C
system("/bin/sh");

实际调用前栈大概是:

PLAINTEXT
push "/bin/sh"call system

如果是:

C
write(1, buf, 8);

压栈顺序是:

PLAINTEXT
push 8push bufpush 1call write

也就是:

PLAINTEXT
参数从右往左入栈返回值放 eax调用者负责清理栈

2. 32 位函数调用栈布局

执行:

C
func(a, b, c);

进入 func 后,栈通常是:

PLAINTEXT
[esp] 返回地址[esp+4] 第 1 个参数 a[esp+8] 第 2 个参数 b[esp+0xc] 第 3 个参数 c

如果函数有标准栈帧:

PLAINTEXT
push ebpmov ebp, espsub esp, xxx

那么:

PLAINTEXT
[ebp+4] 返回地址[ebp+8] 第 1 个参数[ebp+0xc] 第 2 个参数[ebp+0x10] 第 3 个参数 [ebp-4] 局部变量[ebp-8] 局部变量

3. 32 位 ret2libc payload

32 位下调用 system("/bin/sh")

PLAINTEXT
paddingsystem_addrreturn_after_systembinsh_addr

例如:

PYTHON
payload = b"A" * offsetpayload += p32(system)payload += p32(exit)payload += p32(binsh)

为什么这样排?

因为 retsystem 后,栈会变成:

PLAINTEXT
[esp] system 返回地址,也就是 exit[esp+4] system 第 1 个参数,也就是 "/bin/sh"

这正好符合 32 位 cdecl。


4. 32 位多个参数函数

比如:

C
write(1, buf, 8);

payload:

PYTHON
payload = b"A" * offsetpayload += p32(write_plt)payload += p32(main)payload += p32(1)payload += p32(buf)payload += p32(8)

进入 write 时:

PLAINTEXT
[esp] 返回地址 main[esp+4] fd = 1[esp+8] buf[esp+0xc] size = 8

三、x86-64 Linux 调用约定

64 位 Linux 常用的是 System V AMD64 ABI

这是 Linux x86-64 PWN 最重要的调用约定。

1. 前 6 个参数用寄存器传递

顺序是:

PLAINTEXT
第 1 个参数:rdi第 2 个参数:rsi第 3 个参数:rdx第 4 个参数:rcx第 5 个参数:r8第 6 个参数:r9

返回值:

PLAINTEXT
rax

如果参数超过 6 个,多出来的参数才放到栈上。


2. 64 位 ret2libc 调 system

调用:

C
system("/bin/sh");

需要:

PLAINTEXT
rdi = "/bin/sh"rip = system

所以 ROP 链通常是:

PLAINTEXT
paddingpop rdi ; retbinsh_addrsystem_addr

pwntools:

PYTHON
payload = b"A" * offsetpayload += p64(pop_rdi)payload += p64(binsh)payload += p64(system)

因为 pop rdi ; ret 会把栈上的 binsh 弹进 rdi,然后 ret 跳到 system


3. 64 位调用 write

调用:

C
write(1, buf, 8);

需要:

PLAINTEXT
rdi = 1rsi = bufrdx = 8rip = write

ROP:

PYTHON
payload = b"A" * offsetpayload += p64(pop_rdi)payload += p64(1)payload += p64(pop_rsi)payload += p64(buf)payload += p64(pop_rdx)payload += p64(8)payload += p64(write_plt)

如果 gadget 是:

PLAINTEXT
pop rsi ; pop r15 ; ret

那么要多填一个垃圾值:

PYTHON
payload += p64(pop_rsi_r15)payload += p64(buf)payload += p64(0)

四、x86-64 syscall 调用约定

函数调用和系统调用的参数寄存器不完全一样。

Linux x86-64 syscall 参数:

PLAINTEXT
系统调用号:rax 第 1 个参数:rdi第 2 个参数:rsi第 3 个参数:rdx第 4 个参数:r10第 5 个参数:r8第 6 个参数:r9

注意:第 4 个参数是 r10,不是 rcx。


1. execve("/bin/sh", 0, 0)

x86-64 syscall 编号:

PLAINTEXT
execve = 59

需要:

PLAINTEXT
rax = 59rdi = "/bin/sh"rsi = 0rdx = 0syscall

ROP 链:

PYTHON
payload = b"A" * offsetpayload += p64(pop_rax)payload += p64(59)payload += p64(pop_rdi)payload += p64(binsh)payload += p64(pop_rsi)payload += p64(0)payload += p64(pop_rdx)payload += p64(0)payload += p64(syscall)

2. ORW 调用约定

seccomp 禁 execve 时常走 ORW:

PLAINTEXT
open/read/write

x86-64 syscall:

PLAINTEXT
open("flag", 0)rax = 2rdi = flag_addrrsi = 0 read(fd, buf, size)rax = 0rdi = fdrsi = bufrdx = size write(1, buf, size)rax = 1rdi = 1rsi = bufrdx = size

如果是 openat

PLAINTEXT
openat(AT_FDCWD, "flag", 0, 0) rax = 257rdi = -100rsi = flag_addrrdx = 0r10 = 0

这里第 4 个参数必须放 r10


五、栈对齐问题

64 位 Linux 下,调用某些 libc 函数时要求栈 16 字节对齐。

如果不对齐,常见现象是:

PLAINTEXT
system 崩溃movaps 崩溃本地打不通远程打不通

常见解决方法是在 ROP 链前加一个单独的 ret

PYTHON
payload = b"A" * offsetpayload += p64(ret)payload += p64(pop_rdi)payload += p64(binsh)payload += p64(system)

这里的 ret 不是为了执行逻辑,而是为了调整 rsp 对齐。


六、caller-saved 和 callee-saved 寄存器

调用约定还规定哪些寄存器由调用者保存,哪些由被调用者保存。

1. x86-64 System V

调用者保存,也就是函数调用后可能被破坏:

PLAINTEXT
raxrcxrdxrsirdir8r9r10r11

被调用者保存,也就是函数返回后应保持原值:

PLAINTEXT
rbxrbpr12r13r14r15rsp

PWN 中经常遇到:

PLAINTEXT
ret2csu 会用 rbx, rbp, r12, r13, r14, r15

这些寄存器在函数尾部经常被连续 pop 出来,因此适合构造 gadget。


七、ret2csu 和调用约定

64 位程序里有时找不到:

PLAINTEXT
pop rdx ; ret

这时可以利用 __libc_csu_init 里的 gadget。

常见 csu gadget:

PLAINTEXT
pop rbxpop rbppop r12pop r13pop r14pop r15ret

以及:

PLAINTEXT
mov rdx, r15mov rsi, r14mov edi, r13dcall qword ptr [r12 + rbx*8]

这可以帮你设置:

PLAINTEXT
rdi = r13rsi = r14rdx = r15call [r12 + rbx*8]

也就是能调用类似:

C
func(rdi, rsi, rdx)

常用于:

PLAINTEXT
write(1, got, 8)read(0, bss, size)

八、AArch64 调用约定

ARM64 / AArch64 PWN 中也很常见,尤其是 QEMU 题。

1. 普通函数调用

参数寄存器:

PLAINTEXT
第 1 个参数:x0第 2 个参数:x1第 3 个参数:x2第 4 个参数:x3第 5 个参数:x4第 6 个参数:x5第 7 个参数:x6第 8 个参数:x7

返回值:

PLAINTEXT
x0

返回地址寄存器:

PLAINTEXT
x30 / lr

栈指针:

PLAINTEXT
sp

帧指针:

PLAINTEXT
x29 / fp

2. AArch64 syscall

系统调用号:

PLAINTEXT
x8

参数:

PLAINTEXT
x0x1x2x3x4x5

触发 syscall:

PLAINTEXT
svc #0

例如:

PLAINTEXT
execve("/bin/sh", 0, 0) x8 = 221x0 = "/bin/sh"x1 = 0x2 = 0svc #0

九、ARM 32 位调用约定

ARM 32 位常见规则:

PLAINTEXT
第 1 个参数:r0第 2 个参数:r1第 3 个参数:r2第 4 个参数:r3更多参数:栈返回值:r0返回地址:lr栈指针:sp程序计数器:pc

系统调用:

PLAINTEXT
syscall number: r7参数:r0-r6触发:svc 0

十、MIPS 调用约定

MIPS PWN 中常见寄存器:

PLAINTEXT
第 1 个参数:$a0第 2 个参数:$a1第 3 个参数:$a2第 4 个参数:$a3返回值:$v0返回地址:$ra栈指针:$sp

系统调用:

PLAINTEXT
syscall number: $v0参数:$a0, $a1, $a2, $a3触发:syscall

注意 MIPS 有延迟槽:

PLAINTEXT
jalr $t9nop

跳转/调用后面那条指令可能会先执行,这是 MIPS PWN 的重点之一。


十一、Windows x64 调用约定

如果做 Windows PWN,需要知道 Windows x64 和 Linux x64 不一样。

Windows x64 前 4 个参数:

PLAINTEXT
第 1 个参数:rcx第 2 个参数:rdx第 3 个参数:r8第 4 个参数:r9

返回值:

PLAINTEXT
rax

另外 Windows x64 调用函数前通常需要预留:

PLAINTEXT
shadow space = 0x20 字节

也就是调用者要给被调用函数留 32 字节空间。

Linux x64 是:

PLAINTEXT
rdi, rsi, rdx, rcx, r8, r9

Windows x64 是:

PLAINTEXT
rcx, rdx, r8, r9

这个区别很重要。


十二、PWN 中最常见的调用约定总结表

1. x86 32 位 cdecl

PLAINTEXT
参数:栈上传递,从右往左返回值:eax返回地址:[esp]清理栈:调用者

payload:

PLAINTEXT
paddingfuncret_after_funcarg1arg2arg3

2. x86-64 Linux 函数调用

PLAINTEXT
arg1 = rdiarg2 = rsiarg3 = rdxarg4 = rcxarg5 = r8arg6 = r9return = rax

payload:

PLAINTEXT
paddingpop rdi ; retarg1pop rsi ; retarg2pop rdx ; retarg3func

3. x86-64 Linux syscall

PLAINTEXT
syscall number = raxarg1 = rdiarg2 = rsiarg3 = rdxarg4 = r10arg5 = r8arg6 = r9trigger = syscallreturn = rax

4. AArch64 函数调用

PLAINTEXT
arg1 = x0arg2 = x1arg3 = x2arg4 = x3arg5 = x4arg6 = x5arg7 = x6arg8 = x7return = x0return address = x30

5. AArch64 syscall

PLAINTEXT
syscall number = x8arg1 = x0arg2 = x1arg3 = x2arg4 = x3arg5 = x4arg6 = x5trigger = svc #0

十三、做题时怎么用调用约定

ret2libc 看函数调用约定

如果是 32 位:

PYTHON
payload = padding + p32(system) + p32(exit) + p32(binsh)

如果是 64 位:

PYTHON
payload = padding + p64(pop_rdi) + p64(binsh) + p64(system)

区别的根本原因就是调用约定不同。


ROP 看寄存器传参

64 位 ROP 的核心就是找 gadget 控制:

PLAINTEXT
rdirsirdxrax

因为它们决定函数参数和 syscall 参数。


SROP 看 syscall 约定

SROP 本质是伪造寄存器上下文,所以必须清楚:

PLAINTEXT
rax = syscall numberrdi/rsi/rdx/r10/r8/r9 = syscall argsrip = syscall gadget

格式化字符串看参数位置

格式化字符串中:

C
printf(user_input);

%p %p %p %p

会按照调用约定从寄存器和栈里取参数。

64 位下前几个格式化参数可能来自寄存器保存区,后续才是栈上内容,所以要通过:

PLAINTEXT
%6$p%7$p%8$p

这种方式试偏移。


十四、最重要的记忆版

x86 32 位:

PLAINTEXT
参数全在栈上payload = padding + func + ret + arg1 + arg2 + arg3

x86-64 Linux 函数:

PLAINTEXT
rdi, rsi, rdx, rcx, r8, r9返回值 rax

x86-64 Linux syscall:

PLAINTEXT
rax = syscall numberrdi, rsi, rdx, r10, r8, r9syscall

AArch64 函数:

PLAINTEXT
x0-x7 传参x0 返回x30 是返回地址

AArch64 syscall:

PLAINTEXT
x8 = syscall numberx0-x5 = 参数svc #0

调用约定决定参数放在哪里。PWN 构造 payload 的本质,就是按调用约定把参数放到正确的位置,再把控制流跳到目标函数或 syscall。

COMMENTS

评论区

欢迎补充思路、复盘细节,或者指出文章里可以继续深入的地方。