Steganografia w GIF

Wprowadzenie

Witam.
Niedawno, kiedy studiowałem na uniwersytecie, miałem projekt dotyczący przedmiotu "Programowe metody ochrony informacji". Zadanie polegało na stworzeniu programu, który wprowadzałby wiadomość do plików w formacie GIF. Postanowiłem wykonać to w Javie.

W tym artykule opiszę kilka teoretycznych kwestii, a także to, jak tworzono ten mały program.

Część teoretyczna

Format GIF

GIF (ang. Graphics Interchange Format — format do wymiany obrazów) to format przechowywania grafiki, który potrafi przechowywać skompresowane dane bez utraty jakości w formacie do 256 kolorów. Format ten został opracowany w 1987 roku (GIF87a) przez firmę CompuServe w celu przesyłania rastrowych obrazów przez sieci. W 1989 roku format został zmodyfikowany (GIF89a), dodano obsługę przezroczystości i animacji.

Pliki w formacie GIF mają strukturę blokową. Bloki te zawsze mają stałą długość (lub ich długość zależy od niektórych flag), więc praktycznie niemożliwe jest pomylenie się w tym, gdzie jaki blok się znajduje. Struktura najprostszej nieanimowanej grafiki GIF w formacie GIF89a:

Steganografia w GIF

Spośród wszystkich bloków struktury w tym przypadku interesować nas będą blok globalnej palety oraz parametry odpowiedzialne za paletę:

  • CT — obecność globalnej palety. Jeśli ten flag jest ustawiony, to zaraz po deskryptorze logicznego ekranu powinna zaczynać się globalna paleta.
  • Size — rozmiar palety i liczba kolorów obrazka. Wartości tego parametru:

Size
Liczba kolorów
Rozmiar palety, bajty

7
256
768

6
128
384

5
64
192

4
32
96

3
16
48

2
8
24

1
4
12

0
2
6

Metody szyfrowania

Jako metody szyfrowania wiadomości w plikach obrazów będą stosowane:

  • Metoda LSB (Least Significant Bit, najmniej znaczący bit)
  • Metoda uzupełnienia palety

Metoda LSB — powszechnie stosowana metoda steganografii. Polega ona na wymianie ostatnich znaczących bitów w kontenerze (w naszym przypadku bajtów globalnej palety) na bity ukrywanej wiadomości.

W programie będą wykorzystywane dwa ostatnie bity w bajtach globalnej palety w ramach tej metody. Oznacza to, że dla obrazu 24-bitowego, gdzie kolor palety reprezentuje trzy bajty dla koloru czerwonego, niebieskiego i zielonego, po wprowadzeniu wiadomości, każda składowa koloru zmieni się maksymalnie o 3/255 odcieni. Taka zmiana, po pierwsze, będzie niewidoczna lub trudna do zauważenia dla ludzkiego oka, a po drugie, nie będzie różnicowana na niskiej jakości urządzeniach wyjściowych.

Ilość informacji będzie bezpośrednio zależała od wielkości palety obrazu. Ponieważ maksymalny rozmiar palety to 256 kolorów, a jeśli zapisujemy po dwa bity wiadomości w każdej składowej koloru, maksymalna długość wiadomości (przy maksymalnej palecie w obrazie) wynosi 192 bajty. Po wprowadzeniu wiadomości do obrazu rozmiar pliku nie zmienia się.

Metoda rozszerzania palety, działająca tylko dla struktury GIF. Będzie najskuteczniejsza w obrazach o małej palecie. Jej istota polega na tym, że zwiększa rozmiar palety, a tym samym daje dodatkowe miejsce na zapis niezbędnych bajtów w miejscu bajtów kolorów. Biorąc pod uwagę, że minimalny rozmiar palety wynosi 2 kolory (6 bajtów), maksymalny rozmiar wprowadzonej wiadomości może wynosić 256×3–6=762 bajty. Wadą jest niska kryptograficzna ochrona, wprowadzone wiadomości można przeczytać przy użyciu dowolnego edytora tekstu, jeśli wiadomość nie była poddana dodatkowemu szyfrowaniu.

Część praktyczna

Projektowanie programu

