-

haxelion's den

nothing to see here

Latests

Articles

Writeups

Projects

About

LD_NOT_PRELOADED_FOR_REAL

LD_PRELOAD is probably one of the most amusing feature of Linux operating systems. It is the starting piece of dynamic instrumentation, reverse engineering madness and every fun userland rootkits. The problem is it is fairly easy to detect, spoiling the fun for everyone. This article is just a schizophrenic discussion on trying to detect LD_PRELOAD and implementing anti-detection countermeasures.

I hope you are already familiar with LD_PRELOAD, if not, go read one of the many tutorials on the subject. I will only remind that there are only two ways to register a library to be preloaded by ld.so:

  • setting the LD_PRELOAD environment variable to our library path
  • writing the library path in the /etc/ld.so.preload file

The first one has the advantage of being accessible to any users, but is only effective on processes you launch in that environment, meaning it will not affect other users. The second one has the advantage of being loaded on every process of your system but requires root access (on correctly configured machines).

Detecting LD_PRELOAD for dummies

Checking the value of the LD_PRELOAD environment variable, or the presence of /etc/ld.so.preload are the most common but also most obvious detection techniques out there. Barely any code is needed as you can see in the example below.

detect.c

 
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>

int main()
{
    if(getenv("LD_PRELOAD"))
        printf("LD_PRELOAD detected through getenv()\n");
    else
        printf("Environment is clean\n");
    if(open("/etc/ld.so.preload", O_RDONLY) > 0)
        printf("/etc/ld.so.preload detected through open()\n");
    else
        printf("/etc/ld.so.preload is not present\n");
}
% sudo touch /etc/ld.so.preload
% gcc -o detect detect.c 
% LD_PRELOAD= ./detect 
LD_PRELOAD detected through getenv()
/etc/ld.so.preload detected through open()
%

Of course if we can hook any shared library functions using LD_PRELOAD, there is nothing preventing our preloaded library to hook the functions used above and return the “correct” values. Below is an example of such hooks.

stealth_preload.c

 
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <limits.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>

// We will store the real function pointer in here
int (*o_open)(const char*, int oflag) = NULL;
char* (*o_getenv)(const char *) = NULL;

char* getenv(const char *name)
{
    if(!o_getenv)
        // Find the real function pointer
        o_getenv = dlsym(RTLD_NEXT, "getenv");
    if(strcmp(name, "LD_PRELOAD") == 0)
        // This environment variable does not exist, I swear
        return NULL;
    // Everything is ok, call the real getenv
    return o_getenv(name);
}

int open(const char *path, int oflag, ...)
{
    char real_path[PATH_MAX];
    if(!o_open)
        // Find the real function pointer
        o_open = dlsym(RTLD_NEXT, "open");
    // Resolve symbolic links and dot notation fu
    realpath(path, real_path);
    if(strcmp(real_path, "/etc/ld.so.preload") == 0)
    {
        // This file does not exist, I swear.
        errno = ENOENT;
        return -1;
    }
    // Everything is ok, call the real open
    return o_open(path, oflag);
}

// Still many other functions to hook, like fopen, open64, stat, readdir, 
// rename, unlink, etc.
% gcc -shared -fpic -ldl -o stealth_preload.so stealth_preload.c
% LD_PRELOAD=./stealth_preload.so ./detect 
Environment is clean
/etc/ld.so.preload is not present
%

It should be noted that many more functions need to be hooked in order to hide /etc/ld.so.preload. Some have direct effects like readdir(), stat(), open(). Some have undirect effects, like unlink() or rename(), where checking errno can indicate if the file does not exist (ENOENT) or if we do not have the write permission (EACCES) in which case it does exist.

Here is where many people stop their detection and anti-detection attempts, but for the fun of it, let’s go further, much further.

It’s the last time I call you

Ok, sure, we can intercept calls to any shared library including the libc, but what if there was a way to check the environment variables without calling any functions? Indeed, we can check the environment variables by reading the actual piece of memory holding them, environ.

####nocall_detect.c

 
#include <stdio.h>

// This will resolve at linking time
extern char **environ;

int main()
{
    long i, j;
    char env[] = "LD_PRELOAD";
    // Go through all environment strings, the end of the array 
    // is marked by a null pointer.
    for(i = 0; environ[i]; i++)
    {
        // Check is the string begins by LD_PRELOAD
        // I said NO CALL not even to strstr
        for(j = 0; env[j] != '\0' && environ[i][j] != '\0'; j++)
            if(env[j] != environ[i][j])
                break;
        // If the complete chain was found
        if(env[j] == '\0')
        {
            printf("LD_PRELOAD detected through environ\n");
            return;
        }
    }
    printf("Environment is clean\n");
}
% gcc -o nocall_detect nocall_detect.c 
% LD_PRELOAD=./stealth_preload.so ./nocall_detect
LD_PRELOAD detected through environ
%  

