BROP
BROP 即 Blind ROP,需要我们在无法获得二进制文件的情况下(在没有给出题目的情况下,只能通过尝试来确定),通过 ROP 进行远程攻击,劫持该应用程序的控制流,可用于开启了 ASLR、NX 和栈 canary 的 64-bit Linux。
一、基本原理
1.1 介绍
BROP [1] 即 Blind ROP,于2014年被提出。
[1]Bittau A, Belay A, Mashtizadeh A, et al. Hacking blind[C]//2014 IEEE Symposium on Security and Privacy. IEEE, 2014: 227-242.
文章提出:在不拥有目标二进制或源代码副本的情况下,针对崩溃后重新启动的服务,编写远程堆栈缓冲区溢出漏洞攻击是可能的。
许多服务器在崩溃后会重新启动其工作进程,以提高健壮性,包括Apache、nginx、Samba和OpenSSH,这就使得攻击者可以对其利用。BROP攻击假定服务器应用程序存在堆栈漏洞,并且在崩溃后重新启动。该攻击适用于启用了ASLR(地址空间布局随机化)、不可执行(NX)内存和堆栈Canary的现代64位Linux。作者提出,目前无法针对Windows系统,因为他们尚未将攻击适应Windows ABI。
该攻击由两种新技术实现:
- Generalized stack reading:通用的栈读取技术,即用于泄漏Canary的已知技术,泄漏保存的返回地址技术。用以在64位上绕过ASLR(地址随机化)。
- Blind ROP:这种技术可以远程定位ROP gadgets。
这两种技术都有一个共同的想法,即使用单堆栈漏洞根据服务器进程是否崩溃来泄漏信息。堆栈读取技术使用可能的猜测值逐字节覆盖堆栈,直到找到正确的猜测值并且服务器没有崩溃,从而有效地读取(通过覆盖)堆栈。Blind ROP攻击远程来找到 gadgets 来执行写系统调用,之后服务器的二进制文件可以从内存传输到攻击者的套接字。在这一点上,Canary、ASLR和NX已经被解决,可以使用已知的技术进行攻击。
BROP攻击能够在三种新情况下实现强大的通用攻击:
- 破解专有的封闭二进制服务。使用远程服务时可能会注意到崩溃,或者通过远程模糊测试发现崩溃。(专有封闭二进制服务:专有封闭二进制服务是指一种仅限于特定组织、团队或公司使用的二进制软件服务。这种服务通常是由私人公司或组织开发和维护的,并且没有公开的源代码或文档,因此只有授权用户才能使用和访问该服务。)
- 破解一个开源库中的漏洞,该漏洞被认为用于专有的封闭二进制服务。例如,一个流行的SSL库可能存在堆栈漏洞,人们可能会猜测它正被专有服务使用。
- 破解一个二进制文件未知的开源服务器。这适用于手动编译的安装或基于源代码的发行版,如Gentoo。
文章的主要贡献如下:
- 提出了一种在服务器上绕过ASLR的技术(通用堆栈读取)。
- 提出了一种远程查找ROP小工具(BROP)的技术,以便在二进制文件未知时对软件进行攻击。
- 实现了一种工具——Braille,在给定如何在服务器上触发堆栈溢出的输入的情况下,自动构建漏洞。
- 第一个针对nginx最近的漏洞的公开攻击,是通用的64位漏洞,它击败了ASLR、Canary 和 NX。
- 给出了一些针对BROP攻击的防御建议。总之,ASLR必须应用于所有可执行段,并且必须在每次崩溃后重新随机化。
1.2 攻击原理
具体来说,BROP 即 Blind ROP,需要我们在无法获得二进制文件的情况下(在没有给出题目的情况下,只能通过尝试来确定),通过 ROP 进行远程攻击,劫持该应用程序的控制流。即假定存在栈漏洞,在无任何信息的情况下进行攻击。
在已知二进制文件的情况下,我们想要控制程序流程,通常先找到栈溢出漏洞,然后确定栈溢出字节,确定函数返回地址,然后通过gadgets来控制栈进而控制程序流程。参考栈溢出基础知识+函数调用过程 。
那么没有二进制文件的情况下,我们需要解决的问题有:
- 栈溢出字节是多少?
- 怎么寻找 gadgets?
- 如果程序开启了ASLR、NX、Canary要怎么解决?
栈溢出字节可以通过暴力枚举来解决,第三个问题可以用其它常规方法,比如爆破canary、ret2libc l等来解决,因此BROP主要需要解决的问题就是,如何找到gadgets。
攻击条件:
- 程序必须存在溢出漏洞,以便攻击者可以控制程序流程。
- 进程崩溃以后可以重启(重启给暴力枚举创造了条件),而且重启之后的地址与先前的地址一样。(不一样的话,即便找到 gadgets 也不能用)
在BROP中,基本的遵循的思路如下
- 判断栈溢出长度:从 1 开始暴力枚举,直到程序崩溃
- Stack Reading:获取栈上的数据来泄露canary,以及ebp和返回地址。
- Bind ROP:找到足够多的 gadgets 来控制输出函数的参数,并且对其进行调用,比如说常见的 write 函数以及puts函数。
- Build the exploit:利用输出函数来 dump 出程序以便于来找到更多的 gadgets,从而可以写出最后的 exploit。
二、攻击流程
2.1 Stack Reading
Canary:当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行。在Linux中我们将cookie信息称为canary。
Canary在栈中的位置如下:

即Canary在EBP的上面。Canary本身可以通过逐字节爆破来获取,如图所示:(正常Canary第一个字节为’\x00’,下图只是为了举例)

假设某个32位程序的Canary为 51 03 22 94,缓冲区为32字节,那么在输入28字节就会碰到 Canary,如果继续输入,就会覆盖Canary,程序会报错。如果我们输入的是 **’a’28+’\x51*’ 呢 ?按照linux进栈的规则,’\x51’ 会把 Canary的第一位覆盖,覆盖之后Canary并没有发生变化,也就不会报错了。
这也就是爆破Canaty的原理,每个字节有 0x000xff 种可能,也就是256种,只要我们从1试到256,就一定能知道最后一个字节的Canary是多少。当我们知道输入到**’a’*28+’\x51’ 时,下一次输入就不会再报错,然后我们就可以尝试下一个字节,输入‘a’*28+’\x94’+’0255‘**

当我们尝试到03时,程序就不会报错了,前两个字节的Canary就被爆破了。所以我们只要逐字节爆破,在32位中,需要爆破四个字节的Canary,只要尝试4×256=1024次就一定可以爆破Canary,64位8个字节,只需要尝试 8×256=2048 次就一定可以爆破Canary。爆破Canary不是本文的关键,因此爆破Canary的例题,将在下一篇文章花式栈溢出技巧介绍。
2.2 Bind ROP
找到足够多的 gadgets 来控制输出函数的参数,并且对其进行调用,比如说常见的 write 函数以及 puts 函数。具体要做的就是:
- 找gadgets
- 找 plt 表,比如 write、strcmp
2.2.1 找gadgets 和stop gadget
远程查找 gadgets 的基本思想是:覆盖保存的返回地址并检查程序行为来扫描应用程序的文本段。具体是利用填入不同的已知地址顺序来知道所猜的地址是否为可用gadget。一般来说,在返回地址填入一个地址时,会发生两件事:程序将崩溃或挂起,然后连接将关闭或保持打开。大多数时候,程序会崩溃,但当它没有崩溃时,就表示这是一个gadgets。
但是,即便我们找到gadgets,程序没有崩溃,后续也会因为改变了栈结构而发生崩溃,那么我们如何知道程序崩溃是因为什么导致的?如何知道返回地址填入gadgets后程序没有崩溃呢?因此论文提出了一个 stop gadget的概念,能够使程序正常返回的地址。
如图所示(图片截取于论文Hacking blind):

程序在找到一个gadgets后,转去 stop gadget 执行。程序陷入循环,使攻击者一直保持连接状态,也就是其告诉攻击者,其所测试的地址是一段gadget。也就是说有了stop gadget,那些原本会导致程序崩溃的地址还是一样会导致崩溃,但那些正常返回的地址则会通过 stop gadget 进入被挂起的状态。stop gadget 地址一般为 main 或者 _start。
作者将gadgets分为3类:
- Probe:探针,也就是我们想要探测的代码地址。一般来说,都是64位程序,可以直接从0x400000尝试。
- Stop:不会使得程序崩溃的stop gadget的地址。
- Trap:可以导致程序崩溃的地址,直接写 p64(0) 就可以
probe,stop,traps ( trap, trap,…):我们可以通过这样的布局找到不对栈操作的gadgets,如:
- ret;
- xor, rax, rax; 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。
如果可以连续 pop6 个且不崩溃,很有可能就是通用gadgets(详见ret2csu)。
根据之前的:

