Published on Apr 18, 2017
Itamar Turner-Trauring

“This will only hurt for a moment”: code injection on Linux and macOS with LD_PRELOAD

Have you ever wanted to make a program behave differently without modifying the source code? On Linux and macOS (the operating system formerly known as OS X) you can do this with the LD_PRELOAD or DYLD_INSERT_LIBRARIES mechanisms respectively, which allow you to override the system and library calls from a particular process. While this may seem dangerous, it’s actually pretty easy to do and can be quite useful.

In this post I’ll discuss:

  • Why you might want to do this.
  • How these mechanisms work.
  • Some limitations of the mechanisms involved, some of which impact both Linux and macOS, and some of which are limited to macOS.

Code injection for fun and profit

When you run a program it calls out to shared libraries, and to the kernel using system calls. Overriding these calls allows you to override the program’s behavior in a variety of interesting ways.

For example, your program will often need to check the current time. What if you wanted to change it to be a different value? You could change the whole system’s clock, but that’s problematic and may have unexpected side-effects on other programs.

Alternatively, you can use faketime to override the calls that retrieve the current time. Instead of getting the real time a wrapped process will get whatever time you choose to set:

Other uses include making a process look like root when setting file permissions, or pretending you have changed the root of the filesystem.

Here at Datawire, to give another example, we’ve been working on Telepresence, a program that makes a local process appear as if it were in a remote cluster running Kubernetes. Kubernetes runs its own DNS server, with custom domain names like myservice.default.svc.cluster.local, and has its own internal IPs for services. We want these IPs and DNS records to be used by the local process.

There are other ways to achieve this effect, but we’ve been using torsocks, which overrides TCP socket connections
and DNS lookups and routes them through a proxy. The original purpose of torsocks was to route calls through the Tor onion router network, which gives users greater privacy. Here you can see how my external IP changes when I run a process under torsocks. I send a request to, an API that returns the callers IP address, and as you can see torsocks transparently routes my HTTP request through various Tor proxies:

So how do all these programs work?

No process is an island

When you run a program the resulting process cannot operate on its own. It needs functionality from libraries and from the kernel; the libraries may in turn depend on other libraries or on the kernel. Consider this simple C program:

The size of the binary is rather large, considering what it does:

That’s because we compiled it statically: all the code it relies on, other than the kernel, is included in the file. We can watch calls to the kernel, aka system calls, using the strace utility (or dtruss on macOS):

As you can see the printf library call ended up calling the write system call.

From system calls to shared libraries

Most binaries are not distributed as static binaries. Instead of library code being included in the binary, the binary just notes the shared libraries it relies on, and they get loaded at runtime:

We’ve gone from a binary of 888k to only 8k!

So what are these shared libraries the binary relies on? We can list them using ldd (or the similar but not identical otool on macOS):

  • is a way for the Linux kernel to inject kernel code into the process memory, so that certain system calls run faster.
  • is the C standard library, which includes APIs like printf.
  • is the dynamic linker: this is the code that knows how to load other shared libraries, like, into the process memory on startup.

Just like the static binary we can use strace to watch the system calls from running the shared binary:

Notice how this time the binary loaded additional files, the shared library.

In the static binary the implementation of printf was included in the binary itself, part of the extra 880kb of data in that binary.In the shared binary that code lives in, and we can use the ltrace utility to see that call:

Injecting shared libraries

Remember how ld-linux loads shared libraries? It also does some other useful things. In particular, if you set the LD_PRELOAD environment variable it will load the shared libraries set in that variable into the process. (On macOS this variable is called DYLD_INSERT_LIBRARIES.)

This injected library can override functions in other shared libraries, and if we choose call back to the original version. For example, recall that we discovered that printf is implemented using puts. Let’s override puts with the following shared library:

Now we can compile this into a library, and then use it to override the call to puts in our shared binary:

The ld-linux linker loads into the process, and all calls to puts get routed to our overridden version. And that’s how torsocks and faketime and fakechroot all work: by overriding system or library calls with custom versions using the LD_PRELOAD mechanism.

Caveats and limitations

Code injection has its share of problems, of course.

Which functions?

Remember how we compiled a program with printf() but got a binary with puts() instead? More broadly, the library calls you need to wrap in order to inject code are hard to predict.

Some library calls will have multiple variants, some library calls will share internal private implementations with other library calls… none of them are likely to be designed for code injection.

Even worse, different operating systems and compilers will require you to wrap different calls:

  • Our original example using gcc on Linux ended up using puts.
  • On OS X I got a binary that called _printf.
  • On Linux using the clang compiler instead of gcc I got a binary that called printf.

Static binaries and Go

Since LD_PRELOAD and the macOS equivalent work using the dynamic linker, it doesn’t work for static binaries. Notice we don’t get a double print:

Typically the only place you’ll encounter static binaries is when writing Go. The default Go compiler has its own mechanism for calling system calls directly, and tends to ship static binaries. If you want to use LD_PRELOAD with Go your best bet is to use gccgo, the gcc-based Go compiler.

Security problems

For security reasons LD_PRELOAD doesn’t work with suid binaries: the ability to inject arbitrary code into a process running as another user has some obvious problems.

On macOS there is an additional problem. Newer versions of macOS have a security subsystem called System Integrity Protection. For our purposes the problem is that it prevents injecting code via DYLD_INSERT_LIBRARIES (the macOS equivalent of LD_PRELOAD) into any binary in /bin, /sbin, /usr/bin and /usr/sbin.

Luckily, there’s an easy workaround. Just create a new directory, copy all the binaries from /bin, /sbin, /usr/bin and /usr/sbin into that directory, and then add it to the start of your $PATH environment variable. Once the binaries are out of those special directories code injection works just fine, and since they’re only 100MB copying them is quite fast.

Further reading

  • Rafał Cieślak wrote an excellent intro to LD_PRELOAD; I borrowed the dlsym code from there.
  • The man pages for ld-linux and dyld explain the LD_PRELOAD and DYLD_INSERT_LIBRARIES environment variables respectively in more detail.

And if you’re a Kubernetes developer check out Telepresence, a great way to have a local development environment for a remote Kubernetes cluster.