Because it is a simple and direct memory access, there is no way to intercept it. But … once the library is loaded into the program we do not really need that variable anymore. So how could we unset LD_PRELOAD as soon as our library is loaded? Well through the init function of our library. All we have to do is write an init() function and send the -init flag to the linker.

Inside the init() function there is not much to do: we save the value of LD_PRELOAD then remove it from the environment. This means that, as soon as our library is loaded, LD_PRELOAD will disappear from the environment and the program will never have any occasion of catching it because it will not execute any instruction before that. Unfortunately unsetenv() is not very effective at removing a variable and the value can still be found in /proc/self/environ and by running the set command. This is why it is reimplemented in the example below.

Now, if the program forks and loads another binary, our library will not be preloaded anymore because we removed it from the environment. So we need to restore that variable before the call to exec(). exec() is a whole family of functions, but they all redirect to execve(). execve() allows to set the environment through an array which means we need to create a new modified environment array to inject our LD_PRELOAD variable. Now the library will be loaded because LD_PRELOAD is set right before the call. LD_PRELOAD is unset right after the call for the parent process and through the init function for the child process, which means it completely disappears for both processes before they execute any instruction.

####noenviron_preload.c

 
#define _GNU_SOURCE
#include <unistd.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

extern char **environ;

int (*o_execve)(const char *path, char *const argv[], char *const envp[]) = NULL;

char *sopath;

// Called as soon as the library is loaded, the program has not executed any 
// instructions yet.
void init()
{
    int i, j;
    static const char *ldpreload = "LD_PRELOAD";
    // First save the value of LD_PRELOAD
    int len = strlen(getenv(ldpreload));
    sopath = (char*) malloc(len+1);
    strcpy(sopath, getenv(ldpreload));
    // unsetenv() has a weird behavior, this is a custom implementation
    // Look for LD_PRELOAD variable
    for(i = 0; environ[i]; i++)
    {
        int found = 1;
        for(j = 0; ldpreload[j] != '\0' && environ[i][j] != '\0'; j++)
            if(ldpreload[j] != environ[i][j])
            {
                found = 0;
                break;
            }
        if(found)
        {
            // Set to zero the variable
            for(j = 0; environ[i][j] != '\0'; j++)
                environ[i][j] = '\0';
            break;
            // Free that memory
            free((void*)environ[i]);
        }
    }
    // Remove the string pointer from environ
    for(j = i; environ[j]; j++)
        environ[j] = environ[j+1];
}


int execve(const char *path, char *const argv[], char *const envp[])
{
    int i, j, ldi = -1, r;
    char** new_env;
    if(!o_execve)
        o_execve = dlsym(RTLD_NEXT,"execve");
    // Look if the provided environment already contains LD_PRELOAD
    for(i = 0; envp[i]; i++)
    {
        if(strstr(envp[i], "LD_PRELOAD"))
            ldi = i;
    }
    // If it doesn't, add it at the end
    if(ldi == -1)
    {
        ldi = i;
        i++;
    }
    // Create a new environment
    new_env = (char**) malloc((i+1)*sizeof(char*));
    // Copy the old environment in the new one, except for LD_PRELOAD
    for(j = 0; j < i; j++)
    {
        // Overwrite or create the LD_PRELOAD variable
        if(j == ldi)
        {
            new_env[j] = (char*) malloc(256);
            strcpy(new_env[j], "LD_PRELOAD=");
            strcat(new_env[j], sopath);
        }
        else
            new_env[j] = (char*) envp[j];
    }
    // That string array is NULL terminated
    new_env[i] = NULL;
    r = o_execve(path, argv, new_env);
    free(new_env[ldi]);
    free(new_env);
    return r;
}
// You also have to patch all the other variants of exec
$ gcc -o noenviron_preload.so -shared -fpic -ldl -Wl,-init,init noenviron_preload.c
$ LD_PRELOAD=./noenviron_preload.so ./nocall_detect 
Environment is clean
$ 

You should note that many other functions from the exec familly need to be hooked. Also, the code here replaces entirely the value of LD_PRELOAD, but to be exact you should append your library to the variable if it is already set and only remove your library from the variable instead of unsetting it. A program could set LD_PRELOAD with a canary value and watch it disappear after a fork, confirming that some weird LD_PRELOAD magic is going on.

Memory Unknown

(Un)fortunately the environment variable or the ld.so.preload file are not the only way of detecting a preloaded library. Another way, which is a bit more complex to implement, is to read the memory maps of our program and detect the presence of the memory allocated to the preloaded library. As everything is a file under linux, it is located in /proc/self/maps.

Here is the normal process map of cat:

$ cat /proc/self/maps
00400000-0040c000 r-xp 00000000 fe:01 400301                             /usr/bin/cat
0060b000-0060c000 r--p 0000b000 fe:01 400301                             /usr/bin/cat
0060c000-0060d000 rw-p 0000c000 fe:01 400301                             /usr/bin/cat
01c28000-01c49000 rw-p 00000000 00:00 0                                  [heap]
7f40b88f4000-7f40b8a8d000 r-xp 00000000 fe:01 418477                     /usr/lib/libc-2.20.so
7f40b8a8d000-7f40b8c8d000 ---p 00199000 fe:01 418477                     /usr/lib/libc-2.20.so
7f40b8c8d000-7f40b8c91000 r--p 00199000 fe:01 418477                     /usr/lib/libc-2.20.so
7f40b8c91000-7f40b8c93000 rw-p 0019d000 fe:01 418477                     /usr/lib/libc-2.20.so
7f40b8c93000-7f40b8c97000 rw-p 00000000 00:00 0 
7f40b8c97000-7f40b8cb9000 r-xp 00000000 fe:01 412638                     /usr/lib/ld-2.20.so
7f40b8cee000-7f40b8e78000 r--p 00000000 fe:01 453088                     /usr/lib/locale/locale-archive
7f40b8e78000-7f40b8e7b000 rw-p 00000000 00:00 0 
7f40b8e96000-7f40b8eb8000 rw-p 00000000 00:00 0 
7f40b8eb8000-7f40b8eb9000 r--p 00021000 fe:01 412638                     /usr/lib/ld-2.20.so
7f40b8eb9000-7f40b8eba000 rw-p 00022000 fe:01 412638                     /usr/lib/ld-2.20.so
7f40b8eba000-7f40b8ebb000 rw-p 00000000 00:00 0 
7fff1644c000-7fff1646d000 rw-p 00000000 00:00 0                          [stack]
7fff165bf000-7fff165c1000 r--p 00000000 00:00 0                          [vvar]
7fff165c1000-7fff165c3000 r-xp 00000000 00:00 0                          [vdso]

And here is the process map of preloaded cat:

$ LD_PRELOAD=/tmp/noenviron_preload.so cat /proc/self/maps
00400000-0040c000 r-xp 00000000 fe:01 400301                             /usr/bin/cat
0060b000-0060c000 r--p 0000b000 fe:01 400301                             /usr/bin/cat
0060c000-0060d000 rw-p 0000c000 fe:01 400301                             /usr/bin/cat
00ef7000-00f18000 rw-p 00000000 00:00 0                                  [heap]
7fce2e877000-7fce2e87a000 r-xp 00000000 fe:01 411128                     /usr/lib/libdl-2.20.so
7fce2e87a000-7fce2ea79000 ---p 00003000 fe:01 411128                     /usr/lib/libdl-2.20.so
7fce2ea79000-7fce2ea7a000 r--p 00002000 fe:01 411128                     /usr/lib/libdl-2.20.so
7fce2ea7a000-7fce2ea7b000 rw-p 00003000 fe:01 411128                     /usr/lib/libdl-2.20.so
7fce2ea7b000-7fce2ec14000 r-xp 00000000 fe:01 418477                     /usr/lib/libc-2.20.so
7fce2ec14000-7fce2ee14000 ---p 00199000 fe:01 418477                     /usr/lib/libc-2.20.so
7fce2ee14000-7fce2ee18000 r--p 00199000 fe:01 418477                     /usr/lib/libc-2.20.so
7fce2ee18000-7fce2ee1a000 rw-p 0019d000 fe:01 418477                     /usr/lib/libc-2.20.so
7fce2ee1a000-7fce2ee1e000 rw-p 00000000 00:00 0 
7fce2ee1e000-7fce2ee1f000 r-xp 00000000 00:1e 20903                      /tmp/noenviron_preload.so
7fce2ee1f000-7fce2f01f000 ---p 00001000 00:1e 20903                      /tmp/noenviron_preload.so
7fce2f01f000-7fce2f020000 rw-p 00001000 00:1e 20903                      /tmp/noenviron_preload.so
7fce2f020000-7fce2f042000 r-xp 00000000 fe:01 412638                     /usr/lib/ld-2.20.so
7fce2f076000-7fce2f200000 r--p 00000000 fe:01 453088                     /usr/lib/locale/locale-archive
7fce2f200000-7fce2f203000 rw-p 00000000 00:00 0 
7fce2f21e000-7fce2f241000 rw-p 00000000 00:00 0 
7fce2f241000-7fce2f242000 r--p 00021000 fe:01 412638                     /usr/lib/ld-2.20.so
7fce2f242000-7fce2f243000 rw-p 00022000 fe:01 412638                     /usr/lib/ld-2.20.so
7fce2f243000-7fce2f244000 rw-p 00000000 00:00 0 
7fff3d885000-7fff3d8a6000 rw-p 00000000 00:00 0                          [stack]
7fff3d8f4000-7fff3d8f6000 r--p 00000000 00:00 0                          [vvar]
7fff3d8f6000-7fff3d8f8000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

