Код для защиты GIF, PNG и JPEG (к предыдущей статье)
В коментариях к предыдущей заметке о моём предложении по защите графических форматов от внедрённого кода один из читателей попросил привести код, который показывал бы как моё предложение работает.
Для форматов GIF, JPEG и PNG я за полчаса написал следующее (язык — PHP 5.4 и выше):
<?
// //bolknote.ru август 2012
// Защищаем изображения, возращает false в случае неудачи
function ProtectPicture($inname, $outname)
{
// типы изображения, которые мы можем защитить и функции защиты
$enabled = [
IMAGETYPE_JPEG => 'Jpeg',
IMAGETYPE_PNG => 'Png',
IMAGETYPE_GIF => 'Gif',
];
if (list(,, $type) = @getimagesize($inname)) {
if (isset($enabled[$type])) {
if ($fi = @fopen($inname, 'rb')) {
flock($fi, LOCK_SH);
if ($fo = @fopen($outname, 'wb')) {
flock($fo, LOCK_EX);
// если все файлы удалось открыть, то вызываем функцию защиты
$ret = call_user_func('ProtectPicture'.$enabled[$type], $fi, $fo, filesize($inname));
fclose($fo);
fclose($fi);
if (!$ret) {
unlink($outname);
}
return $ret;
} else {
fclose($fi);
}
}
}
}
return false;
}
/* Private functions */
function ProtectPictureJpeg($fi, $fo, $controllen)
{
// заголовок JPEG
if (!@fwrite($fo, fread($fi, 2))) return false;
$protect = '<?php __halt_compiler();';
$length = pack('n', strlen($protect) + 2);
// дописываю секцию COM (comment)
if (!@fwrite($fo, "\xFF\xFE$length$protect")) return false;
// остаток файла
if (@stream_copy_to_stream($fi, $fo) != $controllen - 2) return false;
return true;
}
function ProtectPicturePng($fi, $fo, $controllen)
{
// заголовок PNG
if (!@fwrite($fo, fread($fi, 8))) return false;
// пропускаем кусок IHDR (он должен быть первым))
$len = unpack('N', $lenbin = fread($fi, 4))[1] + 8;
$ihdr = $lenbin . fread($fi, $len);
// Смотрим, не написал ли в этот кусок хакер свой код
if (($pos = strpos($ihdr, '<?')) !== false) {
// в стандарте перечислены поля, которые есть в этом куске — там должно быть 13 байт (+4 — длина)
if ($len != 17) return false;
// эта комбинация может встретиться только в первых восьми байтах заголовка — дальше поля не
// могут принимать такие большие значения
if ($pos > 11) return false;
// если разрешены короткие теги, нужно присмотреться к этому коду
// в столь ограниченном пространстве вред можно натворить только через запуск
// чего-то внешнего
if (ini_get('short_open_tag') && strpos($ihdr, '`', $pos + 2) !== false) {
return false;
}
}
if (!@fwrite($fo, $ihdr)) return false;
// кусок iTXt (text) с нашим кодом
$protect = "<?php __halt_compiler();";
$length = pack('N', strlen($protect));
$sum = pack('N', sprintf('%u', crc32($protect)));
if (!@fwrite($fo, "{$length}iTXt{$protect}{$sum}")) return false;
// остаток файла
if (@stream_copy_to_stream($fi, $fo) != $controllen - 8 - 4 - $len) return false;
return true;
}
function ProtectPictureGif($fi, $fo, $controllen)
{
$protect = "<?php __halt_compiler();";
// заголовок GIF
if (!@fwrite($fo, fread($fi, 6))) return false;
// поле Logical Screen Descriptor
$lsd = fread($fi, 7);
$flag = ord($lsd[4]);
// указана ли Global Color Table?
if ($flag & 128) {
$pixel = 1 + ($flag & 0b111);
$colors = pow(2, $pixel);
$gctlen = $colors * 3;
// читаем Global Color Table
$gct = fread($fi, $gctlen);
// не вставил ли уже туда злоумышленник „<?“?
// меняем у такой комбинации последний бит — это малозаметно,
// безопасность важнее!
if (strpos($gct, '<?') !== false) {
$gct = str_replace('<', '=', $gct);
}
if (!@fwrite($fo, $lsd . $gct)) return false;
} else {
$gctlen = 0;
if (!@fwrite($fo, $lsd)) return false;
}
// пишем комментарий с куском защиты
fwrite($fo, "!\xfe" . chr(strlen($protect)) . $protect . "\0");
if (@stream_copy_to_stream($fi, $fo) != $controllen - 6 - 7 - $gctlen) return false;
return true;
}В коде есть два места где я ищу не встретилась ли комбинация <? раньше, чем я вставляю защиту — это в заголовочном куске (IHDR) в ПНГ и в глобальной таблице цветов у ГИФа.
В случае ПНГ я анализирую заголовок, чтобы понять как там оказалась эта комбинация, она валидна только в первых восьми байтах (на зло остаётся всего 6 байт), кроме того, в этом случае я контролирую размер заголовка, в стандарте там указано 13 байт, хотя, кажется, размер не ограничивается. В общем, в шести байтах особо не разгуляешься, всё что я придумал туда засунуть — `rm *`, т. е. очистка текущего каталога. На этот случай, я контролирую появление символа `. На мой взгляд, вероятность встретить в нормальном ПНГ в заголовке <? после которого идёт ` ничтожно мала.
С ГИФом проще — меняю младший бит у цвета, это вряд ли будет заметно, а безопасность важнее.
Льняная свадьба
Думаю, зря мы забросили традицию отмечать годовщину свадьбы фотосессией, это интереснее, чем просто сходить в ресторан, как мы сделали в прошлом году. Льняную свадьбу (четыре года) мы отметили в стиле начала двадцатого века — революция ещё не случилась, но многие замечательные вещи — электрическое освещение, телефон уже вошли в нашу жизнь.
Съёмки проходили в
За фотографии традиционное спасибище великолепной Маше Зверевой, остальные фотографии можно посмотреть у неё в ЖЖ (из тех, что она сама выбрала).
Также спасибо за трость (видели бы вы как я её красил из балончика с краской) Индире Файзрахмановой, за часы Елене Козаковой и Мире Абраровой, Лилии Ясиной за букет и Инне Каражаковой за причёску жены.
Жена, ты такая молодец, что смогла это всё организовать!