Steganography in GIF

Introduction

Welcome.
Not so long ago, when I was studying at the university, there was a term paper in the discipline "Software methods for protecting information." According to the assignment, it was required to make a program that embeds a message in GIF files. Decided to do it in Java.

In this article, I will describe some of the theoretical points, as well as how this little program was created.

Theoretical part

GIF format

GIF (Eng. Graphics Interchange Format - a format for exchanging images) is a format for storing graphic images, capable of storing compressed data without loss of quality in a format of up to 256 colors. This format was developed in 1987 (GIF87a) by CompuServe for transmitting bitmap images over networks. In 1989, the format was modified (GIF89a), and support for transparency and animation was added.

GIF files have a block structure. These blocks always have a fixed length (or it depends on some flags), so it is almost impossible to make a mistake in where which block is located. The structure of the simplest non-animated GIF image in GIF89a format:

Steganography in GIF

Of all the structure blocks, in this case we will be interested in the global palette block and the parameters responsible for the palette:

  • CT - the presence of a global palette. If this flag is set, then the global palette must begin immediately after the logical screen handle.
  • Size - the size of the palette and the number of colors in the picture. Values ​​of this parameter:

Size
Number of colors
Palette size, bytes

7
256
768

6
128
384

5
64
192

4
32
96

3
16
48

2
8
24

1
4
12

0
2
6

Encryption methods

As methods for encrypting messages in image files, the following will be used:

  • LSB (Least Significant Bit) method
  • Palette completion method

LSB Method is a common method of steganography. It consists in replacing the last significant bits in the container (in our case, the bytes of the global palette) with the bits of the hidden message.

The program will use the last two bits in the bytes of the global palette within this method. This means that for a 24-bit image, where the palette color is three bytes for red, blue, and green, after embedding a message in it, each color component will change by a maximum of 3/255 of a gradation. Such a change, firstly, will be imperceptible or hardly noticeable to the human eye, and secondly, it will not be distinguishable on low-quality information output devices.

The amount of information will directly depend on the size of the image palette. Since the maximum size of the palette is 256 colors, and if two bits of the message are written to the component of each color, then the maximum length of the message (with the maximum palette in the image) is 192 bytes. After embedding a message in an image, the file size does not change.

Palette extension method, which only works for the GIF structure. It will be most effective in images with small palette sizes. Its essence is that it increases the size of the palette, thereby giving additional space for writing the necessary bytes in place of the color bytes. Considering that the minimum size of the palette is 2 colors (6 bytes), then the maximum size of the embedded message can be 256Γ—3–6=762 ​​bytes. The disadvantage is low cryptographic security; you can read the embedded message using any text editor if the message has not been subjected to additional encryption.

Practical part

Program design

All the necessary tools for implementing encryption and decryption algorithms will be in the package com.tsarik.steganography. This package includes an interface Encryptor with methods encrypt ΠΈ decrypt, Class Binary, which provides the ability to work with arrays of bits, as well as exception classes UnableToEncryptException ΠΈ UnableToDecryptException, which should be used in interface methods Encryptor in case of encoding and decoding errors, respectively.

Main program package com.tsarik.programs.gifed will include the program class being run with a static method main, which allows you to run the program; a class that stores program parameters; and packages with other classes.

The implementation of the algorithms themselves will be presented in the package com.tsarik.programs.gifed.gif classes GIFEncryptorByLSBMethod ΠΈ GIFEncryptorByPaletteExtensionMethod. Both of these classes will implement the interface Encryptor.

Based on the structure of the GIF format, you can create a general algorithm for embedding a message in an image palette:

Steganography in GIF

To determine the presence of a message in an image, it is necessary to add a certain sequence of bits to the beginning of the message, which the decoder reads first and checks for correctness. If it does not match, then it is considered that there is no hidden message in the image. Next, you need to specify the length of the message. Then the text of the message itself.

Class diagram of the entire application:

Steganography in GIF

Implementation of the program

The implementation of the entire program can be divided into two components: the implementation of methods for encrypting and decrypting the interface Encryptor, in classes GIFEncryptorByLSBMethod ΠΈ GIFEncryptorByPaletteExtensionMethod, and the implementation of the user interface.

Consider the class GIFEncryptorByLSBMethod.

Steganography in GIF

fields firstLSBit ΠΈ secondLSBit contain the bit numbers of each byte of the image into which the message should be entered and from where the message should be read. Field checkSequence stores a parity bit sequence to ensure recognition of the embedded message. Static Method getEncryptingFileParameters returns the parameters of the specified file and characteristics of the potential message.

Method algorithm encrypt Class GIFEncryptorByLSBMethod:

Steganography in GIF

And his code:

@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();
}

Algorithm and method source code decrypt Class GIFEncryptorByLSBMethod:

Steganography in 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);
}

Class implementation GIFEncryptorByPaletteExtensionMethod will be similar, only the method of saving / reading information is different.

In the class MainFrame wrapper methods are described: encryptImage(Encryptor encryptor) ΠΈ decryptImage(Encryptor encryptor), which handle the results of interface methods Encryptor and interact with the user, i.e. open a file selection dialog, show error messages, etc.; as well as other methods: openImage(), allowing the user to select an image, exit()A that exits the application. These methods are called from Action's corresponding menu items. This class additionally implements helper methods: createComponents() - creating form components, loadImageFile(File f) - loading an image into a special component from a file. Class implementation GIFEncryptorByPaletteExtensionMethod similar to class implementation GIFEncryptorByLSBMethod, the main difference is in the way the bytes of the message are written to and read from the palette.

Program operation

LBS method

Let's say we have an image like this:

Steganography in GIF

In this image, the palette consists of 256 colors (as Paint saves). The first four colors are white, black, red, green. The rest of the colors are black. The bit sequence of the global palette will be as follows:

11111111 11111111 11111111 00000000 00000000 00000000 11111111 00000000 00000000 00000000 11111111 00000000...

Steganography in GIF

After the message is embedded, the underlined bits will be replaced with the bits from the message. The resulting image is almost indistinguishable from the original.

Original
Image with embedded message

Steganography in GIF
Steganography in GIF

Palette extension method

Having opened the image in which the message is placed according to this method, you can find the following picture:

Steganography in GIF

It is clear that this method will not work for full-fledged espionage activities, and it may require additional encryption of the message.

Encryption/decryption in animated images works just like normal static images, and the animation is not broken.

Sources used:

Download:

Source: habr.com

Add a comment