Skip to content

Всё для WordPress, веб-разработки — и не только

🔐 Стеганография изображений на PHP: прячем текст в пикселях через LSB

🔐 Стеганография изображений на PHP: прячем текст в пикселях через LSB

Клик, и ваш encoded.png внешне не отличается от container.jpg. Никто не заподозрит, что внутри лежит скрытое сообщение. Стеганография не делает данные нечитаемыми, как шифрование, она прячет сам факт передачи. И для PHP-разработчика это один из тех приёмов, которые стоит понимать на уровне битов и пикселей.

LSB-стеганография на PHP с модулем GD, не продакшен-инструмент, а учебный полигон. Но полигон, который учит работать с двоичным представлением, битовыми масками и lossless-форматами лучше, чем десяток CRUD-задач. Вы пройдёте путь от «как картинка устроена внутри» до строчки php decrypt.php, которая достаёт hello из пикселей, и поймёте, почему JPEG для такого не подходит.

💡 Быстрый обзор:

  • Разбираем теорию: как младший бит синего канала прячет сообщение и почему глаз этого не видит
  • Пишем три PHP-файла: functions.php (утилиты), encrypt.php (кодирование), decrypt.php (извлечение)
  • Запускаем на реальном JPEG-контейнере и получаем неотличимый визуально PNG-результат
  • Проходим по ограничениям: сжатие, обнаружимость, объём и связка с AES-шифрованием
  • Смотрим готовые PHP-библиотеки и решаем, когда GD хватает, а когда пора брать Python

Как работает LSB-стеганография

LSB (Least Significant Bit), метод, при котором секретное сообщение записывается в младшие биты пикселей. Название буквальное: берём наименее значимый бит цветового канала и заменяем его битом сообщения.

Цифровое изображение, это сетка пикселей. Каждый пиксель кодируется тремя байтами: красный (R), зелёный (G) и синий (B). Комбинация трёх значений от 0 до 255 задаёт итоговый цвет. Изменение младшего бита сдвигает значение канала не более чем на 1, например, RGB(100, 50, 200) превращается в RGB(100, 50, 201). Человеческий глаз разницу не различает.

Возьмём оранжевый пиксель:

  • Красный: 11001001 (201)
  • Зелёный: 11111000 (248)
  • Синий: 00000011 (3)

Младший бит синего канала, единица. Заменим его на бит секретного сообщения, и цвет визуально останется тем же. Обрабатывая пиксели последовательно, мы записываем всё сообщение бит за битом.

Допустим, нужно спрятать последовательность 1101. Берём 4 пикселя и меняем младший бит синего канала:

Пиксель

R

G

Исходный B

LSB заменён

Новый B

1

11001100

10010001

00101011

1

00101011

2

00011000

11110000

11111110

1

11111111

3

11100010

00100101

01010101

0

01010100

4

11111101

00001010

01000011

1

01000011

Декодирование, обратный процесс: собираем младшие биты синего канала тех же четырёх пикселей и получаем 1101. Никакой магии, только битовые операции.

На практике одним синим каналом не ограничиваются: можно задействовать все три (R, G, B), утраивая объём скрываемых данных. В этом туториале для простоты работаем только с синим, так нагляднее видно, что происходит с каждым битом.

Пошаговая реализация на PHP

Понадобится PHP с модулем GD, он предустановлен практически на всех хостингах. Код написан под PHP 8.0+ и проверен вплоть до 8.4. Создадим три файла в одной папке.

Шаг 1: Вспомогательные функции, functions.php

Две утилиты: перевод строки в бинарное представление и обратно.

1<?php
2
3function toBin(string $str): string
4{
5 $result = '';
6 $len = strlen($str);
7
8 while ($len--) {
9 $result = str_pad(
10 decbin(ord($str[$len])),
11 8,
12 '0',
13 STR_PAD_LEFT
14 ) . $result;
15 }
16
17 return $result;
18}
19
20function toString(string $binary): string
21{
22 $result = '';
23 foreach (str_split($binary, 8) as $byte) {
24 $result .= chr((int) base_convert($byte, 2, 10));
25 }
26
27 return $result;
28}

toBin() конвертирует каждый символ строки в 8-битное представление через decbin(ord(...)) и дополняет нулями слева до полного байта. toString() делает обратное: нарезает бинарную строку блоками по 8 бит, переводит каждый блок в десятичное число и получает символ через chr().

Поблочная обработка в toString(), не прихоть. Популярный в туториалах подход через pack('H*', base_convert($binary, 2, 16)) ломается на сообщениях длиннее 31 бита: функция base_convert оперирует целыми числами и при превышении PHP_INT_MAX возвращает мусор. Разбивка по байтам решает проблему на сообщениях любой длины.

Шаг 2: Кодирование, encrypt.php

Читает JPEG-контейнер, вшивает сообщение в младшие биты синего канала и сохраняет результат в PNG. Почему не JPEG? Lossless-формат обязателен: JPEG-сжатие разрушит скрытые биты, и сообщение превратится в шум.

1<?php
2
3require_once 'functions.php';
4
5$message = 'hello';
6$binary = toBin($message);
7$msgLen = strlen($binary);
8
9$src = 'container.jpg';
10$img = imagecreatefromjpeg($src);
11
12for ($x = 0; $x < $msgLen; $x++) {
13 $y = $x;
14 $rgb = imagecolorat($img, $x, $y);
15
16 $r = ($rgb >> 16) & 0xFF;
17 $g = ($rgb >> 8) & 0xFF;
18 $b = $rgb & 0xFF;
19
20 $blueBits = str_pad(decbin($b), 8, '0', STR_PAD_LEFT);
21 $blueBits[7] = $binary[$x];
22 $newB = bindec($blueBits);
23
24 $newColor = imagecolorallocate($img, $r, $g, $newB);
25 imagesetpixel($img, $x, $y, $newColor);
26}
27
28imagepng($img, 'encoded.png');
29
30echo "Закодировано {$msgLen} бит — результат в encoded.png\n";

imagecolorat() берёт цвет пикселя по координатам (x, y). Битовые сдвиги >>16 и >>8 вычленяют R, G, B. Синий канал раскладывается в 8-битную строку, младший бит (индекс 7) заменяется на очередной бит сообщения, и новый цвет записывается через imagesetpixel().

Сообщение кодируется по диагонали (y = x), так проще отследить логику на небольшом примере. В реальном проекте итерируйтесь по всем пикселям построчно и добавьте 32-битный заголовок с длиной сообщения: без него декодер не знает, где остановиться.

Шаг 3: Декодирование, decrypt.php

Извлекает скрытое сообщение: проходит по тем же пикселям и собирает младшие биты синего канала.

1<?php
2
3require_once 'functions.php';
4
5$src = 'encoded.png';
6$img = imagecreatefrompng($src);
7
8$bits = '';
9
10for ($x = 0; $x < 40; $x++) {
11 $y = $x;
12 $rgb = imagecolorat($img, $x, $y);
13
14 $b = $rgb & 0xFF;
15 $blueBits = str_pad(decbin($b), 8, '0', STR_PAD_LEFT);
16 $bits .= $blueBits[7];
17}
18
19echo toString($bits) . "\n";

Здесь 40, жёстко заданное количество бит, ровно 5 байт под слово hello. В боевой версии замените это число на длину из заголовка сообщения, который вы записали первыми 32 битами при кодировании.

Шаг 4: Запуск и проверка

Кладём container.jpg в папку со скриптами. Порядок действий:

  • php encrypt.php, в консоли: «Закодировано 40 бит, результат в encoded.png».
  • php decrypt.php, в консоли: hello.
  • Открываем container.jpg и encoded.png рядом, разницы нет. Визуально они неотличимы.

Сравнить побайтово можно через ImageMagick: magick compare container.jpg encoded.png diff.png подсветит различия в младших битах, практически незаметные глазу. Но самый надёжный способ проверки, запустить decrypt.php и прочитать сообщение.

