tl;dr
- Use the integer overflow to trigger a kernel heap overflow.
- Use the heap overflow to overwrite
tty
structure function pointers to get code execution.
Challenge Points: 986
No of Solves: 7
Challenge Author: Cyb0rG
Challenge description
A long queue awaits you in ring0
To start with , the challenge handout folder comes with bzImage
, rootfs.cpio
, run.sh
and source code files.
We immediately see that smep
and smap
are disabled in the run.sh
.
Analysis
The module implements create
, delete
, edit
and save
functionalities.
Before heading to the functionalities, it is better we refer to the important structures being used for various operations.
Queue structure
1 | typedef struct{ |
Structure of each entry in queue
1 | struct queue_entry{ |
Structure of request from userspace
1 | typedef struct{ |
- Create_kqueue -
1 | static noinline long create_kqueue(request_t request){ |
Here are the observations we can make from the necessary checks happening above -
queueCount
must not exceed 5.request.max_entries
should not be less than 1.request.data_size
should not exceed 0x20.request.data_size
is the size of each queue entry and within a queue , each entry has the samedata_size
.
1 | /* Check if multiplication of 2 64 bit integers results in overflow */ |
- Here we see that multiplication of
sizeof(queue_entry)
andrequest.max_entries+1
is being stored inspace
after making sure that it doesn’t overflow 64 bits. - We see the addition of
sizeof(queue)
and the above result of multiplication being stored inqueue_size
.
So each queue is essentially creating space for it’s entries , the number of entries come from the request.max_entries
which determine the size of the entire queue.
1 | /* All checks done , now call kmalloc */ |
Once above checks are done, the queue is allocated. Also since the main queue has a data field, it’s data is allocated on heap.
After that, the queue structure fields are populated.
Every entry of queue also needs to be allocated memory for storing data , and this happens next.
1 | /* Get to the place from where memory has to be handled */ |
In the above code , we see how we now reach the memory location from where remaining entries of the queue need to be allocated.
- We iterate
max_entries
number of times, populate theidx
field of the kqueue_entry, the data field and finally populate thenext
pointer if more than 1 entries exist.
1 | /* Find an appropriate slot in kqueues */ |
- Finally , after allocating memory for all queue entries, we now store the queue on a global array and increment the
queueCount
.
Before going forward, let’s have a visual look of memory when a queue gets allocated.
1 | 0xffff88801edfc3f8: 0x0000000000000000 -> queue_idx 0x0000000000000020 -> data_size |
After this , queue entries follow -
1 | 0xffff88801edfc420: 0x0000000000000001 -> idx 0xffff88801e3b4e40 -> data |
- Delete Kqueue
1 | static noinline long delete_kqueue(request_t request){ |
- This function just frees the kqueue and nulls out it’s memory.
- Edit Kqueue
1 | static noinline long edit_kqueue(request_t request){ |
- This function basically iterates through the entries of the requested queue and copies
request.data
intokqueue_entry->data
.
- Save Kqueue
1 | /* Now you have the option to safely preserve your precious kqueues */ |
Basic checks which ensure no out of bound access. We also check if request.max_entries
are greater than queue->max_entries
.
1 |
|
- Here ,
request.data_size
is checked againstqueue->queue_size
which obviously paves a straight away path for a heap overflow.
request.data_size
can be large enough and eventually , the memory allocated for the max_entries
will overflow into the next chunk.
Note - This was actually unintended on my part, the proper way of exploiting the challenge was through abusing the integer overflow which I will nevertheless discuss about in a moment.
kzalloc
is called to allocate memory for the new queue.- First , data of main queue is copied to the new queue.
- Subsequently, we iterate through max_entries and copy data of other entries as well.
1 | /* copy all possible kqueue entries */ |
Finally , we mark the queue as saved , note that a saved queue cannot be saved again.
Idea of exploitation
1 | if(__builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space) == true) |
Since there’s no check on request.max_entries
, a 0xffffffff
as max_entries results in integer overflow. This when coupled with the save option can result in a heap overflow.
With the heap overflow , since smep is disabled, we can overwrite it with pointer to userspace shellcode. There is no need of leaks since we have shellcode execution , we can easily control some register which already points to a kernel code address and change it to point to any function we wish to call in kernel.
Conclusion
The challenge could have been made better by enabling smep and smap , it would have been more fun to leak kernel pointers with partial overwrites but yes , the exploit would have been a lot less reliable in that case.
Here is the complete exploit.
Flag - inctf{l3akl3ss_r1p_w1th_u5erSp4ce_7rick3ry}