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, нехай інші вчаться на вашому прикладі.