lkgit was a kernel challenge mimicking a git system in kernel space. It allows us to add files with a commit message into kernel memory, retrieving them and updating the commit message.
The module can be accessed via /dev/lkgit and the functionality is handled via ioctl calls.
The main issue in the implementation is, that lkgit_get_object and lkgit_amend_message use find_by_hash to first get the index of the object to work on and store a reference to it, and then starts copying data to and from it.
Since there is no lock defined anywhere in the module, it’s possible to race this and for example abuse this in lkgit_get_object to delete the object after it was fetched, but before the data is copied from it.
To make racing easier, we can use userfaultfd.
If we can stop execution of lkgit_get_object before the hash gets copied back into the user object and delete the object and replace it with some kernel object, that contains useful data in the first 0x10 bytes and then continue execution, we could use this to leak kernel addresses.
Perfect situation for using userfaultfd, setting up some pages and put our request object into that region, so that the kernel module can read hash and content from it, but will run into a page fault when trying to read message. This way, execution in the kernel module would pause at the desired point and transfer execution into our userfault handler (where we can delete the current object and replace it with a kernel object) and then trigger UFFDIO_COPY, so execution will continue in the kernel module, copying the first 0x10 bytes from that kernel object into our user hash value.
I’ll not go too deep into how to setup userfaultfd for this writeup (just check attached pwn.c how this is done, I added logs and comments to the userfault handling to make it very verbose).
Let’s start with preparing the leak for a kernel address. For this, we want to create an initial file, then try to request it and let the request page fault, when trying to fill the message of our request.
We create a page of size 0x2000 and align our request object in this page, so that hash and content are located in the first page and the message object in the second page.
By this, lkgit_get_object will trigger a page fault, when trying to write to message, passing execution to our userfault handler.
So, now we’re in our userfault handler break_on_read, while the lkgit_get_object function has already fetched the object to return, we can now just store another file with the same hash in the kernel. This will effectively delete the current object and store a new one there. While the object is deleted, let’s put a shmem struct there and return to execution in lkgit_get_object, which will then copy the first 0x10 bytes from the shmem kernel object into the hash of our request.
Let’s see some bad drawing skills for getting a better idea :)
So the last copy_to_user will actually work on the shmem structure which we sneaked into the (deleted) current object in our userfault handler, so we can leak a kernel address from it.
Now that we have a kernel leak, we need to do the opposite way around to be able to write something. Similar to our leaking process, we can use lkgit_amend_message and also “pause” it after acquiring the object, when it tries to read the message to write to the kernel object.
We can then delete the current object and allocate another object, so that the message from this will be allocated over our current object (by which we can control the target->message pointer for the current object in lkgit_amend_message).
When we then continue execution, our new message will be copied to our crafted target->message buffer instead of the original one.
Since the kernel configuration didn’t prohibit usage of usermode_helper, I opted for overwriting modprobe_path and then let it copy the flag and change it permissions, so we can easily read it afterwards.
Everything in place, let’s run it on remote and get another flag :)