Оригинальное изображение


Как-то мне пришла идея, что было бы неплохо иметь способ отправлять секретные сообщения замаскированные как обычные изображения. Результат я назвал Jailbird.


Однажды ты обнаружил себя запертым в камере и тебе понадобилось отправить на волю информацию, чтобы провернуть одно дельце, да так, чтобы охрана ничего не заметила? Чтож, ты нашел подходящее решение!

Ладно-ладно, я шучу, это просто эксперимент.


Сегодня я хотел бы показать вам, как можно сохранить "Гамлета" Шекспира в изображении практически незаметно. (Хе-хе, я думаю, у многих возникала проблема контрабанды Гамлета куда-либо...)


Используй исходники, Люк


Исходный код доступен на Github: https://github.com/ClanCatsStation/Jailbird


Размер


Чтобы начать мы должны знать, как много места нам понадобится. (Размер имеет значение ;)).


Я взял "Гамлета" и поместил его в файл hamlet.txt. Потом создал php скрипт, который назвал size.php.


Чтобы убрать из вызова скрипта php в командной строке установим интерпретатор для запуска:


#!/usr/bin/env php
<?php  

Чтобы получить данные, читаем контент из STDIN:


echo strlen(stream_get_contents(STDIN)); 

Теперь запустим команду:


$ cat hamlet.txt | ./size.php

Получим длину строки / количество байт равное 175132. Попробуем заархивировать:


echo strlen(gzcompress(stream_get_contents(STDIN), 9));  

И получим: 70681 байт.


Jailbird может хранить 1 бит на цвет на пиксель.


Итого 565'448 бит, что означает, что нам нужно 188'483 пикселей. Или изображение размером по-крайней мере 435x435 пикселей.


Добавим эти расчеты в скрипт size.php, что позволит легко узнать, какого размера изображение нам понадобится.


#!/usr/bin/env php
<?php

$neededBits = (strlen(gzcompress(stream_get_contents(STDIN), 9)) + 16) * 8;
$neededPixels = ceil($neededBits / 3);
$neededSize = ceil(sqrt($neededPixels)); 

echo sprintf("bits: %s pixels: %s min-size: %sx%s \n", $neededBits, $neededPixels, $neededSize, $neededSize);  

Почему я добавил 16 байт к длине контента? Нам нужно определять момент окончания данных по наличию какой-то последовательности знаков, я решил, что это будет строка @endOfJailbird;, которая и содержит 16 символов.


Внедрение данных


Собственно то, для чего мы здесь собрались. (А то я что-то разошелся)


Подготовка бинарной строки


Самый простой способ внедрить данные в изображение, подумал я, это сконвертировать их в бинарную строку.


#!/usr/bin/env php
<?php

$content = gzcompress(stream_get_contents(STDIN), 9) . '@endOfJailbird; ';
$data = '';

for($i=0; $i<strlen($content); $i++)  
{
    $data .= sprintf( "%08d", decbin(ord($content[$i])));
}

Функция ord возвращает байт в его ASCII представление.


Затем при помощи функции decbin конвертируем полученное число в бинарное и обернем в sprintf для сохранения ведущих нулей.


На выходе получим довольно большую строку из нулей и единиц


string(565448) "01111000110110101010110011111101110010011001001011100011010110001101001000101100000011001010111001111111001111000000010101111101110100111101110011010000111111010000000111101000000010111001011111001000001100011111110011011110110010001000100010111100000110010101000110010101010100101111110101001001001011010100000000000010001001001001000100001110000000100010110000001100110011100110010000111101011111011001101110101010100110100001110110000000011101001100111111111010101111101101101111011101001000100101010100011001"...  

Запись бит


Тут нам понадобится изображение, в которое мы будем записывать. Передадим путь к изображению первым аргументом в скрипт inject.php.


// нулевой аргумент не нужен
array_shift($argv);

// путь к изображению
$imagePath = array_shift($argv);

Проверим, имеем ли мы доступ к изображению:


if ((!file_exists($imagePath)) || (!is_readable($imagePath)))  
{
    die("The given image does not exist or is not readable.\n");
}

Команда записи будет иметь вид:


$ cat hamlet.txt | ./inject.php cats.png

Изображение cats.png возьмем из (оригинального, прим. пер.) КДПВ:


image


Теперь запустим цикл:


// загрузим изображение с помощью GD
$image = imagecreatefrompng($imagePath);
$imageWidth = imagesx($image);
$imageHeight = imagesy($image);

// нужно отслеживать, какие данные мы пишем
// поэтому создадим индексную переменную
$dataIndex = 0;

// и начнем итерировать по оси Y
for ($iy = 0; $iy < $imageHeight; $iy++)  
{
    // и Х
    for ($ix = 0; $ix < $imageWidth; $ix++)
    {
        $rgb = imagecolorat($image, $ix, $iy);

        // разобьем rgb на массив цветов
        $rgb = [($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF];

        // затем для каждого цвета
        for($ic = 0; $ic < 3; $ic++)
        {
            // проверим, есть ли еще данные
            if (!isset($data[$dataIndex]))
            {
                break 2;    
            }

            $color = $rgb[$ic];
            $bit = $data[$dataIndex];

            // вся магия здесь
            // объяснение ниже
        }

        imagesetpixel($image, $ix, $iy, imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]));
    }
}

Этот код просто проходит последовательно по цветам пикселей и сохраняет изменение цветов обратно в пиксель.


Что?


А вот самая важная штука:


$negative = ($color % 2 == 0);

Это короткая строка кода говорит нам о текущем цвете текущего пикселя четный он или нечентный.


А $bit = $data[$dataIndex]; сообщает нам, четным или нечетным должно быть текущее значение цвета.


Таким образом мы создаем еще один слой данных поверх изображения просто округляя значения цвета до четного или нечетного числа.


Теперь, все что нам нужно сделать, это обновить значения цветов:


// should it be positive
if ($bit == '1')  
{
    // should be positive but is negative
    if ($negative)
    {
        if ($color < 255) {
            $color++;
        } else {
            $color--;
        }
    }
}
// should be negative
else  
{
    // should be negative but is positive
    if (!$negative)
    {
        if ($color < 255) {
            $color++;
        } else {
            $color--;
        }
    }
}

 // set the new color
$rgb[$ic] = $color;

// update the index
$dataIndex++;   

И собственно все! Осталось только сохранить изображение:


imagepng($image, dirname($imagePath) . '/jailbirded_' . basename($imagePath), 0);  

Извлечение данных


Обратный процесс — извлечение данных достаточно просто, после того, как мы разобрались с внедрением.


Создаем новый файл extract.php.


Скрипт извлечения данных также будет получать путь к изображению как аргумент.


#!/usr/bin/env php
<?php

// we dont need the first argument
array_shift($argv);

// get image by argument
$imagePath = array_shift($argv);

if ((!file_exists($imagePath)) || (!is_readable($imagePath)))  
{
    die("The given image does not exist or is not readable.\n");
}

Затем у нас почти такое же итерирование с тем отличием, что нам не надо ничего модифицировать, только проверяем, четное значение или нет.


// load the image with GD
$image = imagecreatefrompng($imagePath);
$imageWidth = imagesx($image);
$imageHeight = imagesy($image);

// create an empty string where our data will end up 
$data = '';

// and start iterating y
for ($iy = 0; $iy < $imageHeight; $iy++)  
{
    // and x
    for ($ix = 0; $ix < $imageWidth; $ix++)
    {
        $rgb = imagecolorat($image, $ix, $iy);

        // split rgb to an array
        $rgb = [($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF];

        // and for every color
        for($ic = 0; $ic < 3; $ic++)
        {
            $color = $rgb[$ic];

            // what is the current pixel
            if ($color % 2 == 0)
            {
                $data .= '0';
            }
            else
            {
                $data .= '1';
            }
        }
    }
}

В переменной $data содержатся сырые данные, которые мы должны сконвертировать в байты:


$content = '';

foreach(str_split($data, 8) as $char)  
{
    $content .= chr(bindec($char));
}

Полученный контент заархивирован, и заказнчивается значением endOfJailbird. Мы можем отбросить все, что идет после него:


// does the jailbird end of line exist?
if (strpos($content, '@endOfJailbird;') === false)  
{
    die('Image does not contain any jailbird data.');
}

// cut the compressed data out,
// decompress it and print it.
echo gzuncompress(substr($content, 0, strpos($content, '@endOfJailbird;')));  

Как результат мы можем запустить скрипт:


$ ./extract.php jailbirded_cats.png 

И получить прекрасную Шекспировскую пьесу обратно.


Заключение


Если поместить 2 изображения рядом, вероятно, разница не будет, хотя вы можете заметить небольшое размытие:


image


Но есть еще пара моментов:


Дело в том, что мы сохраняем изображение без сжатия, иначе это приведет к потере данных. В то время как исходный файл весит 307 KB, после сохранения данных получаем 759 KB. Но я, к сожалению, не вижу вариантов решения этой проблемы.


Также, при отправке изображения в, например, facebook, reddit или twitter изображение будет сжато на серверах этих сервисом и данные, скорее всего, будут потеряны.




В конечном итоге это был довольно веселый эксперимент, я хорошо провел эти несколько часов над ним. Надеюсь вы тоже найдете эту идею интересной и что вы не потратили это время зря. (В ином случае: хе-хе, я украл твое время!)


Never Gonna Give You Up

Поделиться с друзьями
-->

Комментарии (24)


  1. oxidmod
    02.08.2016 11:55
    +16

    поздравляю, вы изобрели стеганографию


    1. PopeyetheSailor
      02.08.2016 11:59

      «Стеганографию» в тегах я отметил, в оригинале автор этот термин не использовал, так возможно, он для себя её и открыл, да.


      1. oxidmod
        02.08.2016 12:35
        +8

        извините, не увидел плашки перевод.
        значит автор открыл для себя стеганографию)


  1. ov7a
    02.08.2016 12:16

    Это чем-нибудь отличается от LSB?


    1. PopeyetheSailor
      02.08.2016 12:24

      Судя по описанию в LSB в wiki, это она и есть, младший бит цвета используется для хранения информации.


    1. Labunsky
      02.08.2016 14:17

      LSB — это больше способ хранения информации в контейнере, алгоритмы же могут быть совсем разные, в том числе и такой простой


  1. Dreyk
    02.08.2016 12:51
    +2

    это же настоящая КОТрабанда!


  1. OLS
    02.08.2016 13:22

    Не робастная стеганография, к сожалению, не имеет практического применения.


    1. PavelMSTU
      02.08.2016 13:26

      Распространенное заблуждение.


  1. nikosias
    02.08.2016 13:24

    После сжатия было бы не плохо еще и шифрануть.


  1. PavelMSTU
    02.08.2016 13:24
    +2

    Как уже писали ранее — это просто LSB.

    Дело в том, что мы сохраняем изображение без сжатия, иначе это приведет к потере данных. В то время как исходный файл весит 307 KB, после сохранения данных получаем 759 KB. Но я, к сожалению, не вижу вариантов решения этой проблемы.
    Еще бы, png использует сжатие, а тут энтропия увеличилась.
    Кстати, есть «атака методом сжатия» — не очень эффективное решение, но если данных внесено много, это позволяет обнаружить стеганографию (и не обязательно LSB)


  1. dimview
    02.08.2016 15:10

    А если таким же образом запихать нужную информацию в младшие биты коэффициентов DCT в JPEG, то размер файла не поменяется и выглядеть результат будет как сильнее сжатый JPEG.


    1. PopeyetheSailor
      02.08.2016 15:17
      +2

      А что если...
      image


  1. boston
    02.08.2016 15:44
    +1

    В первом случае, пожалуй, стоит использовать is_writable, вместо is_readable, в файл же будут записываться данные.


    1. PopeyetheSailor
      02.08.2016 15:55

      Существующий файл только для чтения, для записи результата создается новый png файл:


      И собственно все! Осталось только сохранить изображение:
      imagepng($image, dirname($imagePath) . '/jailbirded_' . basename($imagePath), 0); 


      1. boston
        02.08.2016 15:59

        И то верно, не учел что файл новый создаётся.
        Тогда можно эту проверку на каталог изображения перенести.


  1. RayRom
    02.08.2016 17:05
    +1

    Мда, новое это хорошо забытое старое, еще 20 лет назад мы так в BMP-шки прятали подписи и т.п…


    1. PopeyetheSailor
      02.08.2016 17:09

      Не все такие олдфаги


    1. AndrewTishkin
      11.08.2016 00:17

      Хех, я помню как лет 10 назад, когда ещё небо не было в облаках, а файлопомойками пользоваться не хотелось по ряду причин, так сохранялись файлы на провайдерских или популярных фото-сервисах. Ну и ещё через отсылку файлов себе на e-mail, если не требовалось расшарить файл для незнакомых людей (которым опасно доверять пароль от такого ценного мыльного сейфа)


  1. d_olex
    02.08.2016 18:33
    +1

    Как-то мне пришла идея, что было бы неплохо иметь способ отправлять секретные сообщения замаскированные как обычные изображения

    Ви таки не поверите, но сотни других людей придумали, сделали и описали это задолго до вас — примерно в то же время, когда графические форматы вообще появились как таковые.


    1. PopeyetheSailor
      02.08.2016 18:48
      -1

      Ой-вей


    1. gaki
      03.08.2016 06:32
      -1

      Но не на пхп же!!!


  1. galaxy
    02.08.2016 18:42
    +2

    // should it be positive
    if ($bit == '1')  
    {
        // should be positive but is negative
        if ($negative)
        {
            if ($color < 255) {
                $color++;
    .....
    

    Какой бессмысленный и беспощадный код. С ушами бы хватило:
    $color = $bit ? $color | 0x1 : $color & 0xFE;
    


    Дополнительную ясность коду также добавляет попеременное обозначение одного и того же то понятиями «odd» и «even», то «positive» и «negative»


  1. Deosis
    03.08.2016 07:35
    +1

    Какая-то сферическая стеганография: мы увеличили файл в несколько раз и попросили его не сжимать.
    Png — это контейнер с потоками данных, можно просто вписать в отдельный поток, и стандартными средствами разницы вообще не заметить.