Intercepting C Library Calls with LD_PRELOAD

The LD_PRELOAD environment variable is a mechanism provided by the dynamic linker on Unix-like operating systems that forces specified shared libraries to be loaded before any others, including the standard C library (libc). This behavior enables developers to intercept and override functions—such as malloc, read, or connect—at runtime without modifying or recompiling the target executable. This technique, commonly known as function hooking or interposition, is widely used for profiling, debugging, monitoring system calls, and sandboxing applications.

How the Dynamic Linker Resolves Symbols

When a dynamically linked C program starts, the kernel executes the dynamic linker (typically ld.so or ld-linux.so). The program binary contains a list of undefined external symbols that must be resolved to specific memory addresses provided by shared objects.

Under normal execution, the dynamic linker searches through libraries specified in the binary's DT_NEEDED dynamic section (such as libc.so.6). The linker uses a first-match-wins approach: as soon as it locates a symbol matching an unresolved name, it binds that symbol to the calling code and ignores subsequent definitions in other libraries.

The Role of LD_PRELOAD

The LD_PRELOAD environment variable modifies this search order. When set, the dynamic linker loads the shared libraries listed in LD_PRELOAD before any other standard dependencies. Because these preloaded libraries are placed at the very front of the symbol search scope, their exported symbols take precedence over identically named symbols in standard libraries.

If a preloaded shared object defines a function named open(), any call to open() made by the application will bind to the custom implementation rather than the one located in the system's libc.

Intercepting and Forwarding Calls with dlsym

A practical interceptor usually needs to execute custom logic (such as logging or altering parameters) and then forward the call to the original standard library implementation. This is achieved using the dlsym() function with the special pseudo-handle RTLD_NEXT.

  1. Include headers: The code imports <dlfcn.h> to access dynamic linking functions.
  2. Define the target signature: A function is defined with the exact signature and linkage of the target library function.
  3. Obtain the original pointer: Using dlsym(RTLD_NEXT, "function_name"), the linker searches for the next occurrence of the symbol in the search order—bypassing the preloaded version and returning the address of the real libc implementation.
  4. Execute custom logic: The wrapper performs its operations before or after invoking the real function pointer.

For example, an interception wrapper for puts looks like this:

#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>

int puts(const char *str) {
    int (*original_puts)(const char *);
    original_puts = dlsym(RTLD_NEXT, "puts");

    // Custom interception logic
    original_puts("[INTERCEPTED]");
    
    // Call the original function
    return original_puts(str);
}

Compilation and Execution

The interception code must be compiled into a position-independent shared library:

gcc -Wall -fPIC -shared -o libhook.so hook.c -ldl

Once compiled, the library is injected into the execution path of any dynamically linked binary by setting the variable in the shell environment:

LD_PRELOAD=./libhook.so ./target_program

Limitations and Security Safeguards

While LD_PRELOAD is powerful, the operating system imposes specific constraints:

  • Statically Linked Binaries: Statically compiled programs do not invoke the dynamic linker (ld.so), rendering LD_PRELOAD entirely ineffective.
  • Setuid and Setgid Programs: For security reasons, the dynamic linker disables LD_PRELOAD when executing setuid or setgid binaries unless the executing user is the superuser, or the preloaded library is installed in standard trusted library directories (e.g., /etc/ld.so.preload or secure system paths) and marked with secure permissions.
  • Internal Calls: Calls within libc that resolve internally through hidden symbol visibility or private aliases cannot be intercepted via LD_PRELOAD.