Привет, Хабр! Представьте себе сценарий: вы — аналитик в области цифровой криминалистики. Вам на стол попадает, казалось бы, совершенно безобидное фото из отпуска, изъятое у объекта расследования. На первый взгляд — обычный пейзаж. Анализ пикселей не даёт ничего — нет никаких признаков классической стеганографии. Но интуиция подсказывает: что-то здесь не так.

Что, если секрет спрятан не в самом изображении, а в его «паспорте» — служебных метаданных? Именно здесь, в полях EXIF, и начинается наша охота на цифровых призраков. Сегодня мы погрузимся в мир стегоанализа и научимся вскрывать тайны, которые могут хранить в себе метаданные фотографий.

Где обитает EXIF?

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

  • Изображения: JPEG, TIFF, HEIC/HEIF (формат Apple), WebP и PNG (хотя в PNG для этого есть и свои, более нативные форматы метаданных).

  • RAW-форматы: CR2 (Canon), NEF (Nikon), ARW (Sony) и другие форматы несжатых изображений с камер.

  • Аудио: WAV файлы также могут содержать EXIF-информацию, например, для описания устройства записи.

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

Программа "ChameleonLab". Просмотр метаданных (EXIF)
Программа "ChameleonLab". Просмотр метаданных (EXIF)

Что мы ищем?

Стегоанализ — это не поиск конкретного текста. Это поиск аномалий. Наша задача — найти то, что выделяется на фоне обычных, стандартных метаданных, которые создаёт камера или фоторедактор. Существует несколько подходов, от самых простых до более изощрённых. Давайте рассмотрим их по порядку с примерами на Python.

Для работы с EXIF нам понадобится библиотека piexif.

import piexif
import numpy as np

def get_exif_dict(filepath: str):
    """Извлекает EXIF-данные из файла в виде словаря."""
    try:
        exif_dict = piexif.load(filepath)
        return exif_dict
    except Exception:
        return None

def get_tag_content(exif_dict, ifd_name, tag_name):
    """Получает содержимое конкретного тега."""
    if not exif_dict or ifd_name not in exif_dict:
        return None
    
    # Находим код тега по его имени
    tag_code = piexif.TAGS[ifd_name][tag_name]["code"]
    if tag_code in exif_dict[ifd_name]:
        return exif_dict[ifd_name][tag_code]
    return None

Метод 1: Визуальный осмотр (The Naive Approach)

Самый первый и очевидный шаг — просто посмотреть на все EXIF-теги и их содержимое. Что должно нас насторожить?

  • Аномально длинные строки: В поле Artist (Автор) или Model (Модель камеры) вряд ли будет находиться текст объёмом в несколько килобайт.

  • Бессмысленный набор символов: Если в поле Copyright вы видите что-то вроде LOREMIPSUMDOLORSITAMET..., это нормально. А вот если там aG9sYSBtdW5kbw==, это уже похоже на Base64.

  • Нестандартные теги: EXIF поддерживает пользовательские теги. Их наличие — не всегда признак стеганографии, но повод присмотреться.

Давайте посмотрим, как это выглядит. Допустим, у нас есть изображение stego_image.jpg, в UserComment которого спрятаны данные.

# stego_image.jpg - изображение с внедренными данными
exif_data = get_exif_dict("stego_image.jpg")

if exif_data:
    user_comment = get_tag_content(exif_data, "Exif", "UserComment")
    if user_comment:
        # Первые 8 байт в этом теге обычно указывают кодировку, их можно пропустить
        print(user_comment[8:]) 
        # Вывод: b'CHLB\x01\x80\x04\x95\xd5\x02\x00\x00...'

Даже без глубокого анализа мы видим странные байты, начинающиеся с CHLB. Это наша "кроличья нора".

Метод 2: Статистический анализ (Энтропия)

Обычный текст (например, "My Photo") имеет низкую энтропию — символы в нём предсказуемы. А вот зашифрованные или сжатые данные, наоборот, стремятся к максимальной энтропии, так как каждый байт в них выглядит случайным. Это наш главный козырь.

Мы можем посчитать энтропию для содержимого каждого EXIF-тега. Если она аномально высокая (близкая к 8.0 для байтовых данных), это — серьёзный "красный флаг".

Вот функция для расчёта энтропии Шеннона, похожая на ту, что используется в analysis_utils.py нашего проекта:

def calculate_entropy(data: bytes) -> float:
    """Расчет энтропии Шеннона для байтовой строки."""
    if not data:
        return 0.0
    
    # Считаем количество каждого байта
    byte_counts = np.bincount(np.frombuffer(data, dtype=np.uint8), minlength=256)
    
    # Убираем нулевые значения, чтобы избежать ошибки логарифмирования
    counts = byte_counts[byte_counts > 0]
    
    # Рассчитываем вероятности
    probabilities = counts / len(data)
    
    # Считаем энтропию
    entropy = -np.sum(probabilities * np.log2(probabilities))
    return float(entropy)

Теперь применим это на практике:

# Допустим, мы извлекли два тега
normal_comment = b"This is a normal comment."
# Это base64 от "Hello, Habr!" с небольшим "шумом"
suspicious_comment = b"SGVsbG8sIEhhYnIhSGVsbG8sIEhhYnIhSGVsbG8sIEhhYnIh" 

print(f"Энтропия обычного комментария: {calculate_entropy(normal_comment):.4f}")
# Вывод: Энтропия обычного комментария: 4.1454

print(f"Энтропия подозрительного комментария: {calculate_entropy(suspicious_comment):.4f}")
# Вывод: Энтропия подозрительного комментария: 5.6582

Разница очевидна. Если бы suspicious_comment был зашифрован, его энтропия была бы ещё выше (ближе к 7.5-8.0).

Метод 3: Поиск сигнатур

Многие стеганографические инструменты оставляют "подписи" или "магические числа", чтобы потом найти свои же данные. Например, в нашем проекте Chameleon мы используем сигнатуру CHLB () или CHAMELEON ().

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

  • Заголовки файлов: Наличие в EXIF-теге байт PK (заголовок ZIP-архива) или 7z — явный признак сокрытия файла.

  • Base64-подобные структуры: Длинные строки из символов [A-Za-z0-9+/] с символами = в конце.

def find_signatures(filepath: str):
    """Ищет известные сигнатуры в EXIF-тегах."""
    exif_data = get_exif_dict(filepath)
    if not exif_data:
        print("EXIF не найден.")
        return

    known_signatures = {
        b'CHLB': "Chameleon Stego Signature",
        b'PK\x03\x04': "ZIP Archive Header"
    }

    for ifd in exif_data:
        if ifd == "thumbnail":
            continue
        for tag, content in exif_data[ifd].items():
            if isinstance(content, bytes):
                for sig, name in known_signatures.items():
                    if sig in content:
                        tag_name = piexif.TAGS[ifd].get(tag, {"name": "Unknown"})["name"]
                        print(f"Найдена сигнатура! '{name}' в теге '{tag_name}'")

# find_signatures("stego_image.jpg")
# Вывод: Найдена сигнатура! 'Chameleon Stego Signature' в теге 'UserComment'

Метод 4: Анализ "чёрного ящика" — MakerNote

Тег MakerNote — это мечта для стеганографа и головная боль для аналитика. Каждый производитель камер (Canon, Nikon, Sony) записывает туда служебную информацию в своём собственном, часто бинарном и недокументированном формате.

  • Почему это идеальное место для сокрытия?

    • Большой размер: Тег может содержать десятки килобайт данных.

    • Игнорируется большинством программ: Стандартные просмотрщики его не показывают.

    • Высокая энтропия по умолчанию: Данные в нём уже бинарные, и добавление зашифрованного блока не вызовет подозрений на основе одной лишь энтропии.

  • Как его анализировать? Прямой анализ сложен. Самый надёжный метод — сравнительный. Если у вас есть два фото, сделанных одной и той же камерой с одинаковыми настройками, но в одном из них вы подозреваете стеганограмму, можно сравнить их теги MakerNote. Значительное расхождение в размере — веский повод для беспокойства.

Практическое применение: ChameleonLab

Все описанные выше подходы — не просто теория. Чтение EXIF-данных, статистический анализ и поиск сигнатур реализованы в нашем проекте ChameleonLab. Это кросс-платформенная образовательная лаборатория для стеганографии, которая работает на Windows и macOS. Она полностью бесплатная и создана для того, чтобы каждый мог наглядно изучить методы обнаружения данных.

Официальный сайт проекта: https://chalab.ru/

Заключение

Стегоанализ EXIF — это увлекательный процесс, похожий на расследование. Он наглядно показывает, что для сокрытия данных не всегда нужно изменять пиксели самого изображения. Эффективный поиск в метаданных — это комбинация методов:

  • Начинаем с простого: Визуально осматриваем теги на предмет очевидных аномалий.

  • Углубляемся в математику: Проверяем энтропию содержимого тегов. Высокие значения — наш главный индикатор.

  • Ищем отпечатки пальцев: Ищем известные сигнатуры и заголовки файлов.

  • Заглядываем в тёмные углы: Не забываем про MakerNote, хоть это и сложная задача.

Создание автоматизированного инструмента, который бы проходил по всем этим шагам и выдавал отчёт о "подозрительности" файла — отличная задача для программиста, интересующегося информационной безопасностью. Надеюсь, это руководство станет для вас хорошей отправной точкой в мире охоты на цифровых призраков!

Следите за нашими экспериментами и обновлениями проекта на нашем официальном сайте и Telegram-канале!

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


  1. qw1
    26.09.2025 12:37

    Наивные кошки-мышки. Я вот добавил в конец jpeg-файла, перед маркером EOI кучу рандомных данных, и всё выглядит хорошо: структура файла осталась корректной, никаких лишних тегов нет. И обнаружить это можно лишь написав свой декодер, который увидит, что после декодирования всей картинки остались "лишние" байты в блоке SOS.
    Аналогично можно вставить что угодно в конец Huffman-таблиц, и без полного разбора этих таблиц "криминалист" ничего не найдёт.


  1. berez
    26.09.2025 12:37

    MakerNote в большинстве случаев - это как бы "exif внутри exif", только со своими вендор-специфичными тегами: https://exiv2.org/tags-nikon.html

    Поэтому проверить валидность MakerNotes более-менее реально. Если вместо IFD там лежит некий бинарный блоб - это сигнальчик, что что-то там нехорошо.

    А вот в thumbnail можно запихать довольно крупный кусок бинарных данных. Там, конечно, в большинстве случаев лежит превьюшка в виде мелкого jpeg, но вместо нее можно положить и скрытые данные (до 64 кб).