incomplete.io

LD_PRELOAD shims

There are times when it can be useful to alter the runtime behaviour of a program that didn't anticipate your needs. There are times when you don't necessarily have the source to said piece of code, or rebuilding it is rather too painful. In cases like these, it may be possible to make use of some magic in the Linux linker/loader to get our own way.

When a program runs, any shared libraries it uses are loaded. When the program calls a function from one of those libraries, the linker/loader finds that symbol in the relevant library and redirects the program there. This can also be done at load time, but the default is to be lazy.

By using the LD_PRELOAD environment variable, we can have the linker/loader load, and look for symbols in, a library we specify before looking in the rest of the shared libraries. Instead of calling the real function, our function (with the same prototype) is called instead. This allows us to do whatever we like, including messing with variables passed in, calling the real function and messing with the return value.

By using the dl library, shipped with glibc, we can open a handle to the real library (line 26 below), search for the real symbol (line 32) and eventually call it (line 54 below).

This is the type of technique used by tools like memory profilers, which might wish to intercept calls to malloc() or free().

Our example below is going to take a program that listens on a socket using the bind() call and alters the bind address to be something we specify. There are plenty of applications out there that don't provide a way to specify the bind address. Netcat, does allow us to, but we'll use that in our example so as to protect the identities of the guilty.

First, we tell netcat to start and listen on port 5000, and then check that it's bound to all addresses:

$ nc -l 5000 &
$ sudo netstat -anp |grep :5000
tcp        0      0 0.0.0.0:5000            0.0.0.0:*               LISTEN      5520/nc
$ kill %1
    

We can see from the netstat output above that netcat is listening on 0.0.0.0:5000, which means it's accessible from anywhere.

In order to remedy this, we'll write the following code:

01: #include <stdio.h>
02: #include <stdlib.h>
03: #include <string.h>
04: #include <stdarg.h>
05: #include <errno.h>
06: #include <unistd.h>
07: #include <fcntl.h>
08: #include <dlfcn.h>
09: #include <sys/types.h>
10: #include <sys/stat.h>
11: #include <sys/socket.h>
12: #include <netinet/in.h>
13: #include <netinet/ip.h>
14: #include <arpa/inet.h>
15: 
16: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
17:     /* a function pointer to the real bind call */
18:     static int (*real_bind)(int, const struct sockaddr *, socklen_t);
19:     static void *hnd = NULL;
20:     char *err = NULL;
21:     struct sockaddr_in *sin = (struct sockaddr_in *)addr;
22:     const char *new_addr=NULL;
23: 
24:     /* First time through, find the real bind() call and remember it */
25:     if (real_bind == NULL) {
26:         hnd = dlopen("libc.so.6", RTLD_LAZY);
27:         if(hnd == NULL) {
28:             fprintf(stderr, "Couldn't open libc.so.6: %s", dlerror());
29:             exit(1);
30:         }
31:         err = dlerror();
32:         real_bind = dlsym(hnd, "bind");
33:         err = dlerror();
34:         if (err != NULL) {
35:             fprintf(stderr, "Unable to find the bind() call in libc: %s",
36:                     err);
37:             exit(1);
38:         }
39:     }
40: 
41:     /* Selectively rearrange the bind address */
42:     if(sin->sin_family == AF_INET) {
43:         if(sin->sin_addr.s_addr == INADDR_ANY) {
44:             new_addr = getenv("NEW_BIND_ADDR");
45:             if(new_addr) {
46:                 if(inet_aton(new_addr, &sin->sin_addr) != INADDR_NONE) {
47:                     fprintf(stderr, "Changed bind address to %s", new_addr);
48:                 }
49:             }
50:         }
51:     }
52: 
53:     /* call the real bind() call with our modified bind address */
54:     return real_bind(sockfd, addr, addrlen);
55: }
56: 
57: 

Compiling it is easy. Assuming it's in a file called bind.c, the following should suffice:

gcc -Wall -fPIC -shared -o bind.so bind.c -ldl
    

Now that we have our shim shared library, we can preload it, specify the address we want using the environment variable specified on line 44 and live happily ever after.

$ LD_PRELOAD=./bind.so NEW_BIND_ADDR=127.0.0.1 nc -l 5000 &
Changed bind address to 127.0.0.1
$ sudo netstat -anp |grep :5000
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      5630/nc
$ kill %1
    

And our service is now listening only on the interface our shim has allowed it to.

Twitter: @IncompleteIO