21 agosto 2010

Sigo vivo

Para los que echen en falta más artículos en este blog avisarles de que estoy muy volcado con un amigo en el desarrollo de una herramienta que tiene utilidad directa para los artículos que escribo en este blog.... Así que yo lo veo como un periodo de sequía necesario para permitir más y mejores artículos.

Un saludo a todos.

14 marzo 2010

Esteganografía sonora

En mi anterior artículo desarrollé una de las técnicas conocidas para ocultar mensajes en imágenes. A lo largo de este repasaremos otras técnicas utilizadas para esconder datos, pero esta vez en ficheros de sonido.

Como seguramente recuerde el lector, en mi artículo sobre esteganografía visual, se utilizaba una técnica que se basaba en introducir variaciones en puntos clave de la imagen de manera que, aunque estas variaciones fueran imperceptibles para el ojo humano, pudieran ser recopiladas por un programa informático que supiera en qué puntos buscar y en función de dichas variaciones fuese capaz de reconstruir el mensaje.

Utilizar también esta técnica con ficheros de sonidos es una opción evidente. Con un fichero de sonido que usase un formato sin pérdidas, como por ejemplo los ficheros wav, el algoritmo podría limitarse a codificar ceros y unos en puntos muy concretos procurando que su amplitud fuera mucho menor que la amplitud misma de la señal de audio, la cual se convertiría así en portadora del mensaje oculto. Existen multitud de formatos para codificar ceros y unos sobre señales de amplitud variable. Un ejemplo de estos formatos es la codificación Mánchester que, en su versión del IEEE 802.3, codifica unos como pendientes ascendentes (cambio de nivel bajo a nivel alto) y los ceros como pendientes descendentes (cambios de nivel alto a bajo).
De esta manera, un algoritmo de ocultamiento de datos podría escoger momentos periódicos del fichero de sonido, medir la amplitud en ese punto y forzar que la amplitud de las muestras inmediatamente precedentes y posteriores se ajustasen al nivel necesario para codificar en Manchester los unos y ceros de los que se compone el mensaje a ocultar. Si cada señal Manchester usa como valor medio el de la muestra original, y la deformación de las muestras inmediatamente anteriores y posteriores es de un amplitud reducida, la anomalía introducida es casi imperceptible para un oído humano no avisado. Por supuesto, y como es habitual en esteganografía, esta anomalía también será más perceptible conforme aumentamos la relación " (tamaño del mensaje a ocultar) / (tamaño del mensaje portador)".


(Si pincha en las imágenes siguientes podrá verlas con mayor resolución)





En las gráficas precedentes se pueden ver las ondas de amplitud (para el canal izquierdo y derecho) de un fragmento de un fichero de sonido en formato WAV. La primera pareja de ondas son la del fichero de sonido en su versión original, la segunda corresponde a la misma onda con un mensaje de texto oculto. He señalado en rojo algunas de las alteraciones introducidas en la señal para codificar cada bit, si se acercase el zoom un poco más se distinguirían los escalones propios de la codificación Mánchester.

La anomalía introducida mediante este método toma generalmente la forma de una pequeña distorsión en el sonido, particularmente en los solos agudos se nota como una tenue capa de ruido "blanco" que envuelve al sonido restándole definición. Comparando el fichero de sonido y el original se nota bastante bien, sobre todo en ficheros de sonido donde predominen los vocalistas... aunque si en el fichero predominan instrumentación diversa y "ruidosa" como guitarras eléctricas, o si sencillamente el oyente no está alertado de antemano, puede pasar mucho más desapercibido.

Este método tiene una serie de inconvenientes muy importantes. Para empezar utiliza un formato de audio que actualmente está en desuso salvo para aplicaciones muy concretas. Los ficheros WAV son de un tamaño enorme por lo que la mayor parte de los usuarios utilizan ficheros comprimidos, como por ejemplo MP3, que les permiten tamaños mucho más manejables y una calidad aceptable. Así, los ficheros sin pérdidas suelen quedar relegados a masters y piezas de trabajo de alta calidad para la edición de sonido... por lo que un fichero de este tipo con las anomalías que hemos descrito hasta ahora sería ciertamente llamativo. Por otro lado, este tipo de codificación exige que la reconstrucción del mensaje ocultado se haga en base a medidas muy precisas en momentos muy concretos del fichero de audio, una actualización de las librerías de audio utilizadas para la escritura/lectura puede alterar infinitesimalmente la definición de los puntos sucesivos de muestreo de manera que escuchemos un poco "demasiado pronto" o "demasiado tarde" cada una de las muestras donde están los bits escondidos lo que alteraría irremisiblemente la correcta reconstrucción de la información escondida.

Para experimentar con esta técnica, programé en python una aplicación denominada stegaudio . Para ejecutar stegaudio en linux hace falta la instalación de un par de librerías adicionales de tratamiento de audio. Para hacerlo en Ubuntu no hay más que teclear la siguiente orden:


$ sudo aptitude install python-tk python-tksnack

Una vez hecho esto, ya estamos en condiciones de ejecutar stegaudio.

Para pedirle a stegaudio que esconda un fichero en un archivo .wav tendríamos que introducir la siguiente orden:


$ ./stegaudio.py -i fichero_wav_original.wav -d fichero_a_ocultar -o fichero_wav_portador.wav



Para extraer un fichero escondido, la orden es igualmente sencilla:



$ ./stegaudio.py -x fichero_wav_portador.wav -d fichero_rescatado


A la hora de esconder información, el núcleo de stegaudio es la función hideFile() de la librería libstegaudio.py:

 def hideFile(self, data_file_name):
        """ This function encodes data file into audio 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 = self.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.hostFileSound.write(self.hostFileName)
            return OK

La orden calculateSeparation() compara el tamaño del fichero portador y el que hay que esconder y determina cada cuantas muestras de sonido del portador hay que introducir una anomalía que codifique un bit del fichero a esconder. Obviamente, conforme el intervalo de separación entre anomalías se vaya reduciendo, para archivos a esconder m
ás grandes, estas serán más perceptibles para el usuario que escuche el fichero portador. En cuanto a la orden encodeHeader(), se encarga de cifrar en el tamaño del fichero ocultado al comienzo mismo del fichero portador utilizando para ello 32 anomalías (cada una de llas representaría 1 bit) sin intervalos de separación entre una y otra y distribuidas entre todos los canales de los que disponga el fichero de sonido (p.ej. si fuera un fichero estéreo, 2 canales, cada uno tendría 16 anomalías). Por último, la orden encodeBits() va introduciendo byte a byte el fichero a ocultar en el portador:


 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
        self.channel_number = self.getNumberOfChannels()
        for bit in bit_string:
            self.setBit(self.current_position, int(bit),  self.x)
            if (self.x < self.channel_number-1):
                self.x = self.x + 1
            else:
                self.x = 0
                self.current_position = self.current_position + /
self.step * 2 + self.separation
        self.last_position = self.current_position


Como se puede ver, encodeBits() distribuye los bits por los
diferentes canales de los que consta el fichero portador, de manera que si el fichero tuviera 2 canales uno tendría los bits impares y el otro los impares. Esta distribución entre todos los canales permite ocultar mayor cantidad de información y, al equidistribuir las manipulaciones, permite hacer las anomalías menos llamativas (imaginemos si no como llamaría la atención tener un canal, p.ej el izquierdo, con un sonido limpio y el derecho con algo de "nieve" o ruido). La clave de esta función es a su vez setBit() que se encarga de codificar el bit a ocultar en formato Mánchester sobre la señal portadora:


 def setBit(self, startPosition, value, track = LEFT):
        """ To put a bit in a position. """
        self.original_values = []
        self.modified_values = []
        if (startPosition < (self.hostFileSound.length(unit='SAMPLES') - /
len(self.step_range))):
            for i in range(startPosition, startPosition + /
len(self.step_range)):
                self.channel_values = self.hostFileSound.sample(i)
                self.original_values.append(self.channel_values.split())
        else: # To avoid an index out of bounds in the last few samples.
            for i in range(startPosition,self.hostFileSound.length/
(unit='SAMPLES')):
                self.channel_values = self.hostFileSound.sample(i)
                self.original_values.append(self.channel_values.split())
        self.overall = 0
        for original_value in self.original_values:
            self.overall = self.overall + int(original_value[track]) 
        self.mean_value = self.overall/len(self.step_range)
        # It looks like tkinter has kind of value range limit /
depending of filetype, so I have to put values within this limits.
        if ((self.hostFileSound['fileformat'] == 'WAV') or /
(self.hostFileSound['fileformat'] == 'RAW')):
            if (self.mean_value >= (WAV_MAX - self.delta)):
                self.mean_value = WAV_MAX - self.delta
            elif (self.mean_value <= (WAV_MIN + self.delta)):
                self.mean_value = WAV_MIN + self.delta
        self.modified_values = copy.deepcopy(self.original_values)
        for modified_value in self.modified_values:
            if (value == 0):
                if (self.modified_values.index(modified_value)< /
self.step):
                    modified_value[track] = self.mean_value + /
self.delta
                else:
                    modified_value[track] = self.mean_value - /
self.delta
            else:
                if (self.modified_values.index(modified_value)< /
self.step):
                    modified_value[track] = self.mean_value - /self.delta
                else:
                    modified_value[track] = self.mean_value + /
self.delta
        self.r = 0
        if (startPosition < (self.hostFileSound.length(unit='SAMPLES') - /len(self.step_range))):
            for i in range(startPosition, /
startPosition+len(self.step_range)):
                if (track == RIGHT):
                    self.hostFileSound.sample(i, /
right = self.modified_values[self.r][RIGHT])
                else:
                    self.hostFileSound.sample(i, /
left = self.modified_values[self.r][LEFT])
                self.r =self.r + 1
        else: # To avoid an index out of bounds in the last few samples.
            for i in range(startPosition, /
self.hostFileSound.length(unit='SAMPLES')):
                if (track == RIGHT):
                    self.hostFileSound.sample(i, right = /self.modified_values[self.r][RIGHT])
                else:
                    self.hostFileSound.sample(i, left = /
self.modified_values[self.r][LEFT])
                self.r =self.r + 1

Hay que señalar dos variables muy importantes en la función anter
ior: por un lado self.step_range que define el ancho de un pulso Mánchester (p.ej, un 1 codificado en Mánchester con un step_range 4 se compondría de 2 muestras de nivel bajo y 2 de nivel alto); la otra variable importante es self.delta en la que se define la diferencia de niveles entre muestras bajas y altas en la codificación Mánchester. La anomalía percibible será mayor conforme lo sean los valores tanto de step_range y delta ya que suponen una mayor deformación de la señal original. A nivel teórico, el step_range puede ser perfectamente 2 y el delta 1, pero en la realidad eso puede dar problemas ya que exigiría una precisión milimétrica en la toma de muestras y en su valoración en el extremo receptor a la hora de reconstruir el mensaje oculto. Si el fichero portador se va a transportar en su formato .wav es probable que no haya problema en la extracción del mensaje oculto. Sin embargo, si la idea es grabar ese wav en un CD para hacerlo pasar por un CD de música clásico luego será necesaria la recodificación en formato wav, por lo que pueden producirse pérdidas de muestras aún procurando que las frecuencias de muestreo de todas esas conversiones son las mismas. En este último caso, resulta altamente recomendable aumentar tanto el step_range como el delta al máximo tolerable para asegurar que los pulsos Manchester siguen siendo percibibles después de todas esas conversiones.
No voy a perder tiempo explicando el proceso de extracción de un fichero escondido ya que este proceso es exactamente el recíproco al que hemos cisto hasta ahora.

Si utilizásemos un fichero comprimido con un algoritmos con pérdi
das (mucho más común), como por ejemplo un MP3, la técnica anterior no valdría ya que estos los algoritmos de compresión realizan un muestreo de la señal original y descartan porciones de esta por lo que si en esos fragmentos hay información escondida nos resultará imposible reconstruir el mensaje original. En esos casos, resulta obligado estudiar el algoritmo de compresión utilizado en el fichero concreto que queremos utilizar de portadora para encontrar una manera de esconder datos.
En el caso de fichero MP3 tenemos dos opciones: la primera es trabajar en el dominio de la frecuencia y la segunda aprovechar particularidades del formato de los archivos MP3.
El primer caso es el más académico y complejo. El formato MP3 elige las frecuencias más altas a la hora de descartar. Se basa en que el oído humano común sólo puede percibir las frecuencias más bajas del espectro de frecuencias por lo que eliminar la información referente a las bandas altas permitiría reducir el volumen de datos de los ficheros de sonido sin empeorar sensiblemente su calidad. Aún así, hay gente con un oído particularmente sensible que sí que nota la ausencia de las frecuencias altas y que prefiere utilizar algoritmos sin pérdidas como el WAV (por lo general es la misma gente que se quejó de la pérdida de calidad del sonido de los CD respecto de los vinilos :-P ). Un algoritmo esteganográfico que supiese distinguir unas frecuencias de otras y se centrase en trabajar sólo con las frecuencias bajas podría esconder sus datos sin temor de que el algoritmo de compresión se llevase por delante parte del mensaje oculto. Este enfoque es el seguido por herramientas como mp3stego.

El segundo caso es quizás más sencillo e inmediato. Y es el enfoque usado por algoritmos como el de Achmad. El formato MP3 es muy sencillo. Un fichero MP3 es una mera sucesión de frames independientes entra si. Cada uno de estos frames tiene su propia cabecera, sus propios campos independientes del resto de los frames y recoge la información referida a un instante determinado del sonido grabado. Los lectores de MP3 se limitan
a leer y volcar el contenido de los frames uno detrás de otro hasta que ya no quedan más en el archivo. La importancia de la independencia de estos frames entre si radica en que se pueden eliminar e introducir otros nuevos a voluntad sin que el fichero quede inutilizable.

(Pinche en la imagen siguiente para ver el diagrama con mayor resolución)


Como se puede ver en el diagrama anterior, cada frame va señalado en su comienzo por una secuencia xFFF, el resto de los campos le dicen al lector de MP3 cómo debe hacer el sonido contenido en el campo de datos.


El algoritmo de Achmad saca partido de los denominados frames homogéneos que codifican los silencios infinitesimales que se dan entre unos sonidos y otros. Un frame homogéneo puede tener cualquier contenido en su campo de datos y el lector que lo lea siempre lanzará un silencio. De esta manera, el algoritmo Achmad localiza los frames homogéneos de un fichero y graba en ello el mensaje a ocultar. Para encontrar estos frames sólo hay que buscar aquellos que tienen los bytes a partir del 36 de cada frame fijados a un mismo valor. El cam po de datos comienza en el byte número 36 de este tipo de frames y es ahí donde se puede esconder nuestro mensaje.

(Pinche en la imagen siguiente para ver el diagrama con mayor resolución)



En la imagen anterior se puede ver el contenido hexadecimal de un fichero MP3 y resaltado un frame homogeneo. En dicha imagen salta a la vista que a partir del byte 36 todos los demás tienen el valor OxFF lo que significa que son homogéneos.
El algoritmo Achmad, en su concepción original, cuenta con la ventaja de que no altera el tamaño del fichero y no introduce anomalía alguna en el fichero de sonido por lo que es difícilmente detectable. Por otro lado presenta el inconveniente de que el tamaño del mensaje que se puede ocultar está limitado por el número y tamaño de los frames homogéneos del fichero de sonido elegido como portadora. Para ilustrar este algoritmo, programé una pequeña aplicación denominada stegmp3 que implementa el algoritmo de Achmad y que incorpora como novedad sobre este la posibilidad de introducir frames homogéneos adicionales con el fin de ocultar ficheros de cualquier tamaño. El precio de esta innovación con respecto al algoritmo original es que si el tamaño del fichero a ocultar es demasiado grande y por ello hay que generar muchos frames homogéneos adicionales la anomalía comienza a ser perceptible en forma de silencios intercalados. Vamos a estudiar con más detalle cómo funciona stegmp3.

Para empezar, su uso es exactamente igual que el de stegaudio. Así, para ocultar un fichero no habría más que teclear:


$ ./stegmp3.py -i fichero_mp3_original.mp3 -d fichero_a_ocultar -o fichero_mp3_portador.mp3




Y para extraerlo:


$ ./stegmp3.py -x fichero_mp3_portador.mp3 -d fichero_rescatado



Cuando se llama a stegmp3 este comienza creando un objeto MP3HostFile, el cual analiza en su constructor el fichero mp3 que se ha pasado como parámetro con el fin de sacar una lista con todos los frames presentes en el fichero (mediante la llamada a getFrames() ). Luego esa lista se utiliza 0 bien para localizar frames homogéneos (mediante la llamada a searchForHomogenousFrames() ), o bien para buscar frames con datos ocultos (mediante searchForHostFrames() ) si el programa se usa para extracción.


La extracción de los diferentes frames con
getFrames() es relativamente fácil gracias a la marca OxFF que hay al principio de cada uno.

En cuanto a searchForHomogeneousFrames() examina cada uno de los frames que getFrames() ha depositado en una lista al efecto:





def searchForHomogeneousFrames(self):
        """This function examines frames stored in our list and /
finds out which of them are homogeneous."""
        self.homogeneous_frame = FALSE
        self.first_homogeneous_frame_found = FALSE
        self.counter = 0
        if (self.originalMP3FileName==None):
            print "Searching for frames marked with signature..."
        else:
            print "Searching for homogeneous frames..."
        for frame in self.frame_list:
            for index in range(DATA_OFFSET, frame.size-1):
                if not (struct.unpack('=B',  frame.data[index])[0]) == /
(struct.unpack('=B', frame.data[index+1])[0]):
                    self.homogeneous_frame = FALSE
                    break
                else:
                    self.homogeneous_frame = TRUE
            if self.homogeneous_frame:
                (self.frame_list[self.frame_list.index(frame)])./
homogeneous = TRUE
                self.homogeneous_frame = FALSE
                self.counter += 1
                #We are not going to use first homogenous /
frame to store data. We are going to store hidden
                #data size instead. So we don't count this /
frame as usable.
                if self.first_homogeneous_frame_found: 
                    self.maximum_size_for_hiding += /
(self.frame_list[self.frame_list.index(frame)]).size - /
DATA_OFFSET - self.signature_length
                else:
                    self.first_homogeneous_frame_found = TRUE
                sys.stdout.write("|")
                sys.stdout.flush()
            else:
                sys.stdout.write("X")
                sys.stdout.flush()
        self.maximum_size_for_hiding -= /
STEGMP3_HEADER_SIZE
        print ("\nFound " + str(self.counter) + /
" frames useful for hiding.")
        if (not self.originalMP3FileName==None):
            print ("We can hide up to " + /str(self.maximum_size_for_hiding)  + " bytes.")


Como se puede ver en el código anterior, la identificación se produce comparando entre si los valores a partir del 36 de cada uno de los frames y viendo si están fijados a un mismo valor. Se puede ver también que el primer frame homogéneo no se cuenta a la hora de calcular la capacidad máxima total de los frames homogéneos del fichero, esto es así porque el primer frame de este tipos se utilizará para codificar el tamaño del fichero oculto.

Una vez localizados todos los frames homogéneos del fichero es cuando se llama a hideFile().



def hideFile(self,  data_file_name_to_hide):
        """This function hides data_file_name_to_hide in MP3 file""" 
        self.file_to_hide_name = data_file_name_to_hide
        self.file_to_hide = open(self.file_to_hide_name)
        self.file_to_hide_size = /
(os.stat(data_file_name_to_hide))[stat.ST_SIZE]
        print "Data size to hide: " + str(self.file_to_hide_size)+ /
" bytes..."
        if self.file_to_hide_size > self.maximum_size_for_hiding:
            self.createStubFrames(self.file_to_hide_size,  /self.maximum_size_for_hiding)
        self.file_to_hide_payload = self.file_to_hide.read()
        self.encodeHeader(self.file_to_hide_size)
        print "Hiding " + self.file_to_hide_name + " inside of " + /self.hostMP3FileName + "..."
        self.writeStegFile(self.file_to_hide_payload)
        return OK


Si hideFile() detecta que el fichero que se pretende ocultar excede la capacidad de los frames homogéneos presentes en el fichero de sonido portador, llama a createStubFrames() para crear frames homogéneos adicionales hasta conseguir la capacidad de
seada.

Una vez que se ha asegurado que hay suficientes frames homogéneos como para ocultar el fichero deseado, se procede a llamar a encodeHeader() con el fin de guardar en el primer frame homogéneo el tamaño del fichero ocultado:


def encodeHeader(self, size):
        """This function encodes file to hide size in the very first /
STEGMP3_HEADER_SIZE bytes of the first frame capable for hiding"""
        for index,  frame in enumerate(self.frame_list):
            if frame.homogeneous: #We are searching just for first /
homogeneous frame only.
                self.size_packed = struct.pack('=L', size)
                frame.data = frame.data[:DATA_OFFSET] + self.signature + /
frame.data[DATA_OFFSET + self.signature_length:]
                frame.data = frame.data[:DATA_OFFSET + self.signature_length] /
+ self.size_packed + /
frame.data[DATA_OFFSET + self.signature_length + STEGMP3_HEADER_SIZE:]
                self.frame_list[index] = frame
                break
Hay que resaltar el uso, tanto en esta función como en writeStegFile(), de la variable signature como firma. Su uso se debe a que, una vez rellenados los frames homogéneos con los datos a ocultar su contenido se vuelve variable por lo que ya no vale el mé
todo que hemos usado hasta el momento para distinguirlos. Por eso es necesario marcar con una firma cada frame homogéneo utilizado. A la hora de extraer el mensaje oculto, la función searchForHostFrames() localizará los frames con datos ocultos gracias a que su contenido comienza con dicha firma.

Una vez hecho todo esto es cuando ya se puede ir recorriendo la lista de frames homogéneos volcando en ellos el contenido del fichero a ocultar mediante la llamada a writeStegFile().

El resultado se puede ver en el siguiente volcado hexadecimal del contenido de un fichero MP3 con datos ocultos:

(Pinche en la imagen siguiente para ver el diagrama con mayor resolución)



Fíjese por último que la cadena que utiliza por defecto stegmp3 para marcar los frames homogéneos utilizados es >>D6NT3<<.

Igual que en el caso de stegaudio, no voy a comentar nada del proceso de extracción ya que es lo mismo que el de ocultación pero a la recíproca.

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:

$ 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.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.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.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.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.