Bug 1299686 (CVE-2016-9066)

Integer overflow leading to a buffer overflow in nsScriptLoadHandler

VERIFIED FIXED in Firefox 50

Status

()

Core
DOM
VERIFIED FIXED
8 months ago
25 days ago

People

(Reporter: Samuel Groß, Assigned: baku)

Tracking

({csectype-intoverflow, regression, sec-high})

45 Branch
mozilla52
csectype-intoverflow, regression, sec-high
Points:
---
Bug Flags:
sec-bounty +
in-testsuite ?

Firefox Tracking Flags

(firefox49 wontfix, firefox50 verified, firefox51 verified, firefox52 verified, firefox-esr4550+ verified)

Details

(Whiteboard: [post-critsmash-triage][adv-main50+][adv-esr45.5+])

Attachments

(3 attachments, 1 obsolete attachment)

(Reporter)

Description

8 months ago
Created attachment 8787005 [details]
poc.zip

While loading external script data, the following code is executed in nsScriptLoadHandler (nsScriptLoader.cpp):

nsresult
nsScriptLoadHandler::TryDecodeRawData(const uint8_t* aData,
                                      uint32_t aDataLength,
                                      bool aEndOfStream)
{
  int32_t srcLen = aDataLength;
  const char* src = reinterpret_cast<const char *>(aData);
  int32_t dstLen;
  nsresult rv =
    mDecoder->GetMaxLength(src, srcLen, &dstLen);

  NS_ENSURE_SUCCESS(rv, rv);

  uint32_t haveRead = mBuffer.length();
  uint32_t capacity = haveRead + dstLen;        // [[ 1 ]]
  if (!mBuffer.reserve(capacity)) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  rv = mDecoder->Convert(src,
                      &srcLen,
                      mBuffer.begin() + haveRead,
                      &dstLen);

  NS_ENSURE_SUCCESS(rv, rv);

  haveRead += dstLen;
  MOZ_ASSERT(haveRead <= capacity, "mDecoder produced more data than expected");
  MOZ_ALWAYS_TRUE(mBuffer.resizeUninitialized(haveRead));

  return NS_OK;
}

The calculation of the new capacity at [[ 1 ]] can overflow if enough data is sent by the server (>4GB). In that case, the following call to mBuffer.reserve() won't change the size of mBuffer. Afterwards, mDecoder->Convert() will write data past the end of the 8GB buffer (mBuffer stores char16_t) [1].

poc.py (requires python 3.5) in the attached archive will trigger this condition and cause an access violation while writing data into an unmapped region following the buffer (tested on Firefox 48.0.1 and Aurora):

    Process 97720 stopped
    * thread #1: tid = 0x19afd5, 0x0000000101c66c68 XUL`nsOneByteDecoderSupport::Convert(char const*, int*, char16_t*, int*) [inlined] nsUnicodeDecodeHelper::ConvertByFastTable(aSrcLength=<unavailable>, aDestLength=<unavailable>) + 9 at nsUnicodeDecodeHelper.cpp:206, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x577000000)
    frame #0: 0x0000000101c66c68 XUL`nsOneByteDecoderSupport::Convert(char const*, int*, char16_t*, int*) [inlined] nsUnicodeDecodeHelper::ConvertByFastTable(aSrcLength=<unavailable>, aDestLength=<unavailable>) + 9 at nsUnicodeDecodeHelper.cpp:206
       203    }
       204
       205    for (; src<srcEnd;) {
    -> 206      *dest = aFastTable[*src];
       207      if (*dest == 0xfffd && aErrorSignal) {
       208        res = NS_ERROR_ILLEGAL_INPUT;
       209        break;
    (lldb) bt
    frame #0: 0x0000000101c66c68 XUL`nsOneByteDecoderSupport::Convert(char const*, int*, char16_t*, int*) [inlined] nsUnicodeDecodeHelper::ConvertByFastTable(aSrcLength=<unavailable>, aDestLength=<unavailable>) + 9 at nsUnicodeDecodeHelper.cpp:206
    frame #1: 0x0000000101c66c5f XUL`nsOneByteDecoderSupport::Convert(this=0x0000000112a0e800, aSrc="", aSrcLength=0x00007fff5fbfcc6c, aDest=u"", aDestLength=0x00007fff5fbfcc68) + 207 at nsUCSupport.cpp:273
    frame #2: 0x0000000102a5c3f2 XUL`nsScriptLoadHandler::TryDecodeRawData(this=0x00000001290eb740, aData="", aDataLength=<unavailable>, aEndOfStream=<unavailable>) + 146 at nsScriptLoader.cpp:2752
    frame #3: 0x0000000102a5be73 XUL`nsScriptLoadHandler::OnIncrementalData(this=0x00000001290eb740, aLoader=<unavailable>, aContext=<unavailable>, aDataLength=101, aData="", aConsumedLength=0x00007fff5fbfccf4) + 83 at nsScriptLoader.cpp:2720
    ...


Some notes regarding exploitability:

    - It is possible to use compression to reduce the network load. The attached PoC code does that and thus only needs to send about 18MB of data.

    - It should be possible to place some objects behind the buffer, as mmap will usually allocate adjacent memory regions. This could be done as follows:
        - The server sends the first 0xffffffff bytes of the payload, causing an allocation of an 8GB memory region
        - JavaScript code executes and allocates lots of target objects. At some point new memory is requested via mmap, which should be placed behind the 8GB buffer
        - The server sends the remaining data, overflowing into the previously allocated objects

    - Due to page deduplication and/or compression beeing performed by the kernel, it should be possible to trigger the bug while having less than 8GB of available RAM. At least on my Mac this seems to be the case as memory usage stays relatively low even during the allocations.

    - A 64-bit build of Firefox is required so the bug can be triggered.

    - The data that is being written is not 100% controlled as it must be valid UTF-16

[1] There is a special case when the capacity wraps around to zero. In that case the process will (likely) not crash as the overflow only happens inside the 8GB chunk.

Comment 1

8 months ago
I haven't tried to repro, but from code inspection this seems like a valid issue. Moving to a better component. mrbkap / baku, can either of you take a look? (CC'ing people because of our security groups...)
Group: firefox-core-security → core-security
Component: Security → DOM
Flags: needinfo?(mrbkap)
Flags: needinfo?(amarchesini)
Product: Firefox → Core
(Assignee)

Updated

8 months ago
Assignee: nobody → amarchesini
Flags: needinfo?(amarchesini)
(Reporter)

Comment 2

8 months ago
Created attachment 8787399 [details]
Another Proof-of-Concept

I've created a second PoC that allocates a Uint8Array behind mBuffer, which is then being overflown into. I've tested this on OS X 10.11. It's still a bit hackish since I haven't fully figured out what jemalloc and the OS X kernel are doing during the realloc calls.

If there are no mitigations in place which add guard pages in front of important memory regions (or something similar) it should be possible (at least on OS X) to place something more "interesting" behind mBuffer, which is then overwritten.

Things look a bit different on Linux though. For one, the mmap region on Linux grows towards lower addresses, while it grows towards higher addresses on OS X. I haven't checked Windows. It should still be possible to allocate something in the memory region following the buffer, but the new PoC will likely not work on Linux out of the box.
(Assignee)

Updated

8 months ago
Flags: needinfo?(mrbkap)
(Assignee)

Comment 3

8 months ago
Created attachment 8787402 [details] [diff] [review]
crash.patch
Attachment #8787402 - Flags: review?(bugs)
Keywords: csectype-intoverflow, sec-high
Group: core-security → dom-core-security

Updated

8 months ago
Attachment #8787402 - Flags: review?(bugs) → review+
(Assignee)

Comment 4

8 months ago
Comment on attachment 8787402 [details] [diff] [review]
crash.patch

[Security approval request comment]
How easily could an exploit be constructed based on the patch?

See comment 1.

Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?

No.

Which older supported branches are affected by this flaw?

All. 2015. Bug 1218029

Do you have backports for the affected branches? If not, how different, hard to create, and risky will they be?

Easy to write.

How likely is this patch to cause regressions; how much testing does it need?

zero.
Attachment #8787402 - Flags: sec-approval?
Flags: sec-bounty?
Too late for 49. Sec-approval to land on September 20 (two weeks into next cycle).
status-firefox49: --- → wontfix
status-firefox50: --- → affected
status-firefox51: --- → affected
status-firefox-esr45: --- → affected
tracking-firefox-esr45: --- → ?
Attachment #8787402 - Flags: sec-approval? → sec-approval+
(Assignee)

Comment 6

8 months ago
checkin-needed, but see comment 5!
Keywords: checkin-needed
Keywords: checkin-needed
Whiteboard: [checkin on 9/20]
tracking-firefox-esr45: ? → 50+
Status: UNCONFIRMED → NEW
Ever confirmed: true
(Reporter)

Comment 7

7 months ago
Created attachment 8792243 [details]
PoC Exploit

I've created another PoC. This one spawns (most of the time) Calculator.app on the latest Firefox (48.0.1) for OS X (10.11.6).

The large comment in code.js gives a decent overview over how the exploit works. The very short version is that it forces an allocation of an Arena (filled with ArrayBuffers) behind the buffer that is overflown. It then frees (roughly) one ArrayBuffer in every Arena and triggers GC. Finally it overflows into the |firstFreeSpan| field of the Arena and makes it look like the free slot is inside the inline data of one of the ArrayBuffer objects. When more ArrayBuffer objects are then allocated, one of them will be placed inside the data of another ArrayBuffer, making it accessible from JavaScript. From there, crafting an arbitrary read/write primitive becomes fairly easy.

The PoC currently is version dependent (due to the offsets in code.js) and only works on OS X since it assumes that the mmap region grows upwards.

For a generic mitigation, either randomizing mmap regions or inserting guard pages before/after mmap chunks should work, although I think this type of bug is best mitigated by the kernel...
Attachment #8787399 - Attachment is obsolete: true
https://hg.mozilla.org/integration/mozilla-inbound/rev/78842094a8b1
status-firefox52: --- → affected
Flags: in-testsuite?
Keywords: regression
Whiteboard: [checkin on 9/20]
Version: unspecified → 45 Branch
https://hg.mozilla.org/mozilla-central/rev/78842094a8b1
Status: NEW → RESOLVED
Last Resolved: 7 months ago
status-firefox52: affected → fixed
Resolution: --- → FIXED
Target Milestone: --- → mozilla52
Flags: sec-bounty? → sec-bounty+
Group: dom-core-security → core-security-release

Comment 10

7 months ago
Hi Baku, should we uplift this fix to Beta50, Aurora51 and ESR45.5?
Flags: needinfo?(amarchesini)
(Assignee)

Comment 11

7 months ago
Comment on attachment 8787402 [details] [diff] [review]
crash.patch

[Approval Request Comment]
If this is not a sec:{high,crit} bug, please state case for ESR consideration:
User impact if declined: 
Fix Landed on Version: 51... ?
Risk to taking this patch (and alternatives if risky): zero. patch is very easy.
String or UUID changes made by this patch: none

See https://wiki.mozilla.org/Release_Management/ESR_Landing_Process for more info.

Approval Request Comment
[Feature/regressing bug #]: Script Loader 
[User impact if declined]: a crash could happen if the capacity variable overflows.
[Describe test coverage new/current, TreeHerder]: green, but we don't have a test for this: it's not trivial to write it.
[Risks and why]: zero.
[String/UUID change made/needed]: none.
Flags: needinfo?(amarchesini)
Attachment #8787402 - Flags: approval-mozilla-esr45?
Attachment #8787402 - Flags: approval-mozilla-beta?
Attachment #8787402 - Flags: approval-mozilla-aurora?

Comment 12

7 months ago
Comment on attachment 8787402 [details] [diff] [review]
crash.patch

Sec-high, Aurora51+, Beta50+, ESR45+
Attachment #8787402 - Flags: approval-mozilla-esr45?
Attachment #8787402 - Flags: approval-mozilla-esr45+
Attachment #8787402 - Flags: approval-mozilla-beta?
Attachment #8787402 - Flags: approval-mozilla-beta+
Attachment #8787402 - Flags: approval-mozilla-aurora?
Attachment #8787402 - Flags: approval-mozilla-aurora+
https://hg.mozilla.org/releases/mozilla-aurora/rev/062f306bbfc5
https://hg.mozilla.org/releases/mozilla-beta/rev/89aad2940835
status-firefox50: affected → fixed
status-firefox51: affected → fixed
https://hg.mozilla.org/releases/mozilla-esr45/rev/576f1725a57e14f2d570e6745f88161fe92090bc
status-firefox-esr45: affected → fixed
Flags: qe-verify+
Whiteboard: [post-critsmash-triage]
I didn't managed to reproduce this issue on Mac OS X 10.11.6. I've used both poc.zip and PoC Exploit and tried to run index.html by making a http server with python 3.5. This was done on firefox 48.0.1 in 64 bit as Samuel said in comment 0 in order to trigger the bug. If I understand right this needs to crash (this is what I see: http://imgur.com/a/LXFzA) or maybe I am doing something wrong here.

Andrea, can you please provide me some additional info about how to reproduce this bug? What I'm missing?
Flags: needinfo?(amarchesini)
(Assignee)

Updated

6 months ago
Flags: needinfo?(amarchesini) → needinfo?(mozilla)
(Reporter)

Comment 16

6 months ago
You'll have to use the poc.py script (a custom webserver) from the zip archive since some server side logic is required for the code to work. Are you doing that?
Thank you Samuel, now I'm able to repoduce the crash on 49.0.2 with your info.

This issue is verified fixed on 45.5.0 (20161031153904), 50.0-build2 (20161104212021), 51.0a2 (20161108004019) and 52.0a1 (20161109030210) running Mac OS X 10.11.6. 

I will mark here accordengelly.
Status: RESOLVED → VERIFIED
status-firefox50: fixed → verified
status-firefox51: fixed → verified
status-firefox52: fixed → verified
status-firefox-esr45: fixed → verified
Flags: qe-verify+
Flags: needinfo?(mozilla)
Whiteboard: [post-critsmash-triage] → [post-critsmash-triage][adv-main50+][adv-esr45.5+]
Alias: CVE-2016-9066
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.