Skip to content

LD_PRELOAD: The Linux Library Hijack Your Allowlist Still Calls Harmless

LD_PRELOAD Library Hijack Lab Walkthrough and Detection

LD_PRELOAD: The Linux Library Hijack Your Allowlist Still Calls Harmless

windows kids get DLL sideloading thinkpieces every time Siemens bleeds. linux kids get LD_PRELOAD, a constructor that runs before main, and a process table that still says nginx. your FIM watches /usr/bin. your EDR baselines the binary hash. nobody asked what geteuid resolved to tonight.

Lab only. screenshots build a hello-world target and a toy getuid hook on disposable Kali. do not drop preload libraries onto production hosts, shared jump boxes, or containers you do not own. authorized red-team channel testing and detection engineering only. if you write /etc/ld.so.preload on a shared lab without a snapshot, that is on you.

the trick in one paragraph

the dynamic linker (ld.so) resolves symbols when a dynamically linked ELF starts. LD_PRELOAD (and the root-owned /etc/ld.so.preload) force a shared object into that resolution order. export a symbol the target will call (open, getuid, ssl_read, whatever). your code runs first. call through to the real function with dlsym(RTLD_NEXT, ...) if you want the program to keep working while you steal secrets. this is not new. it is still under-detected in shops that confuse “no new binary on disk in /tmp” with “nothing happened.”

Kali terminal compiling a small lab target and showing ldd output with libc.
Step 1: a boring dynamically linked target. ldd shows libc. that is the attack surface.

step 1: pick a target that actually links

gcc -o target target.c
ldd ./target

static binaries shrug at LD_PRELOAD for libc symbols. most of the internet does not ship static. web pools, agents, CLIs, and “temporary” python wrappers are dynamic. inventory what actually runs with ldd and file before you write detection fiction.

Kali terminal compiling an evil_getuid.so that intercepts getuid via dlsym RTLD_NEXT.
Step 2: a constructor-grade hook in twenty lines. log, mutate, or steal, then call through.

step 2: write the hook

uid_t getuid(void){
  static uid_t (*real)(void);
  if (!real) real = dlsym(RTLD_NEXT, "getuid");
  fprintf(stderr, "[lab-hook] getuid intercepted\n");
  return real();
}

compile with -shared -fPIC and link -ldl. real operators hook password reads, TLS send/recv, or execve. the lab hooks getuid so the screenshot is honest and the blast radius is a print statement.

Kali terminal running the target under LD_PRELOAD and showing the intercept message.
Step 3: LD_PRELOAD fires. argv0 still looks like target. the maps file knows better.

step 3: fire it, then pretend you are persistent

LD_PRELOAD=$PWD/evil_getuid.so ./target

persistence cosplay options, all lab-scoped:

  • user shell rc (~/.bashrc, ~/.profile) exporting LD_PRELOAD
  • systemd user unit Environment=LD_PRELOAD=...
  • wrapper script earlier in PATH that exports and execs the real binary
  • /etc/ld.so.preload if you already have root (global, loud, effective)

setuid binaries ignore LD_PRELOAD from the environment for boring security reasons. /etc/ld.so.preload does not care about your feelings and applies more broadly. that difference matters for both offense scoping and defense alerting.

Kali terminal hunting LD_PRELOAD in process environ and maps, plus example auditd rules.
Step 4: environ + maps + auditd. if your response process only hashes the ELF, you are detecting 2012.

step 4: what defenders should actually collect

  1. execve environ: alert when LD_PRELOAD or LD_LIBRARY_PATH appears on servers that are not developer workstations.
  2. /proc/pid/maps: unexpected .so paths under home directories, tmp, or world-writable locations mapped executable into daemons.
  3. /etc/ld.so.preload: watch writes. any change is a ticket unless you made it.
  4. package integrity: compare on-disk libraries under /lib and /usr/lib to package manager databases. preload often lives elsewhere, but swapped system libs still happen.
  5. container reality: same linker rules apply unless you run fully static or a seccomp/LSM profile that blocks the weird bits. “we are in Kubernetes” is not a mitigation.

why this pairs with the Siemens week

CVE-2026-110936 is uncontrolled search path on Windows. LD_PRELOAD is uncontrolled search path with better documentation on Linux. same mental model: trusted host binary, attacker-controlled library, loader does the trust violation for free. if your purple team only drills DLL sideload on Windows and never sets LD_PRELOAD in a lab, you are half-trained.

controls that are not theater

  1. baseline which units and users are allowed to set LD_* variables. default deny on production.
  2. auditd/eBPF rules for execve with LD_PRELOAD and for writes to /etc/ld.so.preload.
  3. EDR: flag daemon processes with home-directory or tmp .so mappings.
  4. prefer static or carefully vendored builds for high-value agents when feasible. accept the ops cost consciously.
  5. for red team: document the preload path in the ROE, avoid persistence surprises on shared labs, tear down /etc/ld.so.preload changes before you leave.
honesty check: LD_PRELOAD will not save you against a competent seccomp profile, a static binary, or an operator who reads maps. it will wreck a detection program that only hashes argv0. know which one you are.

constructor vs symbol interposition

two common shapes show up in the wild and in labs:

  • symbol interposition: export open/read/getuid/TLS helpers. the target calls you because you won resolution. call through with dlsym(RTLD_NEXT, ...).
  • constructor-only: __attribute__((constructor)) runs at load time even if you never export a hooked symbol. great for “just give me a shell / beacon on process start” implants. maps still show the .so.

the Siemens-week Windows parallel is closer to “host loads helper by relative name.” LD_PRELOAD is “linker loads helper because environment said so.” same family reunion. different invite.

setuid, capabilities, and the lies people tell

glibc ignores LD_PRELOAD from the environment for setuid/setgid binaries. that sentence gets pasted into risk acceptances as “Linux is fine.” it is not fine if:

  • the interesting target is not setuid (most daemons are not)
  • someone already wrote /etc/ld.so.preload
  • the process is a normal user agent with tokens in memory
  • you are inside a container where “setuid” was never the model

capabilities (cap_setuid, etc.) and seccomp profiles change the story again. test your assumptions on a snapshot. do not cite a mailing list from 2004 as current control coverage.

purple-team drill that fits in an afternoon

  1. snapshot a lab VM.
  2. drop the toy getuid hook via user crontab @reboot exporting LD_PRELOAD for a long-running agent.
  3. confirm SOC / EDR: did environ alerting fire? did maps hunting fire? did anyone only hash the agent binary and close the ticket?
  4. repeat with a constructor-only .so and with a staged write to a lab copy of ld.so.preload semantics (file under /tmp that you treat as stand-in, not the real file on a shared host).
  5. write the detection gap down. ship the audit rule. delete the lab artifacts.

sloth take

disk was never the whole story. the linker has been taking attacker suggestions since before your SIEM had a web UI. watch the environment. walk the maps. and stop congratulating yourself for a clean /usr/bin while getuid belongs to someone else.

sources

  • man pages: ld.so(8), dlopen(3), ld.so.preload semantics
  • Siemens ProductCERT / CVE-2026-110936 as the contemporary Windows parallel (CWE-427), for mental-model comparison only
  • common auditd execve/env watch patterns used in detection engineering labs