04 enero 2010

Esteganografía visual

Ya expliqué en qué consistía la esteganografía en uno de mis artículos anteriores. En aquel caso exploramos el arte de ocultar información en el tráfico usual de una red, pero esta no es sino una modalidad muy reciente del arcano arte de ocultar información entre lo cotidiano. Hasta llegar a ese punto, la esteganografía se enfocó a ocultar sus mensajes a los ojos de los demás... burlar a sus oídos y ya no digo a sus sniffers vendría mucho después.
En uno de mis próximos artículos trataré la esteganografía sonora, pero en este nos enfocaremos en la visual.

Ejemplos artísticos de mensajes ocultos a la vista hay muchos. A veces se jugaba con la perspectiva, como en el cuadro de Holbein "Los embajadores" que visto de frente es un retrato habitual en el que sólo llama la atención una mancha bastante rara entre los protagonistas.


Mirando el cuadro en el ángulo correcto, aparece una calavera (aproximadamente desde abajo y a la izquierda ya que el cuadro estaba pensado para ponerse en la pared de una escalinata curva interior):


En este caso se usó una técnica de perspectiva denominada anamorfosis para ocultar el mensaje del artista de que por mucho que el hombre se ilustre y estudie su mundo (simbolizado ello en los instrumentos de la mesa), la muerte siempre está presente y se cobrará su tributo (la calavera).

El historiador griego Herodoto citaba un par de ejemplos tempranos de esteganografía utilizada por los griegos para ocultar mensajes en los que se describían los planes persas de invasión. Uno de esos ejemplos describía como un griego tatuó el mensaje secreto de alerta en la cabeza rapada de uno de sus esclavos, dejó que le volviese a crecer el pelo y lo envió de camino a Grecia para avisar. Teniendo en cuenta lo que tarda el pelo en volver a crecer lo suficiente y el tiempo que implicaba cualquier tipo de viaje en aquella época o bien el mensaje no era tan urgente o es sencillamente una patraña. Otro ejemplo, más verosimil, es el que relata como el espía grababa el mensaje en una tablilla de cera sin ella y luego tapaba el mensaje añadiendola. Luego podía escribir un mensaje inofensivo en la cera de la tablilla para engañar cualquier inspección. Para acceder al mensaje sólo era necesario derretir la cera.

En tiempos más modernos es bastante habitual ver películas de espías que dejan mensajes en espejos cubiertos de vaho. Al desaparecer este, el mensaje se vuelve invisible pero puede visualizarse de nuevo abriendo el grifo de agua caliente y dejando que el vaho cubra de nuevo el cristal.

Todos estos son ejemplos de esteganografía visual. Hay muchos más, tantos como técnicas inventadas por el hombre. Nosotros nos fijaremos en una muy utilizada en informática. Esta técnica explota paradójicamente una de las habilidades en las que los seres humanos son superiores a las máquinas: la capacidad de aproximación. El cerebro humano carece de la precisión de los ordenadores, pero sin embargo es capaz de extraer conclusiones a partir de aproximaciones en las que se conjugan los datos captados y la experiencia previa. Esto, que un niño de tres años ya hace inconscientemente, es desde hace años el campo de batalla diario de los estudiosos de la Inteligencia Artificial. Aplicado al campo de las imágenes esta habilidad nos permite distinguir objetos a pesar de que estos se encuentren deformados o borrosos. Un ejemplo son los habituales captchas que retan a los usuarios a leer unos carácteres intencionadamente d eteriorados con el fin de asegurarse de que el "usuario" es una persona y no un programa de ordenador.

Sin embargo, cuando se trata de detectar detalles esta capacidad de aproximación juega en nuestra contra a no ser que ya estemos alerta. Si no estamos avisados de antemano o especialmente entrenados, lo normal es que veamos una forma, la interpretemos y si deducimos lo que significa facilmente pasemos a otra cosa ignorando los detalles. Un ejemplo de este fenómeno lo tenemos diariamente en los periódicos en el juego de encontrar las 7 diferencias. A no ser que seamos aficionados a ese juego lo normal es que no encontremos ninguna diferencia en el primer vistazo. Sin embargo las diferencias están ahí y no son sino información oculta en una imagen.

Nosotros explotaremos esto jugando con los colores de una imagen. Mientras que los ordenadores son capaces de distinguir con precisión un color de otro parecido en una paleta de miles de colores, lo cierto es que los seres humanos frecuentemente no somos capaces de distinguir dicha dife rencia sobre todo si la superficie de cada uno de los colores es muy reducida. ¿Seríamos capaces de distinguir si un pixel morado es exactamente igual de morado que el pixel que tiene a su lado?, la respuesta es que no, sobre todo si la diferencia es pequeña.