As you can see, right between ld.so memory and the libc.so memory, our library has been loaded. An easy way to detect this is to look if there is anything else than an anonymous map (the one without name and with a bunch of zeroes) between the libc.so memory and ld.so memory.

####memory_detect.c

 
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>

#define BUFFER_SIZE 256

// Avoid to use libc strstr
// Return a pointer after the first location of sub in str
char* afterSubstr(char *str, const char *sub)
{
    int i, found;
    char *ptr;
    found = 0;
    for(ptr = str; *ptr != '\0'; ptr++)
    {
        found = 1;
        for(i = 0; found == 1 && sub[i] != '\0'; i++)
            if(sub[i] != ptr[i])
                found = 0;
        if(found == 1)
            break;
    }
    if(found == 0)
        return NULL;
    return ptr + i;
}

// Try to match the following regexp: libname-[0-9]+\.[0-9]+\.so$
// Not using any libc function makes that code awful, I know
int isLib(char *str, const char *lib)
{
    int i, found;
    static const char *end = ".so\n";
    char *ptr;
    // Trying to find lib in str
    ptr = afterSubstr(str, lib);
    if(ptr == NULL)
        return 0;
    // Should be followed by a '-'
    if(*ptr != '-')
        return 0;
    // Checking the first [0-9]+\.
    found = 0;
    for(ptr += 1; *ptr >= '0' && *ptr <= '9'; ptr++)
        found = 1;
    if(found == 0 || *ptr != '.')
        return 0;
    // Checking the second [0-9]+
    found = 0;
    for(ptr += 1; *ptr >= '0' && *ptr <= '9'; ptr++)
        found = 1;
    if(found == 0)
        return 0;
    // Checking if it ends with ".so\n"
    for(i = 0; end[i] != '\0'; i++)
        if(end[i] != ptr[i])
            return 0;
    return 1;
}

int main()
{
    FILE *memory_map;
    char buffer[BUFFER_SIZE];
    int after_libc = 0;
    memory_map = fopen("/proc/self/maps", "r");
    if(memory_map == NULL)
    {
        printf("/proc/self/maps is unaccessible, probably a LD_PRELOAD attempt\n");
        return 1;
    }
    // Read the memory map line by line
    // Try to look for a library loaded in between the libc and ld
    while(fgets(buffer, BUFFER_SIZE, memory_map) != NULL)
    {
        // Look for a libc entry
        if(isLib(buffer, "libc"))
            after_libc = 1;
        else if(after_libc)
        {
            // Look for a ld entry
            if(isLib(buffer, "ld"))
            {
                // If we got this far then everythin is fine
                printf("Memory maps are clean\n");
                break;
            }
            // If it's not an anonymous memory map
            else if(afterSubstr(buffer, "00000000 00:00 0") == NULL)
            {
                // Something has been preloaded by ld.so
                printf("LD_PRELOAD detected through memory maps\n");
                break;
            }
        }
    }
}
$ gcc -o memory_detect memory_detect.c 
$ LD_PRELOAD=./noenviron_preload.so ./memory_detect 
LD_PRELOAD detected through memory maps
$

Of course this suffers from the same problem as before except that this time we can not pretend the file does not exist, we have to present fake memory maps to the process. Like before we hook the open function (I used fopen() this time, technically you should also hook open(), open64(), openat64(), freopen(), etc), but now we create a temporary file where we copy the true memory maps without the lines related to our preloaded library.

####fakememory_preload.c

 
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>
#include <limits.h>
#include <errno.h>

FILE* (*o_fopen)(const char*, const char*) = NULL;
char *soname = "fakememory_preload.so";

void fakeMaps(char *original_path, char *fake_path, char *pattern)
{
    FILE *original, *fake;
    char buffer[PATH_MAX];
    original = o_fopen(original_path, "r");
    fake = o_fopen(fake_path, "w");
    // Copy original in fake but discard the lines containing pattern
    while(fgets(buffer, PATH_MAX, original))
        if(strstr(buffer, pattern) == NULL)
            fputs(buffer, fake);
    fclose(fake);
    fclose(original);
}

FILE* fopen(const char *path, const char *mode)
{
    char real_path[PATH_MAX], maps_path[PATH_MAX];
    pid_t pid = getpid();
    if(!o_fopen)
        // Find the real function pointer
        o_fopen = dlsym(RTLD_NEXT, "fopen");
    // Resolve symbolic links and dot notation fu
    realpath(path, real_path);
    snprintf(maps_path, PATH_MAX, "/proc/%d/maps", pid);
    if(strcmp(real_path, maps_path) == 0)
    {
        snprintf(maps_path, PATH_MAX, "/tmp/%d.fakemaps", pid);
        // Create a file in tmp containing our fake map
        fakeMaps(real_path, maps_path, soname);
        return o_fopen(maps_path, mode);
    }
    // Everything is ok, call the real open
    return o_fopen(path, mode);
}
$ gcc -o fakememory_preload.so -shared -fpic -ldl fakememory_preload.c
$ LD_PRELOAD=./fakememory_preload.so ./memory_detect 
Memory maps are clean
$

Now if you look at the resulting fake memory maps, you can see there are still some inconsistencies.

$ cat /tmp/1011.fakemaps 
00400000-00401000 r-xp 00000000 fe:03 7342345                            /home/haxelion/documents/den/article/files/memory_detect
00600000-00601000 rw-p 00000000 fe:03 7342345                            /home/haxelion/documents/den/article/files/memory_detect
020cd000-020ee000 rw-p 00000000 00:00 0                                  [heap]
7fabc17c2000-7fabc17c5000 r-xp 00000000 fe:01 411128                     /usr/lib/libdl-2.20.so
7fabc17c5000-7fabc19c4000 ---p 00003000 fe:01 411128                     /usr/lib/libdl-2.20.so
7fabc19c4000-7fabc19c5000 r--p 00002000 fe:01 411128                     /usr/lib/libdl-2.20.so
7fabc19c5000-7fabc19c6000 rw-p 00003000 fe:01 411128                     /usr/lib/libdl-2.20.so
7fabc19c6000-7fabc1b5f000 r-xp 00000000 fe:01 418477                     /usr/lib/libc-2.20.so
7fabc1b5f000-7fabc1d5f000 ---p 00199000 fe:01 418477                     /usr/lib/libc-2.20.so
7fabc1d5f000-7fabc1d63000 r--p 00199000 fe:01 418477                     /usr/lib/libc-2.20.so
7fabc1d63000-7fabc1d65000 rw-p 0019d000 fe:01 418477                     /usr/lib/libc-2.20.so
7fabc1d65000-7fabc1d69000 rw-p 00000000 00:00 0 
7fabc1f6a000-7fabc1f8c000 r-xp 00000000 fe:01 412638                     /usr/lib/ld-2.20.so
7fabc2148000-7fabc214b000 rw-p 00000000 00:00 0 
7fabc2188000-7fabc218b000 rw-p 00000000 00:00 0 
7fabc218b000-7fabc218c000 r--p 00021000 fe:01 412638                     /usr/lib/ld-2.20.so
7fabc218c000-7fabc218d000 rw-p 00022000 fe:01 412638                     /usr/lib/ld-2.20.so
7fabc218d000-7fabc218e000 rw-p 00000000 00:00 0 
7fff53b1e000-7fff53b3f000 rw-p 00000000 00:00 0                          [stack]
7fff53bfc000-7fff53bfe000 r--p 00000000 00:00 0                          [vvar]
7fff53bfe000-7fff53c00000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
$

For example the allocated memory blocks are not contiguous anymore. With a more complete memory maps parser this can be easily fixed, the example from above is only a proof of concept.

The Kernel Whisperer

I have now exhausted the standard techniques that I know of and it is time for assembly and kernel tricks. I hope you are familiar with both.

As you might know, the kernel functions, like open() or fork(), are actually called through a mechanism known as syscall: the syscall function number is put in eax (or rax under x86_64), the arguments in the other registers (see man syscall) and then the “int 0x80” instruction (or “syscall” under x86_64) is executed. This causes a processor interrupt which is catched by the kernel in charge of granting our wishes. The standard C library and system library are merely wrappers around this mechanism, providing a C API for basic OS functions, and those wrappers are what we have been hooking.

So if we directly use syscalls to call kernel functions we are bypassing the entire hooking process. Let’s reimplement the ld.so.preload file detection and memory maps detection but with syscalls this time.

####syscall_detect.c

 
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>

#define BUFFER_SIZE 256

