tl;dr
- Use the integer overflow to trigger a kernel heap overflow.
- Use the heap overflow to overwrite
ttystructure 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 -
queueCountmust not exceed 5.request.max_entriesshould not be less than 1.request.data_sizeshould not exceed 0x20.request.data_sizeis 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+1is being stored inspaceafter 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_entriesnumber of times, populate theidxfield of the kqueue_entry, the data field and finally populate thenextpointer 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.dataintokqueue_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_sizeis checked againstqueue->queue_sizewhich 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.
kzallocis 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}