二进制 PWN 中最基础的调用约定知识
二进制中最常用的调用约定知识,做题时真正会用到的地方:函数传参、返回值、栈布局、ROP 构造、ret2libc、syscall 参数。
二进制中最常用的调用约定知识,做题时真正会用到的地方:函数传参、返回值、栈布局、ROP 构造、ret2libc、syscall 参数。
一、什么是调用约定
调用约定就是规定:函数参数放在哪里返回值放在哪里谁负责清理栈哪些寄存器调用前后必须保持不变函数调用时栈怎么变化 在 PWN 里,调用约定直接影响你怎么构造:ret2libc/ROP chain/ret2syscall/ret2csu/SROP/栈迁移/格式化字符串利用/函数参数劫持
二、x86 32 位调用约定
32 位 Linux PWN 里最常见的是 cdecl。
1. cdecl 基本规则
参数从右往左压栈。
例如:
system("/bin/sh");实际调用前栈大概是:
push "/bin/sh"call system如果是:
write(1, buf, 8);压栈顺序是:
push 8push bufpush 1call write也就是:
参数从右往左入栈返回值放 eax调用者负责清理栈2. 32 位函数调用栈布局
执行:
func(a, b, c);进入 func 后,栈通常是:
[esp] 返回地址[esp+4] 第 1 个参数 a[esp+8] 第 2 个参数 b[esp+0xc] 第 3 个参数 c如果函数有标准栈帧:
push ebpmov ebp, espsub esp, xxx那么:
[ebp+4] 返回地址[ebp+8] 第 1 个参数[ebp+0xc] 第 2 个参数[ebp+0x10] 第 3 个参数 [ebp-4] 局部变量[ebp-8] 局部变量3. 32 位 ret2libc payload
32 位下调用 system("/bin/sh"):
paddingsystem_addrreturn_after_systembinsh_addr例如:
payload = b"A" * offsetpayload += p32(system)payload += p32(exit)payload += p32(binsh)为什么这样排?
因为 ret 到 system 后,栈会变成:
[esp] system 返回地址,也就是 exit[esp+4] system 第 1 个参数,也就是 "/bin/sh"这正好符合 32 位 cdecl。
4. 32 位多个参数函数
比如:
write(1, buf, 8);payload:
payload = b"A" * offsetpayload += p32(write_plt)payload += p32(main)payload += p32(1)payload += p32(buf)payload += p32(8)进入 write 时:
[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 个参数用寄存器传递
顺序是:
第 1 个参数:rdi第 2 个参数:rsi第 3 个参数:rdx第 4 个参数:rcx第 5 个参数:r8第 6 个参数:r9返回值:
rax如果参数超过 6 个,多出来的参数才放到栈上。
2. 64 位 ret2libc 调 system
调用:
system("/bin/sh");需要:
rdi = "/bin/sh"rip = system所以 ROP 链通常是:
paddingpop rdi ; retbinsh_addrsystem_addrpwntools:
payload = b"A" * offsetpayload += p64(pop_rdi)payload += p64(binsh)payload += p64(system)因为 pop rdi ; ret 会把栈上的 binsh 弹进 rdi,然后 ret 跳到 system。
3. 64 位调用 write
调用:
write(1, buf, 8);需要:
rdi = 1rsi = bufrdx = 8rip = writeROP:
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 是:
pop rsi ; pop r15 ; ret那么要多填一个垃圾值:
payload += p64(pop_rsi_r15)payload += p64(buf)payload += p64(0)四、x86-64 syscall 调用约定
函数调用和系统调用的参数寄存器不完全一样。
Linux x86-64 syscall 参数:
系统调用号: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 编号:
execve = 59需要:
rax = 59rdi = "/bin/sh"rsi = 0rdx = 0syscallROP 链:
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:
open/read/writex86-64 syscall:
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:
openat(AT_FDCWD, "flag", 0, 0) rax = 257rdi = -100rsi = flag_addrrdx = 0r10 = 0这里第 4 个参数必须放 r10。
五、栈对齐问题
64 位 Linux 下,调用某些 libc 函数时要求栈 16 字节对齐。
如果不对齐,常见现象是:
system 崩溃movaps 崩溃本地打不通远程打不通常见解决方法是在 ROP 链前加一个单独的 ret:
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
调用者保存,也就是函数调用后可能被破坏:
raxrcxrdxrsirdir8r9r10r11被调用者保存,也就是函数返回后应保持原值:
rbxrbpr12r13r14r15rspPWN 中经常遇到:
ret2csu 会用 rbx, rbp, r12, r13, r14, r15这些寄存器在函数尾部经常被连续 pop 出来,因此适合构造 gadget。
七、ret2csu 和调用约定
64 位程序里有时找不到:
pop rdx ; ret这时可以利用 __libc_csu_init 里的 gadget。
常见 csu gadget:
pop rbxpop rbppop r12pop r13pop r14pop r15ret以及:
mov rdx, r15mov rsi, r14mov edi, r13dcall qword ptr [r12 + rbx*8]这可以帮你设置:
rdi = r13rsi = r14rdx = r15call [r12 + rbx*8]也就是能调用类似:
func(rdi, rsi, rdx)常用于:
write(1, got, 8)read(0, bss, size)八、AArch64 调用约定
ARM64 / AArch64 PWN 中也很常见,尤其是 QEMU 题。
1. 普通函数调用
参数寄存器:
第 1 个参数:x0第 2 个参数:x1第 3 个参数:x2第 4 个参数:x3第 5 个参数:x4第 6 个参数:x5第 7 个参数:x6第 8 个参数:x7返回值:
x0返回地址寄存器:
x30 / lr栈指针:
sp帧指针:
x29 / fp2. AArch64 syscall
系统调用号:
x8参数:
x0x1x2x3x4x5触发 syscall:
svc #0例如:
execve("/bin/sh", 0, 0) x8 = 221x0 = "/bin/sh"x1 = 0x2 = 0svc #0九、ARM 32 位调用约定
ARM 32 位常见规则:
第 1 个参数:r0第 2 个参数:r1第 3 个参数:r2第 4 个参数:r3更多参数:栈返回值:r0返回地址:lr栈指针:sp程序计数器:pc系统调用:
syscall number: r7参数:r0-r6触发:svc 0十、MIPS 调用约定
MIPS PWN 中常见寄存器:
第 1 个参数:$a0第 2 个参数:$a1第 3 个参数:$a2第 4 个参数:$a3返回值:$v0返回地址:$ra栈指针:$sp系统调用:
syscall number: $v0参数:$a0, $a1, $a2, $a3触发:syscall注意 MIPS 有延迟槽:
jalr $t9nop跳转/调用后面那条指令可能会先执行,这是 MIPS PWN 的重点之一。
十一、Windows x64 调用约定
如果做 Windows PWN,需要知道 Windows x64 和 Linux x64 不一样。
Windows x64 前 4 个参数:
第 1 个参数:rcx第 2 个参数:rdx第 3 个参数:r8第 4 个参数:r9返回值:
rax另外 Windows x64 调用函数前通常需要预留:
shadow space = 0x20 字节也就是调用者要给被调用函数留 32 字节空间。
Linux x64 是:
rdi, rsi, rdx, rcx, r8, r9Windows x64 是:
rcx, rdx, r8, r9这个区别很重要。
十二、PWN 中最常见的调用约定总结表
1. x86 32 位 cdecl
参数:栈上传递,从右往左返回值:eax返回地址:[esp]清理栈:调用者payload:
paddingfuncret_after_funcarg1arg2arg32. x86-64 Linux 函数调用
arg1 = rdiarg2 = rsiarg3 = rdxarg4 = rcxarg5 = r8arg6 = r9return = raxpayload:
paddingpop rdi ; retarg1pop rsi ; retarg2pop rdx ; retarg3func3. x86-64 Linux syscall
syscall number = raxarg1 = rdiarg2 = rsiarg3 = rdxarg4 = r10arg5 = r8arg6 = r9trigger = syscallreturn = rax4. AArch64 函数调用
arg1 = x0arg2 = x1arg3 = x2arg4 = x3arg5 = x4arg6 = x5arg7 = x6arg8 = x7return = x0return address = x305. AArch64 syscall
syscall number = x8arg1 = x0arg2 = x1arg3 = x2arg4 = x3arg5 = x4arg6 = x5trigger = svc #0十三、做题时怎么用调用约定
ret2libc 看函数调用约定
如果是 32 位:
payload = padding + p32(system) + p32(exit) + p32(binsh)如果是 64 位:
payload = padding + p64(pop_rdi) + p64(binsh) + p64(system)区别的根本原因就是调用约定不同。
ROP 看寄存器传参
64 位 ROP 的核心就是找 gadget 控制:
rdirsirdxrax因为它们决定函数参数和 syscall 参数。
SROP 看 syscall 约定
SROP 本质是伪造寄存器上下文,所以必须清楚:
rax = syscall numberrdi/rsi/rdx/r10/r8/r9 = syscall argsrip = syscall gadget格式化字符串看参数位置
格式化字符串中:
printf(user_input);%p %p %p %p
会按照调用约定从寄存器和栈里取参数。
64 位下前几个格式化参数可能来自寄存器保存区,后续才是栈上内容,所以要通过:
%6$p%7$p%8$p这种方式试偏移。
十四、最重要的记忆版
x86 32 位:
参数全在栈上payload = padding + func + ret + arg1 + arg2 + arg3x86-64 Linux 函数:
rdi, rsi, rdx, rcx, r8, r9返回值 raxx86-64 Linux syscall:
rax = syscall numberrdi, rsi, rdx, r10, r8, r9syscallAArch64 函数:
x0-x7 传参x0 返回x30 是返回地址AArch64 syscall:
x8 = syscall numberx0-x5 = 参数svc #0调用约定决定参数放在哪里。PWN 构造 payload 的本质,就是按调用约定把参数放到正确的位置,再把控制流跳到目标函数或 syscall。

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