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.
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.”

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.

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.

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.preloadif 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.

step 4: what defenders should actually collect
- execve environ: alert when
LD_PRELOADorLD_LIBRARY_PATHappears on servers that are not developer workstations. - /proc/pid/maps: unexpected
.sopaths under home directories, tmp, or world-writable locations mapped executable into daemons. - /etc/ld.so.preload: watch writes. any change is a ticket unless you made it.
- package integrity: compare on-disk libraries under
/liband/usr/libto package manager databases. preload often lives elsewhere, but swapped system libs still happen. - 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
- baseline which units and users are allowed to set LD_* variables. default deny on production.
- auditd/eBPF rules for execve with LD_PRELOAD and for writes to
/etc/ld.so.preload. - EDR: flag daemon processes with home-directory or tmp
.somappings. - prefer static or carefully vendored builds for high-value agents when feasible. accept the ops cost consciously.
- for red team: document the preload path in the ROE, avoid persistence surprises on shared labs, tear down
/etc/ld.so.preloadchanges before you leave.
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 withdlsym(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
- snapshot a lab VM.
- drop the toy
getuidhook via user crontab@rebootexporting LD_PRELOAD for a long-running agent. - confirm SOC / EDR: did environ alerting fire? did maps hunting fire? did anyone only hash the agent binary and close the ticket?
- 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).
- 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
