Как-то мне пришла идея, что было бы неплохо иметь способ отправлять секретные сообщения замаскированные как обычные изображения. Результат я назвал 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
возьмем из (оригинального, прим. пер.) КДПВ:
Теперь запустим цикл:
// загрузим изображение с помощью 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 изображения рядом, вероятно, разница не будет, хотя вы можете заметить небольшое размытие:
Но есть еще пара моментов:
Дело в том, что мы сохраняем изображение без сжатия, иначе это приведет к потере данных. В то время как исходный файл весит 307 KB, после сохранения данных получаем 759 KB. Но я, к сожалению, не вижу вариантов решения этой проблемы.
Также, при отправке изображения в, например, facebook, reddit или twitter изображение будет сжато на серверах этих сервисом и данные, скорее всего, будут потеряны.
В конечном итоге это был довольно веселый эксперимент, я хорошо провел эти несколько часов над ним. Надеюсь вы тоже найдете эту идею интересной и что вы не потратили это время зря. (В ином случае: хе-хе, я украл твое время!)
Never Gonna Give You Up
Автор: PopeyetheSailor