Как-то мне пришла идея, что было бы неплохо иметь способ отправлять секретные сообщения замаскированные как обычные изображения. Результат я назвал 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
Комментарии (24)
ov7a
02.08.2016 12:16Это чем-нибудь отличается от LSB?
PopeyetheSailor
02.08.2016 12:24Судя по описанию в LSB в wiki, это она и есть, младший бит цвета используется для хранения информации.
Labunsky
02.08.2016 14:17LSB — это больше способ хранения информации в контейнере, алгоритмы же могут быть совсем разные, в том числе и такой простой
PavelMSTU
02.08.2016 13:24+2Как уже писали ранее — это просто LSB.
Дело в том, что мы сохраняем изображение без сжатия, иначе это приведет к потере данных. В то время как исходный файл весит 307 KB, после сохранения данных получаем 759 KB. Но я, к сожалению, не вижу вариантов решения этой проблемы.
Еще бы, png использует сжатие, а тут энтропия увеличилась.
Кстати, есть «атака методом сжатия» — не очень эффективное решение, но если данных внесено много, это позволяет обнаружить стеганографию (и не обязательно LSB)
dimview
02.08.2016 15:10А если таким же образом запихать нужную информацию в младшие биты коэффициентов DCT в JPEG, то размер файла не поменяется и выглядеть результат будет как сильнее сжатый JPEG.
PopeyetheSailor
02.08.2016 15:17+2А что если...boston
02.08.2016 15:44+1В первом случае, пожалуй, стоит использовать
is_writable
, вместоis_readable
, в файл же будут записываться данные.PopeyetheSailor
02.08.2016 15:55Существующий файл только для чтения, для записи результата создается новый png файл:
И собственно все! Осталось только сохранить изображение:
imagepng($image, dirname($imagePath) . '/jailbirded_' . basename($imagePath), 0);
boston
02.08.2016 15:59И то верно, не учел что файл новый создаётся.
Тогда можно эту проверку на каталог изображения перенести.
RayRom
02.08.2016 17:05+1Мда, новое это хорошо забытое старое, еще 20 лет назад мы так в BMP-шки прятали подписи и т.п…
AndrewTishkin
11.08.2016 00:17Хех, я помню как лет 10 назад, когда ещё небо не было в облаках, а файлопомойками пользоваться не хотелось по ряду причин, так сохранялись файлы на провайдерских или популярных фото-сервисах. Ну и ещё через отсылку файлов себе на e-mail, если не требовалось расшарить файл для незнакомых людей (которым опасно доверять пароль от такого ценного мыльного сейфа)
d_olex
02.08.2016 18:33+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»
Deosis
03.08.2016 07:35+1Какая-то сферическая стеганография: мы увеличили файл в несколько раз и попросили его не сжимать.
Png — это контейнер с потоками данных, можно просто вписать в отдельный поток, и стандартными средствами разницы вообще не заметить.
oxidmod
поздравляю, вы изобрели стеганографию
PopeyetheSailor
«Стеганографию» в тегах я отметил, в оригинале автор этот термин не использовал, так возможно, он для себя её и открыл, да.
oxidmod
извините, не увидел плашки перевод.
значит автор открыл для себя стеганографию)