Internet Security Systems - AlertCon(TM)

What You May Have Missed About CVE-2008-0017: A Firefox NULL Dereference Bug

Posted by Justin Schuh on November 26, 2008 at 7:18 AM EST.

Introduction

Earlier this year Mark Dowd released a detailed whitepaper on an application-specific attack against Adobe Flash. The vulnerability itself stemmed from a NULL-pointer dereference, but the real beauty of it was in how Mark leveraged artifacts of the Flash VM to achieve remote code execution. As impressive as the technical details of that exploit are, I think the more important point is how Mark’s approach makes you think about software vulnerabilities.

After several years of easy to classify bugs, we’re getting to a point where we really need to rethink how we determine what is exploitable, and what the severity of a vulnerability is. With that in mind I’m going to provide a somewhat less thorough case study of a recently disclosed Firefox vulnerability (tracked by Mozilla as bug 443299). This vulnerability is a simple crash bug in most cases, but it provides a very interesting pathway for code execution on Firefox 2.0.0.17 or below running on MS Windows platforms. Walking through the development of this exploit provides another useful exercise in understanding how these types of application-specific attacks work.

History

Before delving into the details of the vulnerability, it helps to have a little historical context on its disclosure. At the time ISS discovered this issue Firefox 3.0 was still far from complete and the overwhelming majority of Firefox users were running the 2.0 series on Windows—meaning that at the time of discovery most Firefox users were vulnerable to an exploit against bug 443299. Of course, discovery and confirmation are only the first steps in handling a security vulnerability. The next steps at ISS include internal review to assess impact, and then assisting the protection team in developing signatures and appropriate countermeasures for our security products. Once the protection is developed it must go through a QA and testing process before it is ready for release to customers. Only after all that is in place, and our customers are protected, is the vendor approached to coordinate the public disclosure process.

Coordinating the disclosure process can present its own share of difficulties and delays. (For an extreme example, see David Dewey’s recent post.) Unfortunately, even working with a responsive vendor can be a lot harder when a vulnerability is unique and doesn’t fall into a readily understood classification. This seems to be the case with bug 443299, as the issue took longer to resolve than is usually expected with security bugs. However, it should be noted that the Mozilla team was responsive throughout the process and the vulnerability’s impact to the Firefox 3.0 series (the current preferred version) was significantly less of an issue than it was to the 2.0 and earlier releases.

The Vulnerability

The vulnerability itself results from a straightforward error in handling application/http-index-format responses. The issue occurs when a NULL pointer is dereferenced due to an allocation failure in the nsDirIndexParser::ParseFormat() method. The dereference value is the variable num, shown in the following code.

  do {

    while (*pos && nsCRT::IsAsciiSpace(PRUnichar(*pos)))

      ++pos;

   

    ++num;

 

    if (! *pos)

      break;

 

    while (*pos && !nsCRT::IsAsciiSpace(PRUnichar(*pos)))

      ++pos;

 

  } while (*pos);

 

This first part of the ParseFormat() method simply increments num for every whitespace delimited token in the application/http-index-format response. Assuming a string like "x " is valid, the token count can be increased by one for every two bytes sent. This relationship is important when considering how this count is used in the following code:

  mFormat = new int[num+1];

  mFormat[num] = -1;

After the token count is determined, an integer array of num+1 elements is allocated and assigned to mFormat. In versions of Firefox that do not support exception handling in the C/C++ runtime (such as the 2.0 series on Windows), a NULL will be returned in the event of an allocation failure. No check for a NULL pointer is performed, and the array is immediately terminated with the value -1. As such, every two byte token will result in a four byte allocation, and a failed allocation will result in 0xFFFFFFFF being written to an arbitrary address (offset from zero).

The following code shows how the mFormat array pointer is used after the allocation attempt.

    if (name.LowerCaseEqualsLiteral("description"))

      mHasDescription = PR_TRUE;

   

    for (Field* i = gFieldTable; i->mName; ++i) {

      if (name.EqualsIgnoreCase(i->mName)) {

        mFormat[formatNum] = i->mType;

        ++formatNum;

        break;

      }

    }

This code segment is the only point where an assignment is made into the mFormat integer array. As such, it is possible to supply only invalid tokens for mName and write nothing to the array. So, if mFormat contains a NULL, an access violation can be avoided by not supplying any valid tokens.

Affected Versions

All unpatched versions of Firefox (and other Gecko-based applications) are affected to some extent by this bug. In most cases, the call to new[] will simply result in an unhandled exception and program termination. However, Firefox 2.0.0.17 and earlier series running on Windows are compiled on MSVC 6.0 using the standard C/C++ runtime with C++ exceptions disabled. As such, these versions will return a NULL pointer on allocation failure rather than throw an exception. The returned NULL value can then be dereferenced to perform the exploit.

Exploiting the Bug

Now that we understand the details of the vulnerability, we have the following points to keep in mind:

  1. The value 0xFFFFFFFF is written as the mFormat terminator
  2. Further writing to the mFormat array can be prevented
  3. Two-byte sequences of a token plus separator result in four-byte allocations for mFormat

These constraints give us a clear picture of how our exploit will need to work. Points one and two make it pretty clear that the value 0xFFFFFFFF can be written to any arbitrarily chosen location, as long as it is a multiple of four (sizeof int). However, point three imposes a pretty strict limitation on what target addresses are reasonably available.

Identifying a Range

Because the smallest possible token string is at least one half the size of mFormat, there are practical limitations in how large mFormat can be. The token string must be small enough that it can both be sent over the network reasonably quickly and can fit contiguously in memory. These details mean that the exploit will require exhausting memory to the point that the mFormat array cannot be allocated, but without preventing the allocation of the token string.

With these constraints in mind, the Firefox memory map should provide a rough idea of where to look for overwrite targets. Of course, memory layout will change a bit between minor version releases, but all releases of the same major version should be in roughly the same ballpark.

For simplicity’s sake, this discussion will focus on writeable targets at fixed addresses. First, the main thread stack is eliminated because its upper bound is 0x00130000. Such a low address would require a 1.2MB allocation failure, which is likely to cause other failures in the process before the exploit could be triggered. So, the lower memory locations are unlikely to be useful.

Higher memory locations (such as the .data sections in DLLs) could be a possibility, but once again the size is a prohibitive factor. The lowest available target is js3250 at 0x60137000. Hitting that would require sending a token string more than 800MB long, which is not very practical. Unfortunately, the rest of the available DLLs are at even higher addresses.

In between the stack and the DLLs lies the .data section for the Firefox executable image starting at 0x00AD7000 and roughly 500k in size. Overwriting a location in that range requires a token string of around 5.5MB, which is a much more manageable size to deal with. Also, we can use gzip content encoding to further reduce the size of the transmitted string down to somewhere around 20KB, which is very manageable.

The downside of this location is that it will require exhausting memory for any requests of approximately 11MB or larger. That problem will be addressed shortly; first, a target within the Firefox .data section must be identified. Since the array terminator value is 0xFFFFFFFF, one easy target would be a static or global unsigned integer used as a buffer length. This is where a working knowledge of the Mozilla codebase comes in very handy.

Identifying a Target

During initialization of the of HTML parser the maximum possible tag length is determined by enumerating through the list of all tags and identifying the longest possible tag (in the 2.0 and earlier codebase). This length is stored in the static unsigned integer member variable nsHTMLTags::sMaxTagNameLength. During processing, all HTML tags longer than sMaxTagNameLength are simply ignored. Tags shorter than sMaxTagNameLength are converted to UTF-16 lowercase characters and output to a static local character buffer in the member function nsHTMLTags::LookupTag():

nsHTMLTags::LookupTag(const nsAString& aTagName)

{

  PRUint32 length = aTagName.Length();

 

  if (length > sMaxTagNameLength) {

    return eHTMLTag_userdefined;

  }

 

  static PRUnichar buf[NS_HTMLTAG_NAME_MAX_LENGTH + 1];

 

  nsAString::const_iterator iter;

  PRUint32 i = 0;

  PRUnichar c;

 

  aTagName.BeginReading(iter);

 

  // Fast lowercasing-while-copying of ASCII characters into a

  // PRUnichar buffer

 

  while (i < length) {

    c = *iter;

 

    if (c <= 'Z' && c >= 'A') {

      c |= 0x20; // Lowercase the ASCII character.

    }

 

    buf[i] = c; // Copy ASCII character.

 

    ++i;

    ++iter;

  }

 

  buf[i] = 0;

 

  return CaseSensitiveLookupTag(buf);

}

As such, overwriting sMaxTagNameLength with 0xFFFFFFFF will cause any tag to pass the length check and result in an unbounded copy into buf.

The next step is to identify sMaxTagNameLength and buf in the Firefox executable binary and determine what data follows buf in memory. Upon looking that up, what we’ll find is that sMaxTagNameLength does present a good overwrite target and that buf is followed in memory by a few tables of function pointers. We can test this by starting Firefox in a debugger and checking what function pointers are loaded after buf. A little more time with the debugger should also show that one of those function pointers is always called almost immediately after nsHTMLTags::LookupTag().

Exhausting Memory

In order to overwrite sMaxTagNameLength we need to force all allocations to fail for sizes greater than or equal to the desired write address (calculated as an offset from 0). However, we still need to allow allocation of the roughly 6MB application/http-index-format response necessary to make the mFormat the right size, along with keeping any normal allocations working so the process doesn’t crash unexpectedly before exploit.

While most web browsers are normally very good at consuming arbitrarily large amounts of memory, doing so in a controlled manner is a bit more difficult. After some experimentation, you’ll find that most approaches have definite drawbacks. They either don’t provide fine enough control over the allocation size or they force other errors as the limits of available memory are approached. However, there is at least one object that allows fine-grained control of the allocation size and will not cause an application failure as memory is exhausted. This is the HTML canvas element.

Declaring an HTML canvas and opening a context allocates an image buffer of the following size: height × width × 4 bytes (along with some smaller associated allocations). You can also free that memory on demand by adjusting the height and width of the canvas and reacquiring the context. Finally, canvas fails gracefully when it is unable to allocate the image buffer, and the toDataURL() method can be used with exception handling to identify when the image buffer allocation fails. (The canvas would actually be perfect if it reserved memory without committing it, but that’s a bit much to expect.)

Using the canvas, we can consume enough fixed-size chunks of memory so that it’s impossible to allocate a contiguous buffer greater than or equal to our target size. Here is an example to of a JavaScript function that will consume a fixed number of chunks of an arbitrary size (in 4k multiples) and return true if that size has been exhausted:

function fill(size, count) {

    var width = 1;

    var height = (size >> 2) & ~0x3FF;

    for (;height > 2 * width; height >>= 1) {

        width = (width << 1) | (1 & height);

    }

   

    while (count-- != 0) {

        c = document.createElement('canvas');   

        c.setAttribute('width', width);

        c.setAttribute('height', height);

        cdiv.appendChild(c);

        c.getContext('2d');

        try { c.toDataURL();}

        catch { return true; }

    }

    return false;

}

And here is an example of JavaScript code to free an allocated chunk:

    c = cdiv.childNodes[i];

    c.setAttribute('width', 1);

    c.setAttribute('height', 1);

    ctx = c.getContext('2d');

    ctx = null;

    cdiv.removeChild(c);

Putting It All Together

The easiest approach to trigger the exploit proceeds as follows:

  1. Allocate enough properly sized chunks to fill the entire available address space.
  2. Create an inline frame to request a malformed application/http-index-format response containing the exact number of tokens required to get an offset pointing to sMaxTagNameLength when mFormat is NULL.
  3. Generate a long HTML tag to overflow buf with the appropriate shellcode pointers
  4. Repeat from step two until shellcode executes

Following these steps will hang the browser for a few minutes while memory is exhausted and then execute our shell code. That approach certainly proves that the flaw is exploitable for remote code execution, but there’s still a lot more that can be done to improve the delivery technique.

Refinements

The first major problem with the exploit is that it takes roughly 3 – 5 minutes to run. The easiest way to reduce that duration is to cut down the time spent in JavaScript and reduce the number of transitions between native and JavaScript code. We can accomplish this by generating extremely large chunks first to fill the memory quickly, and then work our way down to the size we need. That should shave at least a minute or two off the execution time.

Another potential issue is when the exploit fails because adequate memory is unavailable for normal operation of the browser process. This problem can be addressed by increasing memory fragmentation so that larger chunks cannot be allocated but smaller ones are available without issue. The best approach is to create a safe buffer of available memory by interleaving allocations of different sized chunks and freeing the smaller ones to leave holes. A little experimentation in this area will produce an exploit that works reliably 100% of the time.

The biggest nuisance with our exploit is that the browser locks up when the canvasses are allocated in a tight loop. We can keep the browser responsive by adding explicit delays with the setTimeOut() function. We can use the getTime() in combination with setTimeOut() to dynamically scale back exhaust rate based on the cumulative delays between calls. With some experimentation we can develop an exploit that runs in the background and leaves the browser responsive until the final seconds of the exploit.

There are also a few more potential refinements that could result from further research. For example, are there any JavaScript objects that exhibit the desirable properties of canvas, but could result in a faster memory exhaust? Or could a faster exhaust be achieved by targeting other memory locations such as those provided by popular plug-ins like Adobe Flash or Talkback bug reporting? Would it be possible to build the exploit entirely into one file using data URLs? Are there any practical exploit methods that do not require the use of JavaScript?

Conclusion

There are still some areas in which this specific attack could benefit from some further research and polish, but overall it’s less of a concern now that Firefox 2.0 has ceded the bulk of its market share to Firefox 3.0. The more important point is that understanding exploits like this can help you develop an appreciation for software vulnerabilities that don’t fit the existing classifications you’re used to. Even something as seemingly innocuous as a NULL pointer dereference can sometimes provide a reliable vector for remote code execution.

Comments or opinions expressed on this Weblog are the opinions of the authors alone. They are not necessarily reviewed in advance by anyone but the individual authors, and neither IBM Internet Security Systems nor any other party necessarily agrees with them. The views expressed by outside contributors and links to outside websites do not represent the views of IBM Internet Security Systems, its management or employees. All content on this Weblog has been made available on an “as-is” basis, and IBM Internet Security Systems shall not be liable for any direct or indirect damages arising out of use of this Weblog.