int syscall_open(char *path, long oflag)
{
    int fd = -1;
    #ifdef __i386__
    __asm__ (
             "mov $5, %%eax;" // Open syscall number
             "mov %1, %%ebx;" // Address of our string
             "mov %2, %%ecx;" // Open mode
             "mov $0, %%edx;" // No create mode
             "int $0x80;"     // Straight to ring0
             "mov %%eax, %0;" // Returned file descriptor
             :"=r" (fd)
             :"m" (path), "m" (oflag)
             :"eax", "ebx", "ecx", "edx"
             );
    #elif __amd64__
    __asm__ (
             "mov $2, %%rax;" // Open syscall number
             "mov %1, %%rdi;" // Address of our string
             "mov %2, %%rsi;" // Open mode
             "mov $0, %%rdx;" // No create mode
             "syscall;"       // Straight to ring0
             "mov %%eax, %0;" // Returned file descriptor
             :"=r" (fd)
             :"m" (path), "m" (oflag)
             :"rax", "rdi", "rsi", "rdx"
             );
    #endif
    return fd;
 }

size_t syscall_gets(char *buffer, size_t buffer_size, int fd)
{
    size_t i;
    for(i = 0; i < buffer_size-1; i++)
    {
        size_t nbytes;
        #ifdef __i386__
        __asm__ (
                 "mov $3, %%eax;" // Read syscall number
                 "mov %1, %%ebx;" // File descriptor
                 "mov %2, %%ecx;" // Address of our buffer
                 "mov $1, %%edx;" // Read 1 byte
                 "int $0x80;"     // Straight to ring0
                 "mov %%eax, %0;" // Returned read byte number 
                 :"=r" (nbytes)
                 :"m" (fd), "r" (&(buffer[i]))
                 :"eax", "ebx", "ecx", "edx"
                 );
        #elif __amd64__
        __asm__ (
                 "mov $0, %%rax;" // Read syscall number
                 "mov %1, %%rdi;" // File descriptor
                 "mov %2, %%rsi;" // Address of our buffer
                 "mov $1, %%rdx;" // Read 1 byte
                 "syscall;"       // Straight to ring0
                 "mov %%rax, %0;" // Returned read byte number
                 :"=r" (nbytes)
                 :"m" (fd), "r" (&(buffer[i]))
                 :"rax", "rdi", "rsi", "rdx"
                 );
        #endif
        if(nbytes != 1)
            break;
        if(buffer[i] == '\n')
        {
            i++;
            break;
        }
    }
    buffer[i] = '\0';
    return i;
}

// Avoid to use libc strstr
char* afterSubstr(char *str, const char *sub)
{
    int i, found;
    char *ptr;
    found = 0;
    for(ptr = str; *ptr != '\0'; ptr++)
    {
        found = 1;
        for(i = 0; found == 1 && sub[i] != '\0'; i++)
            if(sub[i] != ptr[i])
                found = 0;
        if(found == 1)
            break;
    }
    if(found == 0)
        return NULL;
    return ptr + i;
}

// Try to match the following regexp: libname-[0-9]+\.[0-9]+\.so$
// Not using any libc function makes that code awful, I know
int isLib(char *str, const char *lib)
{
    int i, found;
    static const char *end = ".so\n";
    char *ptr;
    // Trying to find lib in str
    ptr = afterSubstr(str, lib);
    if(ptr == NULL)
        return 0;
    // Should be followed by a '-'
    if(*ptr != '-')
        return 0;
    // Checking the first [0-9]+\.
    found = 0;
    for(ptr += 1; *ptr >= '0' && *ptr <= '9'; ptr++)
        found = 1;
    if(found == 0 || *ptr != '.')
        return 0;
    // Checking the second [0-9]+
    found = 0;
    for(ptr += 1; *ptr >= '0' && *ptr <= '9'; ptr++)
        found = 1;
    if(found == 0)
        return 0;
    // Checking if it ends with ".so\n"
    for(i = 0; end[i] != '\0'; i++)
        if(end[i] != ptr[i])
            return 0;
    return 1;
}

int main()
{
    int memory_map;
    char buffer[BUFFER_SIZE];
    int after_libc = 0;

    // If the file was succesfully opened
    if(syscall_open("/etc/ld.so.preload", O_RDONLY) > 0)
        printf("/etc/ld.so.preload detected through open syscall\n");
    else
        printf("/etc/ld.so.preload is not present\n");
    // Open the memory map through a syscall this time
    memory_map = syscall_open("/proc/self/maps", O_RDONLY);
    if(memory_map == -1)
    {
        printf("/proc/self/maps is unaccessible, probably a LD_PRELOAD attempt\n");
        return 1;
    }
    // Read the memory map line by line
    // Try to look for a library loaded in between the libc and ld
    while(syscall_gets(buffer, BUFFER_SIZE, memory_map) != 0)
    {
        // Look for a libc entry
        if(isLib(buffer, "libc"))
            after_libc = 1;
        else if(after_libc)
        {
            // Look for a ld entry
            if(isLib(buffer, "ld"))
            {
                // If we got this far then everythin is fine
                printf("Memory maps are clean\n");
                break;
            }
            // If it's not an anonymous memory map
            else if(afterSubstr(buffer, "00000000 00:00 0") == NULL)
            {
                // Something has been preloaded by ld.so
                printf("LD_PRELOAD detected through memory maps\n");
                break;
            }
        }
    }
}
$ gcc -o syscall_detect syscall_detect.c 
$ LD_PRELOAD=./fakememory_preload.so ./syscall_detect 
/etc/ld.so.preload detected through open syscall
LD_PRELOAD detected through memory maps
$

