由于初学也是因为太笨的原因一直没有彻底学习一下brop,今天因为某些原因必须做一道 brop 把自己逼出来了
写在前面
BROP攻击基于一篇发表在Oakland 2014的论文Hacking Blind,作者是Standford的Andrea Bittau,以下是相关的paper和slide链接:
以及BROP的原网站地址
BROP攻击的目标和前提条件
目标:通过ROP的方法远程攻击某个应用程序 ,劫持该应用程序的控制流。我们可以不需要知道该应用程序的源代码或者任何二进制代码,该应用程序可以被现有的一些保护机制如NX , ASLR , PIE , stack canaries等保护,应用程序所在的服务器可以是32位系统也可以是64位系统 。
而这个攻击实现的两个前提条件是:
1、必须先存在一个已知的stack overflow的漏洞,而且攻击者知道如何触发这个漏洞;
2、服务器进程在crash之后会重新复活,并且复活的进程不会被re-rand(意味着虽然有ASLR保护,但是复活的程序和之前 的进程的地址随机化是一样的)。这个需求其实是合理的,因为当前像nginx , MySQL , Apache , OpenSSH , Samba等服务器应用都是符合这种特性的。
BROP的攻击流程
远程dump内存
由于我们不知道被攻击程序的内存布局,所以首先要做的就是通过某种方式从远程服务器dump出程序的内存到本地,为了做到这点我们需要调用一个系统调用write或puts等的打印函数,但是ROP攻击的角度来看,我们需要的pop rdi`
pop rsi`等的gadgets地址并不知道,特别是当系统 部署了canary 就更难了。
所以我们先把这个问题放一放,先记着这个目标,先解决路上的障碍
攻破Stack Canaries保护
作者提出了一种叫做”stack reading”的方法:
假设这是我们要overflow的栈布局:
首先我们可以尝试任意多次判断overflow的长度(直到canary被破坏crash了)之后我们将buffer填上任意值,然后一个一个字节顺序地进行尝试来还原出真实的canary,(因为程序重启canary不变),由于一个字节只有256种可能,所以我们最多尝试256次就可以找到canary 的某个正确的字节,直到进8个字节的canary 都完整得找到
寻找stop gadget
到目前为止,我们已经找到了合适的canary绕过stack canary 的保护,接下来我们需要寻找一个特殊的gadgets:stop gadget。
一般情况下,如果我们把栈上的return address
覆盖成某些我们随意选取的地址的话,程序 很大程度会crash掉,从而使得攻击者的连接(connection)被关闭,但是存在另外一种情况,即该return address
指向了一块代码区域,当程序的执行流跳到那里之后,程序不会crash,而是进入了无限循环,(比如回到main函数,回到start函数)攻击者能一直保持连接状态。这样的地址我们称之为stop gadget
,这种gadget对于寻找其它的gadgets取到了至关重要的作用
寻找brop_gadget
当有了stop gadget
,我们就可以继续寻找其它有用的gadget(useful gadget)(比如pop rdi这些)
在没有stop gadget
之前 我们是无法确定useful gadget
的;假设我们猜到一个useful gadget如pop rdi ;ret
,但是由于执行完这个gadget
后进程还是会跳到下一个地址,如果该地址是一个非法地址,那么进程到最后还是会carch,在这个过程中攻击者其实不知道这个userful gadget已经执行过了,他看到的结果是程序crash了,从而不认为这是个userful gadget而放弃它
但是如果我们有了stop gadget,那就不一样了,如果我们在需要尝试的gadgets 后面填上了足够多的stop gadget
,如图
这样,如果尝试的gadget 是useful gadget
的话程序 就不会crash。
brop gadget
:在libc_csu_init
的结尾有一长串的 gadget ,我们可以通过偏移来获取 pop rdi;ret
和pop rsi ; pop r15 ; ret
这两个gadget,如图:
用上面提到的找 useful gatget
的方法就很容易找到这个brop gadget
了
寻找plt项
程序的plt表具有比较规整的结构 ,第一个plt表项都是16字节。而且,在每一个表项的6字节偏移处,是该表项对应的韩寒苏解析路径,即程序最初执行该函数的时候,会执行该路径对函数的got 地址进行解析。
另外 ,对于大多数 plt 调用来说,一般都不容易崩溃,即使是使用了比较奇怪的参数。所以,如果我们发现了一系列的长度为16的没有使得程序崩溃的代码段,那么我们有一定的理由相信我们遇到了 plt 表。
控制 RDX
对于没有puts函数的程序来说,一般用来dump 的函数是write,于是就有一个问题,write需要3个参数,也就是说rdx也需要控制 ,但是一般程序中是不存在pop rdx;ret
这样的gadget的;所以作者提出,相比于寻找pop rdx
指令,他认为可以利用strcmp这个函数调用,该函数调用会把字符串的长度赋值给rdx
,从而达到相同的效果。需要提醒的是,并不是所有程序都会调用strcmp函数,当程序中没有strcmp 函数的情况下,我们就得利用其它的方式来控制 rdx 了
那么,我们如何确定 strcmp 函数的plt地址呢?
对于strcmp 来说,作者提出的方法是对其传入不同的参数组合,通过该方法调用返回的结果来进行判断 ,因为brop gadget
的存在,所以我们可以很方便地控制前两个参数,strcmp 会发生如下可能的情况:
1 | strcmp(bad,bad) |
只有最后一种格式,程序才会正常执行;
注:在没有 PIE 保护的时候,64位程序 的ELF文件的0x400000处有7个非零字节
那么我们该如何具体去做呢?一种比较直接的方法就是从头到尾依次扫描每个 plt 表项,但是这个比较麻烦,我们可以选择如下的一种方法
- 利用 plt 表项的慢路径
- 并且利用下一个表项的慢路径的地址来覆盖返回地址
这样,我们就不用来回控制 相应的变量了。
当然我们也可能碰巧到到 strncmp 或 strcasecmp 函数,它们和 strcmp 函数具有一样的效果
寻找输出函数
寻找put@plt
我们已经可以控制 rdi 了,在没有开启 PIE 的情况下,0x400000 处为ELF文件的头部,其内容为\x7fELF
,所以我们可以通过打印 0x400000来判断是否为put@plt
寻找write@plt
这个,如果可能调用wite(1,buf,len)的话就跟上面找puts一样,但是对于寻找或者怎么执行write(socket,buf,len)
,菜鸡还是没搞懂,这里就涉及到寻找文件描述符的事情 ,先挖个坑,有机会来填。。。。。
回到最初的目的——dump 文件
其实不用dump整个文件,我们可以选择dump程序的前一部分,一般0x400000~0x4001000
就够,因为这部分包含了plt表,在plt表中我们就可以得到got表地址,之后再打印got表拿到真实地址后就可以实施攻击了。
例子
还是需要具体的题目来练练手才靠谱
HCTF2016出题人失踪了,我们可以本地把docker搭起来模拟远程
1、确定栈溢出长度
1 | # step 1 |
通过上面的函数我们可以确定,栈溢出的长度为72,并且通过回显的信息可以发现程序并没有开启canary,所以直接开始寻找stop gadget
2、寻找gadget
1 | # step 2 |
这里我们直接尝试64位程序没有开PIE的情况,结果发现了不少地址,我们选取一个貌似返回于是源程序 中的地址
one success addr 0x4006b6
3、寻找brop_gadget
1 | # step 3 |
get_brop_gadget
得到可能的brop_gadget
,check_brop_gadget
用来检查这个brop_gadget
4、确定puts@plt地址
根据得到的brop_gadget我们构造如下payload 来获取puts@plt
1 | pay = 'a'*lenth + p64(pop_rdi) + p64(0x400000) + p64(addr) + p64(stop_gadget) |
1 | # step 4 |
得到地址0x400555
,可以根据plt的结构进行调整得到地址0x400560
,但是直接用也没关系
5、dump文件
有puts函数后我们就可以dump出一部分的文件
1 | # step 5 |
需要注意的是:这里用puts函数来泄露程序 ,如何我们接收到的是””,说明遇到了’\x00’。
接下来用IDA打开程序 edit -> segments -> rebase program
修改Value为0x400000
,再找到地址0x400555
按下 p,将数据转换成函数
1 | seg000:0000000000400555 sub_400555 proc near |
得到puts@got = 0x601018
,当然,如果用地址0x400560
生成函数的话就理直接了,这就跟正常程序中的got表一模一样了。
1 | seg000:0000000000400560 sub_400560 proc near |
之所以地址0x400555
和0x400560
都可以成功调用puts@plt
,是因为jmp qword ptr cs:601018h
前面的几句汇编对程序影响不大,最后程序还是会正常跳到puts@got
,所以这两个地址效果是一样的
6、get shell
1 | # step 6 |