ldd arbitrary code execution (2009)
catonmat.net
catonmat.net
My "Program Librarues HOWTO" says this:
"Beware: do not run ldd on a program you don't trust. As is clearly stated in the ldd(1) manual, ldd works by (in certain cases) by setting a special environment variable (for ELF objects, LD_TRACE_LOADED_OBJECTS) and then executing the program. It may be possible for an untrusted program to force the ldd user to run arbitrary code (instead of simply showing the ldd information). So, for safety's sake, don't use ldd on programs you don't trust to execute."
https://dwheeler.com/program-library/Program-Library-HOWTO/x...
I believe that doc dates from 2000. This info wasn't new then either, it was specifically documented in its man page.
The correct solution is that `ldd` should be safe by default, and require a `--allow-unsafe-execution` flag if it needs to actually execute code.
I think this is pretty well known these days but probably not well known enough!
Whenever I want to quickly check for dynamically linked libraries in a program, I use
readelf -d program
The use case I have is that I want to know what are the dependencies the program needs from the host system.If I wanted to know what are the dependencies of a particular shared library I can use
readelf -d libraryIf I wanted a recursive listing I would use libtree, a 56k .c file.
https://raw.githubusercontent.com/haampie/libtree/master/lib...
$ alias latest_libtree='curl -Lfs https://raw.githubusercontent.com/haampie/libtree/master/libtree.c | cc -o /tmp/libtree -x c - -std=c99 -D_FILE_OFFSET_BITS=64; /tmp/libtree'
$ time latest_libtree /usr/bin/vim
real 0m0.179s
user 0m0.119s
sys 0m0.019s
/usr/bin/vim
├── libpython3.11.so.1.0 [ld.so.conf]
│ ├── libexpat.so.1 [ld.so.conf]
│ └── libz.so.1 [ld.so.conf]
├── libgpm.so.2 [ld.so.conf]
├── libacl.so.1 [ld.so.conf]
├── libsodium.so.23 [ld.so.conf]
├── libselinux.so.1 [ld.so.conf]
│ └── libpcre2-8.so.0 [ld.so.conf]
└── libtinfo.so.6 [ld.so.conf]Probably running `strace` or `perf record -e intel_pt` in a confined environ is the best option to see what a binary really does, short of using ida/disasm/gdb - but then a binary can discover the use of strace too, and behave differently :).
It's turtles all the way down.
I'm also not entirely sure that readelf/binutils was reviewed from sec-perspective, it might not be out of the question that readelf might show things which are then interpreted differently by either kernel (loading a basic static bin or ld.so/interpreter) or by ld.so itself.
The way dynamic linking is done on UNIX-like systems has always seemed odd and a bit of an afterthought to me, and that explains why --- the actual loader in the OS kernel itself knows nearly nothing about dynamic linking. Instead it "redirects" execution to a loader (which is itself not that different from any other static binary) to do that.
The emulating static linking semantics badly bit, though, I do hate, because it just seems so unnecessary and self-inflicted. The entirety of the symbol versioning mess could be reduced to build-time symbol aliases if only we agreed to bind symbols to imported dynamic libraries at build time and not at runtime. (MacOS’s “two-level namespaces” are a hack that tries to retrofit the sane behaviour on top of that, but of course it can’t free you from the underlying mess completely.)
And if we are comparing Unix and its descendants with Windows, isn’t it the case that dynamic linking is, in fact, an afterthought? Windows has had dynamic linking since literally 1.0, because without that you simply can’t fit multiple graphical apps into memory on the kind of machine it was supposed to target. I’d say that early 16-bit Windows essentially is a fancy dynamic linker tied to an overlay manager, plus a cooperative actor/object-like thing, and only then some graphics and input routines on top of all that. At least as far as the application programmer’s mental model is concerned.
Whereas Unix got dynamic linking well into the workstation era, and I remember somebody saying it was an attempt to reduce the memory overhead of Xlib; the classic Unix answer to runtime extensibility would be to spawn a separate process.
This makes it much easier to evolve the system because kernel-land programming is harder than user-land programming. The trade-off is that the user-land run-time loader has its own bootstrapping complexity to deal with.
Sure it does: it can load whatever it wants however it wants once main() gets to run.
That doesn't seem to be what you mean though in context, but I'm not sure what you do mean. You don't like it or don't think it was designed well because dynamic linking is done in userspace?
The OS loader does control the permissions of what can be read and executed. And the user can execute a file. Why would it be better if you had to execute the loader explicitly to run a program? The same security problem would apply if you convinced somebody else to execute your malicious code. Seems like pretty harmless syntactic convenience.
The issue of ldd executing the program I guess is a thing that could be tightened to avoid foot shooting. Lots of unix programs traditionally were happy to give you lots of rope though.
It's both part of the OS and the language's compiler tool chain the program was written in. For the vast majority of Linux executables that's glibc and the ld-linux.so that glibc ships.
And you don't need to hard code the path inside your executable. Static PIE objects for example are dynamic elf objects without an interpreter header. The system loader is capable of launching them just fine.
I ask because ELF and unix dynamic linking is a pretty well thought out set of specifications and processes and it's a huge body of work. It can certainly be criticized, but calling it an afterthought because of .interp doesn't seem very charitable, just trying to understand if that's an informed opinion and if so it would be interesting to hear more.
But isn't it silly that every Linux system needs to place this exact file at this exact path or else no program for another system will possibly be able to run on it?
In any case, modern security considerations have already shown that dynamic loading of foreign code into process memory isn't that much of a good idea after all, unless there are hardware restrictions regarding performace and memory use.
> In any case, modern security considerations have already shown that dynamic loading of foreign code into process memory isn't that much of a good idea after all, [...]
As with many things it all depends on best practices and how you use the technology. Dynamic linking can be a big performance win. It can also be a performance loss. Dynamic linking can also be a big win in terms of code size, which even today is a good thing to have because container images can get bloated and you can need to have lots and lots of them, and it all adds up. None of that means you should be willing to dlopen() untrusted code, say.
And again, you can always implement your own dynamic linking and loading regardless of what such facilities (if any) the host OS provides.
I considered that rather well known though...
Previous discussions:
- [2009] https://news.ycombinator.com/item?id=902958
- [2015] https://news.ycombinator.com/item?id=9629667
- [2022] https://news.ycombinator.com/item?id=30033807
Also formatting inside code-block is broken
edit:
even simpler,
for i in $(objdump -p /path/to/bin | grep NEEDED | awk '{print $2}'); do ldconfig -p | grep $i; done
(… it also recurses down on the dependencies, presumably you can extend your shell script to do that too but at some point why not just use the existing tools?)
[1]: https://manpages.debian.org/bookworm/manpages/ldd.1.en.html#...
That seems like a scary powerful environment variable.
And I also could imagine that tools that allow setting arbitrary env vars might filter common troublemakers like PATH, but almost certainly not niche env vars that change the bahviour of ld-linux.
Update: Here seems to be one example: https://gist.github.com/jerome-pouiller/c403786c1394f53f44a3...
Well, that'd require actually executing the startup code.
This is thanks to $ORIGIN rpaths.
1. Create an executable in `./bin/exe` with say `$ORIGIN/../lib` rpath, and link it to some library `./lib/libfoo.so`
2. Create a symlink one dir up: `ln -s ./bin/exe exe`
3. `ldd exe` will tell you `libfoo.so` is not found
4. `./exe` runs just fine.
This is because the kernel resolves the symlink path, and the dynamic loader receives that, and uses it for $ORIGIN.
`ldd` on the other hand will interpolate $ORIGIN with the symlink location, not its target.
This only "works" for the executable, not for dependencies of dependencies: if a library is located as a symlink in a different directory from its target, $ORIGIN is relative to the symlink, and it's likely to break.
This can be problematic if you wanna put symlinks in say `/usr/local/lib`.
The output is actually more informative too:
$ lddtree `which ssh`
/usr/bin/ssh (interpreter => /lib64/ld-linux-x86-64.so.2)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0
libgssapi_krb5.so.2 => /lib/x86_64-linux-gnu/libgssapi_krb5.so.2
libkrb5.so.3 => /lib/x86_64-linux-gnu/libkrb5.so.3
libkeyutils.so.1 => /lib/x86_64-linux-gnu/libkeyutils.so.1
libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2
libk5crypto.so.3 => /lib/x86_64-linux-gnu/libk5crypto.so.3
libcom_err.so.2 => /lib/x86_64-linux-gnu/libcom_err.so.2
libkrb5support.so.0 => /lib/x86_64-linux-gnu/libkrb5support.so.0
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
vs. $ ldd `which ssh`
linux-vdso.so.1 (0x00007fff7972e000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f1a0030c000)
libgssapi_krb5.so.2 => /lib/x86_64-linux-gnu/libgssapi_krb5.so.2 (0x00007f1a002ba000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f19ffe00000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f1a0029b000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f19ffc1e000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f19ffb84000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1a00499000)
libkrb5.so.3 => /lib/x86_64-linux-gnu/libkrb5.so.3 (0x00007f19ffaaa000)
libk5crypto.so.3 => /lib/x86_64-linux-gnu/libk5crypto.so.3 (0x00007f19ffa7d000)
libcom_err.so.2 => /lib/x86_64-linux-gnu/libcom_err.so.2 (0x00007f1a00293000)
libkrb5support.so.0 => /lib/x86_64-linux-gnu/libkrb5support.so.0 (0x00007f1a00285000)
libkeyutils.so.1 => /lib/x86_64-linux-gnu/libkeyutils.so.1 (0x00007f19ffa76000)
libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007f19ffa65000)
(lddtree opens the files as plain binaries and reads the ELF linking data, it does not execute any code from the files being inspected.)What's easier and more reliable is to work in terms of risks. It's fine if you get exploited, as long as the access you have to sensitive systems is limited, and those sensitive systems have backups, and you can't delete those backups, you can re-deploy systems from scratch using automation if you get compromised, automatically rotate credentials, etc.
Lower the overall risk by setting everything up so the worst case scenario isn't that bad. Then you don't have to worry so much about an "unknown executable" because even if it gets exploited the attacker can't cause too much damage.
Yes, plenty of memory-unsafety vulnerabilities exist, but modern mitigations like stack cookies, ASLR, (and sometimes) sandboxing and PAC make it unlikely that e.g. a buffer overflow is exploitable without other factors such as an information leak from your machine back to the attacker. (This might be the case on publicly-accessible servers, but probably not on your laptop.)
The vulnerability being discussed here is unusually dangerous because it's more like command injection, and mitigations aren't going to help.
I'm not even talking about memory safety. That's just one class of exploits. There are so many more to choose from.
This vuln isn't that dangerous. It requires a special circumstance and trust. Other vulns don't require those things.
Do you browse the Internet? Ever visited an unknown website? Whatever the browser uses to run JS and wasm code on the web ought to be enough.
Browsers haven't ever been safe. There are competitions every year to find new 0days that break out of browser protections, and every year multiple are found. And those are the vulns they'll tell you about.
The interface between the compile-time linker and the dynamic linker is mostly defined by the ELF file format, System V ABI, and processor-specific ABI: many of the details can be found in the elf(5) man page. So in principle, there isn't really a bespoke "ld.so ABI". However, in practice, this can break down a bit on the consumer's side; there was a ruckus a while back when glibc started only including the undocumented DT_GNU_HASH and not the documented DT_HASH for its shared libraries, under the premise that only libc should be concerned with interpreting ELF headers.
At least from the producer's side, the story is better, since dynamic linkers will generally at least accept the standard headers, and ignore any nonstandard headers they don't understand.
If you want to avoid this risk, use something else, like readelf or lddtree.