Недавно наткнулся на хабре на пару постов о сокрытии данных в BMP-файлах (один и два) и решил поделиться своим опытом в этой области. В этот раз будем прятать цветную картинку, а в качестве контейнеров будем использовать еще три изображения.
BMP-файл состоит из заголовка — структуры размером в несколько десятков байт, содержащей основные параметры изображения (размеры, глубину цвета и т.д.), палитры — массива, описывающего цвета, используемые в изображении, и области данных, содержащей последовательность кодов пикселов изображения. Палитра не является обязательной — в большинстве BMP-файлах палитра отсутствует, а область данных содержит описание всех цветовых компонент каждого пикселя изображения в формате RGB (то есть количественные значения красного, зеленого и синего цветов).
В качестве входных изображений используются 24-разрядные bitmap-рисунки, в которых на каждый цвет приходится по 8 бит информации. Прятать данные будем по методу LSB.
Суть алгоритма заключается в том, что секретное изображение разбивается на три цветовых примитива (то есть на оттенки красного, зеленого и синего), а затем каждый примитив записывается в младшие биты одного из изображений-контейнеров. Таким образом после зашифровки каждый контейнер будет содержать в себе одну цветовую составляющую секретного изображения.
Чтобы определить, в какое из изображений прятать R-составляющую, в какое — G, а в какое — B, перед запуском алгоритма определяется количественное значение каждого из трех оттенков в каждом изображении-контейнере. Далее контейнеры выбираются таким образом, чтобы разница в цветах контейнера и цветового примитива секретного изображения была минимальна.
Далее из каждого цветового примитива берется два старших бита и записывается в младшие биты соответствующего цвета у соответствующего контейнера. Два младших бита в двух оставшихся цветах обнуляются. Операция повторяется для каждого пикселя.
Эта функция выполняется для каждого цвета каждого пикселя секретного изображения.
В нашем случае m_secretBits = 2.
Секретное изображение:
Набор исходных изображений для зашифровки:
Те же изображения, но уже с зашифрованным в них секретным изображением:
Как видно на изображениях, визуально искажения не заметны. Прошу прощения, что изображения маленькие — пожалел трафик. На больших изображениях искажения также не заметны — уж поверьте.
Следующей шаг — восстановить спрятанное изображение. Для этого возьмем первый пиксель из каждого изображения-контейнера. Два младших бита каждого цвета в этих пикселях сделаем старшими битами и сложим соответствующие цветовые составляющие (так как во время зашифровки младшие биты не шифруемого цвета обнулялись, то ненулевое значение будет иметь только одна цветовая составляющая в каждом контейнере). Таким образом мы восстановим цвет соответствующего пикселя секретного изображения (с некоторой погрешностью). Далее повторим эту операция для всех пикселей и получим восстановленное секретное изображение.
Эта функция выполняется для каждой тройки соответствующих пикселей изображений-контейнеров.
Секретное изображение после восстановления:
А вот что получится, если хотя бы одно из изображений не будет содержать нужных зашифрованных данных:
Тот же результат будет получен, если перед восстановлением поиграть с масштабированием изображений.
А вот если пересохранить изображения в JPEG со средним качеством, а потом снова в BMP, то дополнительных искажений это практически не вызовет:
Чтобы восстановить изображение, закодированное описанным методом, необходимо использовать все три изображения-контейнера, полученных в результате зашифровки. В противном случае вместо восстановленного изображения мы получим лишь цветовой шум.
Очевидный минус метода — искажение цветов секретного изображения в ходе зашифровки (ведь используются лишь два старших бита из восьми). Нам, конечно, никто не мешает увеличить количество шифруемых бит, но это уже скажется на качестве изображений-контейнеров.
Пару слов о BMP
BMP-файл состоит из заголовка — структуры размером в несколько десятков байт, содержащей основные параметры изображения (размеры, глубину цвета и т.д.), палитры — массива, описывающего цвета, используемые в изображении, и области данных, содержащей последовательность кодов пикселов изображения. Палитра не является обязательной — в большинстве BMP-файлах палитра отсутствует, а область данных содержит описание всех цветовых компонент каждого пикселя изображения в формате RGB (то есть количественные значения красного, зеленого и синего цветов).
Описание метода
В качестве входных изображений используются 24-разрядные bitmap-рисунки, в которых на каждый цвет приходится по 8 бит информации. Прятать данные будем по методу LSB.
Суть алгоритма заключается в том, что секретное изображение разбивается на три цветовых примитива (то есть на оттенки красного, зеленого и синего), а затем каждый примитив записывается в младшие биты одного из изображений-контейнеров. Таким образом после зашифровки каждый контейнер будет содержать в себе одну цветовую составляющую секретного изображения.
Зашифровка изображения
Чтобы определить, в какое из изображений прятать R-составляющую, в какое — G, а в какое — B, перед запуском алгоритма определяется количественное значение каждого из трех оттенков в каждом изображении-контейнере. Далее контейнеры выбираются таким образом, чтобы разница в цветах контейнера и цветового примитива секретного изображения была минимальна.
Далее из каждого цветового примитива берется два старших бита и записывается в младшие биты соответствующего цвета у соответствующего контейнера. Два младших бита в двух оставшихся цветах обнуляются. Операция повторяется для каждого пикселя.
Код функции, записывающей цветовой примитив в контейнер
private Color CombineColorsRGB(Color secretColor, Color containerColor, HidingColor hidingColor)
{
byte r1 = secretColor.R;
byte g1 = secretColor.G;
byte b1 = secretColor.B;
byte r2 = containerColor.R;
byte g2 = containerColor.G;
byte b2 = containerColor.B;
int secretBits = m_secretBits;
int originalBits = 8 - m_secretBits;
switch (hidingColor)
{
case HidingColor.R:
r1 = (byte)(r1 >> originalBits);
r2 = (byte)((r2 >> secretBits) << secretBits);
r2 = (byte)(r1 | r2);
g2 = (byte)((g2 >> secretBits) << secretBits);
b2 = (byte)((b2 >> secretBits) << secretBits);
break;
case HidingColor.G:
g1 = (byte)(g1 >> originalBits);
g2 = (byte)((g2 >> secretBits) << secretBits);
g2 = (byte)(g1 | g2);
r2 = (byte)((r2 >> secretBits) << secretBits);
b2 = (byte)((b2 >> secretBits) << secretBits);
break;
case HidingColor.B:
b1 = (byte)(b1 >> originalBits);
b2 = (byte)((b2 >> secretBits) << secretBits);
b2 = (byte)(b1 | b2);
r2 = (byte)((r2 >> secretBits) << secretBits);
g2 = (byte)((g2 >> secretBits) << secretBits);
break;
}
Color col = Color.FromArgb(r2, g2, b2);
return col;
}
Эта функция выполняется для каждого цвета каждого пикселя секретного изображения.
В нашем случае m_secretBits = 2.
Секретное изображение:
Набор исходных изображений для зашифровки:
Те же изображения, но уже с зашифрованным в них секретным изображением:
Как видно на изображениях, визуально искажения не заметны. Прошу прощения, что изображения маленькие — пожалел трафик. На больших изображениях искажения также не заметны — уж поверьте.
Восстановление изображения
Следующей шаг — восстановить спрятанное изображение. Для этого возьмем первый пиксель из каждого изображения-контейнера. Два младших бита каждого цвета в этих пикселях сделаем старшими битами и сложим соответствующие цветовые составляющие (так как во время зашифровки младшие биты не шифруемого цвета обнулялись, то ненулевое значение будет иметь только одна цветовая составляющая в каждом контейнере). Таким образом мы восстановим цвет соответствующего пикселя секретного изображения (с некоторой погрешностью). Далее повторим эту операция для всех пикселей и получим восстановленное секретное изображение.
Код функции, восстанавливающей секретное изображение из контейнеров
private Color GetSecretColorRGB(Color color1, Color color2, Color color3)
{
byte r1 = color1.R;
byte g1 = color1.G;
byte b1 = color1.B;
byte r2 = color2.R;
byte g2 = color2.G;
byte b2 = color2.B;
byte r3 = color3.R;
byte g3 = color3.G;
byte b3 = color3.B;
int secretBitsDigit = (int)(Math.Pow(2.0, (double)m_secretBits)) - 1;
int originalBits = 8 - m_secretBits;
byte r = (byte)(((r1 & secretBitsDigit) << originalBits) + ((r2 & secretBitsDigit) << originalBits) + ((r3 & secretBitsDigit) << originalBits));
byte g = (byte)(((g1 & secretBitsDigit) << originalBits) + ((g2 & secretBitsDigit) << originalBits) + ((g3 & secretBitsDigit) << originalBits));
byte b = (byte)(((b1 & secretBitsDigit) << originalBits) + ((b2 & secretBitsDigit) << originalBits) + ((b3 & secretBitsDigit) << originalBits));
Color col = Color.FromArgb(r, g, b);
return col;
}
Эта функция выполняется для каждой тройки соответствующих пикселей изображений-контейнеров.
Секретное изображение после восстановления:
А вот что получится, если хотя бы одно из изображений не будет содержать нужных зашифрованных данных:
Тот же результат будет получен, если перед восстановлением поиграть с масштабированием изображений.
А вот если пересохранить изображения в JPEG со средним качеством, а потом снова в BMP, то дополнительных искажений это практически не вызовет:
Итоги
Чтобы восстановить изображение, закодированное описанным методом, необходимо использовать все три изображения-контейнера, полученных в результате зашифровки. В противном случае вместо восстановленного изображения мы получим лишь цветовой шум.
Очевидный минус метода — искажение цветов секретного изображения в ходе зашифровки (ведь используются лишь два старших бита из восьми). Нам, конечно, никто не мешает увеличить количество шифруемых бит, но это уже скажется на качестве изображений-контейнеров.