LD_PRELOAD Hijacking
Forcing the dynamic linker to load an attacker-supplied shared object before libc, letting it override arbitrary library functions process-wide — the classic Linux userland rootkit primitive.
LD_PRELOAD is an environment variable the Linux dynamic linker (ld.so)
honours before it resolves any other library: every shared object listed in it
is loaded first, and its symbols take priority over the same symbols in libc or
any later library. A userland rootkit ships a small .so that redefines
functions like readdir, open, or stat to skip over its own files,
processes, and network connections, then gets that .so preloaded into every
process on the box — either per-process via the environment variable, or
system-wide via /etc/ld.so.preload, a file ld.so reads unconditionally at
startup.
How it works
The system-wide form needs only one line in a file most tools never check:
# /etc/ld.so.preload — one shared-object path per line, loaded into
# EVERY dynamically linked process that starts from now on
/usr/lib/libprocesshider.soThe hijacked library re-exports a libc function it wants to lie about, calling
the real one via dlsym(RTLD_NEXT, ...) and filtering its result:
// simplified process-hiding hook
struct dirent *readdir(DIR *dirp) {
static struct dirent *(*real_readdir)(DIR *) = NULL;
if (!real_readdir)
real_readdir = dlsym(RTLD_NEXT, "readdir");
struct dirent *entry;
while ((entry = real_readdir(dirp)) != NULL) {
if (is_hidden_pid(entry->d_name)) // skip our own /proc/<pid>
continue;
return entry;
}
return NULL;
}Because ps, ls /proc, and most process-listing tools ultimately call
readdir on /proc, a single hooked function hides an entire process from
every userland tool that uses the standard C library to enumerate it —
without touching the kernel.
Detection & analysis
Static analysis: A dropped .so that imports dlsym/dlopen and
re-exports common libc symbols (readdir, open, stat, execve,
connect) with no other purpose is a strong tell; disassemble it and look for
a RTLD_NEXT lookup immediately followed by a string compare against a
hard-coded name or PID.
Dynamic analysis: Check /etc/ld.so.preload for unexpected entries — its
mere existence on most distributions is already unusual. Inspect a live
process's environment for a suspicious LD_PRELOAD value via
cat /proc/<pid>/environ | tr '\0' '\n'. Running a known-good binary (e.g.
ls) with LD_PRELOAD= explicitly cleared and diffing its output against the
system default reveals anything the preloaded library was suppressing.
Detection rule hint: File-integrity-monitor /etc/ld.so.preload (it
should not exist, or should never change, on a hardened host) and alert on any
write to it. Flag any process launched with a non-empty LD_PRELOAD
environment variable pointing outside standard library directories
(/lib, /usr/lib), and cross-check ps/ls-reported process and file
lists against a raw /proc or kernel-level enumeration (e.g. via eBPF) —
a mismatch is the classic cross-view signal for this technique.