ret2text 只有在程序调用了危险函数并且能够使用栈溢出覆盖返回地址的情况下才能够使用,而 ret2shellcode 也需要程序没有开启 NX (No eXecutable) 的情况下才能使用。如果程序开启了 NX 防护,我们就需要考虑别的办法了。

这里介绍第一种: ret2syscall

ret2syscall 概述

顾名思义,ret2syscall 就是返回到系统调用。所以利用 ret2syscall 的攻击就是构造一个 syscall 的 ROP 链,然后利用栈溢出劫持程序流程,转到 ROP 链执行,利用 ROP getshell。

使用前提

  1. 存在栈溢出漏洞,能够覆盖返回地址劫持程序运行流程
  2. 能够找到合适的 gadget 用于构造 ROP
  3. 能够执行系统调用

大概思路

首先查看程序的栈溢出漏洞,利用gdb动态调试 + cyclic 分析出偏移量,填充缓冲区

接着使用 ROPgadget 查找 gadget,获取其地址

最后在payload中构造 ROP 链,完成系统调用。

先讲讲系统调用

这是一个 Linux 系统架构的图片,描述了应用,操作系统内核,系统调用与硬件的大致关系。

pE2Hfw6.png

用户态(User Mode)与内核态(Kernel Mode)

Linux 的进程运行状态包括用户态和内核态,它代表了操作系统中不同的执行环境,并且有着不同的权限级别。

  1. 用户态
  • 在用户态下进程的权限是受限的,在这个模式下,进程不能直接访问硬件资源,也不能执行特权指令。他只能通过系统调用请求内核提供服务

  • 如果进程在用户态做出不被允许的操作(例如,访问违背分配的内存,或者直接操作硬件),会触发异常或错误,导致进程被终止

  1. 内核态
  • 内核态是操作系统内核运行的状态。操作系统内核在此模式下运行,拥有对系统所有资源的完全访问权限。

  • 操作系统提供的系统调用会从用户态切换到内核态,内核会执行这些操作后再将控制权返回给用户态的进程。

系统调用

从上面对用户态与内核态的描述中可以看出来,系统调用就是为了进行高权限操作而从用户态到内核态的转换,也就是说:

系统调用就是用户态进程为了执行特权操作,向内核请求服务的一中机制,本质上是一中从用户态切换到内核态的受控、临时切换。

  • 常见系统调用的例子:

    • read():从文件中读取数据

    • write():向文件写入数据

    • open() / close():打开/关闭文件

    • fork():创建进程

    • exec():执行程序

    • exit():终止进程

    • socket()、bind()、listen():网络通信相关

每一个系统调用都有自己的系统调用号,这决定了系统调用的时候执行哪一个系统调用,我们可以在自己的电脑上查看:

1
2
3
4
$ cat /usr/include/x86_64-linux-gnu/asm/unistd_32.h
......
$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h
......

这些系统调用看起来就像是C语言中的函数,但是其实它们的实现比C函数复杂很多,因为这涉及到内核态与用户态的转换。

流程

  • 执行中断指令 (在x86中是 int 0x80, x86_64中是 syscall)

    用户态程序执行到中断命令 int 0x80syscall 的时候就会发起系统调用,这是 linux 系统为用户程序设计的专门用于系统调用的“入口”

  • 进入内核,处理系统调用

    • 此时 CPU 检测到中断,切换到内核态

    • 查找中断向量表(IVT),跳转到中断处理程序

    • syscall handler 读取 eax,从中获取系统调用号,确定要调用哪一个系统调用

    • 根据调用号,跳转到对应的内核函数

  • 由对应的内核函数完成具体工作

  • 最后执行完毕后返回用户态进程,程序继续运行

ret2syscall 的具体实现

这里就以调用 execve() 系统调用执行 /bin/sh 为例介绍具体实现

execve() 需要三个参数:

1
execve(string, argc, envp)

这三个参数分别应当存放在这几个寄存器中:

  • ebx: 字符串 "/bin/sh"
  • ecx: 参数数组(此处为NULL
  • edx: 环境变量(此处为NULL

构造ROP

这里我们要做的就是从程序中的已有片段中找到能够为我们所利用的片段,然后把它们的地址作为返回地址写到栈上,一个一个执行形成一条链。

  • 所以首先我们需要知道我们要构造怎样的ROP链:

    它要能够直接完成 execve 系统调用,而 execve 系统调用需要:

    • 中断指令 -> int 0x80
    • eax 存储系统调用号 0xb -> pop eax; ret
    • 寄存器 ebx 中存储字符串 "/bin/sh" 的地址 -> pop ebx; ret & '/bin/sh'
    • 寄存器 ecxedx 中存储 '0' -> pop ecx; ret & pop edx' ret
  • 使用ROPgadget查找gadget:

    1
    2
    3
    4
    5
    6
    ROPgadget --binary ./vuln --only 'pop|ret' | grep eax
    ROPgadget --binary ./vuln --only 'pop|ret' | grep ebx
    ROPgadget --binary ./vuln --only 'pop|ret' | grep ecx
    ROPgadget --binary ./vuln --only 'pop|ret' | grep edx
    ROPgadget --binary ./vuln --string '/bin/sh'
    ROPgadget --binary ./vuln --only 'int'
  • ROPgadget中的地址注入payload,然后再send出去。