The dynamic linker is one of the most important yet widely overlooked components of a modern Operating System. Its job is to load and link-in executable code from shared libraries into executables at run time. There are many intricate details on how the dynamic linker does what it does, but one of the more interesting is the use of the LD_LIBRARY_PATH and LD_PRELOAD environment variables. When defined, these variables will let you override the default behaviour of the dynamic linker to load specific code into your executables. For example, using LD_PRELOAD, you can override the C standard library functions in libc with your own versions (think printf, puts, getc, etc.)
Let's see this in action! We'll start by making a simple program to test the (now deprecated) gets() function. Here, we will create a file called test.c and put the follow contents inside it:
#include <stdio.h>
int main (void)
{
char str[128];
printf ("Testing gets()...\n");
gets(str);
return 0;
}
Note that this code is not safe and should not be used for production, but it makes a simple test scenario.
Next, we can compile the source with gcc. (Since gets() is deprecated, we're going to throw in the -w flag to suppress warning messages. We don't really care for this example.)
$ gcc -w -o test test.c
Finally, we can run the program and examine it's output:
$ ./test
Testing gets()...
womp
$
Success! When the executable is run, it links the gets() code from libc into memory and executes that code when we call gets(). Now let's see how we can override libc's implementation with our own. First, we'll write a new version of gets() that we want run. Make a file called mygets.c and enter the following:
char *gets( char *str )
{
printf("Error: Stop using deprecated functions!\n");
return "";
}
Once finished, we can compile this into our own shared object library:
gcc -w -fPIC -shared -o mygets.so mygets.c
Finally, let's run the test executable again, but this time we will call it with LD_PRELOAD to load our custom shared library before dynamically linking libc:
$ LD_PRELOAD=./mygets.so ./test
Testing gets()...
Error: Stop using deprecated functions!
$
As you can see, now our custom code is displaying where we were once being prompted for input. Of course, we could write any code we want to go in here. The only limit is whatever we can think up. This technique could be extremely useful when trying advanced debugging or when trying to replace specific parts of a shared library in your program. You can even take this a step further to create hooks for the original overridden functions.
To illustrate this, let's modify our shared library one more time:
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
char *gets( char *str )
{
printf("Error: Stop using deprecated functions!\n");
char *(*original_gets)( char *str );
original_gets = dlsym(RTLD_NEXT, "gets");
return (*original_gets)(str);
}
We've done a few things here. We have now referenced the stdio header and we use dlsym to find the original gets() function. Notice that we use the RTLD_NEXT pseudo-handle with dlsym. In order to use this handle, we must include the _GNU_SOURCE test macro (otherwise RTLD_NEXT will not be found). This finds the next occurrence of gets() after the current library and allows us to map it to original_gets(). We can then use it in this function with the mapped name.
We can compile our library again to test out the new code (this time linking the dl lib):
$ gcc -w -fPIC -shared -o mygets.so mygets.c -ldl
Using this method, we can run our test executable again:
$ LD_PRELOAD=./mygets.so ./test
Testing gets()...
Error: Stop using deprecated functions!
womp
$
At this point, you should notice the custom code we provided for gets(), followed by the prompt by the original libc function. Hopefully this dispels a little bit of the voodoo and gives you another valuable tool to stash in your belt.