Tales from the crypt: exploiting the .NET EncoderParameter integer overflow vulnerability

Introduction

Back in 2011 I found an integer overflow vulnerability in the .NET Framework. The vulnerability could result in memory corruption due to the fact that memory was allocated based on user-supplied input. An unchecked multiplication could trigger an integer overflow resulting in a buffer being allocated on the heap that is too small for the data being copied into this buffer.

The interesting thing about this bug is that it also allowed attackers to control how much data was copied into the buffer. This was possible due to another bug within the same code. Consequently, reliable exploit code could be written that exploited this issue in order to execute arbitrary code. By exploiting this vulnerability, it is possible for an application running with Partial Trust permissions to break from the CLR sandbox (CAS) and run arbitrary code with Full Trust permissions. Examples of Partial Trusted applications include ClickOnce, XAML Browser Applications (XBAP), ASP.NET (eg, shared hosting) & SilverLight. It must be noted that the affected class is not available for SilverLight applications.

This issue was resolved with the release of MS12-025. It appears the fix was part of a security push for System.Drawing.dll, which was noted by Jeroen Frijters.

Vulnerability details

The vulnerability was found in the EncoderParameter class. One of the constructors takes the length of a user-supplied integer array and multiplies it with 16 (for 32-bits processes). The result is used to allocate a buffer on the heap using AllocHGlobal(). The code does not take into account that the multiplication can result in an integer overflow.

public EncoderParameter(Encoder encoder,
      int[] numerator1, int[] denominator1,
      int[] numerator2, int[] denominator2)
{
[...]
      this.parameterValueType = 8;
      this.numberOfValues = numerator1.Length;
      int num = Marshal.SizeOf(typeof (int));
      this.parameterValue = Marshal.AllocHGlobal(this.numberOfValues * 4 * num);
[...]
}

Supplying an integer array with 268,435,456 elements is enough to trigger the overflow. Doing so results in the allocation of a 0-byte buffer. The following proof of concept code can be used to trigger this vulnerability. Running this code will cause the application to crash.

using System;
using System.Drawing.Imaging;

namespace EncoderParameterCrash
{
   static class Crash
   {
      [STAThread]
      static void Main()
      {
         int[] largeArray = new int[0x10000000];
         EncoderParameter crash = new EncoderParameter(Encoder.Quality,
            largeArray, largeArray, largeArray, largeArray);
      }
   }
}

Controlling the copy

The proof of concept will try to write 4GB of data onto the heap. Since heap segments are a lot smaller than that, copying this amount of data will eventually result in data being copied to non-paged memory, which will cause Windows to terminate the program. This is inconvenient for attackers trying to exploit this issue.

Fortunately for the attacker, it is possible to control how much data is copied to the buffer. This is possible because of a bug in the affected code. When the vulnerable EncoderParameter constructor is invoked, it will check if the element count of the supplied integer arrays is equal. The code fails to check the length of the numerator2 parameter. Supplying a small(er) array as numerator2 parameter will cause an IndexOutOfRangeException exception to be thrown; prematurely ending the copy operation.

public EncoderParameter(Encoder encoder,
      int[] numerator1, int[] denominator1,
      int[] numerator2, int[] denominator2)
{
   this.parameterGuid = encoder.Guid;
   if (numerator1.Length != denominator1.Length ||
      numerator1.Length != denominator2.Length ||
      denominator1.Length != denominator2.Length)
   {
      throw SafeNativeMethods.Gdip.StatusException(2);
   }
[...]

Controlling the instruction pointer

Now that we can control the size of the allocated buffer and the amount of data that is copied into it, we need to find a way to control the instruction pointer. One way to achieved this is by overwriting a function pointer on the heap (virtual function) and cause this function to be called.

The ImageList class was selected as target. This .NET class wraps around the Image List functions from Comctl32.dll, which in turn wrap around the CImageList C++ class. The goal is to create a CImageList instance, overwrite its vtable, and invoke a virtual function. Because the virtual function pointer is overwritten, we can change the execution flow and call our code.

There is one challenge in this approach as it appears that before an Image List virtual function is called, it is first validated whether the supplied Image List (handle) is valid. This is done by checking a magic value; 0x4c4d4948 (HIML). If this magic value is not present, the Image List is considered to be invalid and the virtual function will not be called. This check is implemented in CImageListBase::IsValid().

Figure 1: validation of the Image List handle

This check prevents us from just smashing the heap with the aim of overwriting the targeted function pointer. It requires that our heap is properly aligned and that the magic values are restored when the buffer is overflown into the Image List object. This requires a certain amount of Heap Feng Shui. The EncoderParameter class can also be used for this purpose as it allows us to exactly determine how much heap memory is allocated.

After spending a couple of hours in the debugger, it was possible to pretty reliably control the instruction pointer. Controlling the instruction pointer alone is not enough for arbitrary code execution. We need to know where the payload is located in memory, which is difficult due to ASLR. The payload also needs to be in executable memory or else DEP kicks in.

Bypassing DEP

Bypassing DEP is not as hard as it seems. Any .NET executable loaded in memory will have at least one memory segment that is mapped as executable memory. Storing the payload in such a segment allows it to be executed.

Since we're running our own .NET code, we can load our own (specially crafted) Class library. There is enough unused space in a Class library that can be used for storing payloads. All that is left is to locate the payload in memory and jump to the payload by overwriting a function pointer on the heap. This is where ASLR comes into play.

Figure 2: .NET Class library mapped in memory

Bypassing ASLR

ASLR makes it difficult to predict where our payload will be located in memory. Heap spraying can be used to store our payload in a predictable location, however the heap is not mapped as executable so we can't execute our payload from the heap. Instead a slightly different approach was used.

I discovered that when enough Class libraries are mapped in memory, Windows will start to map newly loaded libraries at predictable locations. This technique ("Library spraying") can be used to load our payload at a predictable location (in executable memory). Now we have all ingredients for arbitrary code execution:

  • Load enough Class libraries in memory, containing our payload.
  • Use Heap Feng Shui to control where the Image List will be created in memory.
  • Create an Image List instance.
  • Exploit vulnerability to overwrite a virtual function of the Image List instance, restoring the magic value(s).
  • Invoke virtual function; jump to the payload.

The ClickOnce attack vector

ClickOnce was chosen for the exploit as it allows .NET applications to be started from a web browser. A ClickOnce application that is not signed and requests no elevated permissions will run in the Internet Security Zone - which has the least privileges. These applications used to start without any (additional) user interaction. The victim just needs to open a link in the browser. ClickOnce applications can also be delivered via email.

When creating the exploit, Microsoft released MS11-044, which changed the way ClickOnce applications are started. In particular, whenever a ClickOnce application is started from the Internet Security Zone, a dialog is always shown even if the application does not request elevated permissions.

This change mitigates the vulnerability as users are now presented with a dialog that prevents the malicious application from running unless the user chooses to do so. In case the application is started from the Intranet Security Zone, the dialog is not shown and the application will start immediately - as long as it does not request elevated permissions. The Intranet Security Zone is only available when it has been enabled on the target system. This is common for corporate networks, but less common for home users.

Once started, the ClickOnce application will run at Medium Integrity Level. Due to this, the exploit effectively bypasses Protected Mode IE (eg, the payload is not restricted by IE's sandbox).

The code

The exploit was converted into a Metasploit module. The end result can be found at the following location: https://www.securify.nl/exploit/SFY20110801/_net_framework_encoderparameter_integer_overflow_vulnerability.html

The exploit code was successfully tested on the following Windows versions:

  • Windows XP Professional SP3 32-bit (with 4GB RAM)
  • Windows Vista Home Premium SP2 32-bit
  • Windows Vista Business SP2 32-bit and 64-bit
  • Windows 7 Home Premium SP1 64-bit
  • Windows 7 Professional SP1 64-bit
  • Windows 7 Enterprise SP1 32-bit and 64-bit

Vragen of feedback?