Это сайт — моя персональная записная книжка. Интересна мне, по большей части, история, своя жизнь и немного программирование.

Код для защиты 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 *`, т. е. очистка текущего каталога. На этот случай, я контролирую появление символа `. На мой взгляд, вероятность встретить в нормальном ПНГ в заголовке <? после которого идёт ` ничтожно мала.

С ГИФом проще — меняю младший бит у цвета, это вряд ли будет заметно, а безопасность важнее.

10 комментариев
(ninjacolumbo.ya.ru) 2012

Женя, fclose, начиная с версии 5.3.2, больше не снимает блокировки.

Евгений Степанищев (bolknote.ru) 2012

Комментарий для ninjacolumbo.ya.ru:

Странно, но у меня этот код без проблем выполняется (PHP 5.4.5):

$fp = fopen(’a’, ’w’);
flock($fp, LOCK_EX);
fclose($fp);
$f = fopen(’a’, ’w’);
flock($f, LOCK_EX);

ninjacolumbo (ninjacolumbo.ya.ru) 2012

Комментарий для Евгения Степанищева:

Действительно странно %)

Евгений Степанищев (bolknote.ru) 2012

Комментарий для ninjacolumbo.ya.ru:

Это же ты, наверное, в комментарии вычитал, да? Наверное соврали или баг был.

ninjacolumbo (ninjacolumbo.ya.ru) 2012

Комментарий для Евгения Степанищева:

Ну я вообще всегда явно снимал блокировки, а тут полез почитать про fclose и увидел такое %)

Artjom Kurapov (kurapov.name) 2012

Спасибо. Надеюсь права/лицензия на использования открытые :)

Евгений Степанищев (bolknote.ru) 2012

Комментарий для kurapov.name:

Открытые :)

Eblinkoff 2014

Извините за глупый вопрос, а не перегрузит ли такой метод защиты сервер при загрузке большого количества картинок одновременно?

Eblinkoff 2014

И вот ещё мне приходит на ум (простите, если глупо), а что если в картинку негодяй вставит код не на php, а на каком-либо другом серверном языке. Тогда Ваша защита к сожалению окажется бесполезной?

Евгений Степанищев (bolknote.ru) 2014

Комментарий для Eblinkoff:

Извините за глупый вопрос, а не перегрузит ли такой метод защиты сервер при загрузке большого количества картинок одновременно?

Каким образом?

И вот ещё мне приходит на ум (простите, если глупо), а что если в картинку негодяй вставит код не на php, а на каком-либо другом серверном языке. Тогда Ваша защита к сожалению окажется бесполезной?

А у вас сервер выполняет код на любом серверном языке или настроен, всё-таки, на какой-то один?