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

Введение

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

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

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

Формат GIF

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

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

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

Из всех блоков структуры в данном случае нас будут интересовать блок глобальной палитры и параметры, отвечающие за палитру:

  • 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 можно составить общий алгоритм внедрения сообщения в палитру изображения:

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

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

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

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

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

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

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

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

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

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

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

И его код:

@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:

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

@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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Скачать:

Источник: habr.com