Zephyr was talking about the ability to dispute CVE assignments, and CVE-2009-5064: “glibc: ldd unexpected code execution issue” came up as an example. MITRE says:
** DISPUTED ** ldd in the GNU C Library (aka glibc or libc6) 2.13 and earlier allows local users to gain privileges via a Trojan horse executable file linked with a modified loader that omits certain LD_TRACE_LOADED_OBJECTS checks. NOTE: the GNU C Library vendor states “This is just nonsense. There are a gazillion other ways to introduce code if people are downloading arbitrary binaries and install them in appropriate directories or set LD_LIBRARY_PATH etc.”
This got me curious about the LD_TRACE_LOADED_OBJECTS
environment variable and what the libc authors were disputing. Googling around got me to this 2009 article by Peteris Krumins. The premise is:
-
ldd
is mostly just a wrapper around invoking the dynamic linker on an executable withLD_TRACE_LOADED_OBJECTS
set. The GNU dynamic linker (e.g./lib/ld-linux.so.2
or/lib64/ld-linux-x86-64.so.2
) checks forLD_TRACE_LOADED_OBJECTS
and if set prints the shared libraries required by the executable instead of running it. -
If you can get a sysadmin to run
ldd
on a malicious executable which was compiled to use a dynamic linker that doesn’t check forLD_TRACE_LOADED_OBJECTS
, that malicious executable will run instead of just having its shared library dependencies printed to stdout.
The blog post jumps through quite a few hoops to use a modified uClibc dynamic linker that doesn’t try to check for LD_TRACE_LOADED_OBJECTS
. Comments on the post pointed out that there was an easier way to do this:
1. Create a test executable
$ cat > test_ld.c #include <stdio.h> int main() { if (getenv("LD_TRACE_LOADED_OBJECTS")) { printf("LD_TRACE_LOADED_OBJECTS is set, and yet I am executing!\n"); } else { printf("LD_TRACE_LOADED_OBJECTS is not set, so I am executing.\n"); } return 0; }
2. Statically compile the test executable into what will be our dynamic linker
$ gcc -static -o test_ld.so test_ld.c
3. Tell the linker to specify our test executable as the dynamic linker to use for our test executable
$ gcc -Wl,-dynamic-linker,test_ld.so test_ld.c -o test_ld
The -Wl,<option>,<option args>
syntax passes <option> and <option args> on to the linker.
The dynamic linker is written into the PT_INTERP
program header in the resulting ELF executable. Chapter 2 of the ELF format specification describes dynamic linking in detail.
4. Run our executable which specifies a custom dynamic linker
$ ./test_ld LD_TRACE_LOADED_OBJECTS is not set, so I am executing. $ LD_TRACE_LOADED_OBJECTS=1 ./test_ld LD_TRACE_LOADED_OBJECTS is set, and yet I am executing!
And we see that running test_ld
, even when LD_TRACE_LOADED_OBJECTS
is set, the program executes. What is actually going on here is that test_ld.so
is getting invoked as the dynamic linker. test_ld
never actually executes, since test_ld.so
didn’t set up the execution environment and jump to the _start
of the executable, which is what a dynamic linker is supposed to do.
I don’t like this example, though, because using the test executable as both the dynamic linker and target executable makes it less obvious what is actually executing. It also over-emphasizes the security implications of LD_TRACE_LOADED_OBJECTS
: if you are running or probing untrusted executables, you don’t need LD_TRACE_LOADED_OBJECTS
to get yourself into trouble.
This is a clearer example:
1. Create and compile a stub dynamic linker
$ cat > stub_ld.c int main() { return 27; } $ gcc -static -o stub_ld.so stub_ld.c
2. Create and compile a test executable that specifies our stub linker as the dynamic linker
$ cat test.c #include <stdio.h> int main() { printf("Hello world\n"); return 0; } $ gcc -Wl,-dynamic-linker,stub_ld.so test.c -o test
3. Run the test executable
$ ./test $ echo $? 27
The fact that "Hello world"
isn’t printed makes it clear that only stub_ld.so
executes.
ldd security improvements
The specific ldd
trick from Krumins’ 2009 article — social engineering a sysadmin into running ldd
on a executable that invokes a custom dynamic linker that ignores LD_TRACE_LOADED_OBJECTS
— doesn’t work on newer systems. On older systems (e.g. RHEL 4), ldd
will use the dynamic linker specified in the executable. On newer systems (e.g. Ubuntu Oneiric) ldd
hardcodes and will only use the GNU dynamic linkers.
Here are the relevant snippets from old and new ldd
versions for comparison, with extraneous code elided.
Older ldd
RTLDLIST="/lib/ld-linux.so.2 /lib64/ld-linux-x86-64.so.2" try_trace() { eval $add_env '"$@"' | cat } for rtld in ${RTLDLIST}; do if test -x $rtld; then verify_out=`${rtld} --verify "$file"` ret=$? case $ret in [02]) RTLD=${rtld}; break;; esac fi done case $ret in 0) try_trace "$file"
Note that if verification returns 0, try_trace
just eval
s the executable directly, so the dynamic linker specified in the executable is invoked. With this ldd
we get:
$ ldd ./test $ echo $? 27
Newer ldd
RTLDLIST="/lib/ld-linux.so.2 /lib64/ld-linux-x86-64.so.2" try_trace() { eval $add_env '"$@"' | cat } for rtld in ${RTLDLIST}; do if test -x $rtld; then verify_out=`${rtld} --verify "$file"` ret=$? case $ret in [02]) RTLD=${rtld}; break;; esac fi done case $ret in 0|2) try_trace "$RTLD" "$file" || result=1
In this case try_trace always use the GNU dynamic linker for our architecture to run the executable, even if something else was specified in PT_INTERP
. With this ldd
we get:
$ ldd ./test linux-vdso.so.1 => (0x00007fff4cbff000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd51eec9000) stub_ld.so => /lib64/ld-linux-x86-64.so.2 (0x00007fd51f287000)
which is the same as if we hadn't specified a custom dynamic linker. Note that our stub_ld.so
is printed as living at the location of the GNU dynamic linker /lib64/ld-linux-x86-64.so.2
.
I'm not sure where the newer version of ldd
is coming from, though. dpkg -S `which ldd`
says libc-bin
, which comes from eglibc, is the package supplying ldd
. I checked out the eglibc and glibc source repositories and neither have the updated ldd
in their histories.