CakeCTF 2022
CakeCTF 2022 was a very enjoyable CTF, and our team, Th3Os managed to get 16 place! We hadn’t played together in a while so this was a very good result. Personally I solved welkerme, which got me really excited. I’ve been getting into pwn lately, so getting the chance to tackle a baby kernel challenge was very interesting. Shoutout to ptr-yudai for making this awesome chall.
** Disclaimer: ** The challenge was given out with a README.md
file which contained a lot of information to guide one through the challenge. In this writeup we will tackle the challenge without consulting it.
Solving The Challenge
Viewing The Files
We are given the below files:
exploit.c
: A template for our exploitMakefile
: The challs makefile to compile our exploit and kernel. Offersrun
anddebug
optionsrun.sh
&&debug.sh
: Essentiallyqemu
one-liners to run the kernels. Each is executed with the correspondingMakefile
optionssrc
: The directory containing the vulnerable driverdriver.c
: The source of the vulnerable driver
vm
: The directory containing the kernel sourcebzImage
: The compressed linux kernelrootfs.cpio
&&debugfs.cpio
: The archived filesystems Now let’s go more into depth on them
Source Code Analysis
Let’s start with our Makefile
and .sh
scripts so we can at least start running the system.
Makefile
|
|
Really if we pay some attention into both options, the only differences we can see are the archived filesystem being opened and the qemu
commands. Other than that, both create a vm/mount
directory where the filesystem is extracted to. Our compiled exploit
is copied into the systems /
dir, and then a cpio
command which recreates the archive from the extracted filesystem (don’t really know why since I tested and extracting the filesystem doesn’t mean our .cpio
is deleted. Maybe it has something to do with the instance idk. If someone who reads this knows hit me up). So all around standard stuff.
run.sh && debug.sh
Let’s check out the .sh
scripts
|
|
Again not that many differences. The main one is that -gdb tcp::12345
is added into debug.sh
so we can perform remote debugging with gdb
. Even though this isn’t s qemu
crash course, let’s discuss the flags:
-m
: Specify the memory size of the vm-nographic
: Disable graphical output-kernel
: Specify the kernel image-append
: Run the specified boot options as kernel commands. Some of them are self-explanatory, likeconsole
,loglevel
. However there are some interesting ones we will discuss later. Guess which they are 👀-no-reboot
: Exit insted of rebooting-cpu
: Specify the CPU model-monitor
: Redirect the monitor to specified device-initrd
: Specify the filesystem-net nic
: Create/Configure a Network Interface Card (NIC) and connect it to the emulated (USB ?) hub or to the netdev nd (don’t know what’s that :lemonthink:)-net user
: Configure a host network backend and connect it to emulated hub 0. In our case it’s the NIC we crated above-gdb
: Acceptgdb
connection Tried my best, you can find more information on them and many more in the official qemu docs
Surprisingly, we haven’t got all that many files left, just exploit.c
and driver.c
. So before running it let’s go look at them
driver.c
|
|
I’m not going to spend a lot of time into how kernel drivers work, after all LiveOverflow does an amazing job in this video. What we mostly care about is the module_ioctl
function and the 2 constants CMD_ECHO
&& CMD_EXEC
. When the function is called, depending on which of the 2 constants was passed, a different switch-case
is executed accordingly. CMD_ECHO
prints the value of the arg
argument, whereas CMD_EXEC
executes a function pointer to whatever function we pass into arg
. The vulnerability is obvious. Use CMD_EXEC
option and pass vulnerable code to arg
. Question is, what is vulnerable code for the kernel? Even before that, how do we attack the kernel? Let’s answer these questions before moving into the exploit.
Studying Linux Kernel Exploitation
A Quick Word On Security Mitigations
When attacking userland programs, one of the first things we do is check the security protections present (e.g. PIE
, Canary
, …). The kernel has it’s fair share of mitigations as well, like stack canaries, Supervisor Mode Execution, Supervisor Mode Access Prevention, and more. You can view them more analytically here. Point is that we don’t have any of them to worry about. Not even canaries, which cannot be disabled, as we have code execution thanks to the function pointer. We will go into a bit more depth for 1 of them later
A Quick Word On Char Devices
One could say that a kernel is like a process. However it’s not the same as a process. For example, we don’t communicate with a kernel module using the standard file descriptors. There are different kernel modules, and ours is a char module. A character module can be accessed like a file by means of filesystem nodes, that can be found in /dev
. There, it can be accesses under the DEVICE_NAME
defined our driver.c
source. In this case #define DEVICE_NAME "welkerme"
, so /dev/welkerme
.
The Simplest Kernel Attack: ret2usr
Let’s draw inspiration from one of the simplest userland buffer overflow attack, ret2shellcode. There, we could control the return address of a stack frame, and return to that address. This let us inject shellcode which would allow us to execute anything we wanted, like for example getting a shell. In cases of remote exploitation, we could get a shell on the remote machine, whereas in cases of privilege escalation, we could become a more privileged user (e.g. root) if the process was running as that user. The difference is that now we are exploiting in kernel space.
Every module in the kernel runs with root privileges. So our driver is running as root. This is the same as a process running as root. But as we’ve touched, it’s not exactly the same. When attacking regular userland processes, we are essentially attacking the process directly. In kernel exploitation, our exploit is running in userland, and is communicating with our module through the file descriptor. So we attempt to elevate the privileges of our process through exploiting the kernel module. Question is, how do we translate that into code?
There exist 2 kernel functions:
prepare_kernel_cred
: Prepare a set of credentials for a kernel service. This can then be used to override a task’s own credentials so that work can be done on behalf of that task that requires a different subjective context.commit_creds
: Install a new set of credentials to the current task Calling them like socommit_creds(prepare_kernel_creds(0))
will escalate the privileges of our process. Then we can just execute asystem("/bin/sh");
. So if we could just get their addresses to call them, we would be set. dsaisadsadsade addresses?
There exists the Kernel Symbol Table. There, any symbol exported by a loaded module can be accessed by any other loadable module. It can be found in /proc/kallsyms
. Let’s try and find these functions there.
|
|
NOTE: To get the above addresses, run the kernel with
make debug
to get root
Once we have them, we can write inline assembly to call them. And that’s it! Normally (and that’s what I did at first), you need to save your user space state before executing code in kernel space. But because of some things saspect and un1c0rn said to me that went a bit over my head, it isn’t necessary. So we can go ahead and write our solver:
|
|
To test it, let’s run make run
to test it
|
|
flag:
CakeCTF{b4s1cs_0f_pr1v1l3g3_3sc4l4t10n!!}
And we got it! This writeup might have been more than what was needed, but I really enjoyed the challenge, it was a great introduction to kernel exploitation.
References:
- https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/
- https://pawnyable.cafe/linux-kernel/
- https://docs.huihoo.com/doxygen/linux/kernel/3.7/cred_8c.html#acb0e83d1dc81cfb305afcd88a9f12fae
- https://www.linux.com/training-tutorials/kernel-newbie-corner-kernel-symbols-whats-available-your-module-what-isnt/
- https://www.oreilly.com/library/view/linux-device-drivers/0596000081/ch02s03.html