php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Sec Bug #68799 Free called on unitialized pointer
Submitted: 2015-01-11 04:08 UTC Modified: 2015-01-20 18:39 UTC
From: endeavor at rainbowsandpwnies dot com Assigned:
Status: Closed Package: EXIF related
PHP Version: 5.4.36 OS: Debian Wheezy
Private report: No CVE-ID: 2015-0232
 [2015-01-11 04:08 UTC] endeavor at rainbowsandpwnies dot com
Description:
------------
/**********************************
* Alex Eubanks                    *
* endeavor@rainbowsandpwnies.com  *
* php 5.4.36 exif free on bad ptr *
* 10 January 2014                 *
**********************************/

/*************
* A foreword *
*************/
// this bug was found with american fuzzy lop! thanks lcamtuf!
/*
* My environment is debian wheezy, both 32 and 64-bit variants
* Php was build from source, not pulled from package repos
* built with: ./configure --enable-exif
*/


Program received signal SIGSEGV, Segmentation fault.
_zend_mm_free_int (heap=0x87bc1a8, p=0xb7c0e13c) at /home/user/php/php-5.4.36/Zend/zend_alloc.c:2100
2100            if (ZEND_MM_IS_FREE_BLOCK(next_block)) {
(gdb) bt
#0  _zend_mm_free_int (heap=0x87bc1a8, p=0xb7c0e13c) at /home/user/php/php-5.4.36/Zend/zend_alloc.c:2100
#1  0x081532d0 in exif_discard_imageinfo (ImageInfo=ImageInfo@entry=0xbfffc044)
    at /home/user/php/php-5.4.36/ext/exif/exif.c:3846
#2  0x08168884 in zif_exif_read_data (ht=1, return_value=0xb7c0d858, return_value_ptr=0x0, this_ptr=0x0, 
    return_value_used=1) at /home/user/php/php-5.4.36/ext/exif/exif.c:4090
#3  0x0839ba81 in zend_do_fcall_common_helper_SPEC (execute_data=<optimized out>)
    at /home/user/php/php-5.4.36/Zend/zend_vm_execute.h:643
#4  0x0835d505 in execute (op_array=<optimized out>) at /home/user/php/php-5.4.36/Zend/zend_vm_execute.h:410
#5  0x082fdd83 in zend_execute_scripts (type=type@entry=8, retval=retval@entry=0x0, file_count=file_count@entry=3)
    at /home/user/php/php-5.4.36/Zend/zend.c:1329
#6  0x082a04cc in php_execute_script (primary_file=primary_file@entry=0xbfffe520)
    at /home/user/php/php-5.4.36/main/main.c:2502
#7  0x0839e404 in do_cli (argc=-1073748704, argc@entry=5, argv=0x7)
    at /home/user/php/php-5.4.36/sapi/cli/php_cli.c:989
#8  0x0806b626 in main (argc=5, argv=0xbffff814) at /home/user/php/php-5.4.36/sapi/cli/php_cli.c:1365

/*************************
* How to trigger the bug *
*************************/

// This vulnerability will cause the following struct to be allocated:

typedef struct {
  char      *value;
  size_t      size;
  int       tag;
} xp_field_type;

// However, the field value is never set. It will contain the value of whatever
// memory was previously in its place. Php will then bail on this jpeg, causing
// a call to _zend_mm_free_int() over this unassigned pointer.

// To trigger the vulnerability, we craft an EXIF entry as such:

struct ExifEntry {
    uint16 tagNumber  = TAG_XP_AUTHOR; // 0x9c9d
    uint16 DataFormat = ascString; // 2
    uint32 nComponents = 0;
    uint32 offsetData = 78; // something within the bounds of the file
};

// In order for this bug to trigger, the value cannot be a NULL pointer at the
// time it is freed. This makes this bug somewhat difficult to trigger.

/****************************
* Location of vulnerability *
*****************************/

// The call to free which triggers this crash can be found at:
#exif.c : 3846
EFREE_IF(ImageInfo->xp_fields.list[i].value);

// In exif_process_string_raw, xp_field_type.value is passed as the
// first argument. When byte_count == 0, it is never allocated or set.

