Стеганография в GIF

Введение


Приветствую.
Не так давно, когда учился в университете, была курсовая по дисциплине «Программные методы защиты информации». По заданию требовалось сделать программу, внедряющую сообщение в файлы формата GIF. Решил делать на Java.

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



Теоретическая часть


Формат GIF

GIF (англ. Graphics Interchange Format — формат для обмена изображениями) — формат хранения графических изображений, способен хранить сжатые данные без потери качества в формате до 256 цветов. Данный формат был разработан в 1987 году (GIF87a) фирмой CompuServe для передачи растровых изображений по сетям. В 1989-м формат был модифицирован (GIF89a), были добавлены поддержка прозрачности и анимации.

Файлы формата GIF имеют блочную структуру. Данные блоки всегда имеют фиксированную длину (либо она зависит от некоторых флагов), так что ошибиться в том, где какой блок находится, практически невозможно. Структура простейшего неанимированного GIF-изображения формата GIF89a:



Из всех блоков структуры в данном случае нас будут интересовать блок глобальной палитры и параметры, отвечающие за палитру:
  • CT — наличие глобальной палитры. Если этот флаг установлен, то сразу после дескриптора логического экрана должна начинаться глобальная палитра.
  • Size — размер палитры и число цветов картинки. Значения данного параметра:

Size Число цветов Размер палитры, байт
7 256 768
6 128 384
5 64 192
4 32 96
3 16 48
2 8 24
1 4 12
0 2 6


Методы шифрования

В качестве методов зашифрования сообщений в файлах изображений будут использоваться:
  • Метод LSB (Least Significant Bit, наименьший значащий бит)
  • Метод дополнения палитры

Метод LSB — распространенный метод стеганографии. Он заключается в замене последних значащих бит в контейнере (в нашем случае байты глобальной палитры) на биты скрываемого сообщения.

В программе будут использоваться в рамках этого метода два последних бита в байтах глобальной палитры. Это означает, что для 24-битного изображения, где цвет палитры представляет собой три байта для красного, синего, и зеленого цветов, после внедрения сообщения в него, каждая составляющая цвета изменится максимум на 3/255 градации. Такое изменение, во-первых, будет незаметно или труднозаметно для человеческого глаза, а во-вторых, не будет различимо на низкокачественных устройствах вывода информации.

Количество информации будет напрямую зависеть от размера палитры изображения. Поскольку максимальный размер палитры 256 цветов и, если записывать по два бита сообщения в составляющую каждого цвета, то максимальная длина сообщения (при максимальной палитре в изображении) составляет 192 байта. После внедрения сообщения в изображение, размер файла не изменяется.

Метод расширения палитры, работающий только для структуры GIF. Он будет наиболее эффективен в изображениях с палитрой небольших размеров. Суть его состоит в том, что он увеличивает размер палитры, тем самым дав дополнительное пространство для записи необходимых байт на месте байт цветов. Если учесть что минимальный размер палитры составляет 2 цвета (6 байт), то максимальный размер внедряемого сообщения может быть 256×3–6=762 байт. Недостаток — низкая криптозащищенность, прочесть внедренное сообщение можно при помощи любого текстового редактора, если сообщение не подвергалось дополнительному шифрованию.

Практическая часть


Проектирование программы

Все необходимые инструменты для реализации алгоритмов шифрования и дешифрования будут находиться в пакете com.tsarik.steganography. Этот пакет включает в себя интерфейс Encryptor с методами encrypt и decrypt, класс Binary, предоставляющий возможность работы с массивами битов, а также классы исключений UnableToEncryptException и UnableToDecryptException, которые должны быть использованы в методах интерфейса Encryptor в случае ошибок кодирования и декодирования соответственно.

Основной пакет программы com.tsarik.programs.gifed будет включать в себя запускаемый класс программы со статическим методом main, позволяющий запускать программу; класс, хранящий в себе параметры программы; и пакеты с другими классами.

Реализация непосредственно самих алгоритмов будет представлена в пакете com.tsarik.programs.gifed.gif классами GIFEncryptorByLSBMethod и GIFEncryptorByPaletteExtensionMethod. Оба этих класса будут реализовать интерфейс Encryptor.

На основе структуры формата GIF можно составить общий алгоритм внедрения сообщения в палитру изображения:



Для определения присутствия сообщения в изображении необходимо в начало сообщения добавлять некую последовательность бит, которую дешифратор считывает в первую очередь и проверяет на корректность. Если она не совпадает, то считается, что в изображении нет сокрытого сообщения. Далее надо указывать длину сообщения. Затем сам текст сообщения.

Диаграмма классов всего приложения:



Реализация программы

Реализацию всей программы можно разбить на две составляющие: реализация методов шифрования и дешифрования интерфейса Encryptor, в классах GIFEncryptorByLSBMethod и GIFEncryptorByPaletteExtensionMethod, и реализация интерфейса пользователя.

Рассмотрим класс GIFEncryptorByLSBMethod.



Поля firstLSBit и secondLSBit содержат номера битов каждого байта изображения, в которые должно заноситься и откуда считываться сообщение. Поле checkSequence хранит контрольную последовательность бит для обеспечения распознавания встроенного сообщения. Статический метод getEncryptingFileParameters возвращает параметры указанного файла и характеристики потенциального сообщения.

Алгоритм метода encrypt класса GIFEncryptorByLSBMethod:



И его код:
@Override
public void encrypt(File in, File out, String text) throws UnableToEncodeException, NullPointerException, IOException {
	if (in == null) {
		throw new NullPointerException("Input file is null");
	}
	if (out == null) {
		throw new NullPointerException("Output file is null");
	}
	if (text == null) {
		throw new NullPointerException("Text is null");
	}
	
	// read bytes from input file
	byte[] bytes = new byte[(int)in.length()];
	InputStream is = new FileInputStream(in);
	is.read(bytes);
	is.close();
	
	// check format
	if (!(new String(bytes, 0, 6)).equals("GIF89a")) {
		throw new UnableToEncodeException("Input file has wrong GIF format");
	}
	
	// read palette size property from first three bits in the 10-th byte from the file
	byte[] b10 = Binary.toBitArray(bytes[10]);
	byte bsize = Binary.toByte(new byte[] {b10[0], b10[1], b10[2]});
	
	// calculate color count and possible message length
	int bOrigColorCount = (int)Math.pow(2, bsize+1);
	int possibleMessageLength = bOrigColorCount*3/4;
	int possibleTextLength = possibleMessageLength-2;// one byte for check and one byte for message length
	
	if (possibleTextLength < text.length()) {
		throw new UnableToEncodeException("Text is too big");
	}
	
	int n = 13;
	
	// write check sequence
	for (int i = 0; i < checkSequence.length/2; i++) {
		byte[] ba = Binary.toBitArray(bytes[n]);
		ba[firstLSBit] = checkSequence[2*i];
		ba[secondLSBit] = checkSequence[2*i+1];
		bytes[n] = Binary.toByte(ba);
		n++;
	}
	
	// write text length
	byte[] cl = Binary.toBitArray((byte)text.length());
	for (int i = 0; i < cl.length/2; i++) {
		byte[] ba = Binary.toBitArray(bytes[n]);
		ba[firstLSBit] = cl[2*i];
		ba[secondLSBit] = cl[2*i+1];
		bytes[n] = Binary.toByte(ba);
		n++;
	}
	
	// write message
	byte[] textBytes = text.getBytes();
	for (int i = 0; i < textBytes.length; i++) {
		byte[] c = Binary.toBitArray(textBytes[i]);
		for (int ci = 0; ci < c.length/2; ci++) {
			byte[] ba = Binary.toBitArray(bytes[n]);
			ba[firstLSBit] = c[2*ci];
			ba[secondLSBit] = c[2*ci+1];
			bytes[n] = Binary.toByte(ba);
			n++;
		}
	}
	
	// write output file
	OutputStream os = new FileOutputStream(out);
	os.write(bytes);
	os.close();
}


Алгоритм и исходный код метода decrypt класса GIFEncryptorByLSBMethod:



@Override
public String decrypt(File in) throws UnableToDecodeException, NullPointerException, IOException {
	if (in == null) {
		throw new NullPointerException("Input file is null");
	}
	
	// read bytes from input file
	byte[] bytes = new byte[(int)in.length()];
	InputStream is = new FileInputStream(in);
	is.read(bytes);
	is.close();
	
	// check format
	if (!(new String(bytes, 0, 6)).equals("GIF89a")) {
		throw new UnableToDecodeException("Input file has wrong GIF format");
	}
	
	// read palette size property from first three bits in the 10-th byte from the file
	byte[] b10 = Binary.toBitArray(bytes[10]);
	byte bsize = Binary.toByte(new byte[] {b10[0], b10[1], b10[2]});
	
	// calculate color count and possible message length
	int bOrigColorCount = (int)Math.pow(2, bsize+1);
	int possibleMessageLength = bOrigColorCount*3/4;
	int possibleTextLength = possibleMessageLength-2;	// one byte for check and one byte for message length
	
	int n = 13;
	
	// read check sequence
	byte[] csBits = new byte[checkSequence.length];
	for (int i = 0; i < 4; i++) {
		byte[] ba = Binary.toBitArray(bytes[n]);
		csBits[2*i] = ba[firstLSBit];
		csBits[2*i+1] = ba[secondLSBit];
		n++;
	}
	byte cs = Binary.toByte(csBits);
	
	if (cs != Binary.toByte(checkSequence)) {
		throw new UnableToDecodeException("There is no encrypted message in the image (Check sequence is incorrect)");
	}
	
	// read text length
	byte[] cl = new byte[8];
	for (int i = 0; i < 4; i++) {
		byte[] ba = Binary.toBitArray(bytes[n]);
		cl[2*i] = ba[firstLSBit];
		cl[2*i+1] = ba[secondLSBit];
		n++;
	}
	byte textLength = Binary.toByte(cl);
	
	if (textLength < 0) {
		throw new UnableToDecodeException("Decoded text length is less than 0");
	}
	if (possibleTextLength < textLength) {
		throw new UnableToDecodeException("There is no messages (Decoded message length (" + textLength + ") is less than Possible message length (" + possibleTextLength + "))");
	}
	
	// read text bits and make text bytes
	byte[] bt = new byte[textLength];
	for (int i = 0; i < bt.length; i++) {
		byte[] bc = new byte[8];
		for (int bci = 0; bci < bc.length/2; bci++) {
			byte[] ba = Binary.toBitArray(bytes[n]);
			bc[2*bci] = ba[firstLSBit];
			bc[2*bci+1] = ba[secondLSBit];
			n++;
		}
		bt[i] = Binary.toByte(bc);
	}
	
	return new String(bt);
}


Реализация класса GIFEncryptorByPaletteExtensionMethod будет аналогичной, только отличается метод сохранения/считывания информации.

В классе MainFrame описаны методы-«обертки»: encryptImage(Encryptor encryptor) и decryptImage(Encryptor encryptor), обрабатывающие результаты методов интерфейса Encryptor и осуществляющие взаимодействие с пользователем, т.е открывают диалог выбора файлов, показывают сообщения об ошибках и т.д.; а также и другие методы: openImage(), дающий возможность пользователю выбора изображения, exit(), осуществляющий выход из приложения. Эти методы вызываются из Action'ов соответствующих пунктов меню. В этом классе дополнительно реализованы вспомогательные методы: createComponents() — создание компонентов формы, loadImageFile(File f) — загрузка изображения в специальный компонент из файла. Реализация класса GIFEncryptorByPaletteExtensionMethod аналогична реализации класса GIFEncryptorByLSBMethod, основное отличие состоит в способе записи и чтении байтов сообщения из палитры.

Работа программы


Метод LBS

Допустим есть такое изображение:



В данном изображении палитра состоит из 256 цветов (так сохраняет Paint). Первые четыре цвета: белый, черный, красный, зеленый. Остальные цвета — черные. Последовательность бит глобальной палитры будет следующая:

11111111 11111111 11111111 00000000 00000000 00000000 11111111 00000000 00000000 00000000 11111111 00000000



После внедрения сообщения подчеркнутые биты будут заменены битами из сообщения. Полученное изображение почти не отличается от оригинала.

Оригинал Изображение с внедренным сообщением
Оригинал Изображение с внедренным сообщением


Метод расширения палитры

Открыв изображение, в которое помещено сообщение по данному методу, можно обнаружиться такую картину:



Понятное дело, что для полноценной шпионской деятельности такой метод не пойдет, и требует, может, дополнительной шифровки сообщения.

Шифрование/дешифрование в анимированных изображениях работает, как и в обычных статических изображениях, при этом анимация не нарушается.

Используемые источники:


Скачать:
Share post

Similar posts

Comments 18

    –5
    Тема интересная. Попробуйте доразвить её и сделать дополнительное шифрование сообщения и извлечение зашифрованного сообщения из изображения.
      +1
      да, не спорю, тема интересная.
      но для меня она была интересна (и даже очень), только в процессе написания курсовой :) сейчас же, это просто осталось как опыт работы с gif-изображениями и стеганографией
      0
      Нечто подобное делали на курсовой на C++ на втором курсе. Препод говорила, что довольно перспективное направление. Суть у нас была в шифровании пароля в картинке, если очень грубо выразиться.
      • UFO just landed and posted this here
        +8
          –3
          Для инвайтов на хабр можно использовать, как дополнительный элемент защиты.
            +1
            Предлагаю моднуть драйвер TrueCrypt для прозрачной работы со стеганографическим контейнером, распиленным на части и засунутым в коллекцию разнородных медиафайлов. Вот це будет курсовая. :)
              0
              Делал подобную работу на 3-м курсе в качестве курсового. Можно было как шифровать так и извлекать текст. Насколько помню — писал на Delphi =)
                +1
                В этой записи есть только одна GIF-картинка (первая), и то без скрытого текста внутри.
                А где же секретный паззл для любопытных?!
                  0
                  осталось текст сжать и положить в конец файла и заново изобрести Rarjpeg, или Rargif, как в данном случае
                    0
                    Вообще, можно добавить в палитру несколько одинаковых цветов. Например, ее можно просто продублировать. При расшифровке смотреть — если для пикселя используется первый цвет, значит закодирована 1, иначе 0. Правда, много данных туда не засунешь, за то незаметно.
                      0
                      Да нет, вот туда-то как раз и засунешь МНОГО. Изображение может быть большого размера в пикселах, может быть анимированным, может не вызывая подозрений весить хоть несколько мегабайт — и каждый байт оттуда будет доступен для сокрытия данных. А мучить палитру, как в статье — практического смысла не имеет.
                      +1
                      Вы серьёзно считаете, что весь этот UML сделал программу понятнее?
                        0
                        int bOrigColorCount = (int)Math.pow(2, bsize+1);

                        Да уж… Вот так можно?
                        int bOrigColorCount = 1 << (bsize + 1);
                          +1
                          Можно. Только, возможно, автор не посчитал нужным оптимизировать эту строку кода. Честно, ну выполнится сдвиг быстро, ну и что с того? :) Он же не в цикле.
                            0
                            я не очень-то заботился об оптимизации, главное — воссоздать алгоритм
                            тем более, я как-то не очень люблю придумывать сверхбыстрые и оптимизированные алгоритмы… меня больше занимает придумывание структуры, взаимодействия между классами/объектами/интерфейсами и т.д.
                            0
                            а почему бы не использовать анимашки? Каждый кадр может иметь свою палитру, дальше всё понятно
                              0
                              ну да, можно было развивать и развивать, но тогда и этого было достаточно для максимальной оценки.

                            Only users with full accounts can post comments. Log in, please.