Here is a welcome digression from my previous Twitter oriented posts. I’m starting to play around with the
LD_PRELOAD feature in the Linux dynamic linker. For those who might not know what this feature is, here is the description from
ld.so (8).
LD_PRELOAD
A whitespace-separated list of additional, user-specified, ELF
shared libraries to be loaded before all others. This can be
used to selectively override functions in other shared
libraries. For setuid/setgid ELF binaries, only libraries in
the standard search directories that are also setgid will be
loaded.
So in pratical term, any libraries you specify in the LD_PRELOAD environment variable will loaded before any system libraries. This means that dynamic symbols in a loading program will be first searched in those libraries before being searched anywhere else.
This means you can override any defined symbol you want in standard libraries.
Let’s start with a rather juvenile example. This will change the behavior of the
read (2) function in order to make the user believe a file might have a different content.
| |
ssize_t read(int fd, void *buf, size_t count) {
static int done = 0;
if (!done) {
char silly_str[] = "Haha you got overriden.\n";
size_t s = count > sizeof(silly_str) ? sizeof(silly_str) : count;
memcpy(buf, silly_str, s);
done = 1;
return s;
}
else return 0;
}
|
If you compile this inside a library that is called, for example,
libread.so, you can test this code by running:
> /bin/cat /etc/fstab
# /etc/fstab: static file system information.
#
...
> LD_LIBRARY_PATH=. LD_PRELOAD=libread.so /bin/cat /etc/fstab
Haha you got overriden.
That in itself is just a rather silly prank you can play on your friend’s computer if you happen to have access to it. Experienced programmer will start seeing potential uses for
LD_PRELOAD. I am getting to that.
The subject of our next example will be the honorable
ls (1).
ls uses the
opendir (3) function to open a directory and browse its files. It should react properly if it can’t open the directory. One way to test this is to make
opendir() return
NULL and observe how the caller reacts. You can do that using
LD_PRELOAD.
|
DIR *opendir(const char *name) {
return NULL;
}
|
> LD_LIBRARY_PATH=. LD_PRELOAD=libls1.so /bin/ls /tmp
/bin/ls: cannot open directory /tmp
What can you do now if you want to preserve part of the behavior of the function, or modify they result it returns? Your preloaded library will then need to use
libdl to dynamically load the function it wants to modify the behavior.
The following example is a very simple override of the
opendir (3) function which open a different directory than what the caller expects. I will explain more in detail the details of this function below.
|
DIR *opendir(const char *name) {
DIR *(*libc_opendir)(const char *name);
*(void **)(&libc_opendir) = dlsym(RTLD_NEXT, "opendir");
return libc_opendir("/tmp");
}
|
libdl is fortunately very simple to use. The naive approach would be to use
dlopen (3) to open the C library, then get the pointer to the function you are calling using
dlsym (3). In theory, this technique is valid and working, but doing that circumvents the LD_PRELOAD mechanisme because preloaded libraries can be chained and calling directly into the C library prevents other caller to override our own function.
In practice, calling
dlopen() on libc on an Ubuntu Karmic system made some program crash and burn for reasons I will not attempt to explain. The next technique should be preferred on Linux system, especially when dealing with the system C library.
dlsym() has an option that makes the Linux dynamic linker search for the right symbol to be override. This is the
RTLD_NEXT flag, which is to be used just for the purpose of wrapper dynamic library functions.
libdl the task of returning the pointer to the right symbol. The RTLD_NEXT option to
dlsym() returns the right symbol.
The next and final example of the use of
LD_PRELOAD will still use the valiant
ls. In time for Christmas, this will modify the output of
ls by randomizing the
d_type field returned in the
dirent structure by
readdir (3). If you use colorized
ls output, and I believe most of you probably do, you should see a pretty display of color whenever you list a directory by preloading this function.
|
struct dirent64 *readdir64(DIR *dir) {
static struct dirent64 *(* libc_readdir64)(DIR *dir) = NULL;
struct dirent64 *dent;
unsigned char rnd_dtype[7] = { DT_UNKNOWN, DT_REG,
DT_DIR, DT_FIFO,
DT_SOCK, DT_CHR,
DT_BLK };
if (libc_readdir64 == NULL) {
*(void **)(&libc_readdir64) = dlsym(RTLD_NEXT, "readdir64");
srand(time(NULL));
}
dent = libc_readdir64(dir);
if (dent != NULL)
dent->d_type = rnd_dtype[rand() % 7];
return dent;
}
|
There is still a problem with this code on my new Ubuntu Hardy machine. The code from the preloaded library hangs before the program terminates. I do not understand why this happen and a search for this bug did not turn up anything. The problem doesn’t happen with Ubuntu Karmic.
There is nothing new about using
LD_PRELOAD this way. Several very nice libraries have been built with the intention of modifying the behavior of typical libraries.
- fakeroot: “fakeroot provides a fake root environment by means of LD_PRELOAD and SYSV IPC (or TCP) trickery.”
- fakechroot: fakechroot provides a fake chroot environment to programs.
- libtrash:“[...] the shared library which, when preloaded, implements a trash can under GNU/Linux”
- cowdancer: cowdancer is an userland implementation of copy-on-write filesystem.
There are
29 projects matching
LD_PRELOAD on
freshmeat.net. You might have used some of them.
The code I have written for this demonstration is available on
BitBucket.