# exif.c : 2715
static int exif_process_string_raw(char **result, char *value, size_t byte_count) {
    /* we cannot use strlcpy - here the problem is that we have to copy NUL
     * chars up to byte_count, we also have to add a single NUL character to
     * force end of string.
     */
    if (byte_count) {
        (*result) = safe_emalloc(byte_count, 1, 1);
        memcpy(*result, value, byte_count);
        (*result)[byte_count] = '\0';
        return byte_count+1;
    }
    return 0;
}

// exif_process_string_raw is called from exif_process_unicode. While the conditional
// if is taken, xp_field->value is not set regardless

# exif.c : 2702
static int exif_process_unicode(image_info_type *ImageInfo, xp_field_type *xp_field, int tag, char *szValuePtr, int ByteCount TSRMLS_DC)
{
    xp_field->tag = tag;    
    
    /* XXX this will fail again if encoding_converter returns on error something different than SIZE_MAX   */
    if (zend_multibyte_encoding_converter(
            (unsigned char**)&xp_field->value, 
            &xp_field->size, 
            (unsigned char*)szValuePtr,
            ByteCount,
            zend_multibyte_fetch_encoding(ImageInfo->encode_unicode TSRMLS_CC),
            zend_multibyte_fetch_encoding(ImageInfo->motorola_intel ? ImageInfo->decode_unicode_be : ImageInfo->decode_unicode_le TSRMLS_CC)
            TSRMLS_CC) == (size_t)-1) {
        xp_field->size = exif_process_string_raw(&xp_field->value, szValuePtr, ByteCount);
    }
    return xp_field->size;
}

// Call to exif_process_unicode takes place from...
# exif.c : 2982

            case TAG_XP_TITLE:
            case TAG_XP_COMMENTS:
            case TAG_XP_AUTHOR:
            case TAG_XP_KEYWORDS:
            case TAG_XP_SUBJECT:
                tmp_xp = (xp_field_type*)safe_erealloc(ImageInfo->xp_fields.list, (ImageInfo->xp_fields.count+1), sizeof(xp_field_type), 0);
                ImageInfo->sections_found |= FOUND_WINXP;
                ImageInfo->xp_fields.list = tmp_xp;
                ImageInfo->xp_fields.count++;
                exif_process_unicode(ImageInfo, &(ImageInfo->xp_fields.list[ImageInfo->xp_fields.count-1]), tag, value_ptr, byte_count TSRMLS_CC);
                break;

// The important variable here is byte_count.

# exif.c : 2828
components = php_ifd_get32u(dir_entry+4, ImageInfo->motorola_intel);

# exif.c : 2842
byte_count_signed = (int64_t)components * php_tiff_bytes_per_format[format];

# exif.c : 2849
byte_count = (size_t)byte_count_signed;

// the value components is attacker controlled, and can be set to 0.

/*********
* Effect *
*********/

I have triggered the bug in 32-bit and 64-bit debian when building from source.

I have put some effort into triggering the bug with php5-cli from the debian
wheezy packages. For some reason, I have NOT been able to trigger the bug using
debian stable packages.

I have been able to gain limited control of the crash in 64-bit debian when
building from source.

With a fresh build of php-5.4.36 in 64-bit debian wheezy, control of the
register being dereferenced can be achieved.

user@debian:~/vulns/php_5.4.36_invalid_free$ gdb --args /usr/local/bin/php -f write_phpsrc_exif2.php
...
(gdb) r
...
Program received signal SIGSEGV, Segmentation fault.
_zend_mm_free_int (heap=0xd9c2d0, p=0x7ffff7fd1520) at /home/user/source/php-5.4.36/Zend/zend_alloc.c:2100
2100            if (ZEND_MM_IS_FREE_BLOCK(next_block)) {
(gdb) x/4i $pc-11
   0x667fa7 <_zend_mm_free_int+167>:    jmpq   *%rax
   0x667fa9 <_zend_mm_free_int+169>:    lea    0x0(%r13,%rbx,1),%r14
   0x667fae <_zend_mm_free_int+174>:    sub    %rbx,0x68(%rbp)
=> 0x667fb2 <_zend_mm_free_int+178>:    testb  $0x1,(%r14)
(gdb) i r
...
rbx            0x4746454443424140       5135868584551137600
...
r13            0x7ffff7fd1510   140737353946384
r14            0x4746c5443b3f5650       5136009321905083984

This is a bit tempermental and you may have to play around with it.

/******
* Fix *
******/

xp_field_type.value should be initialized to 0 after allocation at exif.c:2987.
When freed at exif.c:3846, if xp_field_type.value is NULL no call to free is
made.

# exif.c:3846
EFREE_IF(ImageInfo->xp_fields.list[i].value);

Test script:
---------------
<?php

// You will need this file to trigger the crash
// tfpwn.com/crashashasha_3832478/exif.jpg

/*
* Pollute the heap. Helps trigger bug. Sometimes not needed.
*/
class A {
    function __construct() {
        $a = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa';
        $this->a = $a . $a . $a . $a . $a . $a;
    }
};

function doStuff ($limit) {

    $a = new A;

    $b = array();
    for ($i = 0; $i < $limit; $i++) {
        $b[$i] = clone $a;
    }

    unset($a);

    gc_collect_cycles();
}

$iterations = 3;

doStuff($iterations);
doStuff($iterations);

gc_collect_cycles();

print_r(exif_read_data('exif2.jpg'));

?>


Patches

bug68799fix (last revision 2015-01-11 08:54 UTC by stas@php.net)

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2015-01-11 08:37 UTC] stas@php.net
Looks like much easier to reproduce with mbstring disabled. Not sure if possible with mbstring enabled.
 [2015-01-11 08:54 UTC] stas@php.net