我们可以发现,从找到的 brop_gadget 其中 pop r15;ret 对应的字节码为41 5f c3。后两字节码 5f c3 对应的汇编即为 pop rdi;ret。这是为什么呢?我们可以去查pop rdi, ret的汇编码,也就是5f c3(ret的汇编码是C3),因此我们可以利用pop r15;ret 的汇编码来实现pop rdi; ret的功能。pop rdi;ret 的地址就是gadget + 9。
2.2.2 找 plt 表
程序的plt表具有比较规整的结构,每一个plt表项都是16字节。而且,在每一个表项的6字节偏移处,是该表项对应的函数的解析路径,即程序最初执行该函数的时候,会执行该路径对函数的got地址进行解析。

对于大多数plt调用来说,一般都不容易崩溃,即使是使用了比较奇怪的参数。所以说,如果我们发现了一系列的长度为16的没有使得程序崩溃的代码段,那么我们有一定的理由相信我们遇到了plt表。
找到plt表后,我们可以遍历plt中的表项,也可以利用函数的特性来寻找我们需要的函数。比如puts函数。我们根据brop gadget 偏移可以得到相应的gadgets(详见ret2csu。同时在程序还没有开启PIE保护的情况下,0x400000处为ELF文件的头部,其内容为\x7fELF。所以我们可以根据这个来进行判断。
构造以下输入:
1 | payload = 'a' * length + p64(rdi_ret) + p64(leak_addr) + p64(puts_plt) + p64(stop_gadget) |
如果程序成功打印出来\x7fELF,就说明我们找到puts_plt的地址了。
此时,攻击者已经可以控制输出函数了,那么攻击者就可以输出.text段更多的内容以便于来找到更多合适gadgets。然后对其进行攻击。
三、例题
题目链接:https://buuoj.cn/challenges#axb_2019_brop64 。也可以去buuctf上找,题目名字:axb_2019_brop64
参考链接:https://blog.csdn.net/mcmuyanga/article/details/112904455
尽管buuctf给出了题目文件,但是这道题可以用BROP的方法做,所以可以不用下载。题目比较简单,并没有使用PIE和Canary。
3.1 判断栈溢出长度
首先链接一下靶机,看一下程序:

程序会将输入的内容打印一遍,然后退出。
输入300个字符,发现存在栈溢出漏洞:

然后输入%p、%s、%x等格式化控制字符,看一下有没有格式化字符串漏洞(在后续的文章中格式化字符串漏洞介绍)。

没有发现格式化字符串漏洞。
程序没有溢出时会输出Goodbye,因此我们可以从这里判断溢出长度:
1 | from pwn import * |
得到栈溢出长度216:

3.2 寻找stop gadget
此处我们希望我们能够爆破出main函数的首地址,进而直接让程序回到main函数进行执行。
首先此处我们可以先泄露原来的返回地址,得到基址,进而缩小爆破范围。(也可以不用,一般都是0x400000)
1 | from pwn import * |
事实上,如果程序在读取时并没有多加一个字符,是无法打印返回地址的。在这里,可以打印,说明程序多读取了一个字节\x00。

得到程序的基址0x400000。
然后爆破main地址:
1 | from pwn import * |
这个过程有点长,可能要多等一会儿。中间靶机挂掉了一次,然后重新开了一个靶机,不影响后续。爆破结果:0x4007d6

3.3 寻找BROP gadget
正如我们在原理中所说的,如果找到连续pop 6个的gadget,那么就是通用的gadgets。
1 | from pwn import * |
得到结果:0x40095a

3.4 寻找puts@plt
0x400000程序头的位置,前四个字符为 \x7fELF。因此可以使用0x400000的地址进行测试,如果程序成功打印出来\x7fELF,就说明我们找到puts_plt的地址了:
1 | from pwn import * |
得到puts_plt的地址:0x400635

3.5 利用puts@plt,Dump源文件
1 | from pwn import * |
dump下了4096字节:

用IDA打开,选择下面的Binary file,以二进制文件打开。

编辑 –> 段 –> 编辑程序基址:Edit –> Segments –> Rebase program –> value=0x400000

找到 puts 函数位置,也就是我们之前找到的0x400635,按键盘上的C键显示汇编代码:

找到 puts_got 的地址:0x601018
3.6 溢出
最后按常规方法溢出就可以
1 | from pwn import * |
结果:

- Title: BROP
- Author: wutong
- Created at: 2023-07-17 10:44:41
- Updated at: 2023-07-30 19:27:12
- Link: https://wutong01304.github.io/2023/07/17/BROP/
- License: This work is licensed under CC BY-NC-SA 4.0.