Czas na kolejną część tematyki PHP internals! Tym razem będzie o popularnym formacie PNG, oczywiście w kontekście obsługi tego formatu przez funkcje getimagesize.

PNG


Zacznijmy od sprawdzania sygnatury PNG, czyli znanej nam już funkcji php_getimagetype. Wygląda to następująco (w zasadzie już patrzeliśmy na ten kod przy okazji wywoływania warningów):

 } 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;
 }


Jak widać na początku jest porównywanie 3ch bajtów php_sig_png z wczytanymi trzema bajtami, następnie doczytywane jest 5 bajtów, i porównywana jest cała 8semka bajtów z php_sig_png.

Teraz krótka dygresja. Powyższy kod zawiera całkowicie niegroźny, acz ciekawy błąd typu race condition - między odczytem pierwszych 3ch a kolejnych 5ciu bajtów mija pewien kwant czasu, podczas którego obca aplikacja może zmodyfikować (plik otwierany przez getimagesize nie jest lock'owany) pierwsze 3 bajty z poprawnych na niepoprawne (np. na sekwencje "<? "). W takim wypadku przebieg wyglądał by tak:
PHP: 1. W pliku znajduje się poprawna pełna sygnatura php_sig_png
PHP: 2. Następuje odczyt 3ch pierwszych bajtów
Obcy: 3. Obcy proces nadpisuje pierwsze 3 bajty jakąś sekwencją
PHP: 4. Następuje odczyt 5ciu kolejnych bajtów i porównanie wszystkich 8śmiu bajtów (w filetype jest poprawna sygnatura, mimo iż w pliku już nie)
PHP: 5. Zostaje zwrócony IMAGE_FILETYPE_PNG, mimo iż tak na prawdę plik nie jest już poprawnym plikiem PNG
PHP: 6. Skrypt php przenosi plik gdzieś indziej w tajne miejsce uznając że jest to nadal poprawny PNG
Oczywiście jest to raczej zupełnie niegroźne i ma sens tylko wtedy gdy Obcy proces faktycznie ma dostęp do pliku i gdy z plikiem coś się później dzieje na podstawie magic'a (zresztą, na upartego można celować race conditionem między 4 a 6 i nadpisać nawet większy fragment pliku).
W sumie byłby to ciekawy pomysł na jakieś hackme ;>

Kończąc dygresję przejdźmy do zawartości sygnatury: php_sig_png zawiera następujące bajty: 89 50 4e 47 0D 0A 1A 0A (ciekawostka: ta sygnatura została skontrowana tak aby wykrywać przypadkowe konwersje sekwencji końca linii z 0D 0A na 0A i vice versa (niektóre klienty FTP bywały nadgorliwe)).

Mając już IMAGE_FILETYPE_PNG przejdźmy do funkcji php_handle_png:

 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;
}


Kod jak widać jest króciutki, i odczytuje jedynie fragment nagłówka IHDR, nawet nie upewniając się czy to faktycznie jest ten nagłówek (w prawidłowym pliku PNG chunk IHDR musi być pierwszy), że nie wspomnę już o sprawdzeniu CRC danych (każdy chunk w PNG ma sumę kontrolną na końcu danych).

W związku z tym aby stworzyć ciąg znaków nie będący plikiem PNG, ale przechodzący przez getimagesize wystarczy wziąć sygnaturę png (8 bajtów), i upewnić się że plik będzie miał przynajmniej 8+8+9 bajtów wielkości. Przykładowy ciąg przechodzący walidację wygląda następująco:

<?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);


Wykonanie powyższego kodu powoduje pojawienie się następujących informacji:

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


Jak widać, z uwagi na ustawienie ostatniego bajtu na 0, pole 'bits' w ogóle się w array'u nie pojawiło (patrz część pierwsza ;>). Więc wszystko gra - nie-PNG zostało rozpoznane jako PNG.

Czy da się w przypadku PNG, podobnie jak było w przypadku GIF, stworzyć poprawny obraz który dla którego getimagesize zwróci inną wielkość niż przeglądarki internetowe/graficzne? Z mojej wiedzy wynika że nie.
Problemy są dwa:
1) IHDR musi być pierwszy - gdyby nie musiał być, wystarczyło by wrzucić jakiś inny nagłówek jako pierwszy, a IHDR gdzieś dalej
2) IHDR musi występować dokładnie raz - gdyby można dwa razy wrzucić chunk IHDR, to z dużym prawdopodobieństwem dekodery respektowały by ostatni IHDR który się pojawi
Natomiast standardowy dekoder PNG (czyli libpng) upewnia się że powyższe warunki są spełnione (ciekawe jak tam dekoder Microsoftu... muszę przy okazji rzucić okiem... chyba że i oni korzystają z libpng ;>).

I to by było na tyle jeśli chodzi o PNG i getimagesize. Podsumowując: bez problemu można stworzyć ciąg który przejdzie przez getimagesize i będzie rozpoznany jako PNG, ale nie można stworzyć obrazu o innym rozmiarze niż getimagesize podaje.

Comments:

2009-09-01 12:30:28 = Neonek
{
No dobra, o PNG jest, ale "...o mniej popularnym XBM..." już nie. Gdzie to się podziało ? Race condition to jeden z najbardziej znienawidzonych przeze mnie bugów.
}
2009-09-01 14:26:20 = Gynvael Coldwind
{
@Neonek
Fixed ;> Wstępnie bałem się że post o PNG wyjdzie bardzo krótki, więc chciałem też o XBM napisać. Jednak post o PNG wyszedł niekrótki, więc w końcu usunąłem wszystkie (jak mi się wydawało) referencje dot XBM i postanowiłem napisać o tym później ;>
}

Add a comment:

Nick:
URL (optional):
Math captcha: 4 ∗ 1 + 8 =