Los ordenadores actuales pueden mostrar en pantalla paletas de miles de colores. Para mostrar cada color, el ordenador le asigna un número identificativo de manera que los programas sólo tengan que usar ese número para decirle a la tarjeta gráfica que muestre ese color. Los colores similares tienen números correlativos así que si los visualizásemos consecutivamente veríamos gradientes de color en vez de cambios bruscos. Pongamos, por ejemplo, que una tarjeta gráfica pudiese mostrar imágenes en color verdadero lo que supondría que sería capaz de poner 16.777.216 colores diferentes en pantalla. Para ello cada color debería tener un número identificativo de 24 bits (2^24=16.777.216). Si jugásemos con el bit menos significativo (el de más a la derecha) del número identificativo para codificar información el usuario sería incapaz de notarlo ya que los colores resultantes serían tan parecidos que sería muy difícil distinguirlos entre si.

Así que eso será lo que hagamos, cogeremos una imagen y la recorreremos manipulando el bit menos significativo d e cada pixel de manera que ese bit corresponda a un bit del mensaje que queremos ocultar en la imagen.

Para ilustrarlo he escripto un pequeño programa en Python capaz de esconder un texto en una imagen que tenga formato BMP o PNG. El programa se puede descargar de google code y para ejecutarlo hace falta tener instalada la Python Imaging Library (PIL). En Ubuntu se puede instalar PIL mediante un sencillo comando:

[...]/stegimage$ sudo aptitude install python-imaging

Hecho lo anterior ya podemos ejecutar el programa desde la carpeta donde lo hayamos descomprimido. En dicha carpeta contamos con una imagen que hará de anfitrión de los datos a ocultar (lena_std.png) y el texto a ocultar (texto_a_ocultar.txt ;-) ). Se puede usar cualquier imagen PNG y cualquier texto pero hay que tener en cuenta que se tienen que cumplir determinadas relacio nes entre el tamaño de la imagen y el texto a ocultar, tal y como veremos al estudiar el código del programa. Para no complicarse la vida en la primera prueba lo mejor es usar los archivos de ejemplo que vienen junto al programa. Para ver los parámetros que acepta el programa no hay más que teclear su nombre sin ninguna opción:

/stegimage$ ./stegimage.py
Stegimage
===========

Programmed by: Dante Signal31

Written for experimental purpuses.

This program hides data in image files.

Format ./stegimage.py
-i ----------> image file to use for hiding.
-d -----------> Data file to hide in image file

-o ---------> Output image file with data file hidden inside.
-x ------------> Extract data file (set with -d) from steg file.

-s ------------> S ets how many bits we should hide in each selected pixel (DEFAULT=1)
-h | --help --------------> Print this text.


Examples:
Hiding a file:./stegimage.py -i image_file.bmp -d hidden.txt -o image_steg_file.bmp
Extracting a file:./stegimage.py -x image_steg_file.bmp -d hidden.txt

Siguiendo las indicaciones de la ayuda teclearemos lo siguiente para ocultar el texto de ejemplo (que dicho sea de apaso es un copy-paste de la excelente obra sobre el cifrado en la Segunda Guerra Mundial que Ramón Ceano publicó en Kriptópolis):

/stegimage$ ./stegimage.py -i lena_std.png -d texto_a_ocultar.txt -o lena_steg.png

Como podemos comprobar se ha generado una nueva imagen (lena_steg.png) que es la que oculta el texto secreto. Visualmente las dos son idénticas (para un humano), la primera es la imagen original y la segunda la que contiene el mensaje:




Para extraer el mensaje oculto no tenemos más que hacer lo siguiente:

/stegimage$ ./stegimage.py -d texto_extraido.txt -x lena_steg.png

Ahora estudiaremos el código del programa para entender bien cómo funciona esta técnica de ocultación. Haré un repaso puntual del código, pero habrá fragmentos de este que no plasmaré aquí porque no tienen mayor complicación así que recomiendo que el lector repase el código en su totalidad por su cuenta (no debería resultarle difícil porque he procurado comentarlo bastante y es bastante corto).

La función que se encarga de esconder los datos en la imagen es hideFile a la que se le pasan como parámetro dichos datos (en nuestro ejemplo el texto a esconder):

def hideFile(self, data_file_name):
        """ This function encodes data file into image host file."""
        self.data_file_size = (os.stat(data_file_name))[stat.ST_SIZE]
        self.calculateSeparation(self.getDataFileSize())
        self.data_file = open(data_file_name, 'rb')
        if (self.getSeparation() < 0): #This means that data file is too big.
            return ERROR
        else:
            self.encodeHeader()
            self.byte=self.data_file.read(1)
            self.bytes_written = 0
            while (self.byte):
                self.byte_in_bits = libmymath.int2bin(n=ord(self.byte), count=8)
                self.encodeBits(self.byte_in_bits)
                self.byte = self.data_file.read(1)
                self.bytes_written = self.bytes_written + 1
                self.percentage_done = /
int((float(self.bytes_written) / float(self.data_file_size)) *  100)
                print str(self.percentage_done) + "% : " + str(self.byte_in_bits)
            self.hostFileImage.save(self.hostFileName)
            self.data_file.close()
            return OK

Como se puede ver, esta función averigua el tamaño de los datos que queremos ocultar y posteriormente en la llamada a encodeHeader usará los primeros píxeles de la imagen para codificar en ellos el tamaño de los datos ocultados. En principio mi script cuenta con una constante HEADER_SIZE que fija 32 bits para codificar el tamaño de los datos ocultados por lo que los primeros 32 pixels consecutivos de la imagen tendrán sus bits menos significativos modificados (siempre y cuando STEP=1, como se explicará seguidamente). El número de bits que se modifican por pixels lo fija la constante STEP del fichero stegimage.py aunque el usuario la puede modificar usando el parámetro "-s" en la llamada al programa. En principio STEP está fijado a 1 de manera que sólo se modifica 1 bit por pixel tocado, esto significa que si queremos ocultar un byte de datos (8 bits) tendremos que modificar el valor del bit menos significativo de hasta 8 pixels para que dicho bit corresponda con el respectivo del byte a ocultar. Para recomponer el byte ocultado tendremos que mirar el bit menos significativo de cada uno de los 8 pixels e ir recomponiendo el byte original. Esto tiene una limitación y es que con un STEP=1 sólo podremos ocultar ficheros cuyo tamaño en bits no supere al número de pixel de la imagen tras restar los 32 pixels que consume el codificado de la cabecera. Para superar esta limitación, y ocultar textos o ficheros más extensos, se puede aumentar el valor de STEP para ocultar más bits del mensaje en cada pixel de la imagen pero en ese caso hay que tener en cuenta que la variación de color entre el pixel modificado y sus vecinos es más brusca y la anomalía será más fácilmente detectable, como se puede ver en la imagen de ejemplo siguiente en la que se ocultó el mismo texto que antes con un STEP=16 (fíjese como ahora los pixel modificados sí son claramente detectables en la forma de esas líneas verticales de "ruido"):



Como también se puede ver en la imagen anterior, los bits modificados están equiespaciados para dificultar su detección al dispersar la anomalía generada. La separación entre pixels modificados se calcula con la llamada a calculateSeparation de hideFile en función del tamaño del mensaje a ocultar, el tamaño en pixels de la imagen contenedora y el STEP que hayamos fijado. Hay que tener en cuenta que si se ha usado un STEP diferente de 1 (el de por defecto) habrá que decírselo a stegimage.py a la hora de extraer el mensaje. Si no se hace así, el programa no sabrá en qué pixels fijarse para reconstruir el mensaje (o mejor dicho, usará un STEP=1 y mirará los pixels equivocados). Por ejemplo, si al codificar hubiésemos usado un STEP de 8, la orden de extracción sería la siguiente:

/stegimage$ ./stegimage.py -d texto_extraido.txt -x lena_steg.png -s 8


Una vez calculada la separación mediante calculateSeparation y grabada la cabecera en los primeros pixels mediante encodeHeader, la función hideFile comienza un bucle en el que va leyendo byte a byte el mensaje a ocultar y va grabando esos bytes en los pixels de la imagen mediante la función encodeBits:

 def encodeBits(self, bit_string,  position=0):
        """ This function deals with writing of long bit strings."""
        if (position == 0):
            self.current_position = self.last_position
        else:
            self.current_position = position
#        self.x = 0
        for bit in bit_string:
            self.setBit(self.current_position, bit,  self.x)
            if (self.x < self.step-1):#We store up to STEP data bit in every selected pixel.
                self.x = self.x + 1
            else:
                self.x = 0
                self.current_position = self.current_position + self.separation
        self.last_position = self.current_position
La clave de la función anterior, la que realmente modifica el pixel es setBit:
 def setBit(self, startPosition, value_string,  offset=0):
        """ To put a bit in a position. """
        self.pos_y = int(libmymath.myround((startPosition / self.width),  libmymath.DOWN))
        self.pos_x = (startPosition % self.width)
        self.original_value = self.pixmap[self.pos_x,  self.pos_y]
        self.binary_original_value_string = self.color2binary(self.original_value)
        if (offset == 0):
            self.binary_modified_value_string = /
self.binary_original_value_string[:-(offset+1)] + value_string
        else:
            self.binary_modified_value_string = /
self.binary_original_value_string[:-(offset+1)] + value_string + /
self.binary_original_value_string[-offset:]
        self.modified_value = self.binary2color(self.binary_modified_value_string)
        self.pixmap[self.pos_x,  self.pos_y] = self.modified_value

Una vez codificados todos los bytes del mensaje a ocultar en los pixels de la imagen, hideFunction guarda la imagen modificada en el fichero de salida que fijamos al llamar al programa mediante la función hostFileImage.Save.

Con esto queda explicado el esqueleto del proceso de ocultación de un mensaje en una imagen, el proceso de extracción para obtener de vuelta el mensaje es exactamente igual pero a la inversa. Para más detalle lo mejor es una lectura detenida del código fuente de stegimage.py y de sus librerías adjuntas. Creo que el código está lo suficientemente conciso y comentado como para asegurar una fácil comprensión.

Así damos fin a este artículo, en el que hemos repasado brevemente la historia y las bases del extensísimo campo de la esteganografía visual. Este campo está en constante movimiento, no sólo por su utilidad en el campo de la Inteligencia (con I mayúscula) sino también en el comercial, concretamente en el de la protección de derechos de autor ya que la esteganografía es un recurso excelente para firmar determinadas fotografías de manera que se puede controlar su difusión comercial.