РУССКИЙ
РУССКИЙ
ENGLISH
01.02.2019

Скрывать не скрывая

Еще раз о LSB-стеганографии, хи-квадрате и… сингулярности?
Сегодня снова поворошим старое гнездо и поговорим о том, как скрыть кучку бит в картинке с котиком, посмотрим на несколько доступных инструментов и разберем самые популярные атаки. И казалось бы, при чем тут сингулярность?
Стеганография (дословно с греческого «тайнопись») — наука передачи скрываемых данных (стегосообщения) в других открытых данных (стегоконтейнеров) при сокрытии самого факта передачи данных. Не пугайтесь, на самом деле все не так сложно.

Итак, в каком месте изображения можно спрятать сообщение так, чтобы никто не заметил?

А мест всего два: метаданные и само изображение. Последнее совсем простое, достаточно набрать в гугле «exif». Так что начнем, пожалуй, сразу со второго.
Least Significant Bit
Наиболее популярной цветовой моделью является RGB, где цвет представляется в виде трех составляющих: красного, зеленого и голубого. Каждая компонента кодируется в классическом варианте с помощью 8 бит, то есть может принимать значение от 0
до 255. Именно здесь и прячется наименее значащий бит. Важно понять, что на один RGB-цвет приходится приходится аж три таких бита.

Чтобы представить их более наглядно, проделаем пару небольших манипуляций.

Как и было обещано, возьмем картинку с котиком в png формате.
Разобьем её на три канала и в каждом канале возьмем наименее значащий бит. Создадим три новых изображения, где каждый пиксель обозначает НЗБ. Ноль — пиксель белый, единица соответственно черный.

Получаем вот что.
Но, как правило, изображение встречается в «собранном виде». Чтобы представить НЗБ трех компонент в одном изображении, достаточно компоненту в пикселе, где НЗБ равен единице, заменить на 255, и в обратном случае заменить на 0. Тогда получается вот это:
Но не менее значимый
Представим, что все, что мы видели на последней картинке, это наше и мы в праве делать с этим все, что угодно. Тогда возьмем это как поток битов, откуда мы можем читать и куда мы можем записывать.

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

Для извлечения этих данных прочитаем НЗБ как битовый поток и приведем к нужному виду. Чтобы узнать, сколько битов нужно считать, как правило, в начало записывают размер сообщения. Но это уже детали реализации.

Нужно отметить, что примерно в 50% случаев бит, который мы хотим записать, и бит в картинке будут совпадать и изменять нам ничего не придется.

Вот и все, на этом метод заканчивается.
Почему это работает?
Посмотрите на изображения ниже.

Это незаполненный стегоконтейнер:
А это заполненный на 95%:
Видите разницу? А она есть. Почему так?

Посмотрим на два цвета: (0, 0, 0) и (1, 1, 1), то есть на цвета, различные только НЗБ в каждой компоненте.
Небольшие различия в пикселях при первом, втором и третьем взгляде, заметны не будут. Дело в том, что наш глаз может различить около 10 миллионов цветов, а мозг всего лишь около 150. Модель RGB же содержит 16 777 216 цветов. Попробовать различить их все можно здесь.
Из командной строки
Существует не так много работающих command line тулз в открытом доступе, представляющих LSB-стеганографию.

Самые популярные можно найти в таблице внизу.
А где котик?
И первой в списке атак на LSB-стеганографию выступает визуальная атака. Звучит странно, не правда ли? Ведь котик с секретом никак не выдавал себя как заполненный стегоконтейнер на первый взгляд. Хммм… Нужно всего лишь знать, куда смотреть. Несложно догадаться, что нашего пристального внимания заслуживают только НЗБ.

У заполненого стегоконтейнера изображение с НЗБ выглядит так:
Не верите? Вот вам НЗБ со всех трех каналов отдельно:
Красный канал
Зеленый канал
Синий канал
Это специфичный для сокрытия сообщения «рисунок» в НЗБ. На первый взгляд это кажется простым шумом. Но при рассмотрении проглядывается структура. Здесь видно, что стегоконтейнер заполнен под завязку. Если бы мы взяли сообщение в 30% от вместимости бедного котика, то получили такую бы картину:
Его НЗБ:
~70% котика осталось неизменным.

Тут стоит сделать небольшое отступление и поговорить о размерах. Что такое 30% котика? Размер котика 603х433 пикселей. 30% от этого размера равны 78459 пикселям. В каждый пиксель помещается 3 бита информации. Итого 78459 3 = 235377 бит или чуть меньше, чем 30 килобайт помещается в 30% котика. А в целого котика поместится около 100 килобайт. Такие дела.

