Today was chosen as disclosure day for CVE-2009-1894.
Tavis Ormandy and myself have recently used the fact that pulseaudio was set-uid root to bypass Linux' NULL pointer dereference prevention. This technique is relying on a limitation in the Linux kernel and not on a bug in pulseaudio. But we also found one unrelated bug in pulseaudio.
Since it's set-uid root, we thought we would give pulseaudio a quick look. In the very first lines of main(), you can find the following:
if (!getenv("LD_BIND_NOW")) {
char *rp;
putenv(pa_xstrdup("LD_BIND_NOW=1"));
pa_assert_se(rp = pa_readlink("/proc/self/exe"));
pa_assert_se(execv(rp, argv) == 0);
}
So, pulseaudio is re-executing itself through /proc/self/exe, so that the dynamic linker performs all relocation immediately at load-time.
There is an obvious race condition here. /proc/self/exe is a symbolic link to the actual pathname of the executed command: by creating a hard link to /usr/bin/pulseaudio, we control this pathname, and consequently the file under this pathname. Knowing this, the exploitation is trivial (Note that rename() is atomic, or alternatively note how __d_path() works with deleted entries).
It's also interesting to note that any operation performed on /proc/self/exe is guaranteed by the kernel to be performed on the same inode than the one that got executed (see proc_exe_link), something that two of my colleagues have recently pointed out to me. So if they had re-executed themselves by using /proc/self/exe directly, without going through readlink() first, they would not have been vulnerable. And actually they weren't before, if you read the Changelog, you'll find:
2007-10-29 15:33 lennart * : use real path of binary instead of /proc/self/exe to execute ourselves
Oops! (Thanks to my colleague Mike Mammarella for digging this)
Like the vulnerability in udevd, this is a very good example of a non memory corruption vulnerability which is trivial to exploit very reliably and in a cross-architecture way.
So, why does pulseaudio have the set-uid bit set, you may ask ? For real-time performances reasons it wants to keep CAP_SYS_NICE but will drop all other privileges.
This vulnerability could have been avoided if the principle of least privilege had been followed: Since privileges are not required to re-exec yourself, dropping privileges should have been the first thing pulseaudio did. Here it's only the second thing it does, and it was enough to make most Linux Desktops vulnerable.
If your distribution of choice did not patch this yet, or if you want to reduce your attack surface, you're advised to chmod u-s /usr/bin/pulseaudio. Also note that as with every setuid binary update, you should also check that your users didn't create "backup" vulnerable copies (hardlink), waiting to own your box with known vulnerabilities while you think you are safe from those.
PS: Here are two brain teasers for you:
1. Find a cool way to perform an action after execve() has succeeded in another process, but before main() executes. First, I've used a FD_CLOEXEC read descriptor in a pipe and a SIGPIPE handler, but while it gives good results in practice, there is not guarantee as to when the signal will get delivered. I've finally found (with a hint from Tavis) a 100% reliable way to do it that is always guaranteed to work at first try. Of course, such a level of sophistication is absolutely not needed for this exploit.
2. Since pulseaudio allows you to load arbitrary libraries, it allows you to run arbitrary code with CAP_SYS_NICE as a feature. In the light of NUMA coming to the desktop through QPI, can you do something more interesting than what you would first expect with this?
Hello,
ReplyDeleteOn dpkg based distros, hardlinking will not allow to keep the suid bit on the vulnerable program in case of an upgrade. dpkg removes suid bit before unlinking. See http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=225692
If you want to keep a specific permission on a particular file, you can use dpkg-statoverride that keeps the change accross upgrades.
waiting to hear about your way to execute code before main()...
ReplyDeleteI received only one correct solution for now. I'll wait a few more days before posting solutions.
ReplyDeleteCheers,
No idea if this is the same thing, but for blocking between execution and main the method I thought up seems to work nice enough: http://dividead.wordpress.com/2009/07/21/blocking-between-execution-and-main/
ReplyDeleteI forgot to reply here:
ReplyDeleteSo yeah, this is it, the LD_DEBUG trick still works. You set LD_DEBUG to a bogus value and then you exhaust a pipe like usual and dup2 it to the child's stderr. It solves the part where the parent has to wait for the child to be ready.
And to guarantee that the parent will not perform a certain action before the child is inside execve(), I used vfork().
So the LD_DEBUG trick + vfork() was our "old school" solution and Dividead was the first to find and report to us this solution (at least the first part).
The new school solution, as found by a few individuals, the first mailing me being Gabriel Campana, is to use the magic of execve on fds in /proc (as described in the blog post): hardlink pulseaudio to "sploit", put your shell in "sploit (deleted)", open() "sploit", unlink it and fexecve() and you're done!