Crash Reproduction Series: Microsoft Edge Legacy

SHARE THIS ARTICLE

Follow zecops

During yet another Digital Forensics investigation using ZecOps Crash Forensics Platform, we saw a crash of the Legacy (pre-Chromium) Edge browser. The crash was caused by a NULL pointer dereference bug, and we concluded that the root cause was a benign bug of the browser. Nevertheless, we thought that it would be a nice showcase of a crash reproduction.

Here’s the stack trace of the crash:

00007ffa`35f4a172     edgehtml!CMediaElement::IsSafeToUse+0x8
00007ffa`36c78124     edgehtml!TrackHelpers::GetStreamIndex+0x26
00007ffa`36c7121f     edgehtml!CSourceBuffer::RemoveAllTracksHelper<CTextTrack,CTextTrackList>+0x98
00007ffa`36880903     edgehtml!CMediaSourceExtension::Var_removeSourceBuffer+0xc3
00007ffa`364e5f95     edgehtml!CFastDOM::CMediaSource::Trampoline_removeSourceBuffer+0x43
00007ffa`3582ea87     edgehtml!CFastDOM::CMediaSource::Profiler_removeSourceBuffer+0x25
00007ffa`359d07b6     Chakra!Js::JavascriptExternalFunction::ExternalFunctionThunk+0x207
00007ffa`35834ab8     Chakra!amd64_CallFunction+0x86
00007ffa`35834d38     Chakra!Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > > >+0x198
00007ffa`35834f99     Chakra!Js::InterpreterStackFrame::OP_ProfiledCallIWithICIndex<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > >+0xb8
00007ffa`3582cd80     Chakra!Js::InterpreterStackFrame::ProcessProfiled+0x149
00007ffa`3582df9f     Chakra!Js::InterpreterStackFrame::Process+0xe0
00007ffa`3582cf9e     Chakra!Js::InterpreterStackFrame::InterpreterHelper+0x88f
0000016a`bacc1f8a     Chakra!Js::InterpreterStackFrame::InterpreterThunk+0x4e
00007ffa`359d07b6     0x0000016a`bacc1f8a
00007ffa`358141ea     Chakra!amd64_CallFunction+0x86
00007ffa`35813f0c     Chakra!Js::JavascriptFunction::CallRootFunctionInternal+0x2aa
00007ffa`35813e4a     Chakra!Js::JavascriptFunction::CallRootFunction+0x7c
00007ffa`35813d29     Chakra!ScriptSite::CallRootFunction+0x6a
00007ffa`35813acb     Chakra!ScriptSite::Execute+0x179
00007ffa`362bebed     Chakra!ScriptEngineBase::Execute+0x19b
00007ffa`362bde49     edgehtml!CListenerDispatch::InvokeVar+0x41d
00007ffa`362bc6c2     edgehtml!CEventMgr::_InvokeListeners+0xd79
00007ffa`35fdf8f1     edgehtml!CEventMgr::Dispatch+0x922
00007ffa`35fe0089     edgehtml!CEventMgr::DispatchPointerEvent+0x215
00007ffa`35fe04f4     edgehtml!CEventMgr::DispatchClickEvent+0x1d1
00007ffa`36080f10     edgehtml!Tree::ElementNode::Fire_onclick+0x60
00007ffa`36080ca0     edgehtml!Tree::ElementNode::DoClick+0xf0
[...]

Amusingly, the browser crashed in the CMediaElement::IsSafeToUse function. Apparently, the answer is no – it isn’t safe to use.

Crash reproduction

The stack trace indicates that the function that was executed by the JavaScript code, and eventually caused the crash, was removeSourceBuffer, part of the MediaSource Web API. Looking for a convenient example to play with, we stumbled upon this page which uses the counterpart function, addSourceBuffer. We added a button that calls removeSourceBuffer and tried it out.

Just calling removeSourceBuffer didn’t cause a crash (otherwise it would be too easy, right?). To see how far we got, we attached a debugger and put a breakpoint on the edgehtml!CMediaSourceExtension::Var_removeSourceBuffer function, then did some stepping. We saw that the CSourceBuffer::RemoveAllTracksHelper function is not being called at all. What tracks does it help to remove?

After some searching, we learned that there’s the HTML <track> element that allows us to specify textual data, such as subtitles, for a media element. We added such an element to our sample video and bingo! Edge crashed just as we hoped.

Crash reason

Our best guess is that the crash happens because the CTextTrackList::GetTrackCount function returns an incorrect value. In our case, it returns 2 instead of 1. An iteration is then made, and the CTextTrackList::GetTrackNoRef function is called with index values from 0 to the track count (simplified):

int count = CTextTrackList::GetTrackCount();
for (int i = 0; i < count; i++) {
    CTextTrackList::GetTrackNoRef(..., i);
    /* more code... */
}

While it may look like an out-of-bounds bug, it isn’t. GetTrackNoRef returns an error for an invalid index, and for index=1 (in our case), a valid object is returned, it’s just that one of its fields is a NULL pointer. Perhaps the last value in the array is some kind of a sentinel value which was not supposed to be part of the iteration.

Exploitation

The bug is not exploitable, and can only cause a slight inconvenience by crashing the browser tab.

POC

Here’s a POC that demonstrates the crash. Save it as an html file, and place the test.mp4, foo.vtt files in the same folder.

Tested version:

  • Microsoft Edge 44.18362.449.0
  • Microsoft EdgeHTML 18.18363
<button>Crash</button>
<br><br><br>

<video autoplay controls playsinline>
    <!-- https://gist.github.com/Michael-ZecOps/046e2c97d208a0a6da2f81c3812f7d5d -->
    <track label="English" kind="subtitles" srclang="en" src="foo.vtt" default>
</video>

<script>
    // Based on: https://simpl.info/mse/
    var FILE = 'test.mp4'; // https://w3c-test.org/media-source/mp4/test.mp4
    var video = document.querySelector('video');

    var mediaSource = new MediaSource();
    video.src = window.URL.createObjectURL(mediaSource);

    mediaSource.addEventListener('sourceopen', function () {
        var sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="mp4a.40.2,avc1.4d400d"');

        var button = document.querySelector('button');
        button.onclick = () => mediaSource.removeSourceBuffer(mediaSource.sourceBuffers[0]);

        get(FILE, function (uInt8Array) {
            var file = new Blob([uInt8Array], {
                type: 'video/mp4'
            });

            var reader = new FileReader();

            reader.onload = function (e) {
                sourceBuffer.appendBuffer(new Uint8Array(e.target.result));
                sourceBuffer.addEventListener('updateend', function () {
                    if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
                        mediaSource.endOfStream();
                    }
                });
            };

            reader.readAsArrayBuffer(file);
        });
    }, false);

    function get(url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.responseType = 'arraybuffer';
        xhr.send();

        xhr.onload = function () {
            if (xhr.status !== 200) {
                alert('Unexpected status code ' + xhr.status + ' for ' + url);
                return false;
            }
            callback(new Uint8Array(xhr.response));
        };
    }
</script>

Does mobile DFIR research interest you?

ZecOps is expanding. We’re looking for additional researchers to join ZecOps Research Team. If you’re interested, send us a note at [email protected].

reverse bounty

Researcher? Analyst?

If you get excited about exploits reproduction like we do, you would love ZecOps Reverse Bounty program - details ahead!

Join Reverse Bounty™ >

Partners, Resellers, Distributors and Innovative Security Teams

ZecOps provides the industry-first automated crash forensics platform across devices, operating systems and applications. Learn more about what we do and get our one pager.

Get One Pager >

SHARE THIS ARTICLE