Сегодня снова поворошим старое гнездо и поговорим о том, как скрыть кучку бит в картинке с котиком, посмотрим на несколько доступных инструментов и разберем самые популярные атаки. И казалось бы, при чем тут сингулярность?

Как говорится, если хочешь в чем-то разобраться, то напиши об этом статью на Хабр! (Осторожно, много текста и картинок)



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

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

А мест всего два: метаданные и само изображение. Последнее совсем простое, достаточно набрать в гугле «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-стеганографию.

Самые популярные можно найти в таблице внизу.

Тулза Типы файлов Описание Сокрытие Извлечение
openstego PNG Может использоваться
не только для сокрытия
данных, но и для водяных
знаков. Использует
RandomLSB — улучшенный
алгоритм LSB с записью
в Random Least Significant
Bit. Поддерживает шифрование.
Имеет также GUI.
openstego embed
-mf secret.txt -cf
cover.png -p
password -sf
stego.png
openstego extract
-sf openstego.png
-p abcd -xf
output.txt
stegano PNG Работает не только с
классическим LSB. Имеет
гибкую настройку. Может
использоваться как
модуль Python. Самая
привлекательная
(как по мне).
stegano-lsb
hide --input cover.jpg
-f secret.txt -e
UTF-8 --output
stego.png
stegano-lsb reveal
-i stego.png -e UTF-8
-o output.txt
cloackedpixel PNG, JPG Простенькая тулза.
Плохо справляется с
большим сообщением.
(Точнее вообще никак)
Поддерживает шифрование.
cloackedpixel hide
cover.jpg secret.txt
password
cloackedpixel extract
cover.jpg-stego.png
output.txt password
LSBSteg PNG, BMP Небольшая программа на
Python c читабельным кодом.
LSBSteg encode
-i cover.png -o
stego.png -f
secret.txt
LSBSteg decode
-i stego.png -o
output.txt

А где котик?


И первой в списке атак на 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-ого цвета в исследуемом изображении.

Тогда:

  1. Измеренная частота появления цвета $i = 2k$:

    $n_k = h[2k], ~k \in [0, 127];$


  2. Теоретически ожидаемая частота появления цвета $i = 2k$:

    $n_k^*= \frac{h[2k] + h[2k + 1]}{2}, ~k \in [0,127];$




UPD: Небольшое пояснение к формулам выше
У многих возникнет вопрос: почему мы берем такой индекс? Почему именно 2k?
Нужно держать в голове, что мы работаем с соседними цветами, то есть с цветами (числами), различающимися только наименее значащим битом. Они идут парами последовательно:

$[0(00), 1(01)]~[2(10), 3(11)]~и~ т.д. $



Если количество пикселей цвета 2k и 2k+1 будет сильно различаться, то различаться будут измеренная частота и теоретически ожидаемая, что нормально для незаполненного стегоконтейнера.


Переводя это на 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 \in [0, 255]$

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

$\chi_{k-1}^2 = \sum_{i=1}^{k} \frac{(n_k - n_k^*)^2}{n_k^*};$



И, наконец, P — это вероятность того, что распределения $n_i$ и $n_i^*$ при этих условиях равны (вероятность того, что перед нами заполненый стегоконтейнер). Она рассчитывается при помощи интегрирования функции гладкости:

$P = 1 - \frac{1}{2^{\frac{k-1}{2}}\Gamma(\frac{k-1}{2})}\int_0^{\chi_{k-1}^2} e^{-\frac{x}{2}} x^{\frac{k-1}{2}-1} dx;$



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



Весьма точно, неправда ли?

Ну вот мы и обзавелись математически обоснованной атакой, математику-то не обманешь! Или…??

Shuffle Dance


Идея довольно проста: записывать биты не по порядку, а в случайные места. Для этого надо взять ГПСЧ, настроить его на выдачу одного и того же потока случайности при одном и том же сиде (aka пароле). Не зная пароля, мы не сможем настроить ГПСЧ и найти пиксели, в которых спрятано сообщение. Испытаем это на котике.

Котик (32% заполнености):



Его LSB:



Картинка выглядит шумной, но не подозрительной для неопытного аналитика. А что говорит Хи-квадрат?

А что говорит Хи-квадрат?


Кажется, black hat победили!? Как бы не так…

Регулярность-сингулярность


Еще один статистический метод был Джессикой Фридрих, Мирославом Гольяном и Андреасом Пфитцманом в 2001 году. Он был назван как RS-метод. Оригинальную статью можно взять здесь.

Метод содержит несколько подготовительных этапов.

Изображение разделяется на группы из n пикселей. К примеру, 4 последовательных пикселя в строке. Как правило, такие группы содержат рядом стоящие пиксели.
Для нашего котика с последовательным заполнением в красном канале первыми пятью группами будут:

  • [78, 78, 79, 78]
  • [78, 78, 78, 78]
  • [78, 79, 78, 79]
  • [79, 76, 79, 76]
  • [76, 76, 76, 77]

(Все измерения приводятся в классическом варианте RGB)

Затем мы определяем так называемую дискриминант-функцию или функцию гладкости, которая сопоставляет каждой группе пикселей действительно число. Цель это функции состоит в том, чтобы зафиксировать гладкость или «регулярность» группы пикселей G. Чем шумнее группа пикселей $G=(x_1, ... , x_n)$, тем большее значение дискриминант-функция будет иметь. Чаще всего выбирают «вариацию» группы пикселей или, проще говоря, сумму разностей соседних пикселей в группе. Но так же в ней можно учесть статистические предположения об изображении.

$f(x_1, x_2, ..., x_n) = \sum_{i=1}^{n-1} |x_{i+1}- x_i|$



Значения функции гладкости для группы пикселей из нашего примера:

  • f(78, 78, 79, 78) = 2
  • f(78, 78, 78, 78) = 0
  • f(78, 79, 78, 79) = 3
  • f(79, 76, 79, 76) = 9
  • f(76, 76, 76, 77) = 1

Далее определяется класс функций флиппинга от одного пикселя.

Они должны обладать некоторыми свойствами.

$1.~~~\forall x \in P: ~F(F(x)) = x, ~~P=\{0, ~255\};$


$2.~~~F_1: 0\leftrightarrow 1, ~ 2\leftrightarrow 3,~..., 254\leftrightarrow 255;$


$3. ~~~ \forall x \in P: ~ F_{-1}(x) = F_1(x+1)-1;$



Где $F$ — любая функция из одного класса, $F_1$ — прямая функция флиппинга, а $F_{-1}$ — обратная. В дополнение обычно обозначается тождественная функция флиппинга $F_0$, которая не меняет пиксель.

Функции флиппинга на python могут выглядеть примерно вот так:

def flip(val):
    if val & 1:
        return val - 1
    return val + 1


def invert_flip(val):
    if val & 1:
        return val + 1
    return val - 1

def null_flip(val):
    return val

К каждой группе пикселей мы применяем одну из функций флиппинга и на основании значения функции-дескриминанта до и после флиппинга, мы определяем тип группы пикселей: обычный (Regular), единичный/необычный (Singular), и бесполезный непригодный (unusable). Так как последний тип в дальнейшем не используется, метод был назван по первым буквам ключевых типов. Вот и весь секрет названия, сингулярность здесь ни при чем :)



Мы можем захотеть применить разный флиппинг к разным пикселям, для этого определяют маску М с n значениями -1, 0 или 1.

$F_M(G) = (F_{M(1)}(x_1), F_{M(2)}(x_2),..., F_{M(n)}(x_n))$



Пусть маска для нашего примера будет классическая — [1, 0, 0, 1]. Опытным путем было обнаружено, что лучше всего для этого метода подходят симметричные маски, не содержащие $F_{-1}$. Также удачными вариантами будут: [0, 1, 0, 1], [0, 1, 1, 0], [1, 0, 1, 0]. Применим флиппинг для групп из примера, подсчитаем значение гладкости и определим тип группы пикселей:

  • $F_M$(78, 78, 79, 78) = [79, 78, 79, 79];
    f(79, 78, 79, 79) = 2 = 2 = f(78, 78, 79, 78)
    Unusable группа

  • $F_M$(78, 78, 78, 78) = [79, 78, 78, 79];
    f(79, 78, 78, 79) = 2 > 0 = f(78, 78, 78, 78)
    Regular группа

  • $F_M$(78, 79, 78, 79) = [79, 79, 78, 78];
    f(79, 79, 78, 78) = 1 < 3=f(78, 79, 78, 79) Singular группа

  • $F_M$(79, 76, 79, 76) = [78, 76, 79, 77];
    f(78, 76, 79, 77) = 7 < 9=f(79, 76, 79, 76) Singular группа

  • $F_M$(76, 76, 76, 77) = [77, 76, 76, 76];
    f(77, 76, 76, 76) = 1 = 1 = f(76, 76, 76, 77)
    Unusable группа


Обозначим число регулярных групп для маски M как $R_M$ (в процентных долях всех групп), и $S_M$ для сингулярных групп.

Тогда $R_M + S_M \leq 1$ и $R_{-M} + S_{-M} \leq 1$, для отрицательной маски (все компоненты маски умножены на -1), т.к. $R_M + S_M + U_M = 1$, при этом $U_M$ может быть пустой. Аналогично для отрицательной маски.

Основная статистическая гипотеза состоит в том, что в типичном изображении ожидаемое значение $R_M$ равно значению $R_{-M}$, и то же самое верно для $S_M$ и $S_{-M}$. Это доказывают экспериментальные данные и некоторые танцы с бубном вокруг последнего свойства функции флиппинга.

$R_M \cong S_M~~~~R_{-M} \cong S_{-M}$



Проверим это на нашем маленьком примере? Учитывая небольшой размер выборки, мы можем не подтвердить данную гипотезу. Посмотрим, что же будет с инвертированной маской: [-1, 0, 0, -1].

  • F_M(78, 78, 79, 78) = [77, 78, 79, 77];
    f(77, 78, 79, 77) = 4 > 2 = f(77, 78, 79, 77)
    Regular группа

  • F_M(78, 78, 78, 78) = [77, 78, 78, 77];
    f(77, 78, 78, 77) = 2 > 0 = f(78, 78, 78, 78)
    Regular группа

  • F_M(78, 79, 78, 79) = [77, 79, 78, 80];
    f(77, 79, 78, 80) = 5 > 3 = f(78, 79, 78, 79)
    Regular группа

  • F_M(79, 76, 79, 76) = [80, 76, 79, 75];
    f(80, 76, 79, 75) = 11 > 9 = f(79, 76, 79, 76)
    Regular группа

  • F_M(76, 76, 76, 77) = [75, 76, 76, 78];
    f(75, 76, 76, 78) = 3 > 1 = f(76, 76, 76, 77)
    Regular группа


Ну тут все очевидно.

Однако разность между $R_M$ и $S_M$ стремиться к нулю по мере увеличения длины m встроенного сообщения и мы получаем, что $R_M \cong S_M$.

Забавно, что рандомизация плоскости LSB оказывает противоположное влияние на $R_{-M}$ и $S_{-M}$. Их разность увеличивается с длиной m встроенного сообщения. Объяснение этому явлению можно найти в оригинальной статье.

Вот график $R_M$, $S_M$, $R_{-M}$ и $S_{-M}$ в зависимости от количества пикселей с инвертированными LSB, его называют RS-диаграммой. Ось x представляет собой процент пикселей с инвертированными LSB, ось y — относительное число регулярных и сингулярных групп с масками M и -M, $M=[0~1~ 1 ~0]$.



Суть метода RS-стегоанализа заключается в оценке четырех кривых диаграммы RS и вычислении их пересечения с использованием экстраполяции. Предположим, что у нас есть стегоконтейнер с сообщением неизвестной длины p (в процентахот пикселей), встроенное в младшие разряды случайно выбранных пикселей (то есть с использованием RandomLSB). Наши начальные измерения числа групп R и S соответствуют точкам $R_M(p / 2)$, $S_M(p / 2)$, $R_{-M}(p / 2)$ и $S_{-M}(p / 2)$. Мы берем точки именно от половины длины сообщения, так как сообщение является случайным битовым потоком и в среднем, как было сказано ранее, только одна половина пикселей будет изменена посредством внедрения сообщения.

Если мы инвертируем LSB всех пикселей на изображении и вычислим количество R и S групп, мы получим четыре точки $R_M(1-p / 2)$, $S_M(1-p / 2)$, $R_{-M}(1-p / 2)$ и $S_{-M}(1-p / 2)$. Поскольку эти две точки зависят от конкретной рандомизации LSB, мы должны многократно повторять этот процесс и оценивать $R_M(1/2)$ и $ S_M(1/2)$ из статистических выборок.

Мы можем условно провести прямые через точки $R_{-M}(p / 2)$, $R_{-M}(1-p / 2)$ и $S_{-M}(p / 2)$, $S_{-M}(1-p / 2)$

Точки $R_M(p / 2)$, $R_M(1 / 2)$, $R_M(1-p / 2)$ и $S_M(p / 2)$, $S_M(1 / 2)$, $S_M(1-p / 2)$ определяют две параболы. Каждая парабола и соответствующая линия пересекаются слева. Среднее арифметическое x-координат обоих пересечений позволяет оценить неизвестную длину сообщения p. 

Чтобы избежать долгой статистической оценки средних точек RM(1/2) и SM(1/2), можно принять еще пару соображений:

  1. Точка пересечения кривых $R_M$ и $R_{-M}$ имеет ту же координату x, что и точка пересечения для кривых $S_M$ и $S_{-M}$. Это по существу более строгая версия нашей статистической гипотезы. (см. выше)
  2. Кривые RM и SM пересекаются при m = 50%, или $R_M(1/2) = S_M(1/2)$.

Эти два предположения позволяют получить простую формулу для длины секретного сообщения p. После масштабирования оси x, так что p / 2 становится 0, а 1 — p / 2 становится равным 1, x-координата точки пересечения является корнем из следующего квадратного уравнения



Тогда длину сообщения можно вычислить по формуле:

$p = \frac{x}{x-\frac{1}{2}}$



Тут на сцену выходит наш котик. (Не пора ли дать ему имя?)

Итак, у нас имеется:

  • Regular групп RM(p/2): 23121 шт.
  • Singular групп SM(p/2): 14124 шт.
  • Regular групп с инвертированной маской R-M(p/2): 37191 шт.
  • Singular групп с инвертированной маской S-M(p/2): 8440 шт.
  • Regular групп с инвертированными LSB RM(1-p/2): 20298 шт.
  • Singular групп с инвертированными LSB SM(1-p/2): 16206 шт.
  • Regular групп с инвертированными LSB и с инвертированной маской R-M(1-p/2): 40603 шт.
  • Singular групп с инвертированными LSB и с инвертированной маской S-M(1-p/2): 6947 шт.

(Если у вас очень много свободного времени, то можете подсчитать их самостоятельно, а пока предлагаю поверить мои подсчетам)

На повестке дня у нас осталась одна голая математика. Все же помнят, как решать квадратные уравнения?

$d_0 = 8997 $


$d_{-0} =28751$


$d_1 = 4092$


$d_{-1} = 33656$



Подставив все d в формулу выше, получим квадратное уравнение, которое решим, как учили в школе.

$26178x^2-35988x - 19754 = 0$


$D = (-35988)^2 - 426178*(-19754) = 3363616992$


$x_1 = 1.7951~~~~~~~~x_2 = -0.4204$



Возьмем меньший по модулю корень, то есть $х_2$. Тогда примерная оценка для встроенного в котика сообщения будет такая:

$p = \frac{-0.4204}{-0.4204 - 0.5} = 0.4567$



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

Минус же кроется в большой (очень большой) погрешности данного метода, растущей вместе с длинной сообщения при последовательном встраивании. К примеру, для котика с заполненностью в 30% моя реализация метода дает примерную среднюю оценку на три канала в 0.4633 или в 46% от общей вместимости, при заполненности более 95% — 0.8597. Зато для пустого котика аж 0.0054. И это общая тенденция, не зависящая от реализации. Наиболее точные результаты при обычном LSB метод дает при длине встраиваемого сообщение в 10%+-5%.

Плюс-минус



Чтобы не попасться, надо быть неожиданным и использовать ±1 кодирование. Вместо того чтобы изменять наименьший значащий бит в байте цвета, мы будем весь байт либо увеличивать, либо уменьшать на единицу. Есть только два исключения:

  • мы не можем уменьшить ноль, поэтому мы будем его увеличивать,
  • мы также не можем увеличить 255, так что это значение мы всегда будем уменьшать.

Для всех других значений байта мы совершенно случайно выбираем либо увеличение на единицу, либо уменьшение. Поверх данной манипуляции LSB будет изменяться так, как менялся раньше. Для пущей надежности лучше брать случайные байты для записи сообщения.

Вот наш друг котик:



Внешне внедрение незаметно ровно по той же причине, почему не было видны разницы между (0, 0, 0) и (1, 1, 1).



Срез LSB остается просто шумным из-за записи в случайные места.



Хи-квадрат по-прежнему слеп, а RS-метод дает приблизительную оценку 0.0036.

Чтобы не очень сильно радоваться, прочтите вот эту статью.

У самых внимательных может возникнуть вопрос, как же мы можем достать сообщение, если целые байты изменяются случайно, а пароля для настаивания ГПСЧ у нас нет (лучше использовать разные сиды aka состояния генератора aka пароли для работы с RandomLSB и ±1 кодированием). Ответ максимально прост. Мы достаем сообщение так же, как это делали и без ±1 кодирования. Мы вообще можем не знать о его использовании. Повторюсь, данную уловку мы используем только для обхода автоматических средств детектирования. При внедрении/извлечении сообщения мы работаем только с его LSB и ничем более. Однако при детектировании нам необходимо брать во внимание контекст внедрения, то есть все байты изображения, чтобы построить статистические оценки. Именно в этом и заключается весь успех ±1 кодирования.

Вместо заключения


Еще одна весьма неплохая попытка использовать статистику против LSB-стеганографии была предпринята в методе под названием Sample Pairs. Найти его можно здесь. Его присутствие здесь сделало бы статью слишком академичной, поэтому заинтересованным оставляю это для внеклассного чтения. Но предвосхищая вопросы аудитории, отвечу сразу: нет, он не ловит ±1 кодирование.

И конечно машинное обучение. Современные методы на основе ML дают очень хорошие результаты. Об этом можно почитать тут и тут.

По мотивам этой статьи была написана (пока) небольшая тулза. Она может генерировать данные, осуществлять визуальную атаку раздельно по каналам, подсчитывать RS-, SPA-оценку и визуализировать результаты Хи-квадрат. И она не собирается на этом останавливаться.

Подводя черту, хочется дать пару советов:

  • Внедряйте сообщение в случайные байты.
  • Максимально уменьшайте объем внедряемой информации (вспоминаем дядюшку Хэмминга).
  • Используйте ±1 кодирование.
  • Выбирайте картинки с шумным LSB.
  • UPD от remzalp: Используйте нигде не появлявшиеся изображения.
  • Будьте паиньками!

Буду рада увидеть ваши предложения, дополнения, исправления и другой feedback!

P.S. Хочу выразить особую благодарность PavelMSTU за консультации и мотивационные пинки.

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


  1. remzalp
    07.09.2018 13:23
    +5

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


    1. the_eternal_insomnia Автор
      07.09.2018 23:08

      Абсолютно верно. Спасибо!


    1. ivan386
      08.09.2018 13:44
      +1

      Я как то пытался найти идентичные картинки на разных сайтах которые бы не различались байтами при помощи поиска по картинке. Скачал много визуальных копий одной и той же картинки. Они различались либо размерами изображения, либо размером файла. Хеши разные. Вот только про VCDIFF я тогда ещё не знал и им не сравнивал. Нашёл ли я идентичные копии в итоге не помню.


      Так что найти того самого котика может оказаться не простым делом.


      1. the_eternal_insomnia Автор
        08.09.2018 13:55

        Если в теории есть возможность, то лучше не рисковать. Хотя прятать критически данные LSB-стеганографией само по себе рискованно…


      1. remzalp
        09.09.2018 09:30

        в случае с JPEG скорей всего будет производиться пережатие в момент размещения на сайте. А вот те же PNG не считая метаинформации могут оказаться пиксель-в-пиксель


  1. vak0
    07.09.2018 14:05

    Извиняюсь, если вопрос не совсем по теме. Существуют ли методы стеганографии, устойчивые к преобразованию изображения? Я имею в виду resize, изменение яркости/контрастности/насыщенности/тона, минимальная корректировка геометрии и т.п. В том числе, существует ли метод, который «выдержит», скажем, печать изображения + последующее сканирование. Понятно, что при таких требованиях много информации не спрячешь, но много и не надо, достаточно нескольких байт. Интересует как теория, так и конкретные реализации.


    1. Sklott
      07.09.2018 14:43

      Я думаю если использовать LSB не пикселей, а «частот» которые используются в JPEG и иже с ними, то ко многим из искажений будет устойчивость. Насчет печати/сканирования трудно так сразу сказать, но если есть возможность откалибровать весь процесс, то скорее тоже возможно, чем нет.


    1. ZlobniyShurik
      07.09.2018 15:07

      Скорее всего, это очередной велосипед, причем нерабочий, но за 30 секунд придумался примерно такой метод:

      1. Режем картинку на квадраты где-нибудь так 10x10 пикселей и выше.
      2. изменение яркости центра квадрата — наши биты
      3. чтобы изменение яркости на границах соседних квадратов было не особо заметно — яркость растет/падает плавно к центру квадрата. Яркость границы между квадратами остается оригинальной (можно использовать в качестве «опорного» сигнала).

      Далее традиционно — полезную нагрузку зашифровать, поверх помехоустойчивое кодирование и результат использовать в качестве «модулятора яркости» квадратов.

      Можно сбоку на изоленту даже троичное кодирование прикрутить — яркость+, яркость -, яркость не изменилась, тогда возможный размер полезной нагрузки при прочих равных немного увеличится (а устойчивость кодирования к внешним помехам, соответственно, упадет).

      P.S. Сильно подозреваю, что кто-то где-то нечто подобное уже воплотил (или доказал, что сие работать не будет).


      1. qw1
        08.09.2018 11:50

        Этот метод подразумевает, что есть эталонная картинка, и декодер сравнивает картинку с изменёнными яркостями квадратов с эталоном?


        1. ZlobniyShurik
          08.09.2018 12:52

          С эталонной картинкой такое просчитать вообще без проблем, но как-то неспортивно :)

          Сдается мне, что здесь и без эталонной картинки обойтись можно, опираясь на среднюю яркость границы конкретного квадрата и его содержимого.
          По идее, на реальных изображениях внутри такого «макропикселя» яркость сильно гулять не должна. А если сильно гуляет и алгоритм декодирования ошибся, то, быть может, помехоустойчивое кодирование выручит. Я бы, пожалуй, после кодирования тут же пробное декодирование делал: раскодировалось — хорошо, нет — пробуем немного поиграться с начальными условиями (нарезать на бОльшие квадраты, e.t.c.)

          Строгого математического алгоритма предоставить не могу, повторюсь, идея возникла секунд за 30, после чего была озвучена. Так что, очень даже может быть, что декодировать сие без оригинальной картинки нереально или крайне сложно — тогда идею только в мусор.


          1. qw1
            08.09.2018 13:27

            Я вот представил себе абсолютно произвольную картинку — пусть это будут рандомные данные с неизвестным распределением, попробовал к ней применить ваш концепт, и не получилось.


            1. ZlobniyShurik
              08.09.2018 14:05

              Очень даже может быть, что моя идея — пустышка. Не готов сейчас отстаивать ее жизнеспособность.


    1. Wizard_of_light
      07.09.2018 16:12

      Нуу, самая устойчивая реализация это «если всё будет хорошо — пришлю картинку с котиком».


    1. xi-tauw
      07.09.2018 16:14

      То, что вам нужно называется «водяные знаки».


      1. vak0
        07.09.2018 16:50

        Благодарю за уточнение. Причем, в моем случае, они должны быть еще и невидимые (малозаметные) для человеческого глаза.


        1. Arqwer
          07.09.2018 21:38

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


        1. entze
          08.09.2018 13:33

          Сейчас не скажу, но раньше в Photoshop был встроенный плагин записи копирайта в изображение, причем довольно устойчивый к изменениям.
          Для печати документов есть «желтые точки», но их научились обходить — xakep.ru/2018/06/27/deda-vs-yellow-dots


          1. RolexStrider
            08.09.2018 14:38

            Digimarc назывался.


    1. Mephi1984
      07.09.2018 16:33

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

      Я подумал и пришел к выводу что нельзя это сделать незаметно. Есть такое понятие «динамический диапазон», у человеческого глаза он очень большой, а у камеры низкий. Если сделать в изображении водяные знаки, которые бы читались и различались камерой, то человеческий глаз их тоже заметит, и обратит внимание что с картинкой что-то не так.

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


      1. vak0
        07.09.2018 16:55

        В случае произвольной картинки, видимо, так и есть. Как вырожденный вариант — полностью белый лист. Вряд ли на нем можно спрятать так, чтобы глаз не заметил, а камера увидела.
        Но вот в случае «нормальных» изображений можно, наверное, найти участок с высокой энтропией и добавить туда что-то малозаметное для глаза. Ну, например, кто заметит лишний листик на фотографии кроны дерева? А при этом камерой распознается легко, да и устойчивость к обработке картинки будет высокой.


        1. DGN
          08.09.2018 13:54

          Хмм… а соотношение сторон? Пример — 19:11 — дата восстания против скайнета.

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

          Сообщение можно адресовать группе людей, скажем у модели в руке тетрис, а в нем закодировано морзянкой сообщение. Или постер к матрице, в вертикальных зеленых буковках ASCII код, прочтут только программисты. Креативно составленный ребус может быть хорошим промокодом.


      1. PavelMSTU
        07.09.2018 17:02

        хеш-стеганография методом Machine Learning ;)

        Пост на хабре будет?


        1. tvr
          07.09.2018 17:47

          Хеш-стеганография методом Machine Learning для чайников!

          Надо.


      1. MarvinD
        08.09.2018 13:52

        Проще в местную газету "Из рук в руки" писать объявление про пропажу котика таких-то параметров, а параметры указывают на третьи строки в Большой Советской Энциклопедии, как в старые добрые времена :)


    1. the_eternal_insomnia Автор
      08.09.2018 00:14

      В процессе печати изображения используется цветовая схема CMYK с градацией цвета от 0 до 100 в классическом варианте. Если смотреть на нее долго и очень пристально, то можно увидеть небольшую разницу между соседними цветами. Так что я думаю, ответ на ваш вопрос скорее нет, чем да.



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


  1. Mingun
    08.09.2018 08:13

    Вы пишете:


    Возьмем меньший по модулю корень, то есть х_2.

    И тут же проводите вычисления с корнем х_1 = 1.7951, который больше по модулю. Это, наверное, ошибка, тем более, что результат получился не 0.3256, как вы пишете ниже, а 0.4567.


    1. the_eternal_insomnia Автор
      08.09.2018 12:11

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

      Но спешу заметить, что значение 0.3256 мы получаем для рандомного котика, а не для котика с последовательным встраиванием, как в примере.


  1. ValdikSS
    08.09.2018 13:11

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


    1. the_eternal_insomnia Автор
      08.09.2018 13:20

      Найти шумы, не свойственные JPEG, в PNG это, конечно, мощно.

      И если ты долго смотришь в бездну, то бездна тоже смотрит в тебя.


      1. qw1
        08.09.2018 13:35
        +1

        Почему в оригинальном PNG на котике больше шума, чем на фоне?
        Если это настоящая фотография, то матрице вообще без разницы, где генерировать тепловой шум.

        Возможно, это фото было когда-то JPEG-ом, который удалил шум на низкочастотных участках, либо является монтажом, когда кот с одним шумом вклеен в фон с другим шумом. Обычно фотокамеры снимают в JPEG, мало кто заморачивается с RAW. Скорее всего, png — конверсия

        А какое у вас объяснение?


        1. the_eternal_insomnia Автор
          08.09.2018 13:44
          +1

          Я вынуждена вам признаться… Этот котик был когда-то JPEGом. Ну он был слишком милым, и я не смогла справится с соблазном использовать его в статье. Прошу простить.

          Суть в том, что неподготовленный глаз все равно не будет видеть шум, даже если он там есть (а он там есть по определению).


          1. ValdikSS
            08.09.2018 14:11

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


            1. the_eternal_insomnia Автор
              08.09.2018 14:14

              Еще аргумент к тому, чтобы не заполнять контейнер под завязку :)


      1. entze
        08.09.2018 13:46
        +1

        Использование PNG для фото явно с камеры само по себе подозрительно! :) Опять же, какие массовые сервисы или соцсети позволяют публиковать PNG для открытого доступа.


  1. janatem
    09.09.2018 12:43

    Замечание по поводу LSB из представления RGB. Верно ли, что значения 0 и 1 визуально отличаются друг от друга больше, чем 254 и 255? Более общо: вот эта шкала значений от 0 до 255 насколько сильно отличается от равномерной по восприятию? Мне кажется, что надежнее использовать LSB других величин, например, спектральных коэффициентов, которые используются во многих графических форматах, в том числе JPEG, а также в аудиоформатах. Смысл в том, что для этих lossy форматов уже была проведена работа по изучению того, какую информацию и в каких количествах можно отбрасывать так, чтобы результат не сильно испортился с точки зрения восприятия человеком. Грубо говоря, можно сжать один и тот же файл с разной степенью сжатия и посмотреть на разницу того, что выброшено при более агрессивном сжатии и менее. Таким образом можно найти те биты в менее сжатом представлении, которые можно портить стеганографией. (Разумеется, здесь нужно знать потроха формата.)

    Теперь насчет статистического стеганоанализа. Верно ли, что вопрос о существовании надежного стеганографического контейнера всё еще открыт? Навскидку кажется, что таковой можно построить, если известно распределение LSB в «обычных» картинках. (Хм, не совсем так, а чуть сложнее: условное распределение LSB в зависимости от всего контекста.) Тогда достаточно построить функциональное отображение из равномерного распределения (к которому обычно принадлежит зашифрованное сообщение) в это «природное» распределение. И потом применить его к сообщению и результат засунуть в LSB.


    1. the_eternal_insomnia Автор
      09.09.2018 16:06

      Вопрос о равномерности RGB по восприятию довольно сложен лично для меня. Советую почитать про CIE 1931.
      Можно провести такой эксперимент на коленке:
      сможете ли вы различить, где здесь настоящий белый, почти белый (разница с настоящим белым равна 1) и уже не белый (разница с настоящим белым равна 5)?


      Честно признаться, я смогла бы. Но не играют ли тут внешние факторы? Например, вы точно знаете, что это три различных цвета, вы видите большой участок покрытый этим цветом, а не мааааленький пиксель, вокруг много чисто белого, который создает контраст. Цветовосприятие крайне субъективная штука.
      А здесь различаете настоящий черный, почти черный и уже не черный? Пусть вопрос останется отрытым. :)



      Насчет JPEG тоже есть много нюансов. Я думаю, вам будет интересно это видео.

      Словами «надежный» и «безопасный» надо играться очень осторожно. Будет обидно, если надежный стегоконтейнер придумали и успешно им пользуются, а мы об этом конечно же не знаем :) Нужно определять меру надежности в каждом отдельном случае, потому что то, что надежно сегодня, не будет надежно завтра. Распределение LSB может быть абсолютно любым с моей точки зрения. Изображение может быть псевдослучайным или однотонным.


      1. janatem
        09.09.2018 21:34

        Ну, на мой глаз третья картинка от первой заметно отличается, а вот черные все кажутся одинаковыми. Это всего лишь свидетельство против моего предположения сравнении 0, 1 и 254, 255. Но мой основной тезис был в том, что психометрические модели уже кем-то были разработаны и используются в популярных форматах со сжатием, так что в стеганографической задаче выбора наименее значимых кусочков информации нужно «всего лишь» воспользоваться готовым результатом.

        Словами «надежный» и «безопасный» надо играться очень осторожно.

        Это, конечно, верно. Однако мне бы хотелось получить примерно ту же степень строгости оценки качества стеганографии, что мы имеем сейчас в криптографии. Там существуют оценки стойкости шифров, опирающиеся на математические факты и математические же гипотезы. Ну и на другие практические вещи вроде качества ГСЧ, которые более-менее понятны.


    1. qw1
      09.09.2018 22:15

      Замечание по поводу LSB из представления RGB. Верно ли, что значения 0 и 1 визуально отличаются друг от друга больше, чем 254 и 255?
      Не отличаются физически. В большинстве мониторов 6-битные ЦАП на матрицах, т.е. на 1 не приводит к изменению картинки. Нужно менять значение на 4.

      А вот в HDR-мониторах, которых на вход получают 10 бит на канал, стоят обычно 8-битные матрицы.


  1. actualinfo
    09.09.2018 15:07

    Сотня килобайт — для нынешнего века маловато. Поэтому есть идеи по расширению масштаба. Например использовать НЗБ в звуковом файле, сохраненном во FLAC. Или можно вообще увеличить емкость секретного контейнера в 9 раз, если использовать 48-битный PNG или 24-битный звук. Т.е добавить ещё 8 бит разрядности на каждый канал и записывать в 9 наименее значащих битов ))) Но это будет конечно наглёж, который при битовом анализе покажет кучу шума там где его не должно быть.


    1. the_eternal_insomnia Автор
      09.09.2018 15:11

      Однако многим зловредам это хватает. Взять хотя бы ZeusVM, Triton, Zero.T. Все они использовали стеганографию.