BROP原理及利用
BROP原理及利用
基本介绍
BROP(Blind ROP) 于 2014 年由 Standford 的 Andrea Bittau 提出,其相关研究成果发表在 Oakland 2014,其论文题目是 Hacking Blind,下面是作者对应的 paper 和 slides, 以及作者相应的介绍
- paper
- slide
BROP 是没有对应应用程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流。
攻击条件
- 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
- 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的。
攻击原理
目前,大部分应用都会开启 ASLR、NX、Canary 保护。这里我们分别讲解在 BROP 中如何绕过这些保护,以及如何进行攻击。
基本思路
在 BROP 中,基本的遵循的思路如下
- 判断栈溢出长度
- 暴力枚举
- Stack Reading
- 获取栈上的数据来泄露 canaries,以及 ebp 和返回地址。
- Blind ROP
- 找到足够多的 gadgets 来控制输出函数的参数,并且对其进行调用,比如说常见的 write 函数以及 puts 函数。
- Build the exploit
- 利用输出函数来 dump 出程序以便于来找到更多的 gadgets,从而可以写出最后的 exploit。
栈溢出长度
直接从 1 暴力枚举即可,直到发现程序崩溃。
Stack Reading
如下所示,这是目前经典的栈布局
1 | buffer|canary|saved fame pointer|saved returned address |
要向得到 canary 以及之后的变量,我们需要解决第一个问题,如何得到 overflow 的长度,这个可以通过不断尝试来获取。
其次,关于 canary 以及后面的变量,所采用的的方法一致,这里我们以 canary 为例。
canary 本身可以通过爆破来获取,但是如果只是愚蠢地枚举所有的数值的话,显然是低效的。
需要注意的是,攻击条件 2 表明了程序本身并不会因为 crash 有变化,所以每次的 canary 等值都是一样的。所以我们可以按照字节进行爆破。正如论文中所展示的,每个字节最多有 256 种可能,所以在 32 位的情况下,我们最多需要爆破 1024 次,64 位最多爆破 2048 次。
Blind ROP
基本思路
最朴素的执行 write 函数的方法就是构造系统调用。
1 | pop rdi; ret # socket |
但通常来说,这样的方法都是比较困难的,因为想要找到一个 syscall 的地址基本不可能。。。我们可以通过转换为找 write 的方式来获取。
BROP gadgets
首先,在 libc_csu_init 的结尾一长串的 gadgets,我们可以通过偏移来获取 write 函数调用的前两个参数。正如文中所展示的
find a call write
我们可以通过 plt 表来获取 write 的地址。
control rdx
需要注意的是,rdx 只是我们用来输出程序字节长度的变量,只要不为 0 即可。一般来说程序中的 rdx 经常性会不是零。但是为了更好地控制程序输出,我们仍然尽量可以控制这个值。但是,在程序
1 | pop rdx; ret |
这样的指令几乎没有。那么,我们该如何控制 rdx 的数值呢?这里需要说明执行 strcmp 的时候,rdx 会被设置为将要被比较的字符串的长度,所以我们可以找到 strcmp 函数,从而来控制 rdx。
那么接下来的问题,我们就可以分为两项
- 寻找 gadgets
- 寻找 PLT 表
- write 入口
- strcmp 入口
寻找 GADGETS
首先,我们来想办法寻找 gadgets。此时,由于尚未知道程序具体长什么样,所以我们只能通过简单的控制程序的返回地址为自己设置的值,从而而来猜测相应的 gadgets。而当我们控制程序的返回地址时,一般有以下几种情况
- 程序直接崩溃
- 程序运行一段时间后崩溃
- 程序一直运行而并不崩溃
为了寻找合理的 gadgets,我们可以分为以下两步
寻找 stop gadgets
所谓stop gadget
一般指的是这样一段代码:当程序的执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态。
其实 stop gadget 也并不一定得是上面的样子,其根本的目的在于告诉攻击者,所测试的返回地址是一个 gadgets。
之所以要寻找 stop gadgets,是因为当我们猜到某个 gadgtes 后,如果我们仅仅是将其布置在栈上,由于执行完这个 gadget 之后,程序还会跳到栈上的下一个地址。如果该地址是非法地址,那么程序就会 崩溃。这样的话,在攻击者看来程序只是单纯的 crash 了。因此,攻击者就会认为在这个过程中并没有执行到任何的useful gadget
,从而放弃它。例子如下图
但是,如果我们布置了stop gadget
,那么对于我们所要尝试的每一个地址,如果它是一个 gadget 的话,那么序不会崩溃。接下来,就是去想办法识别这些 gadget。
识别 gadgets
那么,我们该如何识别这些 gadgets 呢?我们可以通过栈布局以及程序的行为来进行识别。为了更加容易地进行介绍,这里定义栈上的三种地址
- Probe
- 探针,也就是我们想要探测的代码地址。一般来说,都是 64 位程序,可以直接从 0x400000 尝试,如果不成功,有可能程序开启了 PIE 保护,再不济,就可能是程序是 32 位了。。这里我还没有特别想明白,怎么可以快速确定远程的位数。
- Stop
- 不会使得程序崩溃的 stop gadget 的地址。
- Trap
- 可以导致程序崩溃的地址
我们可以通过在栈上摆放不同顺序的 Stop 与 Trap 从而来识别出正在执行的指令。因为执行 Stop 意味着程序不会崩溃,执行 Trap 意味着程序会立即崩溃。这里给出几个例子
probe,stop,traps(traps,traps,…)
我们通过程序崩溃与否 (
如果程序在 probe 处直接崩溃怎么判断
) 可以找到不会对栈进行 pop 操作的 gadget,如
- ret
- xor eax,eax; ret
probe,trap,stop,traps
- 我们可以通过这样的布局找到只是弹出一个栈变量的 gadget。如
- pop rax; ret
- pop rdi; ret
- 我们可以通过这样的布局找到只是弹出一个栈变量的 gadget。如
probe, trap, trap, trap, trap, trap, trap, stop, traps
我们可以通过这样的布局来找到弹出 6 个栈变量的 gadget,也就是与 brop gadget 相似的 gadget。
这里感觉原文是有问题的,比如说如果遇到了只是 pop 一个栈变量的地址,其实也是不会崩溃的,,
这里一般来说会遇到两处比较有意思的地方
- plt 处不会崩,,
- _start 处不会崩,相当于程序重新执行。
之所以要在每个布局的后面都放上 trap,是为了能够识别出,当我们的 probe 处对应的地址执行的指令跳过了 stop,程序立马崩溃的行为。
但是,即使是这样,我们仍然难以识别出正在执行的 gadget 到底是在对哪个寄存器进行操作。
但是,需要注意的是向 BROP 这样的一下子弹出 6 个寄存器的 gadgets,程序中并不经常出现。所以,如果我们发现了这样的 gadgets,那么,有很大的可能性,这个 gadgets 就是 brop gadgets。此外,这个 gadgets 通过错位还可以生成 pop rsp 等这样的 gadgets,可以使得程序崩溃也可以作为识别这个 gadgets 的标志。
此外,根据我们之前学的 ret2libc_csu_init 可以知道该地址减去 0x1a 就会得到其上一个 gadgets。可以供我们调用其它函数。
需要注意的是 probe 可能是一个 stop gadget,我们得去检查一下,怎么检查呢?我们只需要让后面所有的内容变为 trap 地址即可。因为如果是 stop gadget 的话,程序会正常执行,否则就会崩溃。看起来似乎很有意思.
寻找 PLT
如下图所示,程序的 plt 表具有比较规整的结构,每一个 plt 表项都是 16 字节。而且,在每一个表项的 6 字节偏移处,是该表项对应的函数的解析路径,即程序最初执行该函数的时候,会执行该路径对函数的 got 地址进行解析。
此外,对于大多数 plt 调用来说,一般都不容易崩溃,即使是使用了比较奇怪的参数。所以说,如果我们发现了一系列的长度为 16 的没有使得程序崩溃的代码段,那么我们有一定的理由相信我们遇到了 plt 表。除此之外,我们还可以通过前后偏移 6 字节,来判断我们是处于 plt 表项中间还是说处于开头。
控制 RDX
当我们找到 plt 表之后,下面,我们就该想办法来控制 rdx 的数值了,那么该如何确认 strcmp 的位置呢?需要提前说的是,并不是所有的程序都会调用 strcmp 函数,所以在没有调用 strcmp 函数的情况下,我们就得利用其它方式来控制 rdx 的值了。这里给出程序中使用 strcmp 函数的情况。
之前,我们已经找到了 brop 的 gadgets,所以我们可以控制函数的前两个参数了。与此同时,我们定义以下两种地址
- readable,可读的地址。
- bad, 非法地址,不可访问,比如说 0x0。
那么我们如果控制传递的参数为这两种地址的组合,会出现以下四种情况
- strcmp(bad,bad)
- strcmp(bad,readable)
- strcmp(readable,bad)
- strcmp(readable,readable)
只有最后一种格式,程序才会正常执行。
注:在没有 PIE 保护的时候,64 位程序的 ELF 文件的 0x400000 处有 7 个非零字节。
那么我们该如何具体地去做呢?有一种比较直接的方法就是从头到尾依次扫描每个 plt 表项,但是这个却比较麻烦。我们可以选择如下的一种方法
- 利用 plt 表项的慢路径
- 并且利用下一个表项的慢路径的地址来覆盖返回地址
这样,我们就不用来回控制相应的变量了。
当然,我们也可能碰巧找到 strncmp 或者 strcasecmp 函数,它们具有和 strcmp 一样的效果。
寻找输出函数
寻找输出函数既可以寻找 write,也可以寻找 puts。一般现先找 puts 函数。不过这里为了介绍方便,先介绍如何寻找 write。
寻找 write@plt
当我们可以控制 write 函数的三个参数的时候,我们就可以再次遍历所有的 plt 表,根据 write 函数将会输出内容来找到对应的函数。需要注意的是,这里有个比较麻烦的地方在于我们需要找到文件描述符的值。一般情况下,我们有两种方法来找到这个值
- 使用 rop chain,同时使得每个 rop 对应的文件描述符不一样
- 同时打开多个连接,并且我们使用相对较高的数值来试一试。
需要注意的是
- linux 默认情况下,一个进程最多只能打开 1024 个文件描述符。
- posix 标准每次申请的文件描述符数值总是当前最小可用数值。
当然,我们也可以选择寻找 puts 函数。
寻找 puts@plt
寻找 puts 函数 (这里我们寻找的是 plt),我们自然需要控制 rdi 参数,在上面,我们已经找到了 brop gadget。那么,我们根据 brop gadget 偏移 9 可以得到相应的 gadgets(由 ret2libc_csu_init 中后续可得)。同时在程序还没有开启 PIE 保护的情况下,0x400000 处为 ELF 文件的头部,其内容为 \ x7fELF。所以我们可以根据这个来进行判断。一般来说,其 payload 如下
1 | payload = 'A'*length +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget) |
攻击总结
此时,攻击者已经可以控制输出函数了,那么攻击者就可以输出. text 段更多的内容以便于来找到更多合适 gadgets。同时,攻击者还可以找到一些其它函数,如 dup2 或者 execve 函数。一般来说,攻击者此时会去做下事情
- 将 socket 输出重定向到输入输出
- 寻找 “/bin/sh” 的地址。一般来说,最好是找到一块可写的内存,利用 write 函数将这个字符串写到相应的地址。
- 执行 execve 获取 shell,获取 execve 不一定在 plt 表中,此时攻击者就需要想办法执行系统调用了。