The pseudo-zero-day

During the course of a project of which I was a part, I had to analyse an old version of PHP in order to show that our project could’ve identified the vulnerability which was reported as a CVE-2015-3329.

But, this blog post will be about something else that we discovered about PHP during the course of the project.

My professor asked me to get some examples from PHP where our static analysis would say that some code is vulnerable, but then the symbolic execution would refute that claim.

So, I started looking for functions in PHP which called memcpy in order to copy some data on to a stack buffer. And I finally found one such function phar_check_str which did the same. And there was a comparison on the number of bytes to be copied, so symbolic execution would tell me that this is a negative.

So, I ran our static analysis code in order to see if it could find a data dependence between the data being used as input in memcpy and the argument passed to this function.

It couldn’t.

Our project uses angr for the static analyses as well as the symbolic execution. So, here we discovered that angr’s DDG was faulting (might’ve also been because of my code though).

“So, static analysis broke, but let’s just symbolically execute this anyway”.

For those of you who are too lazy to look up the code of phar_check_str, here is a snippet which contains the important stuff.

int phar_check_str(const char *ext_str, int ext_len) {
	char test[51];

	if (ext_len >= 50) {
		return FAILURE;
	}
	memcpy(test, ext_str - 1, ext_len + 1);

So, I started up angr and asked the simulation_manager to find me a path to the memcpy call.

I was kind of surprised when angr reported that it found a path. Then I realised that I hadn’t added any constraints. So, I just had to add constraints, and angr would tell me that there isn’t any value which satisfies all these constraints and therefore, this memcpy call cannot be triggered if the ext_len is greater than 50.

So, I added a constraint that ext_len had to be greater than 50.

I was expecting to see an Unsat error. Instead, it showed me this value : 2147483648.

It took me a while to understand the reason behind this. It helps if you look at the value in hex ( 0x80000000 ).

“Ahh, the infamous integer overflow.”

So, I look up the current version of PHP (7) and checked if this bit of code was still in use. And it is.

I was ecstatic, I had discovered my first 0-day. Or so I thought.

I wanted to create a full-fledged exploit or at least, trigger the overflow. So, I started creating a POC.

This bit of code gets called from another function phar_detect_phar_fname_ext (whoever was in charge of naming these functions was definitely not the subtle type) which in turn get’s called from phar_split_fname and phar_open_or_create_filename.

So, following the phar_open_or_create_filename, we see that it gets called from what I believe is the constructor of the Phar class.

So, I had to create a file with a really long name and use the Phar class to open that file which would use this code path.

So, here I ran into obstacle number 1 => really long file name.

In Linux, the maximum length of a file name is 255 characters ( 2^8 – 1). I needed 2^31.

So, I started thinking of another way to trigger this code path and I tried creating a file with a small name, but in the code which required a path to the file, I gave a string which contained 0x80000000 forward slashes. So, essentially,

‘.’ + ‘/’ * 0x80000000 + ‘test’

But, Linux specifies that a path can be a maximum of 4096 characters long ( 2^12).

So, I started looking up file-systems which would allow really large file names and/or file paths. And apparently, the HFS and HFSPlus file-systems do not have any limitation on the file-path length according to this.

So, we could argue that this code path would be triggered on any server running PHP on OSX(which uses HFSPlus). But I was too adamant to leave it like that.

I noticed that you don’t necessarily need to open a file to trigger this code path. You could specify a file which does not exist and this code path would be triggered before it tries to create such a file. You just need to set a variable readonly to false in the php.ini file.

Alright, so, the next thing was to create an object of the Phar class with a really long file name and the integer 1 as the second argument ( the integer 1 tells PHP that such a file should be created and might not necessarily exist).

And here we have obstacle number 2 => memory limitations.

Trying to create a string of size 0x80000000 gave me another error. So I started looking up documentation of PHP regarding string limits. And, as of PHP 7.0, there are no limits to the size of variables in 64 bit PHP. The largest variable you can create in 32 bit PHP is 2^31 – 1.  This is because, PHP 5.0 stored the sizes of variables in an integer data type which meant that the largest possible size of a variable is INT_MAX – 1 ( 0x7fffffff).

But I was running 64 bit PHP 7.0 and it was throwing an error about memory limits. And I found out that there is a hard limit to the maximum amount of memory that PHP can use which is defined in the php.ini file. If you set that to -1, PHP will use as much memory as the operating system will allow.

So, with this information, I went on to modify the php.ini file and then tried creating a Phar object with the same arguments. No errors this time, but the vulnerable code path wasn’t getting triggered.

Obstacle number 3 => size validation.

Looking through the code of the constructor, we see that there is a macro which checks if the size of the string is INT_MAX. If so, it simply returns to the caller. Another dead end.

But remember that we had another route to this vulnerable function via the phar_split_fname function. Following that function’s caller, we see that it get’s called from what I believe is the constructor of the PharFileInfo class. This was the only place where the macro wasn’t used to validate the length of the string.

So, my focus shifted on to this path.

I tried writing a script which created an object of PharFileInfo with a file name of size 2^31 and the integer 1 as second argument.

But here again, there was something going wrong.

So I started stepping through every instruction using GDB and I reached this point in phar_split_fname where it calls a macro CHECK_NULL_PATH.

#define CHECK_NULL_PATH(p, l) (strlen(p) != (size_t)(l))

Doesn’t look like anything that could throw off our exploit. strlen(p) should return INT_MAX and the value of l is INT_MAX. So this comparison should return false without a doubt.

But, the issue arises when we look at this a bit more closely.

In a 64 bit Ubuntu, an int is 32 bits long and size_t is 64 bits long. If you look at the types.h header file for the definition of size_t, you would see the following

typedef long unsigned int size_t;

So, when you type cast an int to size_t, it get’s extended to 64 bits. And since, int is a signed data type, the extension would a signed extension.

Which basically means that our 0x80000000 would be converted into 0xffffffff80000000.

There’s no way we can generate a string that long with the current architecture ( unless I had a super-computer just lying around).

Now, here is where things get conceptually tricky. The vulnerability exists in phar_check_str, but it can never really be triggered. So, is it really a bug ?

It is not that the PharFileInfo constructor employs adequate verification to make sure that this vulnerability is not triggered. The vulnerability is not triggered just because of an unintended consequence of a comparison. So, that means that it could be triggered in a situation where type casting int to size_t does not sign extend it.

In 32 bit systems, size_t and int are both 32 bit data types, but the maximum memory that 32 bit PHP can use is 2^31. So, maybe, somewhere in the future, if PHP decides to use unsigned int for recording the size of variables, it would be possible to create a string of size 2^31. And in that situation, this type casting int to size_t wouldn’t change a thing.

So, this unintended consequence of the CHECK_NULL_PATH macro might be why no fuzzer has discovered this vulnerability before ( because you can never really trigger it). But, when you look at it from an academic point of view, this is a bug which could be triggered (and possibly lead to arbitrary code execution) if the PHP org’s decided, on one fine day, that variable sizes would be stored in unsigned integer data types from now on.

I reported this bug to the PHP dev’s and they’ve fixed it. But, they don’t seem to recognise this as having security implications.

This discovery prompts us to think about the effectiveness of fuzzers. You could fuzz this same code for years and you wouldn’t get a positive report about this bug. But if someone were to slightly tweak the PHP internals, this bug would surface.

Maybe there are more of such bugs lying around in the vast code base of different projects. An automated approach to find such bugs would be a pretty interesting topic.

So, I guess I’ll just have to wait a bit longer for my first 0-day.