Як сховати текст у зображенні: стеганографія на прикладі JPEG

01.10.2025 1 хвилин Автор: Lady Liberty

Стеганографія в JPEG – це сучасний спосіб приховування даних у звичайних зображеннях, який використовується як для навчальних експериментів, так і для практичних завдань кібербезпеки. На відміну від простих методів зміни пікселів, у JPEG застосовується робота з DCT-коефіцієнтами, де змінюється лише молодший біт.

Секрет у картинці

JPEG — це формат, який став стандартом для зберігання та передачі фотографій завдяки балансу між якістю та розміром файлу. Щоб зрозуміти його особливості, потрібно розібратися у фундаментальних принципах його роботи.

Перехід від RGB до YCbCr

На відміну від звичної системи RGB, у JPEG використовується простір YCbCr. Він складається з трьох компонентів:

  • Y (яскравість) — визначає світлову складову зображення.

  • Cb — відображає різницю між яскравістю та синім кольором.

  • Cr — відображає різницю між яскравістю та червоним.

Такий поділ ґрунтується на особливостях людського зору: людина краще сприймає зміни яскравості, ніж колірні відмінності. Це дозволяє ефективніше стискати дані без суттєвої втрати візуальної якості.

Блокова структура та стиснення

JPEG не зберігає інформацію про кожен піксель напряму. Замість цього застосовується DCT-стискання (дискретне косинусне перетворення):

  • Зображення розбивається на блоки розміром 8×8 пікселів.

  • Кожен блок переводиться у частотний простір.

  • Високочастотні компоненти, особливо в каналах Cb і Cr, зменшуються залежно від рівня стиснення.

Цей підхід дозволяє значно скоротити обсяг даних, зберігаючи водночас зорово важливу інформацію.

DCT та його роль

Дискретне косинусне перетворення представляє блок у вигляді матриці коефіцієнтів, які описують різні частотні складові:

  • Низькі частоти — відповідають за плавні переходи, великі об’єкти та фон. Їхня зміна впливає на загальну яскравість.

  • Середні частоти — описують контури та основні текстури, формуючи сприйняття форми об’єктів.

  • Високі частоти — відображають дрібні деталі, різкість і чіткість. Саме вони відповідають за відчуття деталізації.

Чому JPEG заслуговує на окрему увагу

Цей формат не просто зменшує розмір файлу — він є унікальним прикладом того, як інженери використали властивості людського зору для оптимізації зберігання даних. JPEG поєднує в собі математику, психологію сприйняття та інженерну простоту, ставши основою для цифрової епохи фотографії.

DCT для всіх трьох каналів, наприклад
Воно ж у вигляді матриці

Що ми робитимемо?

У кожному елементі матриці змінюватиметься найменш значущий біт — на той, який потрібно для передавання прихованих даних.

Як це може виглядати

Але спершу подивимося, як зміни цієї матриці позначаться на зображенні.

Зачіпаємо лише високі частоти
Зачіпаємо лише середні частоти
Зачіпаємо лише низькі частоти
Торкаємося всіх частот

У прикладах наведено зміну частоти на 70% для кращої наочності. Проте для конкретного завдання достатньо змін лише на 1 біт, що робить їх абсолютно непомітними людському оку. Таким чином, приховане повідомлення в JPEG формується шляхом поділу його на біти та послідовної модифікації останнього біта кожного елемента матриці відповідно до потрібного значення.

Технічну реалізацію отримання DCT не прикладаю, оскільки вона досить об’ємна. Прекрасний приклад – owencm/js-steg . Використовуючи його, можна працювати із матрицями DCT.

Простий приклад збереження та отримання повідомлення:

/**
  * Перетворює наше повідомлення в байти
  */
function textToBytes(text){
  let encoder = new TextEncoder();
  return encoder.encode(text);
}

/**
  * Виконує зміни з матрицями
  */
function modifyCoefficients(coefficients){
  
  //Наше повідомлення
  let message = "Message";
  let data = textToBytes(message);

  //coefficients[0] -> усі блоки Y
  //coefficients[1] -> усі блоки Cb
  //coefficients[2] -> усі блоки Cr

  //coefficients[0][0] //64 елементи матриці DCT

  //Змінюємо дані в матриці
  //Для прикладу працюємо тільки з Y-каналом
  let lumaCoefficients = coefficients[0]; 
  for (let i = 0, bitIndex = 0; i < lumaCoefficients.length; i++) {
    for (let j = 0; j < 64; j++) {
      
      if(bitIndex < data.length * 8){

        let bit = (data[Math.floor(bitIndex / 8)] >> (7 - (bitIndex % 8))) & 1;

        //Змінюємо останній біт
        lumaCoefficient
/**
  * Перетворює байти у рядок
  */ 
function bytesToText(bytes){
  let uint8Array = new Uint8Array(bytes);
  let decoder = new TextDecoder();
  return decoder.decode(uint8Array);
}

function readCoefficients(coefficients) {

    //coefficients[1] - Y
    //coefficients[2] - Cb
    //coefficients[3] - Cr

    let bytes = [];
    let dataBitIndex = 0;
    let currentByte = 0;

    //Працюємо також лише з Y-каналом
    let lumaCoefficients = coefficients[1]; 

    for (let i = 0; i < lumaCoefficients.length; i++) {
      for (let j = 0; j < 64; j++) {
        
        let bit = lumaCoefficients[i][j] & 1;

        currentByte = (currentByte << 1) | bit;
        dataBitIndex++;

        if (dataBitIndex % 8 === 0) {
          bytes.push(currentByte);
          currentByte = 0;
        }
      
      }
    }

    return bytesToText(bytes);
  
}

//Зчитування повідомлення
jsSteg.getCoefficients(objectURL, function(coefficients){
    console.log(readCoefficients(coefficients));
});
Ну і результат

Код більш розгорнутого рішення можна знайти на GitHub (використання AES, приховування файлів у зображенні). Кому цікаво – можна погратися з тим, які частоти ми використовуємо для зберігання інформації (від цього залежить візуальна зміна зображення).

X від 0 до 7, Y від 0 до 7

Висновок

JPEG — це не лише формат збереження фото, а й зручний інструмент для приховування даних. Використання DCT-коефіцієнтів дозволяє змінювати лише найменш значущий біт, роблячи повідомлення непомітним для ока. Вибір частот визначає баланс між якістю та надійністю приховування, а простота реалізації робить метод цікавим і для навчання, і для практики.

Підписатися
Сповістити про
0 Коментарі
Найстаріші
Найновіше Найбільше голосів
Знайшли помилку?
Якщо ви знайшли помилку, зробіть скріншот і надішліть його боту.