Stop Tracing Me!

Now you might be thinking it is over, that there is no way you can do anything against syscalls with LD_PRELOAD, the only way is to implement kernelspace hooking. Well … not really.

We might be asking the kernel to execute a system function for us, but he never said he was going to do it. More specifically, there are two ways of modifying syscall behavior under linux:

  • SECCOMP which allows restricting the syscalls a process can make. It is meant to be used to sandbox processes but it is a little bit trickier when said process is not aware there should be a sandbox in the first place.
  • Ptrace which is used to debug processes and allows stopping the process before and after each syscall.

So the idea would be to ptrace the process, stop it before each syscall and, if it is an open syscall, redirect the control flow to a hook function.

The first problem is to ptrace ourself. We can not directly ptrace ourself because it makes no sense, a debugger can not debug itself. But if we fork a child process, that new process can continue the normal program execution while its parent debugs it. The only detail is we call sleep() before to give enough time to the parent process to attach, just in case the child would get scheduled before the parent by the kernel.

The second problem is to redirect the control flow to our hook function. Fortunately, under x86_64, the syscall argument convention is the same as the amd64 gnu ABI, the arguments are placed in RDI, RSI, RDX, etc. We just need to emulate a call through the ptrace interface by pushing the return address on the stack and changing the instruction pointer to the first instruction of the hook function.

Unfortunately the x86 gnu ABI is different. It requires the arguments to be placed on the stack but the syscall convention uses registers. Again, we can emulate this using the ptrace interface to push those registers onto the stack. The real problem is when we return from our hook function, because we need to clear those arguments from the stack. The solution is implemented as inline assembly inside the hook function. This assembly “simply” moves the stack 12 bytes up (12 bytes is the arguments size) before returning.

The last problem is to avoid hooking the syscalls made by our hook function. A simple variable checked by the hooking code is used there, the only trick being that the variable needs to be copied from the parent process to the child using ptrace.

Below is the full implementation of the open syscall emulation.

####nosyscall_preload.c

 
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <limits.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <asm/unistd.h>


// Some useful defines to make the code architecture independent
#if defined(__i386__)
#define REG_SYSCALL ORIG_EAX
#define REG_SP esp
#define REG_IP eip 
#elif defined(__x86_64__)
#define REG_SYSCALL ORIG_RAX
#define REG_SP rsp
#define REG_IP rip 
#endif

long NOHOOK = 0;
char *soname = "nosyscall_preload.so";

void fakeMaps(char *original_path, char *fake_path, char *pattern)
{
    FILE *original, *fake;
    char buffer[PATH_MAX];
    original = fopen(original_path, "r");
    fake = fopen(fake_path, "w");
    // Copy original in fake but discard the lines containing pattern
    while(fgets(buffer, PATH_MAX, original))
        if(strstr(buffer, pattern) == NULL)
            fputs(buffer, fake);
    fclose(fake);
    fclose(original);
}

long open_gate(const char *path, long oflag, long cflag) 
{
    char real_path[PATH_MAX], maps_path[PATH_MAX];
    long ret;
    pid_t pid;
    pid = getpid();
    // Resolve symbolic links and dot notation fu
    realpath(path, real_path);
    snprintf(maps_path, PATH_MAX, "/proc/%d/maps", pid);
    if(strcmp(real_path, "/etc/ld.so.preload") == 0)
    {
        // This file does not exist, I swear.
        errno = ENOENT;
        ret = -1;
    }
    else if(strcmp(real_path, maps_path) == 0)
    {
        snprintf(maps_path, PATH_MAX, "/tmp/%d.fakemaps", pid);
        // Create a file in tmp containing our fake map
        NOHOOK = 1; // Entering NOHOOK section
        fakeMaps(real_path, maps_path, soname);
        ret = open(maps_path, oflag);
    }
    else
    {
        // Everything is ok, call the real open
        NOHOOK = 1; // Entering NOHOOK section
        ret = open(path, oflag, cflag);
    }
    // Exiting NOHOOK section
    NOHOOK = 0;
    #ifdef __i386__
    // Tricky stack cleaning and return in the x86 case
    // We need to clean the 3 arguments (12 bytes) that were pushed on the stack
    __asm__ __volatile__ ("mov %0, %%eax;" // set the return value
                          "mov (%%ebp), %%ecx;" // move saved ebp 12 bytes up
                          "mov %%ecx, 0xc(%%ebp);"
                          "mov 0x4(%%ebp), %%ecx;" // move saved eip 12 bytes up
                          "mov %%ecx, 0x10(%%ebp);"
                          "add $0xc, %%ebp;" //move stack base 12 bytes up
                          "leave;" // normal leave and return
                          "ret;"
                          :
                          :"m" (ret)
                          :
                          );
    #endif
    return ret;
}

