Skip to content

Commit d074bf5

Browse files
authored
Update how-to-kernel-pwn.md
1 parent be731f8 commit d074bf5

1 file changed

Lines changed: 82 additions & 10 deletions

File tree

main/content/post/how-to-kernel-pwn.md

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ This guide will
1414
- Adding custom components to the stock image (namely custom kernel modules and init scripts)
1515
- Discuss enabling / disabling specific kernel protections
1616
- Demonstrate the use of containerization to deploy as a CTF challenge
17+
- Show a prototype of a fully reproducible kernel challenge build system
1718

18-
## Building your image and running it with qemu
19+
## Getting Started
20+
21+
### Building your first image
1922

2023
To start, go to the the [buildroot download's page](https://buildroot.org/download.html) to download an LTS version of buildroot. From here, you can unzip it with `tar -xvf /path/to/my/file.tar.gz`.
2124

@@ -35,21 +38,79 @@ make -j<max logical core number>`
3538
```
3639
The output will be in `output/images/`.
3740

41+
### Running your image
42+
3843
This output image will, by default, contain a `bzImage` file, which holds the kernel itself, an `rootfs.ext4` file, which holds the root filesystem, and a `start-qemu.sh` script which will boot qemu for you.
3944

4045
Now, when you launch you'll see a login screen. From here, I'd recommend setting up a custom user for the challenge. In this case, you can change the shell for the `root` user to `/bin/false`, and add a new user to `/etc/passwd` and `/etc/shadow`.
4146

42-
## Adding custom components on startup
47+
## Building a challenge
4348

44-
This can be done with a custom `init` script. In order to pass a custom `init` script, we need a custom `initramfs`. You can either build this into the kernel, or you can build this and run qemu with flags for a custom initramfs. The latter is the approach I'd recommend taking for CTFs, because you'll often want an easy and quick way of updating init scripts, and waiting for a full recompile takes a lot of time.
49+
### Writing your kernel module
4550

46-
### Adding your custom kernel module
51+
Writing kernel modules is often _hard_ for beginners, because it's fundamentally working within a much larger codebase that's sparsely documented and can have disasterous consequences. For simpler CTF challenges, you really don't need much more than the basics. As a starting point, I've included the example below, which allows you to write directly into a buffer with no size check.
52+
53+
```C
54+
#include <linux/kernel.h>
55+
#include <linux/uidgid.h>
56+
#include <linux/module.h>
57+
#include <linux/proc_fs.h>
58+
#include <linux/uaccess.h>
59+
#include <linux/cred.h>
60+
#include <linux/sched.h>
61+
#include <linux/init_task.h>
62+
63+
64+
MODULE_DESCRIPTION("My suuuuper secure kernel module");
65+
MODULE_AUTHOR("emily747");
66+
MODULE_LICENSE("GPL");
67+
68+
#define PROC_NAME "challenge"
69+
70+
static ssize_t challenge_write(struct file *file,
71+
const char __user *user_buffer,
72+
size_t count,
73+
loff_t *ppos)
74+
{
75+
char stack_buffer[64];
76+
char* ptr = stack_buffer;
77+
78+
printk(KERN_INFO "[challenge] Writing %zu bytes\n", count);
79+
if (!access_ok(user_buffer, count))
80+
return -EFAULT;
81+
__copy_from_user(ptr, user_buffer, count);
82+
83+
return count;
84+
}
85+
86+
static const struct proc_ops proc_fops = {
87+
.proc_write = challenge_write,
88+
};
4789
48-
I'd recommend placing most of your custom code in the same place. So, you'll want to copy your compiled `chall.ko` into something like `/challenge/chall.ko`.
90+
static struct proc_dir_entry *proc_entry;
91+
92+
int challenge_init(void)
93+
{
94+
proc_entry = proc_mkdir("challenge", NULL);
95+
proc_create("kboff", 0666, proc_entry, &proc_fops);
96+
97+
printk(KERN_INFO "[challenge] Hello!\n");
98+
printk(KERN_INFO "[challenge] Try to connect to me at /proc/challenge/kboff.\n");
99+
return 0;
100+
}
101+
102+
void challenge_exit(void)
103+
{
104+
printk(KERN_INFO "[challenge] Goodbye!\n");
105+
}
106+
107+
module_init(challenge_init);
108+
module_exit(challenge_exit);
109+
```
49110
50111
### Compiling your kernel module
51112
52-
This is a more complex issue, but in short, you can use the Kernel's build system to build your challenge against the Kernel's specific headers. An example `Makefile` can be seen below:
113+
You'll need to use use the kernels build system to build your challenge against the kernel's specific headers. An example `Makefile` can be seen below:
53114
54115
```Makefile
55116
obj-m += challenge.o
@@ -67,6 +128,17 @@ clean:
67128
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
68129
```
69130
131+
This can be run like `make KERNELDIR=/path/to/kernel/headers/`, and will output a `challenge.ko` file.
132+
133+
Importantly, this is both architecture and version dependent. In effect, you'll either want to compile on the target itself (feasible if it's a larger VM for researching) or cross-compile with headers from that VM. In the below examples, I mostly do the latter, predominantly because it's significantly faster and avoids needing to run our compilation stage in the VM itself.
134+
135+
### Adding custom components on startup
136+
137+
To add custom components on startup, use a custom `init` script. In order to pass a custom `init` script, we need put the script in the filesystem itself. You can either build this with the kernel or patch the filesystem after the fact. The latter is the approach I'd recommend taking for CTFs, because you'll often want an easy and quick way of updating `init`, modules, etc., and waiting for a full recompile takes a lot of time.
138+
139+
### Adding your custom kernel module
140+
141+
I'd recommend placing most of your custom code in the same place. So, you'll want to copy your compiled `chall.ko` into something like `/challenge/chall.ko`. Anecdotally people tend to dislike it when they have to tear apart a filesystem looking for the module. In my opinion it's quite a useful skill (and quite easy, a simple `grep` for the file extension would do, but I tend to include the specific `chall.ko` as a file in the distributable seperately).
70142
71143
### Writing an init script
72144
@@ -85,11 +157,11 @@ exec busybox init
85157
86158
Notice in the above script that the actual script switches to a login shell at the final step in the `busybox init`. If you wanted, you could simply rebuild the init script from scratch to launch directly into your binary. This would allow you to give a userspace pwn into kernel pwn if you wanted to (for instance, shellcoding into kernel pwn). Alternatively, you can set a users shell to your custom binary, which would launch them directly into the userspace program.
87159
88-
Alternatively, if you wanted to build a jail breakout challenge, this same process could be used to provide kernel isolation.
160+
If you wanted to build a jail breakout challenge, this same process could be used to provide kernel isolation. A good example of this would be a seccomp restricted shellcoding challenge, where you need to disable seccomp by exploiting a kernel module. Some examples of this format are available on [pwn.college](https://pwn.college/system-security/kernel-security/).
89161
90-
## How do I disable \[X\] protection?
162+
### How do I disable \[X\] protection?
91163
92-
Some protections can be enabled or disabled at runtime (assuming a build with support for it). These include KASLR, SMAP, SMEP, NX, and PTI. These can be disabled by adding flags in qemu. For instance, to disable KASLR, you can use `-append "nokaslr"` (multiple options are delimited with a space), which will disable KASLR at runtime.
164+
Some protections can be enabled or disabled at runtime (assuming a build with support for it). These include KASLR, SMAP, SMEP, NX, and PTI (though, this can change depending on the architecture --- this is generally the case for x86/x86-64 though). These can be disabled by adding flags in qemu. For instance, to disable KASLR, you can use `-append "nokaslr"` (multiple options are delimited with a space), which will disable KASLR at runtime.
93165
94166
Some protections can only be changed at compile time, namely canaries and SLUB/SLAB hardening. These can be changed by editing the `CONFIG_STACKPROTECTOR`, `CONFIG_SLAB_FREELIST_HARDENED`, etc. options in your `kconfig`. The easiest way to use a custom `kconfig` with buildroot is to use the menu build option (`make menuconfig`).
95167
@@ -113,7 +185,7 @@ At this point, depending on your build configurations, it may be necessary to tw
113185
114186
## Running with docker (but unhinged)!
115187
116-
To take this a step further, we can completely automate the build process, so that the entire thing is done in Docker. An example of this can be seen below:
188+
To take this a step further, we can completely automate the build process, so that the entire thing is done in Docker. The major benefit of this is completely reproducable builds, but the major downside being the fact that it relies on docker caching layers to stop the kernel from fully rebubilding itself every time. In production, I recommend splitting this into two (one for building the filesystem / kernel, and the other for running VM). An example of this can be seen below:
117189
118190
```Dockerfile
119191
FROM debian:unstable@sha256:cc1675ddb1073d19ba9ef6fe9b9c625eceb02fccb9c0f7afbb4e60f16325c91d AS fs_builder

0 commit comments

Comments
 (0)