Introducción
Saludos.
No hace mucho tiempo, cuando estaba en la universidad, tuve un trabajo de curso en la asignatura "Métodos de programación para la protección de la información". El encargo consistía en crear un programa que incrustara mensajes en archivos del formato GIF. Decidí hacerlo en Java.
En este artículo describiré algunos aspectos teóricos, así como la manera en que se creó este pequeño programa.
Parte teórica
Formato GIF
GIF (en inglés, Graphics Interchange Format — formato para el intercambio de imágenes) es un formato de almacenamiento de imágenes gráficas que puede almacenar datos comprimidos sin pérdida de calidad en un formato de hasta 256 colores. Este formato fue desarrollado en 1987 (GIF87a) por la empresa CompuServe para la transmisión de imágenes rasterizadas a través de redes. En 1989, el formato fue modificado (GIF89a) y se añadieron soporte para la transparencia y la animación.
Los archivos del formato GIF tienen una estructura de bloques. Estos bloques siempre tienen una longitud fija (o puede depender de ciertos flags), por lo que es casi imposible equivocarse en la ubicación de los bloques. La estructura de la imagen GIF no animada más simple en el formato GIF89a es la siguiente:

De todos los bloques de la estructura, en este caso nos interesarán el bloque de paleta global y los parámetros relacionados con la paleta:
CT.— existencia de paleta global. Si este flag está establecido, la paleta global debe comenzar inmediatamente después del descriptor de la pantalla lógica.Tamaño— tamaño de la paleta y número de colores de la imagen. Los valores de este parámetro son:
Tamaño
Número de colores
Tamaño de la paleta, 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
Métodos de cifrado
Como métodos de encriptación de mensajes en archivos de imágenes se utilizarán:
- Método LSB (Least Significant Bit, el bit menos significativo)
- Método de adición de paleta
Método LSB — un método común de esteganografía. Consiste en reemplazar los últimos bits significativos en el contenedor (en nuestro caso, los bytes de la paleta global) por los bits del mensaje oculto.
El programa utilizará los últimos dos bits en los bytes de la paleta global. Esto significa que, para una imagen de 24 bits, donde el color de la paleta está representado por tres bytes para los colores rojo, azul y verde, después de inyectar el mensaje, cada componente de color cambiará un máximo de 3/255 grados. Este cambio, en primer lugar, será imperceptible o difícil de notar para el ojo humano, y en segundo lugar, no será distinguible en dispositivos de salida de baja calidad.
La cantidad de información dependerá directamente del tamaño de la paleta de la imagen. Dado que el tamaño máximo de la paleta es de 256 colores y, si se graban dos bits del mensaje en cada componente de color, la longitud máxima del mensaje (con la paleta máxima en la imagen) será de 192 bytes. Después de inyectar el mensaje en la imagen, el tamaño del archivo no cambia.
Método de expansión de paleta, que solo funciona para la estructura GIF. Será más efectivo en imágenes con paletas de pequeños tamaños. Su esencia radica en que aumenta el tamaño de la paleta, proporcionando así espacio adicional para grabar los bytes necesarios en el lugar de los bytes de color. Considerando que el tamaño mínimo de la paleta es de 2 colores (6 bytes), la longitud máxima del mensaje inyectado puede ser 256×3–6=762 bytes. La desventaja es la baja criptoseguridad; el mensaje inyectado se puede leer con cualquier editor de texto si no ha sido sometido a encriptación adicional.
Parte práctica
Diseño del programa
Todas las herramientas necesarias para implementar algoritmos de cifrado y descifrado estarán en el paquete com.tsarik.steganography. Este paquete incluye la interfaz Encryptor con métodos encrypt y decrypt, la clase Binary, que proporciona la posibilidad de trabajar con arreglos de bits, así como las clases de excepción UnableToEncryptException y UnableToDecryptException, que deben ser utilizadas en los métodos de la interfaz Encryptor en caso de errores de codificación y decodificación respectivamente.
El paquete principal del programa com.tsarik.programs.gifed incluirá la clase ejecutable del programa con el método estático main, que permite ejecutar el programa; una clase que almacena los parámetros del programa; y paquetes con otras clases.
La implementación de los propios algoritmos se presentará en el paquete com.tsarik.programs.gifed.gif clases GIFEncryptorByLSBMethod y GIFEncryptorByPaletteExtensionMethod. Ambas clases implementarán la interfaz Encryptor.
Basado en la estructura del formato GIF, se puede desarrollar un algoritmo general para incrustar un mensaje en la paleta de la imagen:

Para determinar la presencia de un mensaje en la imagen, es necesario agregar al comienzo del mensaje una determinada secuencia de bits, que el descifrador lee primero y verifica su validez. Si no coincide, se considera que no hay mensaje oculto en la imagen. A continuación, se debe indicar la longitud del mensaje. Luego, el propio texto del mensaje.
Diagrama de clases de toda la aplicación:

Implementación del programa
La implementación de todo el programa se puede descomponer en dos componentes: la implementación de los métodos de cifrado y descifrado de la interfaz Encryptor, en las clases GIFEncryptorByLSBMethod y GIFEncryptorByPaletteExtensionMethod, y la implementación de la interfaz de usuario.
Consideremos la clase GIFEncryptorByLSBMethod.

Campos firstLSBit y secondLSBit contienen los números de bits de cada byte de la imagen, en los que se debe almacenar y desde los cuales se debe leer el mensaje. El campo checkSequence almacena la secuencia de bits de control para garantizar el reconocimiento del mensaje incrustado. El método estático getEncryptingFileParameters devuelve los parámetros del archivo especificado y las características del mensaje potencial.
El algoritmo del método encrypt de la clase GIFEncryptorByLSBMethod:

Y su código:
@Override
public void encrypt(File in, File out, String text) throws UnableToEncodeException, NullPointerException, IOException {
if (in == null) {
throw new NullPointerException("El archivo de entrada es nulo");
}
if (out == null) {
throw new NullPointerException("El archivo de salida es nulo");
}
if (text == null) {
throw new NullPointerException("El texto es nulo");
}
// leer bytes del archivo de entrada
byte[] bytes = new byte[(int)in.length()];
InputStream is = new FileInputStream(in);
is.read(bytes);
is.close();
// verificar formato
if (!(new String(bytes, 0, 6)).equals("GIF89a")) {
throw new UnableToEncodeException("El archivo de entrada tiene un formato GIF incorrecto");
}
// leer propiedad del tamaño de la paleta de los primeros tres bits en el décimo byte del archivo
byte[] b10 = Binary.toBitArray(bytes[10]);
byte bsize = Binary.toByte(new byte[] {b10[0], b10[1], b10[2]});
// calcular el conteo de colores y la longitud posible del mensaje
int bOrigColorCount = (int)Math.pow(2, bsize + 1);
int possibleMessageLength = bOrigColorCount * 3 / 4;
int possibleTextLength = possibleMessageLength - 2; // un byte para el chequeo y un byte para la longitud del mensaje
if (possibleTextLength < text.length()) {
throw new UnableToEncodeException("El texto es demasiado grande");
}
int n = 13;
// escribir secuencia de chequeo
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++;
}
// escribir longitud del texto
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++;
}
// escribir mensaje
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++;
}
}
// escribir archivo de salida
OutputStream os = new FileOutputStream(out);
os.write(bytes);
os.close();
}
Algoritmo y código fuente del método decrypt de la clase GIFEncryptorByLSBMethod:

@Override
public String decrypt(File in) throws UnableToDecodeException, NullPointerException, IOException {
if (in == null) {
throw new NullPointerException("El archivo de entrada es nulo");
}
// leer bytes del archivo de entrada
byte[] bytes = new byte[(int)in.length()];
InputStream is = new FileInputStream(in);
is.read(bytes);
is.close();
// verificar formato
if (!(new String(bytes, 0, 6)).equals("GIF89a")) {
throw new UnableToDecodeException("El archivo de entrada tiene un formato GIF incorrecto");
}
// leer la propiedad del tamaño de la paleta de los primeros tres bits en el décimo byte del archivo
byte[] b10 = Binary.toBitArray(bytes[10]);
byte bsize = Binary.toByte(new byte[] {b10[0], b10[1], b10[2]});
// calcular el conteo de colores y la posible longitud del mensaje
int bOrigColorCount = (int)Math.pow(2, bsize+1);
int possibleMessageLength = bOrigColorCount*3/4;
int possibleTextLength = possibleMessageLength-2; // un byte para el chequeo y un byte para la longitud del mensaje
int n = 13;
// leer la secuencia de verificación
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("No hay mensaje cifrado en la imagen (la secuencia de chequeo es incorrecta)");
}
// leer la longitud del texto
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("La longitud del texto decodificado es menor que 0");
}
if (possibleTextLength < textLength) {
throw new UnableToDecodeException("No hay mensajes (la longitud del mensaje decodificado (" + textLength + ") es menor que la longitud posible del mensaje (" + possibleTextLength + "))");
}
// leer los bits de texto y hacer bytes de texto
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);
}
Implementación de la clase GIFEncryptorByPaletteExtensionMethod será similar, solo difiere en el método de guardar/leer información.
En la clase MainFrame se describen los métodos "wrapper": encryptImage(Encryptor encryptor) y decryptImage(Encryptor encryptor), que procesan los resultados de los métodos de la interfaz Encryptor y llevan a cabo la interacción con el usuario, es decir, abren un diálogo para seleccionar archivos, muestran mensajes de error, etc.; así como otros métodos: openImage(), que permite al usuario seleccionar una imagen, exit(), que cierra la aplicación. Estos métodos se llaman desde Acción'de los puntos de menú correspondientes. En esta clase se implementan además métodos auxiliares: createComponents() — creación de componentes del formulario, loadImageFile(File f) — carga de una imagen en un componente especial desde un archivo. La implementación de la clase GIFEncryptorByPaletteExtensionMethod es análoga a la implementación de la clase GIFEncryptorByLSBMethod, la principal diferencia radica en la forma de escribir y leer los bytes del mensaje desde la paleta.
Funcionamiento del programa
Método LBS
Supongamos que hay tal imagen:

En esta imagen, la paleta consta de 256 colores (así lo guarda Paint). Los primeros cuatro colores son: blanco, negro, rojo, verde. Los demás colores son negros. La secuencia de bits de la paleta global será la siguiente:
11111111 11111111 11111111 00000000 00000000 00000000 11111111 00000000 00000000 00000000 11111111 00000000…

Después de la inserción del mensaje, los bits subrayados serán reemplazados por los bits del mensaje. La imagen resultante apenas se diferencia de la original.
Original
Imagen con mensaje insertado

![]()
Método de expansión de paleta
Al abrir la imagen en la que se ha colocado el mensaje de este modo, se puede encontrar la siguiente situación:

Es obvio que para una actividad de espionaje completa este método no sirve y requiere tal vez un cifrado adicional del mensaje.
El cifrado/des cifrado en imágenes animadas funciona igual que en las imágenes estáticas normales, sin que la animación se vea afectada.
Fuentes utilizadas:
Descargar:
Fuente: habr.com