The following patch has been added/updated:

Patch Name: bug68799fix
Revision:   1420966468
URL:        https://bugs.php.net/patch-display.php?bug=68799&patch=bug68799fix&revision=1420966468
 [2015-01-11 08:55 UTC] stas@php.net
Please see if the attached patch fixes the issue.
 [2015-01-11 21:07 UTC] endeavor at rainbowsandpwnies dot com
Unfortunately I'm unable to verify the patch as I don't have permissions.

Clicking on the patch renders, "ERROR: You have no access to bug #68799"

Setting the pointer to NULL when the xp_field_type is created should be enough to fix the issue. Free will only be called over the pointer if the pointer is not NULL.

--- php-5.4.36/ext/exif/exif.c	2014-12-16 12:41:23.000000000 -0600
+++ exif.c	2015-01-10 22:00:57.137928420 -0600
@@ -2985,6 +2985,7 @@
 			case TAG_XP_KEYWORDS:
 			case TAG_XP_SUBJECT:
 				tmp_xp = (xp_field_type*)safe_erealloc(ImageInfo->xp_fields.list, (ImageInfo->xp_fields.count+1), sizeof(xp_field_type), 0);
+				tmp_xp.value = NULL;
 				ImageInfo->sections_found |= FOUND_WINXP;
 				ImageInfo->xp_fields.list = tmp_xp;
 				ImageInfo->xp_fields.count++;
 [2015-01-12 00:12 UTC] stas@php.net
Ah, sorry, I thought patches can be visible to the original submitter. Here is the patch on gist: https://gist.github.com/smalyshev/bdc81a4e0768eb705744
 [2015-01-12 02:57 UTC] endeavor at rainbowsandpwnies dot com
I see no reason why this patch shouldn't work fine.

I would set value = NULL immediately after the memory was allocated on line 2987, but that's just a matter of preference. This patch should be equivalent.

I'm not overly familiar with PHP's testing system. Unfortunately, passing the test does _not_ mean the bug doesn't exist. It means either the bug does not exist, or the pointer was allocated over null bytes, which is not an uncommon occurrence. However, it's maybe the best test you're going to get.
 [2015-01-14 14:19 UTC] remi@php.net
-CVE-ID: +CVE-ID: 2015-0232
 [2015-01-14 14:19 UTC] remi@php.net
please use: CVE-2015-0232 PHP: Sec Bug #68799 [NEW]: Free called on unitialized pointer
 [2015-01-20 18:42 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=2fc178cf448d8e1b95d1314e47eeef610729e0df
Log: Fix bug #68799: Free called on unitialized pointer
 [2015-01-20 18:42 UTC] stas@php.net
-Status: Open +Status: Closed
 [2015-01-21 00:41 UTC] tyrael@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=21bc7464f454fec18a9ec024c738f195602fee2a
Log: Fix bug #68799: Free called on unitialized pointer
 [2015-01-21 09:45 UTC] jpauli@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=55001de6d8c6ed2aada870a76de1e4b4558737bf
Log: Fix bug #68799: Free called on unitialized pointer
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Thu Nov 21 11:01:29 2024 UTC