void init()
{
    pid_t program;
    // Forking a child process
    program = fork();
    if(program != 0)
    {
        // Parent process which will debug the program in the child process
        int status;
        long syscall_nr;
        struct user_regs_struct regs;
        // We attach to the child
        if(ptrace(PTRACE_ATTACH, program) != 0)
        {
            printf("Failed to attach to the program.\n");
            exit(1);
        }
        waitpid(program, &status, 0);
        // We are only interested in tracing SYSCALLs
        ptrace(PTRACE_SETOPTIONS, program, 0, PTRACE_O_TRACESYSGOOD);
        while(1)
        {
            ptrace(PTRACE_SYSCALL, program, 0, 0);
            waitpid(program, &status, 0);
            if(WIFEXITED(status) || WIFSIGNALED(status))
                break; // Stop tracing if the parent process terminates
            else if(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP|0x80)
            {
                // Getting the syscall number
                syscall_nr = ptrace(PTRACE_PEEKUSER, program, sizeof(long)*REG_SYSCALL);
                // Is it an open syscall ?
                if(syscall_nr == __NR_open)
                {
                    // Getting the value of NOHOOK in the child process
                    NOHOOK = ptrace(PTRACE_PEEKDATA, program, (void*)&NOHOOK);
                    // Only hook the syscall if it's not in a NOHOOK section
                    if(!NOHOOK)
                    {
                        // Now we are going to simulate a call
                        // First get the register state
                        ptrace(PTRACE_GETREGS, program, 0, &regs);
                        // Under x86 we need to push the arguments on the stack
                        #ifdef __i386__
                        regs.REG_SP -= sizeof(long);
                        ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.edx);
                        regs.REG_SP -= sizeof(long);
                        ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.ecx);
                        regs.REG_SP -= sizeof(long);
                        ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.ebx);
                        #endif
                        // Push return address on the stack
                        regs.REG_SP -= sizeof(long);
                        ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.REG_IP);
                        // Set RIP to open_gate address
                        regs.REG_IP = (unsigned long) open_gate;
                        // Finnally set the register
                        ptrace(PTRACE_SETREGS, program, 0, &regs);
                    }
                }
                //We always get a second signal after the syscall
                ptrace(PTRACE_SYSCALL, program, 0, 0);
                waitpid(program, &status, 0);
            }
        }
        exit(0);
    }
    else
    {
        // Child process
        // Sleep a bit to give the parent process enough time to attach
        sleep(0);
    }
}
gcc -o nosyscall_preload.so -shared -fpic -Wl,-init,init nosyscall_preload.c
LD_PRELOAD=./nosyscall_preload.so ./syscall_detect
/etc/ld.so.preload is not present
Memory maps are clean

The big advantage of this approach is that we don’t need to hook all the variants of open() or fopen() because they all use the same syscall (except openat(), but you should be able to figure out how to patch it).

The Endless Game

Of course now our program can try to detect if it is being ptraced, but since we can hook any syscall we want this can also be countered. Another problem is all the side effects created by our tricks (e.g. if you read the /etc directory ld.so.preload is still there and our fake memory maps has address incoherences).

Two other detection mechanisms are also worth mentionning:

  • The LD_DEBUG and LD_TRACE_LOADED_OBJECTS environment variables which can make ld.so output debug informations about the libraries being loaded. The same trick used in noenviron_preload.c can be used to remove those variables when execve() is called.
  • The lsof program can list open file descriptors, including the one used for our preloaded shared library. It finds those informations in the /proc/self/fd/ directory. Simply hiding that file descriptor is enough to make it disappear.

Assuming skills and knowledge are not the limiting factor, the winning side will always be the one that can adapt and compile last.

Acknowledgments

Many thanks to @doegox and hastake (he is hard to find, rumor has it that he is hiding from Vatican Secret Service) for sparking and correcting some of the ideas in here.

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License .