Blog‎ > ‎

Exploiting CVE-2016-1903: Memory Read via gdImageRotateInterpolated

posted Feb 10, 2016, 5:13 PM by Emmanuel Law   [ updated Feb 15, 2016, 6:23 PM]
Vulnerability Background

This isn’t a vulnerability with a large impact, but it’s one that I thoroughly enjoyed exploiting. Hence this blogpost.
The issue is in the PHP function imagerotate() :

 Resource imagerotate ( resource $image , float $angle , int $bgd_color)


The function takes in an $image, rotates it by $angle degrees and fills any empty space left over as a side-effect of the rotation with $bgd_color.

$image can be a palette-based image or a true color image. In the case of a palette-based image, $bgd_color is an index into the image's color palette.

Here’s an example of how the function is used in practice:

 1 2 3 4 5 6 7 8 9101112131415
<?php//Create square image of 100 *100$mypic=imagecreate(100,100);//Allocate 3 colors into the PALLETEimagecolorallocate($mypic,0xFF,0,0); //Red @PALETTE[0]imagecolorallocate($mypic,0,0,0xFF);// Blue @ PALETTE[1]imagecolorallocate($mypic,0,0xFF,0);// Green @ PALETTE[2]// Fill the Square with PALETTE[2] (Green)imagefill($mypic,0,0,2);//Rotate the image by 90 degrees and fill empty space with the color at Pal    ette[1] (BLUE)$mypic2=imagerotate($mypic,45,1);

The code creates an image as follows:



The crux of the vulnerability is in the function’s handling of the $bgd_color parameter to imagerotate(). That is an index to the Color Pallette. Looking at the PHP code, we can see that the palette is stored within the image’s gdImageStruct structure as red, green, blue and alpha arrays:

