Автор: raoulduke666
Написано специально для xss.pro (c)
Привет, читатель.
Статья больше про исследование и тестирование метода, я не пытаюсь навязать все нижеописанное и не заявляю что это 100% рабочее шифрование. Но как по мне идея шифрования через обобщение нейросетей достойна внимания.
Поэтому чтобы это работало, yolo-seg должен быть обучен на своем датасете. Одна и та же модель должна быть у обоих участников. Веса модели и датасет должны храниться в строгом секрете.
Любой человек в здравом уме скажет, что yolo не сможет уникально сегментировать изображения и на этом моменте закроет статью.
Эмбеддинги из 4 и 16 слоя это 192 числа, типа float32, обычно в диапазоне от -10 до 10. Самый важный момент, который позволяет создать уникальный ключ для каждого изображения. По крайней мере это позволяет учитывать малейшие изменения в изображении. Учитывается все изображение, сам класс и его фон. Даже если меняется всего 1 пиксель, создается уникальный ключ.
Провел тест из 1000 одинаковых изображений где рандомно заменены 1-5 пикселей. Результат в статье.
На 2 разных системах всегда генерируются одинаковые ключи к одним и тем же изображениям. Самое важное условие чтобы на изображении был класс, который может быть сегментирован, иначе ключа просто не будет и сообщение нельзя будет отправить.
Чтобы расшифровать сообщение нужно получить изображение, на основе которого был получен ключ и само зашифрованное сообщение. Через такую же модель yolo-seg получаются эмбеддинги чтобы повторить ключ.
Эмбеддинги — это компактное числовое представление изображения, которое извлекается из внутренних слоёв нейронной сети. В контексте метода они являются результатом обработки изображения моделью yolo11n-seg и представляют собой массив из 192 чисел с плавающей запятой (float32). Эти числа описывают различные аспекты изображения: объект (например, кот), фон, текстуры, края и даже отдельные пиксели. Каждое число отражает вклад определённой особенности изображения, выявленной нейросетью. Но самое важное что каждое число имеет свой диапазон влияния. И этот уровень влияния определяется датасетом, обучением, разметкой, пайлпайном обучения и инференса.
В моём методе эмбеддинги состоят из двух частей:
Сперва загружается изображение.
Изображение (maryJane.png размером 1088x1920 пикселей) загружается и преобразуется в тензор с тремя каналами (RGB). Тензор имеет размер [1, 3, 1088, 1920], где:
Модель прогоняет тензор через свои слои. Каждый слой выполняет операции свёртки, нормализации и активации, создавая всё более абстрактные представления изображения.
На выходе слоя 4 получается тензор размером [1, 128, 34, 60], где:
Я использую механизм хуков (register_forward_hook) для получения активаций слоёв 4 и 16 во время обработки изображения. Это позволяет заглянуть внутрь модели и извлечь промежуточные данные, которые более уникальны чем итоговый результат сегментации.
Усреднение активаций.
Чтобы получить массив из 192 чисел (128 + 64), активации каждого слоя усредняются по пространственным измерениям (высоте и ширине).
Округление.
На разных системах результат может отличаться. Вычисления на GPU и CPU дают разные результаты. Поэтому они округляются до 4 знаков после запятой. Это по прежнему сохраняет устойчивость к изменению даже 1 пикселя.
Почему выбраны слои 4 и 16?
Выбор слоёв 4 и 16 не случаен.
Чувствительность YOLO11n-seg.
Изменение одного пикселя, например с [100, 100, 100] на [101, 100, 100]) влияет на активации слоя 4, потому что он анализирует локальные детали. Это изменение распространяется на тензор [1, 128, 34, 60], слегка сдвигая некоторые значения.
Слой 16 менее чувствителен к отдельным пикселям, но может уловить изменения, если они влияют на форму объекта или контекст
После усреднения сдвиг в активациях приводит к изменению одного или нескольких чисел в эмбеддингах.
Работа с SHA-256.
Эмбеддинги (192 числа) преобразуются в байты и хешируются с помощью SHA-256.
SHA-256 чувствителен к входным данным, изменение одного числа в эмбеддингах на 0.0001 приводит к совершенно другому хешу. Это гарантирует, что ключ и IV для каждой версии изображения будут уникальными.
Пример.
Чтобы проверить метод я сгенерировал 1000 копий изображений своей кошки, заменив на каждом по 1-5 пикселей рандомно, RGB-значения выбирались случайно.
Как только изображения сгенерировались, запускаю код который симулирует работу шифрования, а в конце сравнивает результаты. Если хотя бы 1 раз результаты повторятся, метод не рабочий. Можно попробовать учесть эмбеддинги из еще одного слоя, но пока нет нужды делать этого не нужно.
Мы все прекрасно понимаем, что метод и так не рабочий, рано или поздно копии ключей случатся, но ведь интересно когда это случится. По большей части это обусловлено обучением и датасетом, чем больше и разнообразнее тем лучше результат в плане уникальности в теории. Но есть еще очень большое количество параметров, которые могут влиять на это.
Размер обучаемой модели.
Разнообразие датасета и общее количество изображений.
Качество разметки.
Пайплайн обучения.
Пайплайн инференса.
Теперь проведем тест.
Результат ?
Тестирование пройдено успешно, 0 копий. В следующем попробую собрать просто разные, но похожие изображения, без замены пикселей. Для меня такой результат говорит о том что метод имеет место быть или как минимум еще одно тестирование можно провести.
Датасет частично должен быть собран в реальной жизни буквально. Можно шифроваться на чем угодно. Какие самые популярные изображения в сети ? Голые девки и кошки. Уловили ? Идете на улице и делаете фотографии котов
Затем мешаете с фотографиями из сети, желательно так чтобы зацепить в датасет как можно больше пород.
Понимаю, что это та еще задница, но метод считается "околобезопасным" только при таких условиях.
Если размечать лень берите самую маленькую модель и делайте сегментацию на 500 изображениях. Тут можно сделать разметку намеренно криво, в теории признаки должны стать более непредсказуемыми. А чтобы модель сегментировала все изображения, то нужно выкрутить conf=0.2, так появится неуверенная, кривая, непредсказуемая сегментация.
Даже если второе тестирование будет тоже успешным и получится доказать решение проблем с коллизией окончательно. То это не отменяет того факта, что если атакующий получит доступ к модели, то вся переписка тут же будет расшифрована. Поэтому ему нужна не только модель, но и пайплайн инференса и вообще код в целом. Иначе взлом шифра займет какое то время. Откуда атакующий узнает про 4 и 16 слой ? Из этой статьи
, но это уже другая история.
В предыдущих разделах я описал, как эмбеддинги, полученные из модели yolo11n-seg, используются для генерации ключа. Теперь я опишу полный код шифрования и расшифровки, а затем подробно объясню, как он работает, опираясь на принципы, описанные ранее.
Код показывает, как изображение превращается в ключ для шифрования сообщения через AES-CBC, и как то же изображение используется для расшифровки.
Код реализует метод шифрования, описанный в статье, и состоит из трёх ключевых функций:
В начале кода включается детерминизм для PyTorch, чтобы эмбеддинги были одинаковыми на разных системах.
Как писал ранее в разделе про эмбеддинги, это устраняет вычислительные различия между CPU и GPU, обеспечивая одинаковые ключи для одного изображения. Без детерминизма эмбеддинги могут слегка отличаться, что приведёт к неверному ключу при расшифровке.
Обработка изображения (process_image)
Функция process_image загружает изображение и извлекает эмбеддинги из слоёв 4 и 16 модели YOLO11n-seg.
Загрузка. Изображение (maryJane.png, 1088x1920) читается с помощью OpenCV и преобразуется в тензор [1, 3, 1088, 1920]. Размер 1088 кратен 32, так требует архитектура yolo.
Извлечение активаций. Хуки (register_forward_hook) захватывают активации слоёв 4 и 16
На выходе. Слой 4 - тензор [1, 128, 34, 60], слой 16 - [1, 64, 17, 30].
Усреднение. Активации усредняются по высоте и ширине, давая 128 чисел (слой 4) и 64 числа (слой 16). В итоге 192 числа (float32, диапазон -10 до 10).
Функция возвращает эмбеддинги, которые затем используются для генерации ключа.
Генерация ключа и IV (generate_key_and_iv)
Функция преобразует эмбеддинги в ключ (16 байт) и IV (16 байт) для AES-CBC.
Объединение. Эмбеддинги (128 + 64 числа) объединяются в массив из 192 чисел.
Округление. Числа округляются до 4 знаков (decimals=4), чтобы устранить шум, сохраняя чувствительность к пикселям. Еще я это делал для того чтобы запустить на разных системах.
Хеширование. Массив преобразуется в байты и хешируется через SHA-256, давая 32 байта. Первые 16 байт — ключ, следующие 16 — IV.
SHA-256 обеспечивает уникальность хэша. Изменение одного пикселя меняет эмбеддинги.
Шифрование (encrypt_data)
Функция шифрует сообщение через AES-CBC.
Код.
Вход. Сообщение ("xss.pro".encode()), ключ и IV.
Паддинг. Сообщение дополняется до кратности 16 байт.
Шифрование. AES-CBC шифрует сообщение, используя ключ и IV
Выход. "9c901f2b17cd0a91a9ef3995310db530"
Настройка детерминизма
Как и в шифровании, код начинается с настройки детерминизма PyTorch.
Это гарантирует, что эмбеддинги, полученные из изображения (maryJane.png), будут такими же, как при шифровании. Без детерминизма даже незначительные вычислительные различия при вычислениях на CPU и GPU дадут разные эмбеддинги, что приведёт к неверному ключу и провалу расшифровки.
Обработка изображения (process_image)
Функция process_image идентична той, что используется в шифровании. Она загружает изображение и извлекает эмбеддинги из слоёв 4 и 16 модели.
Функция возвращает эмбеддинги, которые должны совпадать с теми, что использовались при шифровании, если изображение не изменилось.
Генерация ключа и IV (generate_key_and_iv)
Функция идентична шифрованию.
Расшифровка (decrypt_data)
Функция восстанавливает сообщение через AES-CBC.
Вход. Зашифрованное сообщение (например, 9c901f2b17cd0a91a9ef3995310db530), ключ и IV.
Расшифровка. AES-CBC декодирует данные, используя ключ и IV, полученные от эмбеддингов.
Удаление паддинга. Функция unpad убирает дополнение, добавленное при шифровании.
Выход. Исходное сообщение. "xss.pro"
Возможно будет 2 часть с автоматизацией разметки и изоляцией ИИ.
Написано специально для xss.pro (c)
Привет, читатель.
Статья больше про исследование и тестирование метода, я не пытаюсь навязать все нижеописанное и не заявляю что это 100% рабочее шифрование. Но как по мне идея шифрования через обобщение нейросетей достойна внимания.
Обзор метода.
Само сообщение шифруется через симметричное шифрование AES-CBC, где нужен ключ (16 байт) и IV (начальный вектор, чтобы шифр был разным). Ключ генерируется из эмбеддингов, полученных моделью. Но особенность метода в том, что изображение не является секретом, любой желающий может увидеть изображение и зашифрованное сообщение. Но при этом изображение не является публичным ключом. Ключ может получить только конкретная модель нейросети.
Поэтому чтобы это работало, yolo-seg должен быть обучен на своем датасете. Одна и та же модель должна быть у обоих участников. Веса модели и датасет должны храниться в строгом секрете.
Любой человек в здравом уме скажет, что yolo не сможет уникально сегментировать изображения и на этом моменте закроет статью.
Эмбеддинги из 4 и 16 слоя это 192 числа, типа float32, обычно в диапазоне от -10 до 10. Самый важный момент, который позволяет создать уникальный ключ для каждого изображения. По крайней мере это позволяет учитывать малейшие изменения в изображении. Учитывается все изображение, сам класс и его фон. Даже если меняется всего 1 пиксель, создается уникальный ключ.
Провел тест из 1000 одинаковых изображений где рандомно заменены 1-5 пикселей. Результат в статье.
На 2 разных системах всегда генерируются одинаковые ключи к одним и тем же изображениям. Самое важное условие чтобы на изображении был класс, который может быть сегментирован, иначе ключа просто не будет и сообщение нельзя будет отправить.
Чтобы расшифровать сообщение нужно получить изображение, на основе которого был получен ключ и само зашифрованное сообщение. Через такую же модель yolo-seg получаются эмбеддинги чтобы повторить ключ.
Эмбеддинги yolo11n-seg.
В основе метода лежит использование эмбеддингов, полученных из модели YOLO11n-seg, для генерации ключа из изображения. Эмбеддинги представляют собой числовое описание изображения, которое позволяет учесть как общие характеристики (например, объект на изображении), так и мелкие детали (изменение одного пикселя). В этом разделе я подробно объясню, что такое эмбеддинги, как они формируются в модели YOLO11n-seg, почему выбраны слои 4 и 16, как обеспечивается их чувствительность к изменениям изображения и как они используются для создания уникального ключа.
Эмбеддинги — это компактное числовое представление изображения, которое извлекается из внутренних слоёв нейронной сети. В контексте метода они являются результатом обработки изображения моделью yolo11n-seg и представляют собой массив из 192 чисел с плавающей запятой (float32). Эти числа описывают различные аспекты изображения: объект (например, кот), фон, текстуры, края и даже отдельные пиксели. Каждое число отражает вклад определённой особенности изображения, выявленной нейросетью. Но самое важное что каждое число имеет свой диапазон влияния. И этот уровень влияния определяется датасетом, обучением, разметкой, пайлпайном обучения и инференса.
В моём методе эмбеддинги состоят из двух частей:
- 128 чисел из 4 слоя. Отвечает за мелкие детали изображения, фон
- 64 числа из 16 слоя, Описывает более крупные структуры, такие как форма класса, его цвет.
Если изменить один пиксель изображения, например, с [100, 100, 100] на [101, 100, 100], некоторые числа в эмбеддингах слегка изменятся:[0.1234, -0.5678, 1.2345, -0.0987, 2.3456, 0.6789, -1.5432, 0.4321, ...]
Эти небольшие изменения играют ключевую роль: они обеспечивают чувствительность метода к минимальным различиям в изображении. Это позволяет генерировать уникальный ключ для каждой версии изображения.[0.1235, -0.5678, 1.2344, -0.0987, 2.3457, 0.6789, -1.5431, 0.4321, ...]
Формирование эмбеддингов.
YOLO11n-seg изначально нейронная сеть, предназначенная для обнаружения и сегментации объектов. Она состоит из 23 слоёв, каждый из которых обрабатывает изображение на разном уровне абстракции. В моём методе я использую не итоговые маски сегментации, а промежуточные активации двух слоёв — 4 и 16, которые извлекаются во время обработки изображения.
Сперва загружается изображение.
Изображение (maryJane.png размером 1088x1920 пикселей) загружается и преобразуется в тензор с тремя каналами (RGB). Тензор имеет размер [1, 3, 1088, 1920], где:
- 1 количество изображений,
- 3 каналы цвета,
- 1088 потому что 1080 не кратно 32, а самое близкое число 1088. Это связано с архитектурой модели.
Модель прогоняет тензор через свои слои. Каждый слой выполняет операции свёртки, нормализации и активации, создавая всё более абстрактные представления изображения.
На выходе слоя 4 получается тензор размером [1, 128, 34, 60], где:
- 128 — количество каналов,
- 34x60 — уменьшенное пространственное разрешение изображения.
- 64 — каналы,
- 17x30 — ещё меньшее разрешение.
Я использую механизм хуков (register_forward_hook) для получения активаций слоёв 4 и 16 во время обработки изображения. Это позволяет заглянуть внутрь модели и извлечь промежуточные данные, которые более уникальны чем итоговый результат сегментации.
Python:
activations = {}
layers[4].register_forward_hook(lambda m, i, o: activations.update({"layer_4": o}))
layers[16].register_forward_hook(lambda m, i, o: activations.update({"layer_16": o}))
with torch.no_grad():
model(image, conf=0.4, imgsz=(1088, 1920))
Усреднение активаций.
Чтобы получить массив из 192 чисел (128 + 64), активации каждого слоя усредняются по пространственным измерениям (высоте и ширине).
- Для слоя 4: тензор [1, 128, 34, 60] → массив [128] Усреднение по 34x60,
- Для слоя 16: тензор [1, 64, 17, 30] → массив [64] Усреднение по 17x30
Python:
embeddings = [activations[k].mean(dim=[2, 3]) for k in ["layer_4", "layer_16"]]
Округление.
На разных системах результат может отличаться. Вычисления на GPU и CPU дают разные результаты. Поэтому они округляются до 4 знаков после запятой. Это по прежнему сохраняет устойчивость к изменению даже 1 пикселя.
Python:
flat_embeddings = np.round(torch.cat(embeddings).cpu().numpy(), decimals=4)
Почему выбраны слои 4 и 16?
Выбор слоёв 4 и 16 не случаен.
- Слой 4: Ранний слой модели, который обрабатывает изображение на низком уровне. Он чувствителен к мелким деталям, таким как отдельные пиксели, края, текстуры и локальные цветовые переходы. Активации слоя 4 содержат 128 каналов, что позволяет уловить широкий спектр низкоуровневых признаков.
- Слой 16: Более глубокий слой, который анализирует изображение на высоком уровне. Он распознаёт крупные структуры, такие как форма объекта (например, кота), его поза и общий контекст сцены. Активации слоя 16 содержат 64 канала, что достаточно для описания объектов, но менее детализировано, чем слой 4.
Чувствительность YOLO11n-seg.
Изменение одного пикселя, например с [100, 100, 100] на [101, 100, 100]) влияет на активации слоя 4, потому что он анализирует локальные детали. Это изменение распространяется на тензор [1, 128, 34, 60], слегка сдвигая некоторые значения.
Слой 16 менее чувствителен к отдельным пикселям, но может уловить изменения, если они влияют на форму объекта или контекст
После усреднения сдвиг в активациях приводит к изменению одного или нескольких чисел в эмбеддингах.
Работа с SHA-256.
Эмбеддинги (192 числа) преобразуются в байты и хешируются с помощью SHA-256.
SHA-256 чувствителен к входным данным, изменение одного числа в эмбеддингах на 0.0001 приводит к совершенно другому хешу. Это гарантирует, что ключ и IV для каждой версии изображения будут уникальными.
Python:
hash_digest = hashlib.sha256(flat_embeddings.tobytes()).digest()
key = hash_digest[:16] # Ключ (16 байт)
iv = hash_digest[16:32] # IV (16 байт)
Пример.
- Исходное изображение, эмбеддинги [0.1234, -0.5678, 1.2345, ...]. Ключ c87bce1c05dd27f6f9c256399c43aade.
- Изображение с изменённым пикселем, эмбеддинги [0.1235, -0.5678, 1.2344, ...]. Ключ 1279567999291717328cd268d7bde881.
-
Тестирование уникальности ключа.
Как обсуждалось выше, ключевая проблема, уникальность ключей для каждого изображения.
Чтобы проверить метод я сгенерировал 1000 копий изображений своей кошки, заменив на каждом по 1-5 пикселей рандомно, RGB-значения выбирались случайно.
Python:
import os
import random
from PIL import Image
def modify_random_pixels(image_path, output_dir, iterations):
# Открываем исходное изображение
original_img = Image.open(image_path)
width, height = original_img.size
img_format = original_img.format # Переименовано, чтобы избежать конфликта с функцией format()
# Создаем папку для результатов, если её нет
os.makedirs(output_dir, exist_ok=True)
for i in range(iterations):
# Создаем копию исходного изображения
modified_img = original_img.copy()
pixels = modified_img.load()
# Определяем количество пикселей для изменения (1-5)
num_pixels_to_change = random.randint(1, 5)
for _ in range(num_pixels_to_change):
# Выбираем случайный пиксель
x = random.randint(0, width - 1)
y = random.randint(0, height - 1)
# Генерируем случайный цвет
if modified_img.mode == 'RGB':
new_color = (
random.randint(0, 255),
random.randint(0, 255),
random.randint(0, 255)
)
elif modified_img.mode == 'RGBA':
new_color = (
random.randint(0, 255),
random.randint(0, 255),
random.randint(0, 255),
pixels[x, y][3] # Сохраняем альфа-канал
)
else:
# Для других режимов (например, grayscale) преобразуем в RGB
modified_img = modified_img.convert('RGB')
pixels = modified_img.load()
new_color = (
random.randint(0, 255),
random.randint(0, 255),
random.randint(0, 255)
)
# Изменяем пиксель
pixels[x, y] = new_color
# Сохраняем результат
base_name = os.path.splitext(os.path.basename(image_path))[0]
output_path = os.path.join(output_dir, f"{base_name}_mod_{i+1}.{img_format.lower()}")
modified_img.save(output_path, format=img_format)
print(f"Создано: {output_path} (изменено {num_pixels_to_change} пикселей)")
if __name__ == "__main__":
input_image = "images/maryJane.png" # Путь к исходному изображению
output_directory = "new_images" # Папка для сохранения результатов
num_iterations = 1000 # Количество изображений для генерации
modify_random_pixels(input_image, output_directory, num_iterations)
Как только изображения сгенерировались, запускаю код который симулирует работу шифрования, а в конце сравнивает результаты. Если хотя бы 1 раз результаты повторятся, метод не рабочий. Можно попробовать учесть эмбеддинги из еще одного слоя, но пока нет нужды делать этого не нужно.
Мы все прекрасно понимаем, что метод и так не рабочий, рано или поздно копии ключей случатся, но ведь интересно когда это случится. По большей части это обусловлено обучением и датасетом, чем больше и разнообразнее тем лучше результат в плане уникальности в теории. Но есть еще очень большое количество параметров, которые могут влиять на это.
Размер обучаемой модели.
Разнообразие датасета и общее количество изображений.
Качество разметки.
Пайплайн обучения.
Пайплайн инференса.
Теперь проведем тест.
Python:
import cv2
import numpy as np
import torch
from ultralytics import YOLO
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import hashlib
import os
from collections import defaultdict
import time
# Настройка детерминизма
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
torch.use_deterministic_algorithms(True)
# Ограничение числа ядер CPU (например, 2 ядра)
torch.set_num_threads(4) # Уменьшает нагрузку на CPU
def generate_key_and_iv(embeddings):
flat_embeddings = torch.cat([e.flatten() for e in embeddings]).cpu().numpy()
flat_embeddings = np.round(flat_embeddings, decimals=4)
hash_digest = hashlib.sha256(flat_embeddings.tobytes()).digest()
return hash_digest[:16], hash_digest[16:32]
def encrypt_data(data, key, iv):
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
ct_bytes = cipher.encrypt(pad(data, AES.block_size))
return ct_bytes
def process_image(image_path, model):
image = cv2.imread(image_path)
if image is None:
raise ValueError(f"Не удалось загрузить изображение: {image_path}")
original_shape = image.shape[:2]
yolo_size = (max(32, (original_shape[0] + 31) // 32 * 32), max(32, (original_shape[1] + 31) // 32 * 32))
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.uint8)
image_tensor = torch.from_numpy(image_rgb).permute(2, 0, 1).float() / 255.0
image_tensor = image_tensor.clamp(0.0, 1.0).unsqueeze(0).to(model.device)
activations = {}
def get_activation(name):
def hook(module, input, output):
activations[name] = output
return hook
layers = model.model.model
layers[4].register_forward_hook(get_activation("layer_4"))
layers[16].register_forward_hook(get_activation("layer_16"))
with torch.no_grad():
results = model(image, conf=0.4, imgsz=yolo_size)
embeddings = []
for layer_name in ["layer_4", "layer_16"]:
activation = activations.get(layer_name, torch.zeros(1))
embedding = activation.mean(dim=[2, 3]) if len(activation.shape) == 4 else torch.zeros(1)
embeddings.append(embedding)
return embeddings
def test_images(image_folder, model, message, delay=0.5):
results = []
image_files = [f for f in os.listdir(image_folder) if f.endswith('.png')]
if len(image_files) != 1000:
print(f"Предупреждение: Найдено {len(image_files)} изображений, ожидалось 1000")
# Прогоняем каждое изображение
for idx, image_file in enumerate(image_files, 1):
image_path = os.path.join(image_folder, image_file)
print(f"Обработка изображения {idx}/1000: {image_file}")
try:
embeddings = process_image(image_path, model)
key, iv = generate_key_and_iv(embeddings)
ct_bytes = encrypt_data(message, key, iv)
results.append({
'image': image_file,
'key': key.hex(),
'iv': iv.hex(),
'ciphertext': ct_bytes.hex()
})
# Задержка для снижения нагрузки
time.sleep(delay) # Пауза 0.5 секунды между изображениями
except Exception as e:
print(f"Ошибка при обработке {image_file}: {str(e)}")
# Проверяем уникальность ключей
key_to_images = defaultdict(list)
for result in results:
key_to_images[result['key']].append(result['image'])
# Выводим результаты
print("\n=== Результаты теста ===")
print(f"Обработано изображений: {len(results)}")
print(f"Уникальных ключей: {len(key_to_images)}")
duplicates_found = False
for key, images in key_to_images.items():
if len(images) > 1:
duplicates_found = True
print(f"\nОШИБКА: Одинаковый ключ найден для {len(images)} изображений!")
print(f"Ключ: {key}")
print("Изображения:")
for img in images:
print(f" - {img}")
if not duplicates_found:
print("\nУСПЕХ: Все ключи уникальны! Метод прошёл тест.")
else:
print("\nПРОВАЛ: Найдены одинаковые ключи. Метод недостаточно чувствителен!")
# Выводим все результаты
print("\nПодробные результаты:")
for result in results:
print(f"Изображение: {result['image']}")
print(f" Ключ: {result['key']}")
print(f" IV: {result['iv']}")
print(f" Шифротекст: {result['ciphertext']}\n")
return results, duplicates_found
# Основной код
device = torch.device("cpu")
model = YOLO("yolo11n-seg.pt").to(device)
image_folder = "modified_images" # Папка с 1000 изображениями
message = "XSS,is".encode()
delay = 0.5 # Задержка в секундах
results, duplicates_found = test_images(image_folder, model, message, delay)
Результат ?
Тестирование пройдено успешно, 0 копий. В следующем попробую собрать просто разные, но похожие изображения, без замены пикселей. Для меня такой результат говорит о том что метод имеет место быть или как минимум еще одно тестирование можно провести.
Про безопасность.
В своей статье я опираюсь на результаты предобученной модели yolo11n-seg. В боевых условия учить ее нужно самому, на кастомном датасете, объяснять думаю не нужно.
Датасет частично должен быть собран в реальной жизни буквально. Можно шифроваться на чем угодно. Какие самые популярные изображения в сети ? Голые девки и кошки. Уловили ? Идете на улице и делаете фотографии котов
Понимаю, что это та еще задница, но метод считается "околобезопасным" только при таких условиях.
Если размечать лень берите самую маленькую модель и делайте сегментацию на 500 изображениях. Тут можно сделать разметку намеренно криво, в теории признаки должны стать более непредсказуемыми. А чтобы модель сегментировала все изображения, то нужно выкрутить conf=0.2, так появится неуверенная, кривая, непредсказуемая сегментация.
Даже если второе тестирование будет тоже успешным и получится доказать решение проблем с коллизией окончательно. То это не отменяет того факта, что если атакующий получит доступ к модели, то вся переписка тут же будет расшифрована. Поэтому ему нужна не только модель, но и пайплайн инференса и вообще код в целом. Иначе взлом шифра займет какое то время. Откуда атакующий узнает про 4 и 16 слой ? Из этой статьи
Реализация.
В предыдущих разделах я описал, как эмбеддинги, полученные из модели yolo11n-seg, используются для генерации ключа. Теперь я опишу полный код шифрования и расшифровки, а затем подробно объясню, как он работает, опираясь на принципы, описанные ранее.
Код показывает, как изображение превращается в ключ для шифрования сообщения через AES-CBC, и как то же изображение используется для расшифровки.
Шифрование.
Python:
import cv2
import numpy as np
import torch
from ultralytics import YOLO
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import hashlib
# Настройка детерминизма
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
torch.use_deterministic_algorithms(True)
def generate_key_and_iv(embeddings):
flat_embeddings = torch.cat([e.flatten() for e in embeddings]).cpu().numpy()
flat_embeddings = np.round(flat_embeddings, decimals=4)
hash_digest = hashlib.sha256(flat_embeddings.tobytes()).digest()
return hash_digest[:16], hash_digest[16:32]
def encrypt_data(data, key, iv):
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
ct_bytes = cipher.encrypt(pad(data, AES.block_size))
return ct_bytes
def process_image(image_path, model):
image = cv2.imread(image_path)
if image is None:
raise ValueError("Не удалось загрузить изображение!")
original_shape = image.shape[:2]
yolo_size = (max(32, (original_shape[0] + 31) // 32 * 32), max(32, (original_shape[1] + 31) // 32 * 32))
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.uint8)
image_tensor = torch.from_numpy(image_rgb).permute(2, 0, 1).float() / 255.0
image_tensor = image_tensor.clamp(0.0, 1.0).unsqueeze(0).to(model.device)
activations = {}
def get_activation(name):
def hook(module, input, output):
activations[name] = output
return hook
layers = model.model.model
layers[4].register_forward_hook(get_activation("layer_4"))
layers[16].register_forward_hook(get_activation("layer_16"))
with torch.no_grad():
results = model(image, conf=0.4, imgsz=yolo_size)
embeddings = []
for layer_name in ["layer_4", "layer_16"]:
activation = activations.get(layer_name, torch.zeros(1))
embedding = activation.mean(dim=[2, 3]) if len(activation.shape) == 4 else torch.zeros(1)
embeddings.append(embedding)
return embeddings
# Шифрование
device = torch.device("cpu")
model = YOLO("yolo11n-seg.pt").to(device)
image_path = "images/maryJane.png"
message = "xss.pro".encode()
embeddings = process_image(image_path, model)
key, iv = generate_key_and_iv(embeddings)
ct_bytes = encrypt_data(message, key, iv)
# Сохранение шифротекста и IV
with open("ciphertext.bin", "wb") as f:
f.write(iv + ct_bytes) # IV (16 байт) + шифротекст
print("Ключ:", key.hex())
print("IV:", iv.hex())
print("Шифротекст:", ct_bytes.hex())
Код реализует метод шифрования, описанный в статье, и состоит из трёх ключевых функций:
- Обработка изображения (process_image),
- Генерация ключа и IV (generate_key_and_iv),
- Шифрование и расшифровка (encrypt_data, decrypt_data). Я разберу каждую часть, связав её с предыдущими разделами.
В начале кода включается детерминизм для PyTorch, чтобы эмбеддинги были одинаковыми на разных системах.
Python:
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
torch.use_deterministic_algorithms(True)
Как писал ранее в разделе про эмбеддинги, это устраняет вычислительные различия между CPU и GPU, обеспечивая одинаковые ключи для одного изображения. Без детерминизма эмбеддинги могут слегка отличаться, что приведёт к неверному ключу при расшифровке.
Обработка изображения (process_image)
Функция process_image загружает изображение и извлекает эмбеддинги из слоёв 4 и 16 модели YOLO11n-seg.
Загрузка. Изображение (maryJane.png, 1088x1920) читается с помощью OpenCV и преобразуется в тензор [1, 3, 1088, 1920]. Размер 1088 кратен 32, так требует архитектура yolo.
Извлечение активаций. Хуки (register_forward_hook) захватывают активации слоёв 4 и 16
Python:
layers[4].register_forward_hook(get_activation("layer_4"))
layers[16].register_forward_hook(get_activation("layer_16"))
На выходе. Слой 4 - тензор [1, 128, 34, 60], слой 16 - [1, 64, 17, 30].
Усреднение. Активации усредняются по высоте и ширине, давая 128 чисел (слой 4) и 64 числа (слой 16). В итоге 192 числа (float32, диапазон -10 до 10).
Функция возвращает эмбеддинги, которые затем используются для генерации ключа.
Генерация ключа и IV (generate_key_and_iv)
Функция преобразует эмбеддинги в ключ (16 байт) и IV (16 байт) для AES-CBC.
Python:
def generate_key_and_iv(embeddings):
flat_embeddings = torch.cat([e.flatten() for e in embeddings]).cpu().numpy()
flat_embeddings = np.round(flat_embeddings, decimals=4)
hash_digest = hashlib.sha256(flat_embeddings.tobytes()).digest()
return hash_digest[:16], hash_digest[16:32]
Объединение. Эмбеддинги (128 + 64 числа) объединяются в массив из 192 чисел.
Округление. Числа округляются до 4 знаков (decimals=4), чтобы устранить шум, сохраняя чувствительность к пикселям. Еще я это делал для того чтобы запустить на разных системах.
Хеширование. Массив преобразуется в байты и хешируется через SHA-256, давая 32 байта. Первые 16 байт — ключ, следующие 16 — IV.
SHA-256 обеспечивает уникальность хэша. Изменение одного пикселя меняет эмбеддинги.
Шифрование (encrypt_data)
Функция шифрует сообщение через AES-CBC.
Код.
Python:
def encrypt_data(data, key, iv):
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
ct_bytes = cipher.encrypt(pad(data, AES.block_size))
return ct_bytes
Вход. Сообщение ("xss.pro".encode()), ключ и IV.
Паддинг. Сообщение дополняется до кратности 16 байт.
Шифрование. AES-CBC шифрует сообщение, используя ключ и IV
Выход. "9c901f2b17cd0a91a9ef3995310db530"
Расшифровка.
Используется то же самое изображение, модель ИИ и зашифрованное сообщение.
Python:
import cv2
import numpy as np
import torch
from ultralytics import YOLO
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import hashlib
# Настройка детерминизма
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
torch.use_deterministic_algorithms(True)
def generate_key_and_iv(embeddings):
flat_embeddings = torch.cat([e.flatten() for e in embeddings]).cpu().numpy()
flat_embeddings = np.round(flat_embeddings, decimals=4)
hash_digest = hashlib.sha256(flat_embeddings.tobytes()).digest()
return hash_digest[:16], hash_digest[16:32]
def decrypt_data(ct_bytes, key, iv):
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
pt_bytes = unpad(cipher.decrypt(ct_bytes), AES.block_size)
return pt_bytes
def process_image(image_path, model):
image = cv2.imread(image_path)
if image is None:
raise ValueError("Не удалось загрузить изображение!")
original_shape = image.shape[:2]
height, width = original_shape
yolo_size = (max(32, (height + 31) // 32 * 32), max(32, (width + 31) // 32 * 32))
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.uint8)
image_tensor = torch.from_numpy(image_rgb).permute(2, 0, 1).float() / 255.0
image_tensor = image_tensor.clamp(0.0, 1.0).unsqueeze(0).to(model.device)
activations = {}
def get_activation(name):
def hook(module, input, output):
activations[name] = output
return hook
layers = model.model.model
layers[4].register_forward_hook(get_activation("layer_4"))
layers[16].register_forward_hook(get_activation("layer_16"))
with torch.no_grad():
results = model(image, conf=0.4, imgsz=yolo_size)
embeddings = []
for layer_name in ["layer_4", "layer_16"]:
activation = activations.get(layer_name, torch.zeros(1))
if len(activation.shape) == 4:
embedding = activation.mean(dim=[2, 3])
else:
embedding = torch.zeros(1)
embeddings.append(embedding)
return embeddings
# Основной код расшифровки
device = torch.device("cpu")
model = YOLO("yolo11n-seg.pt").to(device)
image_path = "images/mary1_1_modified_10.png"
ct_bytes = bytes.fromhex("9c901f2b17cd0a91a9ef3995310db530") # Замените на реальное значение
# Расшифровка
embeddings = process_image(image_path, model)
key, iv = generate_key_and_iv(embeddings)
decrypted_message = decrypt_data(ct_bytes, key, iv)
print("Расшифрованное сообщение:", decrypted_message.decode())
Настройка детерминизма
Как и в шифровании, код начинается с настройки детерминизма PyTorch.
Python:
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
torch.use_deterministic_algorithms(True)
Это гарантирует, что эмбеддинги, полученные из изображения (maryJane.png), будут такими же, как при шифровании. Без детерминизма даже незначительные вычислительные различия при вычислениях на CPU и GPU дадут разные эмбеддинги, что приведёт к неверному ключу и провалу расшифровки.
Обработка изображения (process_image)
Функция process_image идентична той, что используется в шифровании. Она загружает изображение и извлекает эмбеддинги из слоёв 4 и 16 модели.
Python:
layers[4].register_forward_hook(get_activation("layer_4"))
layers[16].register_forward_hook(get_activation("layer_16"))
Функция возвращает эмбеддинги, которые должны совпадать с теми, что использовались при шифровании, если изображение не изменилось.
Генерация ключа и IV (generate_key_and_iv)
Функция идентична шифрованию.
Python:
def generate_key_and_iv(embeddings):
flat_embeddings = torch.cat([e.flatten() for e in embeddings]).cpu().numpy()
flat_embeddings = np.round(flat_embeddings, decimals=4)
hash_digest = hashlib.sha256(flat_embeddings.tobytes()).digest()
return hash_digest[:16], hash_digest[16:32]
Расшифровка (decrypt_data)
Функция восстанавливает сообщение через AES-CBC.
Python:
def decrypt_data(ct_bytes, key, iv):
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
pt_bytes = unpad(cipher.decrypt(ct_bytes), AES.block_size)
return pt_bytes
Вход. Зашифрованное сообщение (например, 9c901f2b17cd0a91a9ef3995310db530), ключ и IV.
Расшифровка. AES-CBC декодирует данные, используя ключ и IV, полученные от эмбеддингов.
Удаление паддинга. Функция unpad убирает дополнение, добавленное при шифровании.
Выход. Исходное сообщение. "xss.pro"
Возможно будет 2 часть с автоматизацией разметки и изоляцией ИИ.
Последнее редактирование: