CSAW Quals 2021 - Krypto
[ pwn , kernel ]

I've always wanted to get into kernel development, and messing around with the new crypto subsystem seems like a great first idea. I even thought of a really catchy name for it!

Krypto is a linux kernel exploitation challenge from CSAW Qualifier 2021.

In this challenge we are given the LKM source code, krypto.c.
The kernel version is 5.4.0 and smap/smep are disabled.


#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <crypto/rng.h>

MODULE_AUTHOR("Nick Gregory");

#define KRYPTO_RNG_CMD 0x1337

struct rng_params {
    char *buf;
    size_t buf_len;

static int krypto_open(struct inode *inode, struct file *file)
    struct crypto_rng *rng = NULL;

    rng = crypto_alloc_rng("ansi_cprng", 0, 0);
    if (IS_ERR(rng)) {
        return -ENOMEM;

    // fun fact! the kernel docs don't include this so the example doesn't actually work!
    char seed[32] = {0};
    crypto_rng_reset(rng, seed, sizeof(seed));

    file->private_data = rng;

    return 0;

static int krypto_rng(struct file *file, struct rng_params *params)
    char *kbuf = NULL;
    int ret = 0;
    size_t len = params->buf_len;

    if (len == 0 || len > 0x1000) {
        return -EINVAL;

    kbuf = kzalloc(len, GFP_KERNEL);
    if (!kbuf) {
        return -ENOMEM;

    ret = crypto_rng_get_bytes(file->private_data, kbuf, len);

    if (ret < 0) {
        goto out_free;

    memcpy(params->buf, kbuf, params->buf_len);

    return ret;

static long krypto_ioctl(struct file *file, unsigned cmd, unsigned long arg)
    switch (cmd) {
    case KRYPTO_RNG_CMD:
        return krypto_rng(file, (struct rng_params *)arg);
        return -EINVAL;

static int krypto_release(struct inode *inode, struct file *file)
    return 0;

static struct file_operations krypto_fops = {
    .owner          = THIS_MODULE,
    .open           = krypto_open,
    .release	    = krypto_release,
    .unlocked_ioctl = krypto_ioctl,

static struct miscdevice krypto_device = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "krypto",
    .fops = &krypto_fops,
    .mode = S_IRUGO,

static int __init mod_init(void)
    return misc_register(&krypto_device);

static void __exit mod_exit(void)


Analyzing the LKM

In short the krypto_rng ioctl allows to write at most 0x1000 bytes to an arb location in memory using crypto_rng_get_bytes.

Because krypto_open seeds the random number generator using the same seed (char seed[32] = {0};) we can predict the bytes produced by crypto_rng_get_bytes.

To communicate with the module we use the rng_params struct:

struct rng_params {
    char *buf;       // Where to write the random bytes
    size_t buf_len;  // How many bytes

Finding the vuln

Obviously we have a write primitive in the kernel, but where and what we want to write?
Also there is a TOCTOU in krypto_rng, which we can use with userfaultfd to make the race reliable.


Winning the race with userfaultfd

To make TOCTOU easier I used userfaultfd.

Use crypto_rng_get_bytes to overwrite modprobe_path

To predict which bytes crypto_rng_get_bytes will produce I just dumped 0x1000 random bytes and then used a python script.

with open("./dump.raw", "rb") as f:
    data = f.read()

find = b"/tmp/a\x00"
found = []
i, j = 0, 0

data = data[256:] # The first 256 bytes are produced during the leak part 
                  # of the exploit, skip them
for b in data:
    if(len(found) == len(find)):
    if b == find[j]:
        found.append((i, chr(b)))

$ python3 predict.py
[(87, '/'), (133, 't'), (758, 'm'), (767, 'p'), (823, '/'), (1021, 'a'), (1124, '\x00')]

Final exploit

link to exploit.c

flag: flag{N3ver_trust_the_user...l@nd}