typedef struct gdImageStruct {	/* Palette-based image pixels */	unsigned char **pixels;	int sx;	int sy;	/* These are valid in palette images only. See also	   'alpha', which appears later in the structure to	   preserve binary backwards compatibility */	int colorsTotal;	int red[gdMaxColors];	int green[gdMaxColors];	int blue[gdMaxColors];	int open[gdMaxColors];......................	int alpha[gdMaxColors];


The size of each array is gdMaxColors, defined elsewhere as 255. The function does not check that the passed in $bgd_color falls between these arrays’ valid ranges of 0 - 255, potentially resulting in an out of bound lookup to the arrays. Thus, if we run the following code, where $bgd_color is a large number (0x6667), we get the following image:


$mypic2=imagerotate($mypic,45,0x6667);




Notice the maroon color in the background? The color came from the undefined memory located at red[0x6667], blue[0x6667], green[0x6667] and alpha[0x6667].

By "deciphering" the background color, we can attempt to determine the bytes that were at that memory location, allowing us to perform an arbitrary memory read!


Exploiting the Vulnerability to Read Memory

Here's a high level visualization of how the image's color palette looks like in memory:

Here's how PHP computes the the background color using the RGBA word-order convention:

gdTrueColorAlpha(r, g, b, a) (((a) << 24) + \			((r) << 16) + \			((g) << 8) +  \			(b))

Thus given a background color, we can obtain the underlying bytes of memory that were at that color’s memory location by:
  • Step 1: Breaking up the color into the individual RBG Alpha components. Each components leaks one of the bytes in the memory location
  • Step 2: Repeat step 1 over the out-of-range palette indices that correspond to the memory we want to read, e.g: imagerotate($mypic,45, 256....)
  • Step 3: Reconstitute the pieces of memory leaked in Step 1 and 2 into contiguous chunks.

Step 1 should be easy right? Looking at the PHP code above, breaking a color back into its RBG Alpha compoments should be as easy  as a couple of bit shifting here and a couple of bit shifting there. Apparently Not! This is where it gets tricky (and fun)! There are a couple of issues:


Issue 1: PHP's RGBA internal representation 
By convention, each RGBA component are typically represented by 1 single byte. For example, this color with 50% opacity is represented as 0x80FF9000 (ARGB convention) where:
  • 0x80 = Alpha (50% opacity)
  • 0xFF = RED
  • 0x90 = Green
  • 0x00 = Blue
If PHP  had used this typical convention of 1 byte per component, reversing a background color back into the underlying memory would be extremely trivial. Instead, PHP stores each RGBA component as 32-bit Integers (4 bytes), despite only requiring 1 byte! Thus, this is a more accurate high level visualization of a PHP image's color palette, shown when computing the background color at index x:

Since PHP uses 4 bytes per color component, the formula to calculate the background color can be  better visualized as:

This implies that given a single background color, it is only possible to obtain the first byte of red[x] from the least significant byte of the background color. The other bytes (MSB to 2nd LSB) from the background color are a "tangle" of other the color components and appear to be impossible to separate. 

The trick is to correlate a whole bunch of background colors. For example, given a background color computed from palette[x], we will know the LSB of Red[X]. This is denoted by ! in cell AX[1st Byte]: 




Now what if we increase the palette index by 256 and compute the background from index x+256? This will give us BX[1st Byte] (This is the LSB of RED[X]). Since we now know BX[1st Byte], we can infer AX[2nd Byte] because background color Palette[X][2ndByte]= AX[2nd Byte] + BX[1st Byte] :




If we continue on another 256 bytes and compute the background from the x+512 index, we can infer even more bytes:

Thus this is how we can solve the issue of PHP using 32-bit integers for RGBA component representation and infer bytes in the original bytes in out-of-range memory. The only other tricky thing to take note of when inferring the bytes is to consider potential carry bits.




Issue 2: PHP converts the palette-based image to a true color image after rotation

The vulnerability is triggered by imagerotate() and the vulnerable path is only taken when the image is a palette-based image. This is how the code is implemented in PHP:


 1 2 3 4 5 6 7 8 910
gdImagePtr gdImageRotateInterpolated(const gdImagePtr src, const float angle, int bgcolor){......	if (src->trueColor == 0) {		if (bgcolor >= 0) {                        //Vulnerable line:			bgcolor =  gdTrueColorAlpha(src->red[bgcolor], src->green[bgcolor], src->blue[bgcolor], src->alpha[bgcolor]);		}		gdImagePaletteToTrueColor(src); //convert to true color	}



Notice the following:
  • Line 4: Checks that the image is palette-base
  • Line 7: The vulnerable code with array index out of bounds
  • Line 9: Converts image from palette based to true color
The Implication of line 9 is that for each image, you can only trigger the vulnerable code once before it is being coverted to a true color. You might ask, why can't we just generate a bunch of different images and trigger the vulnerable code once on each image? This is because, each image gets allocated in different parts of PHP heap, meaning that it would virtually be impossible to read from the same contiguous out-of-range memory using the above technique.

To solve this issue, here is my pseudo code:
  1. Cal CreateImage ()
  2. Trigger Vulnerable on Image to extract some bytes from the background color. After this, the image now is converted into true color and the vulnerablity can't be triggered on that same image anymore.
  3. Call imagedestroy(); This destroy the image and frees the memory back into PHP Zend Memory Manager Cache
  4. Immediately invoke a CreateImage(). This creates a new image. PHP Zend Memory Manager Cache will allocate the image with the same memory from its cache that was freed in step 3. Since this is now a new image, it's created as a palette-based image.
  5. We can now trigger the vulnerability on the image again. 
  6. Rinse and repeat for the entire out-of-range memory that you want to read

Issue 3:Alpha array is at a weird offset
Notice that the arrays for Red, Blue, Green are all contiguous ? Not so for the Alpha array. It is at an offset which makes things a pain. This is something that has to be considered when writing the exploit.


Original bug report can be found here.

Here's my POC to read contiguous chunks of memory. It outputs memory like so: