Making a Kernel CTF (PUCon'24 pwn CTF)
Recently I had the pleasure of designing some challenges for a CTF held at my alma mater, University of the Punjab. Despite having reasonable experience with solving kernel challenges in the past, this was my first time actually making a kernel challenge on my own.
The idea was pretty simple, write a module that had a simple buffer overflow that led to RIP control. The execution however, was not that simple, so I decided to write this blog to document everything it took to set my challenges up.
Environment¶
The challenge was going to be hosted on CTFd's managed hosting so it had to be able to run in a Docker container. Since the container was going to run on CTFd infrastructure, we couldn't be sure what kernel we were going to get. To get past this we decided to run our challenge inside a qemu
VM inside our container.
VM¶
Initially for the guest OS, we went with the syzcaller debootstrap script (https://github.com/google/syzkaller/blob/master/tools/create-image.sh) that sets up a fully featured Debian image for us. After setting it up we found out the VM was using way more memory than what CTFd allowed for one container so we switched to using a simple initramfs
with busybox
setup.
Our setup was similar to hxp-ctfs
kernel ROP challenge (https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/) with a modified run.sh
script.
Since the exploit was supposed to run locally on the VM, we provided a way for the contestants to upload their exploit as soon as they connected by reading in a link at the start and downloading a file from there and putting it in the initramfs
.
Container¶
The container environment was pretty simple. cpio
and gzip
were required to modify the initramfs
and put the contestants exploits into the VM. wget
was used to download the exploit. qemu
was used to run the VM.
Initially we used ynetd
to serve the challenge but for some reason, one night before the CTF was supposed to star, ynetd
decided to bail on us and started sending EOF on stdin
as soon as we attempted to connect. For this reason we shifted to using socat
. Unfortunately this meant the shell that was served was unstable by default and we only figured out how to connect to it stably after the competition ended. Fortunately the kernel challenges were able to be solved with unstable shells.
Kernel¶
The kernel we chose was v6.6.16
and we applied some of our own patches to make it easier to exploit.
Added Exports¶
Some symbols were exported so we could directly use them to make a win function in our module.
kernel/cred.c | |
---|---|
kernel/reboot.c | |
---|---|
Removing Safety Checks in Code¶
We had to remove the size check in copy_to_user
and copy_from_user
to make sure our module would actually receive more bytes than the buffer could hold.
include/linux/uaccess.h
Removing Safety Checks in Config¶
We used defconfig
which was based on x86_64_defconfig
and edited it to turn off mitigations.
CONFIG_CC_HAS_RETURN_THUNK=n
CONFIG_CALL_PADDING=n
CONFIG_HAVE_CALL_THUNKS=n
CONFIG_CALL_THUNKS=n
CONFIG_PREFIX_SYMBOLS=n
CONFIG_RETPOLINE=n
CONFIG_RETHUNK=n
CONFIG_CPU_UNRET_ENTRY=n
CONFIG_CALL_DEPTH_TRACKING=n
CONFIG_CPU_IBPB_ENTRY=n
CONFIG_CPU_IBRS_ENTRY=n
CONFIG_CPU_SRSO=n
CONFIG_STACKPROTECTOR=n
CONFIG_STACKPROTECTOR_STRONG=n
Retpoline and return thunk proved hard to turn off for some reason. Setting their variables to n
wouldn't work and they'd get set to y
as soon as we started the build. To get past this we had to remove the options directly from arch/x86/Kconfig
Vulnerable Module¶
The module was pretty simple. It created a device that you could write to and read from. The vulnerability was in the unchecked read by the copy_from_user
function that we patched earlier. copy_from_user
was used to read unbounded data written from user-space into a kernel buffer of size 256 bytes.
We added a file_sending_system
win function that escalated privileges using commit_creds(&init_cred)
and then read a file from the file-system using the kernel_read_file_from_path
function defined in fs/kernel_read_file.c
. We had to add pragmas to ensure this function would not be optimized out by the compiler since it wasn't being called anywhere.
vuln.c | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
|
Deployment¶
After setting all that up locally, it was time to deploy the challenge on CTFd. This was as simple as deploying any other challenge. First we built the Docker image for the challenge and tagged it. Then we pushed it to the CTFd repo and it miraculously worked...... after 3 days of debugging and rewriting.