前言:
这一周并没有好好学习东西,也不能怨的了课太多,还是怪自己没有把心静下来,这个月的麻烦事情实在是多,虽然每天都要上课到七点半,但还是荒废了两个小时的学习时间,蛮愧疚的,重新打起精神来好好学技术,希望以后的自己能时刻警戒一下自己。
介绍:
BROP其实就是在不知道应用程序的源代码或者是二进制文件下对程序进行攻击。相当于盲打,本质上我觉得是爆破。
CTF-wiki上面解释的很不错,我就不具体在多重复什么了。
https://ctf-wiki.github.io/ctf-wiki/pwn/stackoverflow/medium_rop/#brop
详解:
栈溢出的长度:
直接从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次。
基本思路:
寻找stop gadgets:
所谓
stop gadget
一般指的是这样一段代码:当程序的执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态。之所以要寻找stop gadgets,是因为当我们猜到某个gadgtes后,如果我们仅仅是将其布置在栈上,由于执行完这个gadget之后,程序还会跳到栈上的下一个地址。如果该地址是非法地址,那么程序就会crash。这样的话,在攻击者看来程序只是单纯的crash了。因此,攻击者就会认为在这个过程中并没有执行到任何的
useful gadget
,从而放弃它。
但是,如果我们布置了stop gadget
,那么对于我们所要尝试的每一个地址,如果它是一个gadget的话,那么程序不会崩溃。接下来,就是去想办法识别这些gadget。
识别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
· probe, trap, trap, trap, trap, trap, trap, stop, traps
我们可以通过这样的布局来找到弹出6个栈变量的gadget,也就是与brop gadget相似的gadget。这里感觉原文是有问题的,比如说如果遇到了只是pop一个栈变量的地址,其实也是不会崩溃的,,这里一般来说会遇到两处比较有意思的地方
- plt处不会崩,,
- _start处不会崩,相当于程序重新执行。
之所以要在每个布局的后面都放上trap,是为了能够识别出,当我们的probe处对应的地址执行的指令跳过了stop,程序立马崩溃的行为。
- 寻找PLT
如下图所示,程序的plt表具有比较规整的结构,每一个plt表项都是16字节。而且,在每一个表项的6字节偏移处,是该表项对应的函数的解析路径,即程序最初执行该函数的时候,会执行该路径对函数的got地址进行解析。
此外,对于大多数plt调用来说,一般都不容易崩溃,即使是使用了比较奇怪的参数。所以说,如果我们发现了一系列的长度为16的没有使得程序崩溃的代码段,那么我们有一定的理由相信我们遇到了plt表。除此之外,我们还可以通过前后偏移6字节,来判断我们是处于plt表项中间还是说处于开头。
- Libc_csu_initz
其实在libc_init处还有两处被忽略的pop地址,如果你对init特别熟悉的话。
- 控制RDX
rdx只是我们用来输出程序字节长度的变量,只要不为0即可。一般来说程序中的rdx经常性会不是零。但是为了更好地控制程序输出,我们仍然尽量可以控制这个值。但是,在程序
1
pop rdx; ret
这样的指令几乎没有。那么,我们该如何控制rdx的数值呢?这里需要说明执行strcmp的时候,rdx会被设置为将要被比较的字符串的长度,所以我们可以找到strcmp函数,从而来控制rdx。
注意:在没有PIE保护的时候,64位程序的ELF文件的0x400000处有7个非零字节。
之前,我们已经找到了brop的gadgets,所以我们可以控制函数的前两个参数了。与此同时,我们定义以下两种地址
- readable,可读的地址。
- bad, 非法地址,不可访问,比如说0x0。
那么我们如果控制传递的参数为这两种地址的组合,会出现以下四种情况
- strcmp(bad,bad)
- strcmp(bad,readable)
- strcmp(readable,bad)
- strcmp(readable,readable)
只有最后一种格式,程序才会正常执行。
有一种比较直接的方法就是从头到尾依次扫描每个plt表项,但是这个却比较麻烦。我们可以选择如下的一种方法
- 利用plt表项的慢路径
- 并且利用下一个表项的慢路径的地址来覆盖返回地址
这样,我们就不用来回控制相应的变量了。
当然,我们也可能碰巧找到strncmp或者strcasecmp函数,它们具有和strcmp一样的效果。
- 寻找输出函数:
可以寻找puts和write函数
- 寻找write_plt
当我们可以控制write函数的三个参数的时候,我们就可以再次遍历所有的plt表,根据write函数将会输出内容来找到对应的函数。需要注意的是,这里有个比较麻烦的地方在于我们需要找到文件描述符的值。一般情况下,我们有两种方法来找到这个值
- 使用rop chain,同时使得每个rop对应的文件描述符不一样
- 同时打开多个连接,并且我们使用相对较高的数值来试一试。
- 寻找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)
举例(HCTF2016出题人失踪了):
确定栈溢出长度:
1 | def getbufferflow_length(): |
寻找stop gadgets:
1 | def get_stop_addr(length): |
这里我们直接尝试64位程序没有开启PIE的情况,因为一般是这个样子的,如果开启了,那就按照开启了的方法做,结果发现了不少,我选择了一个貌似返回到源程序中的地址
1 | one success stop gadget addr: 0x4006b6 |
识别brop gadgets:
构造如下,get_brop_gadget是为了得到可能的brop gadget,后面的check_brop_gadget是为了检查。
1 | def get_brop_gadget(length, stop_gadget, addr): |
这样,我们基本得到了brop的gadgets地址0x4007ba
确定puts_plt地址:
根据上面,所说我们可以构造如下payload来进行获取
1 | payload = 'A'*72 +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget) |
具体:
1 | def get_puts_addr(length, rdi_ret, stop_gadget): |
最后根据plt的结构,选择0x400560作为puts@plt
泄漏puts_got地址:
在我们可以调用puts函数后,我们可以泄露puts函数的地址,进而获取libc版本,从而获取相关的system函数地址与/bin/sh地址,从而获取shell。我们从0x400000开始泄露0x1000个字节,这已经足够包含程序的plt部分了。代码如下
1 | def leak(length, rdi_ret, puts_plt, leak_addr, stop_gadget): |
最后,我们将泄露的内容写到文件里。需要注意的是如果泄露出来的是“”,那说明我们遇到了’\x00’,因为puts是输出字符串,字符串是以’\x00’为终止符的。之后利用ida打开binary模式,首先在edit->segments->rebase program 将程序的基地址改为0x400000,然后找到偏移0x560处,如下
1 | seg000:0000000000400560 db 0FFh |
然后按下c,将此处的数据转换为汇编指令,如下
1 | seg000:0000000000400560 ; ------------------------------------------ |
这说明,puts@got的地址为0x601018
完整EXP:
1 | ##length = getbufferflow_length() |