A box of chocolate

my public personal notebook

TUCTF 2017 Pwn500 Writeup

Unlink yourself from the chains of earthly possessions.

We have a binary which gives heap-related wisdoms, and a file mm.c which is a self-implemented dynamic memory management code used in the binary. The heap chunks defined in mm.c have the following structure (taken from mm.c comments):

 *                    block     block+8          block+size-8   block+size    *
 *  Allocated blocks:   |  HEADER  |  ... PAYLOAD ...  |  FOOTER  |           *
 *                                                                            *
 *                    block     block+8          block+size-8   block+size    *
 *  Unallocated blocks: |  HEADER  |  ... (empty) ...  |  FOOTER  |           *
 *                                                                            *

Allocated chunk addresses are aligned to 16 bytes. The chunk's real size (with header and footer size included) is stored in both chunk's header and footer, and bit 0 of the size indicates if the chunk is in use or not.

mm.c defines malloc and free, and free implements a simple chunk coalescing procedure, which won't leave immediate contiguous free chunks in memory:

/* Coalesce: Coalesces current block with previous and next blocks if either
 *           or both are unallocated; otherwise the block is not modified.
 *           Returns pointer to the coalesced block. After coalescing, the
 *           immediate contiguous previous and next blocks must be allocated.
 */
static block_t *coalesce(block_t * block) 
{
    block_t *block_next = find_next(block);
    block_t *block_prev = find_prev(block);

    bool prev_alloc = extract_alloc(*(find_prev_footer(block)));
    bool next_alloc = get_alloc(block_next);
    size_t size = get_size(block);

    if (prev_alloc && next_alloc)              // Case 1
    {
        return block;
    }

    else if (prev_alloc && !next_alloc)        // Case 2
    {
        size += get_size(block_next);
        write_header(block, size, false);
        write_footer(block, size, false);
    }

    else if (!prev_alloc && next_alloc)        // Case 3
    {
        size += get_size(block_prev);
        write_header(block_prev, size, false);
        write_footer(block_prev, size, false);
        block = block_prev;
    }

    else                                        // Case 4
    {
        size += get_size(block_next) + get_size(block_prev);
        write_header(block_prev, size, false);
        write_footer(block_prev, size, false);

        block = block_prev;
    }
    return block;
}

mm.c uses each chunk's size to find the previous and next chunks in memory. When determining the previous chunk's address, mm.c will look at the previous chunk's footer:

/*
 * find_prev: returns the previous block position by checking the previous
 *            block's footer and calculating the start of the previous block
 *            based on its size.
 */
static block_t *find_prev(block_t *block)
{
    word_t *footerp = find_prev_footer(block);
    size_t size = extract_size(*footerp);
    return (block_t *)((char *)block - size);
}

And the previous chunk's allocation status is also determined by its footer:

bool prev_alloc = extract_alloc(*(find_prev_footer(block)));

When reversing the binary, in give_wisdom function, we'll see that the program malloc a text field with our given size, and then calls readbytes with our size:

f:id:dakutenpura:20171127024211p:plain

readbytes calls fgets with sz = bufsize + 1, to ensures that it can read bufsize bytes and a terminating null character:

uint32_t __cdecl readbytes(char *buf, uint32_t bufsize)
{
  fgets(buf, bufsize + 1, stdin);
  return bufsize + 1;
}

Because of that, we can write a single null byte to a chunk's footer. When we free the next chunk, mm.c will think that this text chunk is freed and will consolidate with it, even when this chunk is actually still in use, which might lead to a chunk shrinking attack. But during malloc, the program still uses chunk headers to navigate the freelist (which we still can't control), so this might not be useful for us yet.

Back to give_wisdom, we'll see that the program assigns the text_size field of newWisdom with the result of readbytes, which is size + 1:

...
v4 = readbytes(v2, size);
newQuote->text = newWisdom;
newQuote->text_size = v4;
...

And then later, in modify_wisdom, we can modify text_size bytes of the text chunk, which is also size + 1:

f:id:dakutenpura:20171127024354p:plain

This means we can fully control the least significant byte of a chunk's footer.

Great! Now we can exploit the program like the following:

  1. We'll gib 3 wisdoms, so our controllable part of the heap will look like this (one wisdom will have a metadata chunk of size 0x20 and a text chunk with user-controlled size): f:id:dakutenpura:20171127030521p:plain
  2. We then modify the 2nd wisdom, and enlarge its text chunk's footer to include both the 2nd wisdom's metadata chunk and the 1st's text chunk: f:id:dakutenpura:20171127030258p:plain
  3. We take the 3rd wisdom. When freeing 3rd's metadata chunk, coalesce will think that 2nd's text chunk is freed (because bit 0 of its footer is 0) and will try to consolidate backward. Because of our faked size, find_prev will return the 1st's text chunk instead of 2nd's text chunk, and the program will mark that chunk as freed during coalescing: f:id:dakutenpura:20171127033959g:plain Because 1st's text chunk is marked as freed, malloc will use this part of memory to serve new requests. But 1st's text chunk is still being referenced by the program, so we can say that the 1st wisdom's text chunk is "forgotten".
  4. We'll give a new wisdom, then modify the first wisdom. The new wisdom's metadata will be stored where 1st's text chunk is, so when we modify 1st wisdom's content, we're actually modify the new wisdom's metadata. We can then point the new wisdom's text_ptr field to the GOT table, then take that wisdom to get libc's base address. f:id:dakutenpura:20171127035228p:plain
  5. In checksec we can see that this binary have partial RELRO, so we can overwrite its GOT table. After getting libc's base, we will give another wisdom, modify 1st's wisdom to point its text_ptr to atoi@got, then modify it to write atoi@got with the calculated address of system@libc for given libc file in the problem description. After this, when we trigger atoi during the menu prompt, we'll call system instead; supply it with /bin/sh to get our shell.

Here's the exploit code:

gist.github.com

And here's the script in action:

f:id:dakutenpura:20171127023623p:plain