2009-10-14:

PHP getimagesize internals (part 3): PNG

php:security:easy
Seems I'm a little behind on the English side of the mirror, so it's time to fix that with another PHP internals topic! This time I'll tell you the story of the PNG format, of course in the context of it's support in the getimagesize function.

PNG


Let's start with checking how the PNG signature is detected - this means opening the source of the php_getimagetype function. As you may remember, I've already described this code a little in the first post of the series, during the warning hunt, so the piece may already be known to you.

 } else if (!memcmp(filetype, php_sig_png, 3)) {
   if (php_stream_read(stream, filetype+3, 5) != 5) {
     php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Read error! (2)");
     return IMAGE_FILETYPE_UNKNOWN;
   }
   if (!memcmp(filetype, php_sig_png, 8)) {
     return IMAGE_FILETYPE_PNG;
   } else {
     php_error_docref(NULL TSRMLS_CC, E_WARNING, "PNG file corrupted by ASCII conversion");
     return IMAGE_FILETYPE_UNKNOWN;
 }


The code begins with comparing 3 bytes of php_sig_png with 3 read bytes. The next step is reading another 5 bytes, and comparing the whole 8 bytes with the php_sig_png (side note: at this moment the first 3 bytes in the file might already be changed to something else by external source; this note of course is nothing more then an more or less interesting details, and nothing more; it might be an interesting idea for some hackme).

The php_sig_png is an 8 byte array containing: 89 50 4e 47 0D 0A 1A 0A (another side note: this signature is constructed in such a way that it enables to detect invalid line conversion from 0D 0A sequence to 0A or vice versa (some overreactive FTP clients).

After the IMAGE_FILETYPE_PNG image type is set, a call to the php_handle_png function takes place. This function contains the following code:

 struct gfxinfo *result = NULL;
 unsigned char dim[9];

 if (php_stream_seek(stream, 8, SEEK_CUR))
   return NULL;

 if((php_stream_read(stream, dim, sizeof(dim))) < sizeof(dim))
   return NULL;

 result = (struct gfxinfo *) ecalloc(1, sizeof(struct gfxinfo));
 result->width  = (((unsigned int)dim[0]) << 24) + (((unsigned int)dim[1]) << 16) + (((unsigned int)dim[2]) << 8) + ((unsigned int)dim[3]);
 result->height = (((unsigned int)dim[4]) << 24) + (((unsigned int)dim[5]) << 16) + (((unsigned int)dim[6]) << 8) + ((unsigned int)dim[7]);
 result->bits   = (unsigned int)dim[8];
 return result;
}


As one can see, the code is rather short for such a complex format like PNG. This code reads only a part of the IHDR chunk, without even checking if it is a valid header (in a valid PNG file the IHDR chunk must be the first chunk of a file), not to mention not checking the CRC of the data (each chunk in PNG has a checksum of the data).

So, to create an array of bytes that are not a PNG file, but are accepted as one, it's enough to take the 8 bytes from php_sig_png, and make sure that the array is at least 8+8+9 bytes long.

An example string (array of bytes) that passes through the getimagesize() as a PNG image looks like this:

<?php
$data = "\x89\x50\x4e\x47\x0D\x0A\x1A\x0AXXXXYYYY" . pack("NNC", 1024, 768, 0);
$a = getimagesize("data://text/plain;base64," . base64_encode($data));
var_dump($a);


The above code displays this information:

array(5) {
 [0]=>
 int(1024)
 [1]=>
 int(768)
 [2]=>
 int(3)
 [3]=>
 string(25) "width="1024" height="768""
 ["mime"]=>
 string(9) "image/png"
}


Since I've set the 'bits' field to 0, the result array does not contain the 'bits' field (check out the first part of PHP Internals ;>).
So, our string works, and it's detected as a valid PNG image.

Is it possible, similar to the GIF case, to create a PNG image that size returned by the getimagesize() function differs from the size of the image displayed by the web/image browsers?
As far as I know no, it is not possible.

There are two problems:
1) The IHDR chunk must be the first chunk in the file (and it is checked by libpng) - in case this would not be true, we might just put some comment chunk or something before the IHDR chunk, so the getimagesize() would read the data from the comment chunk, and the browser would read if from the proper IHDR header; this of course is not the case
2) There can be only one IHDR chunk - so we cannot create two IHDR chunks, when the second would be interpreted by the browser (it probably would overwrite the data read from the first one)
Well, but libpng checks are OK, and that lib is used in the most PNG readers (I think Microsoft has it's own PNG reader, however I am not familiar with it's implementation - need to check that later ;>).

And that's that. To sum up: we can create a fake-PNG byte string that passes through the getimagesize() without any problems, but it is not possible to create a PNG that size might be misinterpreted.

Add a comment:

Nick:
URL (optional):
Math captcha: 2 ∗ 9 + 1 =