Jugando con la esteganografía: El comienzo (parte 1)

No recuerdo cómo ni por qué llegué a un termino llamado «esteganografía«. Lo que sí recuerdo es que me resultó interesante y comencé a curiosear del tema. Leí un poco de aquí y de allá sobre esta parte de la criptología y me recorrió una no tan extraña sensación por el cuerpo que me invitaba a indagar, probar, investigar o como os dé la gana llamarlo. La cuestión es que pensé en hacer mi propio programa que de una forma básica pusiera en práctica la esteganografía para, después, poco a poco, ir haciendo pruebas y sacar conclusiones. Pero ¿qué es la esteganografía? Según un extracto de la wikipedia:

La esteganografía (del griego στεγανος (steganos):cubierto u oculto, y γραφος (graphos): escritura), es la parte de la criptología en la que se estudian y aplican técnicas que permiten el ocultamiento de mensajes u objetos, dentro de otros, llamados portadores, de modo que no se perciba su existencia. (http://es.wikipedia.org/wiki/Esteganograf%C3%ADa)

Fácil, ¿no?

Bien, para mi pequeño programa he utilizado la esteganografía en imagenes, es decir, ocultar «cosas» en una imagen sin que se perciba. Antes de explicar cómo lo he hecho y mostrar los resultados, os dejo el código del programa que podéis encontrar en https://github.com/NachE/stegapy (donde además lo iré actualizando). Lo he llamado Stegapy:

Nota: He borrado los comentarios del código, haz click aquí para ver la versión con comentarios (en inglés)

#!/usr/bin/env python
# Stegapy
# Copyright 2013 J.A. Nache
# See LICENSE for details.

#python-PIL
import Image, sys, os

class UnestegFile:

	def __init__(self, pathOrigImage, pathDestFile):

		self.pathOrigImage = pathOrigImage
		self.pathDestFile = pathDestFile

		self.file = open(self.pathDestFile, 'wb')
		self.imageObj = Image.open(self.pathOrigImage)
		self.imgPix = self.imageObj.load()

	def readImgInBit(self):
		for x in range(0, self.imageObj.size[0]):
			for y in range(0, self.imageObj.size[1]):
				RGB = self.imgPix[x,y]
				for color in RGB:
					yield bin(color)[-1:]

	def unHide(self):
		bitcount = 0
		bytecount = 0
		byte = str()
		size = str()
		maxeof = 4294967295 # The largest file size in bytes we can hide
		bytes = self.readImgInBit()

		print("\nUnhidding file, wait...\n")
		for bit in bytes:
			byte += bit
			bitcount = bitcount + 1

			if bitcount == 8:
				bytecount = bytecount + 1

				size +=byte
				if bytecount == 4:
					print('-> Size of hidden file: '+
							str(int( size ,2))+' bytes')
					print('\tAn extra 4 bytes (32bits) at header are skipped')
					print('\tThese extra 4 bytes are used to save the original file size')
					print('\tso its not part of file.')
					maxeof = int(size ,2)+4

				if bytecount > 4:
					self.file.write( chr( int( byte ,2)) )

				byte = str()
				bitcount = 0

				if bytecount == maxeof:
					break
		print ('\nDone')
		print ('==================================')
		print (str(bytecount - 4)+' bytes writed on file '+self.pathDestFile)

	def closeFile(self):
		self.file.close()

class EstegFile:

	def __init__(self, pathOrigFile, pathOrigImage, pathDestImage):

		self.pathOrigFile = pathOrigFile
		self.pathOrigImage = pathOrigImage
		self.pathDestImage = pathDestImage

		self.imageObj = Image.open(self.pathOrigImage)
		self.imgPix = self.imageObj.load()

		self.file = open(self.pathOrigFile, 'rb')

		self.imageObjNew = Image.new("RGB", self.imageObj.size, "white")
		self.imgPixNew = self.imageObjNew.load()

	def saveDest(self):
		self.imageObjNew.save(self.pathDestImage,"PNG")

	def readFileInBit(self):
		fsize = os.fstat(self.file.fileno()).st_size
		for bit in bin(fsize)[2:].zfill(32):
			yield bit

		while True:
			character = self.file.read(1)
			if not character:
				break
			else:
				byte = bin( ord( character ) )[2:].zfill(8)
				for bit in byte:
					yield bit

	def buildNew(self):
		inbytes = self.readFileInBit()
		bitscount = 0;

		print("\nHidding file, wait...")
		for x in range(0, self.imageObj.size[0]):
			for y in range(0, self.imageObj.size[1]):

				RGB = self.imgPix[x,y]
				RGBnew = ()

				for color in RGB:
					try:
						RGBnew = RGBnew + ( int( bin(color)[:-1]+inbytes.next() ,2), )
						bitscount = bitscount + 1

					except StopIteration:
						RGBnew = RGBnew + (color, )

				self.imgPixNew[x,y] = RGBnew

		print ('\nDone')
		print ('==================================')
		print ('Orig File: '+self.pathOrigFile)
		print ('Hided with image: '+self.pathOrigImage)
		print ('Result File: '+self.pathDestImage)
		print ('==================================')
		print ('Hidded '+str(bitscount)+' bits | '
				+str(bitscount*0.125)+' bytes | '
				+str(bitscount*0.000122070312)+' kilobytes')
		print ('Extra 32 bits are used (represent the file size)')

def show_about():
	print('\n   Stegapy v0.1')
	print('   Copyright 2013 J.A. Nache under GPL v3\n')

def show_help():
	show_about()
	print ('\n   Usage')
	print ('   =========')
	print ('   Hide file:')
	print ('   '+sys.argv[0]+' h <File To Hide> <Source Image> <New Image File>')
	print ('\n   Unhide file:')
	print ('   '+sys.argv[0]+' u <Image with hidden data> <Dest File>\n')

try:
	if sys.argv[1] == 'h':
		show_about()
		sg = EstegFile(sys.argv[2], sys.argv[3], sys.argv[4])
		sg.buildNew()
		sg.saveDest()

	elif sys.argv[1] == 'u':
		show_about()
		sg = UnestegFile(sys.argv[2], sys.argv[3])
		sg.unHide()
		sg.closeFile()
	else:
		show_help()

except IndexError:
	show_help()

El código de Stegapy aquí arriba es capaz de ocultar cualquier archivo en una imagen (por el momento imagen PNG), además del acto contrario: extraer el archivo oculto. Esto nos lleva al siguiente punto:

¿Cómo funciona la esteganografía en imágenes? (En Stegapy, más bien)

Para ser más precisos, esta es la forma en la que Stegapy hace uso de la «inserción en el bit menos significativo«. Lo primero que debemos saber es que cada pixel de una imágen se puede descomponer en tres valores: Rojo, Verde y Azul, o más comúnmente RGB por sus siglas en inglés, Red Green Blue (sí, en la viña del señor hay de todo, pero me he centrado en el RGB). El rango de valores de cada color va desde el 0 hasta el 255 o dicho en binario, desde el 00000000 hasta el 11111111. De este modo, un pixel completamente blanco de una imagen tendrá un valor RGB de R=11111111 G=11111111 B=11111111. Un pixel completamente negro tendrá un valor RGB de R=00000000 G=00000000 B=00000000, y, como es lógico, un pixel completamente rojo tendrá un valor RGB de R=11111111 G=00000000 B=00000000.

La idea principal de la inserción en el bit menos significativo es utilizar el mencionado bit menos significativo de cada color (el situado más a la derecha) y sustituirlo por los diferentes valores bit a bit del archivo a ocultar. Digamos por ejemplo que queremos ocultar la letra A mayúscula, que tiene un valor binario de 01000001 dentro de una imagen. Para ello necesitaríamos ocho colores (son ocho bits). Dado que cada pixel contiene tres colores (R, G y B), vamos a necesitar casi tres pixels. Digamos que los tres primeros pixels de nuestra imagen tienen unos valores tal que:

Pixel 1: R=01010011 G=00011001 B=11011010
Pixel 2: R=01010101 G=10101010 B=11111100
Pixel 3: R=11111111 G=00000000 B=11111111

Ahora queremos ocultar nuestra letra A mayúscula con valor binario 010000001 en estos pixels, por lo que usando nuestro bit menos significativo, quedaría tal que:

Pixel 1: R=01010010 G=00011001 B=11011010
Pixel 2: R=01010100 G=10101010 B=11111100
Pixel 3: R=11111110 G=00000001 B=11111111

El resultado final serán tres pixeles ligeramente modificados, pero lo suficientemente inapreciable como para que el conjunto no sufra un cambio brusco.

Una vez que sabemos cómo funciona Stegapy, llega el momento de hacer pruebas y comprobar los resultados, lo que nos lleva al siguiente punto:

Obteniendo los resultados de la inserción en el bit menos significativo (Esteganografía)

Para las siguientes pruebas voy a utilizar el texto de la licencia GPLv3 de Stegapy que se puede encontrar en: https://github.com/NachE/stegapy/blob/master/LICENSE. Decir tiene que Stegapy recorre los pixels de la imagen de arriba a abajo, por lo que la información oculta irá formando columnas de izquierda a derecha.

Para las pruebas voy a utilizar una imagen cualquiera, digamos…, esta:

Yo raro sin datos ocultos
Imagen para pruebas con Stegapy

Ahora, vamos a insertar el texto propuesto, el texto que queremos ocultar, dentro de esta imagen y comparar el resultado:

Imagen original
Imagen original
Imagen con datos ocultos
Imagen con datos ocultos

¿Alguna diferencia? A simple vista parece que la imagen no ha sufrido ningún cambio, ¿pero qué tal si echamos un vistazo con un editor gráfico a la imagen generada? Vamos allá:

Modificando los valores de nivel parece que se aprecia algo, pero levemente
Modificando los valores de nivel parece que se aprecia algo, pero levemente

Según los datos obtenidos y las pruebas realizadas, parece que a simple vista no se aprecia cambio alguno, no obstante modificando los niveles en los colores se intuye una columna de datos que el ojo más hábil será capaz de ver. Para tratar de forzar el florecimiento de esta columna, vamos a hacer un último experimento, coloquemos unas franjas horizontales de color blanco (con pixeles de valor 11111111) en la imagen original y volvamos a esconder nuestro texto en esta nueva imagen para después tratarla con nuestro editor gráfico:

Imagen con franjas y pasada por editor gráfico
Imagen con franjas y pasada por editor gráfico

En esta ocasión, tras someter la imagen con datos ocultos a nuestro editor gráfico, observamos como aparece una columna de datos en las franjas blancas, lo que nos indica que cuanto más ruido tiene nuestra imagen, menos perceptible es al ojo humano.

Y este es el fin de la primera parte. Para las siguientes partes de la saga investigaré técnicas de detección de datos ocultos mediante esteganografía y experimentaré con nuevas formas de ocultación para dificultar en mayor medida la detección de nuestros datos.

Hala, hasta la próxima.