Привет, Хабр!

Сегодня я хочу поделиться историей создания одного из моих проектов — десктопного приложения для стеганографии, которое я назвал "ChameleonLab". Это не просто очередной скрипт для LSB-метода, а полноценный инструмент с графическим интерфейсом, поддержкой разных типов файлов, шифрованием и, что самое интересное, встроенными утилитами для стегоанализа.

Идея заключалась в том, чтобы создать удобную «лабораторию», где можно не только спрятать данные, но и исследовать, насколько незаметно это получилось. Мы пройдем путь от базового алгоритма до интеграции с Matplotlib и анализа аномалий в Office-документах.

Программа ChameleonLab. Вкладка "Встраивание"
Программа ChameleonLab. Вкладка "Встраивание"

Технологический стек:

  • Язык: Python 3

  • GUI: PyQt6

  • Обработка данных: NumPy, Pillow

  • Аудио: Soundfile

  • Криптография: Cryptography

  • Визуализация: Matplotlib

Статья будет полезна тем, кто интересуется информационной безопасностью, обработкой данных на Python или разработкой десктопных приложений с помощью PyQt. Поехали!

Часть 1. Сердце Хамелеона: Протокол и ядро стеганографии

В основе большинства простых стеганографических утилит лежит метод LSB (Least Significant Bit). Его суть в замене наименее значащих битов в байтах файла-контейнера (например, в цветовых компонентах пикселей изображения) на биты секретного сообщения. Изменение одного LSB в байте 1100101**0** на 1100101**1** почти не влияет на итоговый цвет, делая его невидимым для глаза.

Просто записать данные в LSB — плохая идея. Как их потом найти? Как узнать, где они заканчиваются? Поэтому я разработал простой, но эффективный протокол.

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

  1. Магическое число (4 байта): Уникальная сигнатура b'S6KB', которая позволяет нашему приложению мгновенно определить, есть ли в файле «свои» данные.

  2. Длина полезной нагрузки (4 байта): Общий размер данных, которые мы прячем (имя файла + само содержимое).

  3. Количество используемых бит (1 байт): Значение n_bits (от 1 до 8), которое использовалось при встраивании.

  4. Полезная нагрузка: Сами данные, упакованные в формате имя_файла.расширение|бинарное_содержимое.

Таким образом, при извлечении мы сначала ищем магическое число, перебирая n_bits от 1 до 8. Если находим, читаем заголовок, узнаем точную длину сообщения и извлекаем его, не перебирая весь файл до конца.

Вот ключевая логика из файла steganography_core.py:

steganography_core.py
import numpy as np
import math

# Уникальная сигнатура для подтверждения, что данные скрыты нашей программой
MAGIC_NUMBER = b'S6KB'

def hide(carrier_bytes: np.ndarray, secret_data: bytes, n_bits: int, secret_filename: str = None) -> np.ndarray:
    """
    Прячет секретные данные в массиве байт.
    Формат контейнера: [4 байта длина] [1 байт n_bits] [MAGIC] [имя_файла|данные]
    """
    # ... (проверки) ...

    # Упаковываем данные
    data_to_package = secret_data
    if secret_filename:
        data_to_package = secret_filename.encode('utf-8') + b'|' + secret_data

    data_to_hide = MAGIC_NUMBER + data_to_package
    metadata_len = len(data_to_hide).to_bytes(4, 'big')
    n_bits_val = int(n_bits).to_bytes(1, 'big')
    metadata = metadata_len + n_bits_val

    full_data = metadata + data_to_hide
    full_bits = data_to_binary(full_data)

    # ... (проверка вместимости) ...

    # Применяем маску и вставляем биты
    carrier_flat = carrier_bytes.flatten().copy().astype(np.uint8)
    mask = ~((1 << n_bits) - 1) & 0xFF
    carrier_flat &= mask # Обнуляем последние n_bits

    # Встраиваем данные по частям
    bit_index = 0
    total_bytes_needed = math.ceil(len(full_bits) / n_bits)
    for i in range(total_bytes_needed):
        chunk = full_bits[bit_index : bit_index + n_bits].ljust(n_bits, '0')
        value = int(chunk, 2)
        carrier_flat[i] |= value
        bit_index += n_bits

    return carrier_flat.reshape(carrier_bytes.shape).astype(carrier_bytes.dtype)

Функция reveal работает в обратном порядке: ищет MAGIC_NUMBER, читает метаданные и восстанавливает исходный файл.

Программа ChameleonLab. Вкладка "Извлечение"
Программа ChameleonLab. Вкладка "Извлечение"

Часть 2. Невидимый щит: Шифрование данных

LSB скрывает факт наличия сообщения, но не его содержимое. Любой, кто извлечет LSB-плоскость, сможет прочитать наши данные. Чтобы это исправить, я добавил шифрование с помощью библиотеки cryptography.

Я выбрал Fernet — это высокоуровневая реализация симметричного шифрования (AES128 в режиме CBC). Ключ генерируется из пароля пользователя с помощью PBKDF2, что защищает от атак по словарю и радужных таблиц.

crypto_utils.py
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
import os

def generate_key_from_password(password: str, salt: bytes) -> bytes:
    """
    Генерирует ключ Fernet из пароля и соли.
    """
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=100000, # Стандартное безопасное количество итераций
    )
    return base64.urlsafe_b64encode(kdf.derive(password.encode()))

def encrypt_data(data: bytes, password: str) -> bytes:
    """
    Шифрует данные, используя пароль.
    """
    salt = os.urandom(16) # Генерируем новую соль для каждого шифрования
    key = generate_key_from_password(password, salt)
    f = Fernet(key)
    encrypted_data = f.encrypt(data)

    # Сохраняем соль вместе с зашифрованными данными
    return salt + encrypted_data

def decrypt_data(token: bytes, password: str) -> bytes:
    """
    Расшифровывает данные, используя пароль.
    """
    salt = token[:16]
    encrypted_data = token[16:]
    key = generate_key_from_password(password, salt)
    f = Fernet(key)
    return f.decrypt(encrypted_data)

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

Часть 3. Не только картинки: Поддержка Office и аудио

Ограничиваться изображениями было бы скучно. Я расширил поддержку на аудиофайлы (.wav, .flac) и, что самое интересное, документы MS Office (.docx, .xlsx, .pptx).

  • Аудио: Принцип тот же, что и с изображениями. Аудиофайл читается как NumPy-массив сэмплов, и LSB-модификации применяются к ним.

  • Документы Office: Здесь подход совсем другой и, на мой взгляд, более изящный.

Файлы .docx, .xlsx, .pptx на самом деле являются ZIP-архивами, содержащими XML-файлы и другие ресурсы. Стандарт Open XML позволяет встраивать произвольные пользовательские данные в так называемые Custom XML Parts. Это легитимный механизм, который не повреждает файл и не вызывает подозрений у антивирусов.

Программа ChameleonLab. Вкладка "Встраивание". Работа с .docx
Программа ChameleonLab. Вкладка "Встраивание". Работа с .docx

Процесс встраивания выглядит так:

  1. Распаковать исходный .docx во временную папку.

  2. Создать новый XML-файл (customXml/item1.xml) с нашими данными, закодированными в HEX для безопасности.

  3. Найти файл связей (word/_rels/document.xml.rels для docx) и добавить в него запись, которая связывает основной документ с нашим Custom XML Part.

  4. Запаковать временную папку обратно в ZIP-архив и переименовать его в .docx.

openxml_stego.py
import zipfile
import shutil
import os
from xml.etree import ElementTree as ET

...
CUSTOM_XML_PART = 'customXml/item1.xml'
CUSTOM_SCHEMA_URI = 'https://schemas.chameleonlab.app/steganography/v1'
RELATIONSHIP_TYPE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml'

...
def embed_payload(src_path: str, dest_path: str, payload: bytes, file_type: str):
    # ...
    # 1. Распаковка
    with zipfile.ZipFile(src_path, 'r') as docx_zip:
        docx_zip.extractall(temp_dir)

    # 2. Создание Custom XML
    payload_hex = payload.hex()
    root = ET.Element('chameleonData', {'xmlns': CUSTOM_SCHEMA_URI})
    root.text = payload_hex
    # ... запись файла ...

    # 3. Обновление файла связей .rels
    rels_path = os.path.join(temp_dir, doc_rels_path)
    # ... парсинг, поиск нового rId ...
    ET.SubElement(rels_root, 'Relationship', {
        'Id': new_rid,
        'Type': RELATIONSHIP_TYPE,
        'Target': os.path.relpath(f'/{CUSTOM_XML_PART}', f'/{os.path.dirname(doc_rels_path)}').replace('\\', '/')
    })
    # ... запись файла .rels ...

    # 4. Запаковка обратно
    shutil.make_archive(dest_path.replace(os.path.splitext(dest_path)[1], ''), 'zip', temp_dir)
    # ...

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

Часть 4. Собираем лабораторию: Интерфейс на PyQt6

Хороший инструмент должен быть удобным. Для GUI я выбрал PyQt6. Структура приложения ui_components.py основана на QMainWindow с боковым меню из QPushButton и центральным QStackedWidget для переключения между экранами.

Несколько интересных моментов в реализации UI:

  • Drag-and-Drop: Чтобы сделать загрузку файлов интуитивной, я создал кастомный виджет DropArea, унаследованный от QLabel, который переопределяет события dragEnterEvent и dropEvent.

  • Векторные иконки без файлов: Чтобы не таскать с собой папку с иконками, я встроил их прямо в код как SVG-строки. Функция svgto_icon рендерит их в QPixmap на лету с помощью QSvgRenderer. Это делает приложение полностью самодостаточным.

ui_components.py
SVGICONS = {
    "embed": """
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
            <path fill="#10b981" d="M9.4 16.6L5.8 13l3.6-3.6L8 8l-5 5 5 5 1.4-1.4zM15.6 16.6l3.6-3.6-3.6-3.6L17 8l5 5-5 5-1.4-1.4z"/>
        </svg>
    """,
    # ... другие иконки ...
}
  • Многопоточность для тяжелых задач: Пакетный анализ папки может занять много времени и «заморозить» интерфейс. Чтобы этого избежать, я вынес логику анализа в отдельный класс BatchAnalysisWorker, который запускается в QThread. Он общается с основным потоком через сигналы (progress, file_analyzed, finished), что обеспечивает отзывчивость UI.

Программа ChameleonLab. Пакетный анализ
Программа ChameleonLab. Пакетный анализ

Часть 5. Инструменты стегоаналитика: Видим невидимое

Это самая интересная часть приложения. Вкладка "Научный анализ" позволяет загрузить оригинал и стего-контейнер и сравнить их по нескольким параметрам.

Для визуализации я интегрировал Matplotlib в PyQt. Для этого используется FigureCanvasQTAgg — специальный виджет, который может отображать графики Matplotlib.

Какие анализы мы проводим?

  1. Гистограммы яркости: Простое LSB-встраивание может незначительно, но заметно изменить гистограмму распределения цветов. На графике это видно как «сглаживание» пиков и долин.

  2. Карта разницы (Heatmap): Визуализация, где измененные пиксели подсвечиваются. Позволяет увидеть, в каких областях изображения были внесены правки.

  3. Атака Хи-Квадрат (Chi-Squared): Мощный статистический тест. Его идея в том, что после встраивания случайных (например, зашифрованных) данных распределение значений в LSB-плоскости становится неестественно равномерным. Тест вычисляет p-value — вероятность того, что такое распределение возникло случайно. Очень низкое p-value (например, < 0.05) является сильным признаком наличия скрытых данных.

Программа ChameleonLab. Гистограммы яркости
Программа ChameleonLab. Гистограммы яркости
Программа ChameleonLab. Карта разницы (Heatmap)
Программа ChameleonLab. Карта разницы (Heatmap)
Программа ChameleonLab. Атака Хи-Квадрат (Chi-Squared)
Программа ChameleonLab. Атака Хи-Квадрат (Chi-Squared)
analysis_utils.py
import numpy as np
from scipy.stats import chi2

def chi_squared_attack(image: np.ndarray) -> tuple:
    # ...
    # Извлекаем LSB плоскость (массив из 0 и 1)
    lsb_bits = (image.flatten().astype(np.uint8)) & 1

    # Считаем количество 0 и 1
    n1 = np.sum(lsb_bits)
    n0 = lsb_bits.size - n1

    expected = lsb_bits.size / 2.0
    
    chi2_stat = ((n0 - expected)**2 / expected) + ((n1 - expected)**2 / expected)

    p_value = 1.0 - chi2.cdf(chi2_stat, 1) # 1 степень свободы

    return chi2_stat, p_value, [(n0, n1)]
  1. Метрики качества (MSE, PSNR, SNR):

    • MSE (Mean Squared Error): Среднеквадратичная ошибка. Чем она ниже, тем меньше отличий.

    • PSNR (Peak Signal-to-Noise Ratio): Пиковое отношение сигнала к шуму. Универсальная метрика для оценки искажений. Значения выше 35-40 дБ обычно говорят о том, что изменения незаметны для глаза.

  2. RS-анализ и анализ ДКП: Это более продвинутые методы. RS-анализ позволяет оценить примерную длину встроенного сообщения, анализируя группы пикселей. Анализ ДКП (дискретное косинусное преобразование) эффективен для поиска стеганографии в JPEG-файлах, так как он выявляет аномалии в частотных коэффициентах.

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

Бонус: Визуализатор LSB — погружение на битовый уровень

Иногда графиков и метрик недостаточно — хочется своими глазами увидеть, что же происходит с пикселями. Для этого я добавил отдельную вкладку — "Визуализатор LSB".

Это интерактивный инструмент, который отображает увеличенную сетку пикселей для оригинального и стего-изображения. При наведении курсора на пиксель в специальной панели отображается полная информация:

  • Координаты пикселя (X, Y).

  • RGB-значения для оригинала и стего-файла.

  • Полное бинарное представление каждого цветового канала (R, G, B) для обоих изображений.

Самое главное — в бинарном представлении те самые наименее значащие биты, которые мы модифицировали, подсвечиваются другим цветом. Это превращает абстрактный алгоритм LSB в наглядный и понятный процесс. Можно буквально "увидеть" спрятанную информацию, сравнивая бинарные строки. Этот инструмент оказался невероятно полезным для отладки и в образовательных целях.

Реализовано это через кастомный виджет PixelGridView, который отрисовывает сетку и отслеживает положение мыши, а форматирование бинарной строки с подсветкой делается с помощью простого HTML.

Программа ChameleonLab. Визуализатор LBS
Программа ChameleonLab. Визуализатор LBS

Где скачать программу?

Для всех, кто хочет просто попробовать приложение в действии, не углубляясь в исходный код, доступна готовая сборка под Windows 10, 11. Скачать последнюю версию ChameleonLab можно по этой ссылке: ChameleonLab 1.2.0.1

Заключение

Проект "ChameleonLab" стал для меня отличной возможностью погрузиться в мир стеганографии, поработать с обработкой изображений, криптографией и собрать все это в едином десктопном приложении. PyQt6 показал себя как мощный и гибкий инструмент, а связка с NumPy и Matplotlib позволяет реализовывать сложную вычислительную логику и визуализацию.

Конечно, есть куда расти: можно добавить поддержку других стеганографических алгоритмов (например, F5 для JPEG), реализовать сетевую стеганографию или улучшить методы анализа.

Надеюсь, эта статья была для вас интересной. Спасибо за внимание! Буду рад ответить на вопросы в комментариях.

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


  1. Vladislav_Dudnikov
    19.08.2025 16:43

    А исходники открыты?


  1. Lomakn Автор
    19.08.2025 16:43

    Здравствуйте. Пока исходники не выкидываем, но думаю выкинем. Надо сделать программу народной. На днях новая версия с крупными обновлениями и фиксами и новая статья.


  1. Lomakn Автор
    19.08.2025 16:43

    Наша вторая статья и крупное обновление: https://habr.com/ru/articles/938868/


  1. Demmidovich
    19.08.2025 16:43

    Очень интересная статья и разработан на программа. Пытался и сам сделать что то подобное, но ни как. Можете подробней рассказать о методике работы с docx форматом. Просто когда я побывал добавлять, то проблема в том, что офис говорит что что-нитак


    1. Lomakn Автор
      19.08.2025 16:43

      • Спасибо за интерес проявленный к статье. Постараемся тогда сделать более подобную реализацию метода внедрения в офисные документы с программный кодом