Но мы же здесь вами не просто так. Как же все-таки обмануть глаза?
Первая мысль: засунуть сообщение в шум. Но не тут то было. Далее фрагмент заполненного стегоконтейнера и его LSB.
Прикладывая небольшое усилие, мы все-таки можем различить знакомую структуру. Не теряем надежды, господа!
Хи-хи-хи
Много вещей ломается статистикой, знаете ли.

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

Старый добрый хи-квадрат для этого начали использовать Андреас Весфилд и Андреас Пфитцманн из Университета Дрездена в своей работе «Attacks on Steganographic Systems», которую можно найти здесь.

Здесь и далее будем говорить об атаках в рамках одной цветовой плоскости, или в контексте RGB об атаках на один канал. Результаты каждой атаки можно привести к среднему и получить результат для «собранного» изображения.

Итак, атака «Хи-квадрат» основывается на том предположении, что вероятность одновременного появления соседних (отличных на наименее значащий бит) цветов (pair of values) в незаполненном стегоконтейнере крайне мала. Это действительно так, можешь поверить. Если говорить другими словами, то количество пикселей двух соседних цветов существенно отличается для пустого контейнера. Все, что нам нужно сделать, это посчитать количество пикселей каждого цвета и применить пару формул. На самом деле, это простая задачка на проверку гипотезы с использованием критерия хи-квадрат.

Немного математики?

Пусть h — массив, на i-ом месте содержащий количество пикселей i-ого цвета в исследуемом изображении.

Тогда:
Переводя это на Python получится что-то вроде этого:

for k in range(0, len(histogram) // 2): expected.append(((histogram[2 * k] + histogram[2 * k + 1]) / 2)) observed.append(histogram[2 * k])

Где histogram — количество пикселей цвета i в изображении, i ∈ [ 0 , 255 ]" role="presentation" style="display: inline-block; line-height: normal; overflow-wrap: normal; white-space: nowrap; float: none; direction: ltr; max-width: none; max-height: none; min-width: 0px; min-height: 0px; color: rgb(34, 34, 34); font-family: -apple-system, system-ui, Arial, sans-serif; background-color: rgb(255, 255, 255); position: relative;"> i ∈ [ 0 , 255 ]

Хи-квадрат критерий для количества степеней свободы k-1 рассчитывается следующим образом (k — количество различных цветов, то есть 256):




χ k − 1 2 = ∑ i = 1 k ( n k − n k ∗ ) 2 n k ∗ ;" role="presentation" style="display: inline-block; line-height: normal; text-align: left; word-spacing: normal; overflow-wrap: normal; white-space: nowrap; float: none; direction: ltr; max-width: none; max-height: none; min-width: 0px; min-height: 0px; position: relative;"> χ k − 1 2 = ∑ i = 1 k ( n k − n k ∗ ) 2 n k ∗ ;


И, наконец, P — это вероятность того, что распределения n i" role="presentation" style="display: inline-block; line-height: normal; overflow-wrap: normal; white-space: nowrap; float: none; direction: ltr; max-width: none; max-height: none; min-width: 0px; min-height: 0px; color: rgb(34, 34, 34); font-family: -apple-system, system-ui, Arial, sans-serif; background-color: rgb(255, 255, 255); position: relative;"> n i и n i ∗" role="presentation" style="display: inline-block; line-height: normal; overflow-wrap: normal; white-space: nowrap; float: none; direction: ltr; max-width: none; max-height: none; min-width: 0px; min-height: 0px; color: rgb(34, 34, 34); font-family: -apple-system, system-ui, Arial, sans-serif; background-color: rgb(255, 255, 255); position: relative;"> n i ∗ при этих условиях равны (вероятность того, что перед нами заполненый стегоконтейнер). Она рассчитывается при помощи интегрирования функции гладкости:




P = 1 − 1 2 k − 1 2 Γ ( k − 1 2 ) ∫ 0 χ k − 1 2 e − x 2 x k − 1 2 − 1 d x ;" role="presentation" style="display: inline-block; line-height: normal; text-align: left; word-spacing: normal; overflow-wrap: normal; white-space: nowrap; float: none; direction: ltr; max-width: none; max-height: none; min-width: 0px; min-height: 0px; position: relative;"> P = 1 − 1 2 k − 1 2 Γ ( k − 1 2 ) ∫ 0 χ k − 1 2 e − x 2 x k − 1 2 − 1 d x ;


Эффективнее всего применять хи-квадрат не ко всему изображению, а только к его частям, например, к строкам. Если посчитанная вероятность для строки больше 0.5, то строку в оригинальном изображении закрасим красным. Если меньше, то зеленым. Для котика с 30% заполненностью, картина будет выглядеть следующим образом: