xorl %eax, %eax

CVE-2010-3179: Mozilla Firefox document.write Memory Corruption

with 4 comments

I was quite bored of writing a post today but I saw (on twitter) that this vulnerability had gained a lot of attention and I was curious about it. So, after having a quick look at it. Here is my blog post.
The Mozilla Firefox Security Advisory states that this vulnerability was reported by Alexander Miller and affects Mozilla Firefox before 3.5.14 and 3.6 before 3.6.11, Thunderbird before 3.0.9 and 3.1 before 3.1.5 and SeaMonkey prior to 2.0.9 release. I’m starting to see why it was getting all that attention :P
Anyway, the buggy code can be found under gfx/thebes/src/gfxWindowsFonts.cpp in Mozilla’s source code…

class Uniscribe
{
   ...
private:
 
     // Append mItems[aIndex] to aDest, adding extra items to aDest to ensure
     // that no item is too long for ScriptShape() to handle. See bug 366643.
     nsresult CopyItemSplitOversize(int aIndex, nsTArray<SCRIPT_ITEM> &aDest) {
        aDest.AppendElement(mItems[aIndex]);
        const int itemLength = mItems[aIndex+1].iCharPos - mItems[aIndex].iCharPos;
        if (ESTIMATE_MAX_GLYPHS(itemLength) > 65535) {
            // This items length would cause ScriptShape() to fail. We need to
            // add extra items here so that no item's length could cause the fail.
 
            // Get cluster boundaries, so we can break cleanly if possible.
            nsTArray<SCRIPT_LOGATTR> logAttr;
            if (!logAttr.SetLength(itemLength))
                return NS_ERROR_FAILURE;
            HRESULT rv= ScriptBreak(mString+mItems[aIndex].iCharPos, itemLength,
                                    &mItems[aIndex].a, logAttr.Elements());
            if (FAILED(rv))
                return NS_ERROR_FAILURE;
 
            const int nextItemStart = mItems[aIndex+1].iCharPos;
            int start = FindNextItemStart(mItems[aIndex].iCharPos,
                                          nextItemStart, logAttr, mString);
 
            while (start < nextItemStart) {
                SCRIPT_ITEM item = mItems[aIndex];
                item.iCharPos = start;
                aDest.AppendElement(item);
                start = FindNextItemStart(start, nextItemStart, logAttr, mString);
            }
        } 
        return NS_OK;
    }

So, this private CopyItemSplitOversize() C++ method is used to append items passed to it to the ‘aDest’array. The mItems[] is an array of ‘SCRIPT_ITEM’ which is defined at build/wince/shunt/include/mozce_defs.h.

typedef struct tag_SCRIPT_STATE { 
  WORD uBidiLevel :5; 
  WORD fOverrideDirection :1; 
  WORD fInhibitSymSwap :1; 
  WORD fCharShape :1; 
  WORD fDigitSubstitute :1; 
  WORD fInhibitLigate :1; 
  WORD fDisplayZWG :1; 
  WORD fArabicNumContext :1; 
  WORD fGcpClusters :1; 
  WORD fReserved :1; 
  WORD fEngineReserved :2; 
} SCRIPT_STATE;

typedef struct tag_SCRIPT_ANALYSIS {
  WORD eScript      :10; 
  WORD fRTL          :1; 
  WORD fLayoutRTL    :1; 
  WORD fLinkBefore   :1; 
  WORD fLinkAfter    :1; 
  WORD fLogicalOrder :1; 
  WORD fNoGlyphIndex :1; 
  SCRIPT_STATE s ; 
} SCRIPT_ANALYSIS;

typedef struct tag_SCRIPT_ITEM { 
   int iCharPos; 
   SCRIPT_ANALYSIS a; 
} SCRIPT_ITEM;

Moving back to CopyItemSplitOversize() we can start examining it. First of all, it will use ‘aDest’ object’s AppendElement() to append the item for the given index ‘aIndex’ to ‘aDest’ object. Next, integer ‘itemLength’ is used to store the length of the item by calculating the position of the next item minus the position of the requested item. After that, using ESTIMATE_MAX_GLYPHS() which is a macro from the same C++ file that you can see below,

#define ESTIMATE_MAX_GLYPHS(L) (((3 * (L)) >> 1) + 16)

it checks that the item’s length isn’t greater than 65535 because an item with such length would break ScriptShape() as the comment says. To fix this, they check it and add extra items. If the item has size less than 0xFFFF it will simply return ‘NS_OK’. However, if it is greater it will start of by attempting to initialize a new array ‘logAttr’ with length of ‘itemLength’. If this succeeds, it will invoke ScriptBreak() Windows API routine (which is used for retrieving information for determining line breaks) passing huge item to it and the new ‘logAttr’ object as the ‘SCRIPT_LOGATTR’ argument.
If this doesn’t return an error, two new integers will be initialized, ‘nextItemStart’ with the position’s value of the next item and ‘start’ with the actual starting position of that item using FindNextItemStart().
Finally, it will enter a ‘while’ loop that will iterate as long as the position’s value is greater than the starting location of that item. Inside that loop, the previously used item will be changed to have the ‘item.iCharPos’ position value of the next item. Then it is appended to the ‘aDest’ object and FindNextItemStart() is called to continue with the next item.

So, first of all we can see that this bug is Windows specific but what’s the bug anyway?
Of course, that the code still passes a huge text buffer to ScriptBreak() API function. As Jonathan Kew pointed out:

This is the 1.9.2 equivalent of patch 1 in bug 553963 on trunk. The basic issue
is that CopyItemSplitOversize does not handle large items correctly; it calls
Uniscribe's ScriptBreak() function with a huge text buffer (42 million chars,
in the testcase here), which fails. In a debug build, several assertions get
triggered, but in a release build we end up with a textRun in an inconsistent
state, leading to invalid array accesses, etc.

So, to fix this the patch will prevent calling ScriptBreak() with huge text buffers. First of all…

-#define MAX_ITEM_LENGTH 32768
+// This is _slightly less_ than half of the maximum analysis window
+// we use with ScriptBreak, so that in typical cases we end up re-processing
+// only a small amount of overlap when we move the analysis window forward.
+#define MAX_ITEM_LENGTH      32499
 
-
+// Limit length of text passed to Uniscribe APIs, to avoid failure there.
+#define MAX_UNISCRIBE_LENGTH 65000

Two new constant values were defined that you can read above (the comments are pretty clear here). And CopyItemSplitOversize() was changed to initially use this values…

         const int itemLength = mItems[aIndex+1].iCharPos - mItems[aIndex].iCharPos;
-        if (ESTIMATE_MAX_GLYPHS(itemLength) > 65535) {
+        if (ESTIMATE_MAX_GLYPHS(itemLength) > MAX_UNISCRIBE_LENGTH) {
             // This items length would cause ScriptShape() to fail. We need to

and then remove the buggy ScriptBreak() call…

             nsTArray<SCRIPT_LOGATTR> logAttr;
-            if (!logAttr.SetLength(itemLength))
-                return NS_ERROR_FAILURE;
-            HRESULT rv= ScriptBreak(mString+mItems[aIndex].iCharPos, itemLength,
-                                    &mItems[aIndex].a, logAttr.Elements());
-            if (FAILED(rv))
-                return NS_ERROR_FAILURE;
 
             const int nextItemStart = mItems[aIndex+1].iCharPos;
-            int start = FindNextItemStart(mItems[aIndex].iCharPos,
-                                          nextItemStart, logAttr, mString);
+            int start = mItems[aIndex].iCharPos;

And add the following code in the ‘while’ loop…

             while (start < nextItemStart) {
-                SCRIPT_ITEM item = mItems[aIndex];
-                item.iCharPos = start;
-                aDest.AppendElement(item);
-                start = FindNextItemStart(start, nextItemStart, logAttr, mString);
+                // ScriptBreak will fail for strings longer than 64K,
+                // so we do the analysis using a "sliding window" over the
+                // huge item.
+                int analysisLen = PR_MIN(nextItemStart - start,
+                                         MAX_UNISCRIBE_LENGTH);
+                if (!logAttr.SetLength(analysisLen)) {
+                    return NS_ERROR_FAILURE;
+                }
+                HRESULT rv = ScriptBreak(mString + start,
+                                         analysisLen,
+                                         &mItems[aIndex].a,
+                                         logAttr.Elements());
+                if (FAILED(rv)) {
+                    return NS_ERROR_FAILURE;
+                }
+
+                int analysisLimit = start + analysisLen;
+                start = FindNextItemStart(start, analysisLimit,
+                                          logAttr, mString);
+                int prevStart = start;
+                while (start < analysisLimit) {
+                    SCRIPT_ITEM item = mItems[aIndex];
+                    item.iCharPos = start;
+                    aDest.AppendElement(item);
+                    prevStart = start;
+                    start = FindNextItemStart(start, analysisLimit,
+                                              logAttr, mString);
+                }
+
+                // If the analysis window didn't reach the end of the entire
+                // original item, reset start so that the final (perhaps
+                // badly-terminated) item we just created will be merged with
+                // the following section of the text.
+                if (start < nextItemStart) {
+                    start = prevStart;
+                }
             }

This is a new processing approach. Since ScriptBreak() fails with strings longer than 64,000 characters, the code uses a new integer variable ‘analysisLen’ to get the length of the item that doesn’t exceed ‘MAX_UNISCRIBE_LENGTH’ and make the call to ScriptBreak(). It will then use another new variable ‘analysisLimit’ to store the value of the item’s start plus the length that was retrieved earlier. It will update the object through a while loop similar to the previous one and at last, if the calculated length was not containing the entire text buffer, it will update ‘start’ to calculate it separately and merge it.
Public method Itemize() was also updated like this:

 
-        if (ESTIMATE_MAX_GLYPHS(mLength) > 65535) {
+        if (ESTIMATE_MAX_GLYPHS(mLength) > MAX_UNISCRIBE_LENGTH) {
             // Any item of length > 43680 will cause ScriptShape() to fail, as its

The test case (expv.html) that was submitted for this vulnerability is as simple as this…

<html>
<head>
<script language="JavaScript" type="Text/Javascript">
    var eip = unescape("%u4141%u4141");
    var string2 = unescape("%u0000%u0000");
    var finalstring2 = expand(string2, 49000000);
    var finaleip = expand(eip, 21000001); 
document.write(finalstring2);
document.write(finaleip);
function expand(string, number) {
    var i = Math.ceil(Math.log(number) / Math.LN2),
        result = string;
    do {
        result += result;
    } while (0 < --i);
    return result.slice(0, string.length * number);
}
</script>
</head>
<body>
</body>
</html>
<html><body></body></html>

It uses expand() function on ‘string2’ that will expand that string 40,900,000 times. Then the same thing is done on the second string (21,000,001 times) which will eventually overwrite the EIP pointer (that’s why the author named it that way) and two ‘document.write()’ calls for the two strings are executed to trigger the vulnerability.
My question was answered. Simple to trigger (I don’t know about its exploitation), quite a large number of affected versions and software and it’s for Windows. Not bad, not bad at all…

Written by xorl

October 26, 2010 at 18:24

Posted in vulnerabilities

4 Responses

Subscribe to comments with RSS.

  1. Nice write up as always! Were you able to reproduce an exploitable looking crash? I only got out of memory errors (C++ exceptions), despite having 2GB of ram.

    From the screen shot attached to the bug, it’s not even clear if the original reporter actually got a crash.

    jduck

    October 26, 2010 at 18:38

  2. Unfortunately, I’m not able to test it right now since I don’t have even a single Windows box available.
    I’ll try out tomorrow and post my results as a comment here :)

    xorl

    October 26, 2010 at 19:27

  3. I was unable to repro this bug. I’ve tried multiple vuln FF versions on Vista/Server 2008 (x86, single CPU to get as close as possible to author’s configuration). No go, just OOM…

    As jduck stated, from the pic in bug-report it’s not even clear if the FF actually crashed. It sure didn’t crashed here. But then again.. this bug was apparently paid $3000. For a simple OOM bug?

    EdiS

    October 26, 2010 at 21:31

  4. same here, just OOM

    xorl

    October 27, 2010 at 09:36


Leave a comment