Wszystkie niezbędne narzędzia do realizacji algorytmów szyfrowania i deszyfrowania będą znajdować się w pakiecie com.tsarik.steganography. Ten pakiet zawiera interfejs Encryptor z metodami szyfruj i decrypt, klasę Binary, oferującą możliwość pracy z tablicami bitów, a także klasy wyjątków UnableToEncryptException i UnableToDecryptException, które powinny być używane w metodach interfejsu Encryptor w przypadku błędów kodowania i dekodowania odpowiednio.

Główny pakiet programu com.tsarik.programs.gifed będzie zawierał uruchamialną klasę programu z metodą statyczną main, umożliwiającą uruchomienie programu; klasę, która przechowuje parametry programu; oraz pakiety z innymi klasami.

Realizacja samych algorytmów zostanie przedstawiona w pakiecie com.tsarik.programs.gifed.gif klasami GIFEncryptorByLSBMethod i GIFEncryptorByPaletteExtensionMethod. Oba te klasy będą implementować interfejs Encryptor.

Na podstawie struktury formatu GIF można stworzyć ogólny algorytm wprowadzenia wiadomości do palety obrazu:

Steganografia w GIF

Aby określić obecność wiadomości w obrazie, należy na początku wiadomości dodać pewną sekwencję bitów, którą deszyfrator odczytuje w pierwszej kolejności i sprawdza jej poprawność. Jeśli nie pasuje, uznaje się, że w obrazie nie ma ukrytej wiadomości. Następnie należy określić długość wiadomości. Potem sam tekst wiadomości.

Diagram klas całej aplikacji:

Steganografia w GIF

Realizacja programu

Realizacja całego programu może zostać podzielona na dwie części: realizację metod szyfrowania i deszyfrowania interfejsu Encryptor, w klasach GIFEncryptorByLSBMethod i GIFEncryptorByPaletteExtensionMethod, oraz realizację interfejsu użytkownika.

Rozważmy klasę GIFEncryptorByLSBMethod.

Steganografia w GIF

Pola firstLSBit i secondLSBit zawierają numery bitów każdego bajtu obrazu, do których należy umieszczać i z których odczytywać wiadomość. Pole checkSequence przechowuje kontrolną sekwencję bitów w celu zapewnienia rozpoznawalności wbudowanej wiadomości. Statyczna metoda getEncryptingFileParameters zwraca parametry wskazanego pliku i charakterystyki potencjalnej wiadomości.

Algorytm metody szyfruj klasy GIFEncryptorByLSBMethod:

Steganografia w GIF

I jego kod:

@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");
	}
	
	// czytaj bajty z pliku wejściowego
	byte[] bytes = new byte[(int)in.length()];
	InputStream is = new FileInputStream(in);
	is.read(bytes);
	is.close();
	
	// sprawdz format
	if (!(new String(bytes, 0, 6)).equals("GIF89a")) {
		throw new UnableToEncodeException("Plik wejściowy ma niewłaściwy format GIF");
	}
	
	// odczytaj właściwość rozmiaru palety z pierwszych trzech bitów w dziesiątym bajcie pliku
	byte[] b10 = Binary.toBitArray(bytes[10]);
	byte bsize = Binary.toByte(new byte[] {b10[0], b10[1], b10[2]});
	
	// oblicz liczbę kolorów i możliwą długość wiadomości
	int bOrigColorCount = (int)Math.pow(2, bsize+1);
	int possibleMessageLength = bOrigColorCount*3/4;
	int possibleTextLength = possibleMessageLength-2; // jeden bajt na kontrolę i jeden bajt na długość wiadomości
	
	if (possibleTextLength < text.length()) {
		throw new UnableToEncodeException("Tekst jest za duży");
	}
	
	int n = 13;
	
	// zapisz sekwencję kontrolną
	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++;
	}
	
	// zapisz długość tekstu
	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++;
	}
	
	// zapisz wiadomość
	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++;
		}
	}
	
	// zapisz plik wyjściowy
	OutputStream os = new FileOutputStream(out);
	os.write(bytes);
	os.close();
}

Algorytm i kod źródłowy metody decrypt klasy GIFEncryptorByLSBMethod:

Steganografia w GIF

