Thursday, February 24, 2011

I got accidental code execution via glibc?!

The story of Chromium security bug 48733, with guest Cris Neckar, part I

It has been a long time now, but the story of Chromium security bug 48733 deserves to be told. It involves intrigue in glibc and even gcc; and notably I accidentally executed arbitrary code whilst playing with this bug!

The bug was reported in July 2010, and there were instantly some WTF aspects. It caused a full browser crash on Linux, and the trigger seemed to be a long string. Such a case would tend to suggest a buffer overflow; but these are very unusual in Chromium code. Upon further investigation, the crash was occurring in the glibc function fnmatch():

int fnmatch(const char *pattern, const char *string, int flags);

And what was very strange was the trigger was not the pattern (which is a complicated string format), but simply the string itself. Further investigation narrowed the problem down to any long-ish (few megabytes+) string, if the locale was set to UTF8. A simple C test program is included at the end of the post. And here comes the killer: I was playing around and ran the program like this on my 32-bit Ubuntu 9.04 machine:

./a.out 1073741796

And accidentally achieved arbitrary code execution! The "A" characters making up the large input string actually correspond to the instruction inc %ecx so I wound up executing a bunch of those.

So what was going on?
Probably best to tackle the list of interesting points in bullet form:

  • glibc had a bug where it would use alloca() for the length of a user-supplied UTF8 string, times four (with additional integer overflow in the times four). This is good for at least a crash, because alloca() extends the stack, which is typically limited to a few MB.

  • It seems uncommon for Linux distributions to compile packages with gcc flags that defend against stack extension attacks -- more about that in part II.

  • 32-bit Ubuntu releases used to lack DEP. Perhaps they still do? This permits the execution of code contained within heap chunks, and is key to the accidental code execution achieved.

  • But how did EIP get redirected? The number passed to a.out above is a bit magic; glibc multiplies it by 4 (sizeof(wchar_t)) before passing it to alloca(), which ends up with the value 2^32 - 112. This wraps the stack pointer, causing an effective decrease in the stack of 112 bytes.

  • The decrease in stack size leads to all sorts of havoc; we're not sure, but most likely a local variable (in a subfunction of the function that called alloca()), pointing to the incoming heap string -- got plonked on top a saved EIP. I no longer have the old version of Ubuntu to test with, and more recent glibcs are fixed, so I can't confirm.

  • Note that stack extension bugs like this often sidestep a lot of system defenses, such as stack canaries (which are left undamaged) and ASLR (a valid address is automatically filled in). It's another case where Ubuntu could really have used DEP; see my older Firefox exploit for further proof!


How does part I end?
Of course, we reported the bug upstream to glibc: http://sourceware.org/bugzilla/show_bug.cgi?id=11883. The somewhat terse response notes that the issue was fixed but not in which version. Because of this, no glibc security advisories were released; so apologies if your older but still supported Linux distribution might still have vulnerabilities in this area.

Although certainly not a bug in Chromium, we still paid the bug finder $1337 under the Chromium Security Reward program. We did this partly just because we can, and we love encouraging all security research. But also, we were able to work around this glibc bug in Chromium fairly trivially -- so we did so in short order. As can be seen from the Chromium bug, we had all users protected in under 20 days from the original report, despite it not being our fault!


#include <err.h>
#include <fnmatch.h>
#include <locale.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, const char* argv[]) {
size_t num_as;
char* p;
setlocale(LC_ALL, "en_US.UTF8");
if (argc < 2) {
errx(1, "Missing argument.");
}
num_as = atoi(argv[1]);
if (num_as < 5) {
errx(1, "Need 5.");
}
p = malloc(num_as);
if (!p) {
errx(1, "malloc() failed.");
}
memset(p, 'A', num_as);
p[num_as - 1] = '\0';
p[0] = 'f';
p[1] = 'o';
p[2] = 'o';
p[3] = '.';
fnmatch("*.anim[1-9j]", p, 0);
return 0;
}

5 comments:

Anonymous said...

For note, Ubuntu 9.04 is already end-of-life. It sounds like you were not running with a PAE kernel, so there was no hardware NX. NX emulation was added in 9.10:

https://wiki.ubuntu.com/Security/Features/Historical#nx

Upstream glibc's use of alloca has caused problems before; I wish they'd cut that out. :)

I'd be curious to understand what the downsides to using -fstack-check would be; in the past it hasn't been a clear win. What's your opinion on that?

Anonymous said...

Most maintainers would have responded with something along the lines of: "Thanks for reporting the bug. ..."

But then this is Drepper: "I cannot reproduce any problem. I did check in changes to keep the alloca use limited."

He commited a fix after receiving the bug report.

Anonymous said...

hm neat ;]

There was once a similar bug like this in msft-land, but from the compiler where it was doing an implicit multiplication in generated code which led to the double multiplication thing quite a bit.

From memory, most everyone's alloca() just expands to more or less a sub esp, x although I seem to remember msft added an assert or something.

@anonymous, re: drepper
"Dearest Drepper,
checking the return value of setuid/asprintf is probably pretty pro."

mikhail said...

What is the significance of the value 112? if i have a 64bit machine, should the value to be passed to a.out be 2^64 - 112?

Anonymous said...

"Further investigation narrowed the problem down to any long-ish (few megabytes+) string"

"... long-ish (few megabytes+)..."

*cries in embedded*