Hooking Shared Library

In this article I am going to explain how to create and hook a shared library, i.e., intercept a call to a shared library and replace it with custom code.

I am going to touch the following topics:


Create a shared library

We need first to create the shared library with the function that will be called instead of the real one.

    // libput.c
    #include <dlfcn.h>
    #include <string.h>

    int puts(const char *message){

        int (*original_puts)(const char *message);
        int result;
        
        original_puts = dlsym(RTLD_NEXT, "puts");
        if(strcmp(message, "Hello world!") == 0) 
            result = original_puts("Goodbye, cruel world!");
        else
            result = original_puts(message); 

        return result;
    }

In line 7, we declare a function pointer to a function with the same signature of the function that we want to call later on.

In line 10, the function dlsym looks for the next (RTLD_NEXT) occurrence of the symbol puts (following the location order.)

In line 11, if the string that we are going to print is “Hello world!” then we are going to call the original put with a different message.

We need to compile the shared library as:

$ gcc libput.c -o libput.so -fPIC -shared -ldl -D_GNU_SOURCE

The flags used are:

  • -fPIC flag produces position independent code (compiler’s flag).

  • -shared flag produces a shared object that can be linked with others to form an executable (linker’s flag).

  • -ldl flag is used to link the dynamic linking library where the code of dlsym is stored (linker’s flag).

  • -D_GNU_SOURCE is used to access the definition of dlsym function, see man dlsym (compiler’s flag).

To see if the library that we use this program:

    // helloworld.c
    #include <stdio.h>

    int main(){
        
        puts("Hello world!");
        puts("Hello to you!");
        
        return 0;
    }

We compile the code normally as $ gcc -o helloworld helloworld.c .

To run this program using the library that we wrote we can use the LD_PRELOAD method (see the Runtime Dynamic Library Location Order) as:

    $ LD_PRELOAD="./libput.so" ./helloworld
    Goodbye, cruel world!
    Hello to you!

If we run the program normally we would get:

    $ ./helloworld
    Hello world!
    Hello to you!

Hook to hook "ls" to hide a file

In this example I am going to show how to write a shared library to hook the function called by ls in order to hide a specific file.

The function used by ls is readdir with signature struct dirent *readdir(DIR *dirp); . The returned structure is:

    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 shared library will be:

    // libreaddir.c
    #define SECRET_FILE "secret.txt"
    #include <dlfcn.h>
    #include <dirent.h>
    #include <sys/types.h>
    #include <string.h>

    struct dirent *readdir(DIR *dirp) {
        struct dirent *(*original_readdir)(DIR *);
        struct dirent *ret;
        
        original_readdir = dlsym (RTLD_NEXT, "readdir");
        while((ret = original_readdir(dirp))){
            if(strstr(ret->d_name,SECRET_FILE) == 0 ) break;
        }
        return ret;
    }

In line 2, I define the name of the file that we want to hide from ls.

In line 8, I am defining the function that I want to replace (i.e., readdir) with the same signature as the original function.

In line 9, I declaring a variable that will point to the original readdir function.

In line 12, I am looking for the next readdir symbol (following the Runtime Dynamic Library Location Order)) so that I can call this function.

In line 13,14, the function will return the a structure dirent as defined before. Remember that as stated in the $ man readdir,

The readdir() function returns a pointer to a dirent structure representing the next directory entry in the directory stream pointed to by dirp. It returns NULL on reaching the end of the directory stream or if an error occurred.

The readdir function will be called as many times as the number of entities in the directory (as many as ls -a1 | wc -l). If the SECRET_FILE is found then we are going to iterate over the while loop once more to find the structure of the next dirent structure (if any). The result is that the structure of the secret file is skipped (if found).

The compilation and running of ls is similar as before:

    $ gcc libreaddir.c -o libreaddir.so -fPIC -shared -ldl -D_GNU_SOURCE
    $ LD_PRELOAD="./libreaddir.so" ls

You will notice that if you create a file named as the value of SECRET_FILE it won’t show up in the results.

Runtime Dynamic Library Location Order

The shared libraries needed by the executables when it is launched are searched following this order:

  1. Firstly, the environment variable LD_PRELOAD is used. The variable is defined by the content of /etc/ld.so.preload that contains a list of shared libraries separated by space. You can also define this variable yourself.

  2. The second place where shared libraries are searched is the DT_RPATH field in the ELF format. It has a higher priority only in the absence of RUNPATH (defined later on). If RUNPATH is defined then this search path is ignored. It represent a search path to find the shared libraries. Since it is defined in the ELF it needs to be defined at compile time. The DT_RPATH can be set with gcc as:
    $ gcc -Wl,-R/home/username/projects/ -lcustomlibrary
    It can be modified by the chrpath program but since it is defined at compile time, it can’t be extended above than the length of the previously defined path. RPATH can be set also by defining the environment variable LD_RUN_PATH. The path specified can be also a relative path witch is not great.

  3. After RPATH, the system is looking for the environment variable LD_LIBRARY_PATH. It represent the next search path where to look for shared libraries.

  4. Next in the lookup order we have RUNPATH. It represent the DT_RUNPATH field in the ELF format. When RUNPATH is defined then the RPATH is ignored. You can define the RUNPATH as:
    $ gcc -Wl,-R /home/username/projects/ -Wl,--enable-new-dtags -lcustomlibrary
    You can also modify an existing DT_RUNPATH using patchelf. It is very smilar to RPATH and when defining RUNPATH also RPATH will be defined.

  5. The next path where shared libraries are searched is the file /etc/ld.so.cache. It is usually set set by ldconfig at the end of the installation process. This utility scan the path given as argument and finds all the dynamic libraries and adds them to /etc/ld.so.cache. The given path is also added to the list of paths that contains libraries in /etc/ld.so.conf.

  6. The last standard location to search for libraries are the folders /lib, /usr/lib and /usr/local/lib.



Additional resources: