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.
- Include headers: The code imports
<dlfcn.h>to access dynamic linking functions. - Define the target signature: A function is defined with the exact signature and linkage of the target library function.
- 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 reallibcimplementation. - 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 -ldlOnce 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_programLimitations 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), renderingLD_PRELOADentirely ineffective. - Setuid and Setgid Programs: For security reasons,
the dynamic linker disables
LD_PRELOADwhen 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.preloador secure system paths) and marked with secure permissions. - Internal Calls: Calls within
libcthat resolve internally through hidden symbol visibility or private aliases cannot be intercepted viaLD_PRELOAD.