Ограничения метода

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

  • Сжатие убивает данные. JPEG пересчитывает блоки пикселей 8×8 с квантованием, младшие биты не выживают. Поэтому encrypt.php сохраняет результат строго в PNG (lossless). Один проход через JPEG-компрессию, и скрытые биты превратятся в шум. Альтернативные форматы: BMP, TIFF без сжатия.

  • Обнаружимость. LSB-заполнение оставляет статистические артефакты: гистограмма синего канала меняется предсказуемым образом. RS-анализ и χ²-тест детектят скрытое сообщение за секунды. Без шифрования LSB, это сокрытие, а не защита: факт наличия сообщения вычисляется, даже если содержимое не читается.

  • Объём данных. Каждый пиксель отдаёт один бит в синем канале. Изображение 1000×1000 пикселей вмещает до 125 КБ скрытого текста, таков математический предел, следующий из определения LSB-метода. На практике после PNG-сжатия полезный объём меньше, плюс часть битов уходит на заголовок с длиной.

  • Не криптография. Стеганография не заменяет шифрование, а дополняет его. Правильный пайплайн: шифруете сообщение (AES-256), затем прячете шифротекст в изображение. Тогда даже обнаружение скрытого канала не раскрывает содержимое.

  • PHP GD, базовый инструмент. GD даёт низкоуровневый доступ к пикселям, и для обучения этого более чем достаточно. Но экосистема Python для стеганографии заметно богаче: библиотека stegano (LSB + шифрование), Stegano (набор методов) и steghide (C++, но есть Python-байндинги). На чистом PHP есть несколько библиотек: kzykhys/Steganography, минимальная LSB-реализация для PNG, seba/steganography, современная библиотека с LSB-манипуляцией, и pateon/steganography, PHP 8.5+ с поддержкой GD и Imagick. Но production-grade решений уровня steghide на PHP на 2026 год нет.

⁉️🤔 Частые вопросы

Можно ли спрятать текст прямо в JPEG?

Нет. JPEG-сжатие делит изображение на блоки 8×8 и применяет квантование, младшие биты не выживают. Для сокрытия в JPEG используют другой подход: прячут данные в коэффициентах дискретно-косинусного преобразования (DCT), а не в пикселях. Алгоритмы: F5, JSteg, OutGuess. Готовых PHP-библиотек под это практически нет, проще взять Python.

Чем LSB отличается от стеганографии в метаданных?

LSB меняет пиксельные данные, сообщение спрятано «внутри» изображения. Метаданные (EXIF, XMP, tEXt-чанки PNG) лежат в заголовках файла и читаются любым просмотрщиком свойств. Метаданные извлечь проще, зато они переживают JPEG-сжатие, LSB на это не способен.

Как убедиться, что encoded.png действительно содержит скрытое сообщение?

Визуально, никак. Сравните файлы побайтово: magick compare container.jpg encoded.png diff.png (ImageMagick) покажет различия в младших битах. Они практически незаметны глазу, но утилита подсветит изменённые пиксели. Самый надёжный способ, запустить php decrypt.php и прочитать сообщение.

Насколько это вообще безопасно?

Без шифрования, небезопасно. LSB без предварительного AES-шифрования, это сокрытие, а не защита. Факт передачи скрытых данных детектится статистическими тестами за секунды. Связка «AES-шифрование + LSB-сокрытие» даёт приемлемый уровень для пет-проектов, но не для чувствительных данных.

Есть ли готовые PHP-библиотеки для стеганографии?

Да, но все они, учебные или легковесные. kzykhys/Steganography, минимальная LSB-реализация, работает только с PNG. seba/steganography, посвежее, с чистым API для encode/decode текста в изображения. pateon/steganography, PHP 8.5+, поддерживает и GD, и Imagick. Для серьёзных задач смотрите в сторону Python: stegano (LSB + шифрование) и Stegano (набор методов, включая генератор стего-изображений).

Стеганография на PHP: база для обучения или готовый инструмент?

LSB-стеганография на PHP с GD, отличный полигон для понимания того, как данные прячутся на уровне пикселей. Вы написали рабочий цикл encode/decode, разобрались с битовыми операциями и увидели на практике, почему «невидимое» изменение оставляет следы в статистике изображения.

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

Хотите копнуть глубже, реализуйте кодирование сразу в трёх цветовых каналах и добавьте 32-битный заголовок с длиной сообщения. Получится мини-инструмент, который в 3 раза эффективнее базовой версии и уже тянет на курсовую работу. Исходный код выкладывайте на GitHub, пусть другие учатся на вашем примере.