@Override
public String decrypt(File in) throws UnableToDecodeException, NullPointerException, IOException {
	if (in == null) {
		throw new NullPointerException("Plik wejściowy jest pusty");
	}
	
	// odczyt bajtów z pliku wejściowego
	byte[] bytes = new byte[(int)in.length()];
	InputStream is = new FileInputStream(in);
	is.read(bytes);
	is.close();
	
	// sprawdzenie formatu
	if (!(new String(bytes, 0, 6)).equals("GIF89a")) {
		throw new UnableToDecodeException("Plik wejściowy ma zły format GIF");
	}
	
	// odczyt właściwości rozmiaru palety z pierwszych trzech bitów 10. bajtu z pliku
	byte[] b10 = Binary.toBitArray(bytes[10]);
	byte bsize = Binary.toByte(new byte[] {b10[0], b10[1], b10[2]});
	
	// obliczenie liczby kolorów i możliwej długości wiadomości
	int bOrigColorCount = (int)Math.pow(2, bsize + 1);
	int possibleMessageLength = bOrigColorCount * 3 / 4;
	int possibleTextLength = possibleMessageLength - 2;	// jeden bajt na kontrolę i jeden bajt na długość wiadomości
	
	int n = 13;
	
	// odczyt sekwencji kontrolnej
	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("Nie ma zaszyfrowanej wiadomości w obrazie (Sekwencja kontrolna jest niepoprawna)");
	}
	
	// odczyt długości tekstu
	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("Zdekodowana długość tekstu jest mniejsza niż 0");
	}
	if (possibleTextLength < textLength) {
		throw new UnableToDecodeException("Brak wiadomości (Zdekodowana długość wiadomości (" + textLength + ") jest mniejsza niż Możliwa długość wiadomości (" + possibleTextLength + "))");
	}
	
	// odczyt bitów tekstu i utworzenie bajtów tekstu
	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);
}

Implementacja klasy GIFEncryptorByPaletteExtensionMethod będzie podobna, tylko różni się metodą zapisywania/odczytywania informacji.

W klasie MainFrame opisano metody-«opakowania»: encryptImage(Encryptor encryptor) i decryptImage(Encryptor encryptor), przetwarzające wyniki metod interfejsu Encryptor i realizujące interakcję z użytkownikiem, t.j. otwierają okno wyboru plików, wyświetlają komunikaty o błędach itd.; a także inne metody: openImage(), umożliwiająca użytkownikowi wybór obrazu, exit(), realizująca wyjście z aplikacji. Te metody są wywoływane z Akcja‘ów odpowiednich punktów menu. W tej klasie dodatkowo zaimplementowano metody pomocnicze: createComponents() — tworzenie komponentów formularza, loadImageFile(File f) — ładowanie obrazu do specjalnego komponentu z pliku. Implementacja klasy GIFEncryptorByPaletteExtensionMethod jest analogiczna do implementacji klasy GIFEncryptorByLSBMethod, główna różnica polega na sposobie zapisu i odczytu bajtów wiadomości z palety.

Działanie programu

Metoda LBS

Załóżmy, że jest taki obraz:

Steganografia w GIF

Na tym obrazku paleta składa się z 256 kolorów (tak zapisuje Paint). Pierwsze cztery kolory to: biały, czarny, czerwony, zielony. Pozostałe kolory to odcienie czerni. Sekwencja bitów globalnej palety będzie następująca:

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

Steganografia w GIF

Po zaszyfrowaniu wiadomości podkreślone bity zostaną zastąpione bitami z wiadomości. Otrzymany obrazek prawie nie różni się od oryginału.

Oryginał
Obrazek z wbudowaną wiadomością

Steganografia w GIF
Steganografia w GIF

Metoda rozszerzania palety

Otwierając obrazek, w który wstawiono wiadomość w tym metodzie, można zobaczyć taki widok:

Steganografia w GIF

Jasne jest, że taka metoda nie nadaje się do profesjonalnej działalności szpiegowskiej i wymaga być może dodatkowego szyfrowania wiadomości.

Szyfrowanie/dekryptowanie w animowanych obrazach działa tak samo, jak w zwykłych statycznych obrazach, a animacja nie jest naruszona.

Źródła używane:

Pobierz:

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster