
🔐 Стеганография изображений на 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 |
|
|
| → |
|
2 |
|
|
| → |
|
3 |
|
|
| → |
|
4 |
|
|
| → |
|
Декодирование, обратный процесс: собираем младшие биты синего канала тех же четырёх пикселей и получаем 1101. Никакой магии, только битовые операции.
На практике одним синим каналом не ограничиваются: можно задействовать все три (R, G, B), утраивая объём скрываемых данных. В этом туториале для простоты работаем только с синим, так нагляднее видно, что происходит с каждым битом.
Пошаговая реализация на PHP
Понадобится PHP с модулем GD, он предустановлен практически на всех хостингах. Код написан под PHP 8.0+ и проверен вплоть до 8.4. Создадим три файла в одной папке.
Шаг 1: Вспомогательные функции, functions.php
Две утилиты: перевод строки в бинарное представление и обратно.
1 <?php 2 3 function 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 20 function 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 3 require_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 12 for ($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 28 imagepng($img, 'encoded.png'); 29 30 echo "Закодировано {$msgLen} бит — результат в encoded.png\n";
imagecolorat() берёт цвет пикселя по координатам (x, y). Битовые сдвиги >>16 и >>8 вычленяют R, G, B. Синий канал раскладывается в 8-битную строку, младший бит (индекс 7) заменяется на очередной бит сообщения, и новый цвет записывается через imagesetpixel().
Сообщение кодируется по диагонали (y = x), так проще отследить логику на небольшом примере. В реальном проекте итерируйтесь по всем пикселям построчно и добавьте 32-битный заголовок с длиной сообщения: без него декодер не знает, где остановиться.
Шаг 3: Декодирование, decrypt.php
Извлекает скрытое сообщение: проходит по тем же пикселям и собирает младшие биты синего канала.
1 <?php 2 3 require_once 'functions.php'; 4 5 $src = 'encoded.png'; 6 $img = imagecreatefrompng($src); 7 8 $bits = ''; 9 10 for ($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 19 echo 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, пусть другие учатся на вашем примере.



