Hifolks, in this post I'm going to walk through how to setup the linux kernel for debugging. I will also demonstrate that the setup works by setting a break-point to a test driver I wrote myself. All the code will be available from my gitlab, all the links to my gitlab will be re-posted at the end.
The setup I describe here re-uses some parts of the syzkaller setup, and for good reason later on in the post series I will break into a tutorial for the syzkaller tool as well. So lets get on with it.
Okay so we want to study kernel exploitation but given that the kernel isn't something totally accessible in userspace, its not as convenient to debug as userpace stuff, we need a bit of a run up before we can actually poke and prod the kernel to figure out how to write our exploits. So there's a number of important steps to how we get this done, here's what we're going to do:
We also need to be able to build our kernel because there may be build options that are important to configure in order to control exploit protection or include modules and functionality to the kernel when needed.
Okay so before we get going with launching our Qemu instances and debugging modules we need an environment. For convenience sake I'm working off of a fresh Ubuntu 18.04.5 LTS machine. I'll document the processes from fresh install to first successful kernel build.
We're just a couple steps from sending the final build commands, before we get to that lets make sure the kernel config is ready to rock. Because we're working on a Linux host we can simply swipe the .config for the virtual machine's Ubuntu kernel like so:
Great, now we need to enable some options for debug symbols, kaslr and other awesome things. So open the .config somewhere in a text editor and make sure you either add or modify the file so these options are set:
Once you're kernel is build we need to start thinking about how to build a file system for this. Here I'm going to cheat and steal some tips from the syzkaller folks. We need to first download syzkaller, as follows:
The -s is a shorthand for -gdb tcp::1234, which means the gdbserver will be hosted at port 1234. -S tells qemu not to start the cpu automatically, this gives us a chance to set a breakpoint before the kernel starts executing.
We give the "c" command to continue execution. We can now set some of our own breakpoints. As part of the tutorial I've included a custom IOCTL driver and app code (code that invokes the ioctl from userspace), i thought this would be nifty since it shows full ability to develope and debug a driver, something crucial to hunting down modern bugs and exploit development. Anyway lets code and build our own module.
The code for debug_driver.c and debug_driver_app.c as we well as the Makefile are available at this repo -kernel-exploit-development. All you need to do is download the repo and stick this in its own folder under [kernel_dir]/drivers/. To build the module the we need to set the "M" variable in the kernel make script:
Now we need to get this module on our qemu host somehow, I do this the hard way, I'm sure there's all sorts of nifty ways to scp files onto the qemu host but I actually just re-create the image after copying the drivers to a folder to be baked into the start up filesystem. First we need to edit create-image.sh so it includes everything in a folder we specify, that way we can just dump stuff in the folder and run create-image.sh whenever we want those files on a live instance.
Okay so we have a module, we have a symbol file debug_driver.ko, with stuff we need to set breakpoints. Lets load the module into the kernel, then check where it gets loaded before we actually set the breakpoint:
So thats the breakpoint hit! We achived our goal for this post, if you'd like to explore more try setting more breakpoints and before moving on to the next post make sure to get your gdb foo up. Next post is going to look at exploitation of stack vulnerabilities.
Disclaimer: This post will cover basic steps to accomplish a privilege escalation based on a vulnerable driver. The basis for this introduction will be a challenge from the hxp2020 CTF called "kernel-rop". There's (obviously) write-ups for this floating around the net (check references) already and as it turns out this exact challenge has been taken apart in depth by (ChrisTheCoolHut and @_lkmidas), for part two I'll prepare a less prominent challenge or ignore those CTF challenges completely... So, this here very likely won't include a ton of novelty compared to what's out there already. However, that's not the intention behind this post. It's just a way for me to persist the things I learned during research and along the way to solving this one. Another reason for this particular CTF challenge is its simplicity while also being built around a fairly recent kernel. A perfect training environment :)!
With that out of the way, let's get right into it. The primary goal for kernel pwning is unlike for user land exploitation to not directly spawn a shell but abuse the fact that we're having control of vulnerable kernel code that we hope to abuse to elevate privileges in a system. Spawning a shell only comes after, at least in your typical CTF-style scenarios. Sometimes having an arbitrary read-write primitive may already be enough to exfiltrate sensitive information or overwrite security critical sections.
Luckily, the environment is fully under our control, so for testing purposes we can toggle the mitigations to make our life a tad easier for the exploit development process :)! Furthermore, we can see that the provided file system initramfs.cpio.gz is supplied in a compressed manner, so when we want to include our exploit we would need to unpack the file system, place our payload and pack it again. This is tedious, even more so in development cycles of an exploit. Having convenient scripts for these steps helps a lot.
The two things we should (or have to) do first are unpacking the file system and extracting vmlinuz into a vmlinux. For the first one, we can just use gunzip and cpio to extract this archive. When done, we're presented with a basic file system structure:
With that out of the way, we're set to start our exploitation journey. Let's quickly sift through the kernel driver hackme.ko and see what we're presented with. Loading it in a disassembler reveals that we only have a handful of functions:
hackme_release, hackme_open, hackme_init, and hackme_exit are mostly uninteresting (at least for this challenge) as they're only a necessary evil to (de-)register the kernel module and properly initialize it. That leaves us with only hackme_write and hackme_read. As for the hackme_read function that allows reading from /dev/hackme it looks as follows:
I found the disassembly here to be a tad confusing at first, at least in terms of how the __memcpy has been set up. Hence, I rewrote the disassembly into better readable C. Essentially, what is happening here is the following:
The code should be pretty self-explanatory, but the gist is that we're writing a user-controlled amount of data from a small fixed sized buffer (tmp) in the large hackme_buf, which we later return to the user. After reading data from tmp we do have a sanity check of some sort that checks whether our requested amount is less than 0x1000 bytes. With the buffer being read from only being 0x80 bytes large that's rather useless. This results in us easily being able to read out-of-bounds here. However, following that, we have a more strict sanity check in __check_object_size that verifies 3 things:
We check all of these 3 boxes with ease, so as a result, the requested data is written back to us into user land, and we got ourselves a sweet opportunity for a memory leak! The hackme_write counterpart is semantically identical, with the difference of allowing us as an attacker to send data to the driver:
I'll leave it to you to translate this code snippet to C-equivalent source code. An important note here though is that since the hackme_write function is semantically the same, it does not give us an out-of-bounds read, but an (almost arbitrary large) out-of-bounds write as we're writing user controlled data in the very constraint tmp buffer here! With that, we already have identified our primitives for this challenge.
Next, recall that strategy-wise kernel exploitation in general aims not at spawning a shell first thing (what good would a shell for a non-root user do anyway), but at elevating privileges to the highest possible level first. However, the general idea of how to approach this e.g. via ROP applies equally to user land and kernel land with only minor differences. First things first, though. We saw in our static analysis that we have a nice memory leak in the hackme_read function. Let's set up our "exploit" and see what we can get back from the driver:
The above code already gives it away, we're reading 320 bytes, which is reading 0xc0 bytes past the tmp buffer. Adding the exploit to the file system, starting the environment (./run.sh) and executing the exploit gives us back plenty of data, including an evident looking stack canary at index 2, 16, and 30:
The one at index 2 seems weird as this should still be in bounds. Maybe since tmp is not properly initialized, the system just decides to leave it with uninitialized data, which happens to be the kernel stack canary for whatever reason (if you know better LMK!). Anyhow, we found out the hard way that a kernel stack canary is in place regardless of all the disabled mitigations. Then again, we were able to leak it first thing here at a sensible offset of 17 * 8 bytes (0x88), which is located just past the tmp buffer when using the one at index 16.
The next step would be testing if we can take control over rip when writing to the vulnerable driver, since we know the buffer size to fill, the canary, and its offset that sounds doable. We're going to add a function that creates a payload, which inserts the stack canary at the correct offset, which we found just earlier. In addition to that, we will add three dummy values, which when looking at the function epilogue of hackme_write earlier are the three registers rbx, r12 (IDA named it data), andrbp. Analogously, we can observe the same pattern of popping these three registers in the function epilogue in hackme_read. This is a noticeable difference to user land exploitation. We need to compensate for these three pop instructions before being able to overwrite the return address:
3a8082e126