Buscando y buscando librerías para ocultar mensajes en imágenes utilizando Python encontré algunas, pero ninguna funcionó en mi máquina. Ya fuera al momento de instalarla o al momento de utilizarla.
Así que decidí hacerlo a mano, y aprender un poco del proceso. Al final, pude ocultar texto para más tarde recuperarlo. A esta técnica se le llama esteganografía.
No utilicé ninguna librería, sólo PIL para obtener los pixeles de una imagen. Los métodos explicados aquí puede que sean redundantes, pero son bastante explicativos.
Primeramente necesitamos los pixeles como un arreglo de dos dimensiones. El código es sencillo, con la altura y anchura de la imagen hacemos un ciclo dentro de otro. Luego, accedemos a los pixeles en la posición x, y. Y los vamos imprimiendo.
from PIL import Image
imagen = Image.open("back_to_the_future.png") # Aquí puedes cambiar el nombre de la imagenpixeles = imagen.load()
tamaño = imagen.size
anchura = tamaño[0]
altura = tamaño[1]
print("La anchura de la imagen es {}".format(anchura))
print("La altura de la imagen es {}".format(altura))
for x in range(anchura):
for y in range(altura):
pixel = pixeles[x, y]
print(pixel)
Nota: para evitar que salgan miles de líneas en la terminal, puedes presionar Ctrl + C unos segundos después de ejecutar el script, ya que se imprimirán muchas de ellas.
Como se observa, cada pixel se compone de R, G, B.
Sólo tenemos que modificar un poco el código anterior. Quedaría así:
from PIL import Image
imagen = Image.open("back_to_the_future.png") # Aquí puedes cambiar el nombre de la imagenpixeles = imagen.load()
tamaño = imagen.size
anchura = tamaño[0]
altura = tamaño[1]
print("La anchura de la imagen es {}".format(anchura))
print("La altura de la imagen es {}".format(altura))
for x in range(anchura):
for y in range(altura):
pixel = pixeles[x, y]
rojo = pixel[0]
verde = pixel[1]
azul = pixel[2]
print("Rojo: {}".format(rojo))
print("Verde: {}".format(verde))
print("Azul: {}".format(azul))
Y ahora ya imprime el valor de cada color, por cada pixel.
Ya vimos cómo obtener el nivel de cada color, ahora podemos modificarlo. Pero antes, veamos cómo obtener los bits que escribiremos.
Sé que este método no será el mejor ni el más óptimo, pero repito que todo es con fin de explicar las cosas adecuadamente.
Necesitamos escribir una función que, dada una cadena, devuelva una lista de bits. Por ejemplo, si escribo “Hola” debe devolver [‘0’, ‘1’, ‘0’, ‘0’, ‘1’, ‘0’, ‘0’, ‘0’, ‘0’, ‘1’, ‘1’, ‘0’, ‘1’, ‘1’, ‘1’, ‘1’, ‘0’, ‘1’, ‘1’, ‘0’, ‘1’, ‘1’, ‘0’, ‘0’, ‘0’, ‘1’, ‘1’, ‘0’, ‘0’, ‘0’, ‘0’, ‘1’].
Se ve complicado al inicio, pero no lo es. Primero, convierte cada letra al número que le corresponde en el código ascii. Luego, cada código es convertido a binario (un byte) y más tarde, por cada bit en ese byte creamos una lista:
defobtener_representacion_ascii(caracter):
return ord(caracter)
defobtener_representacion_binaria(numero):
return bin(numero)[2:].zfill(8)
defobtener_lista_de_bits(texto):
lista = []
for letra in texto:
representacion_ascii = obtener_representacion_ascii(letra)
representacion_binaria = obtener_representacion_binaria(representacion_ascii)
for bit in representacion_binaria:
lista.append(bit)
return lista
print("Hola en bits es")
print(obtener_lista_de_bits("Hola"))
Al ejecutarlo, se ve esto:
Pero ahora que lo pienso, necesitamos poner unos bits de terminación. Es decir, si tenemos una imagen muy grande y sólo queremos ocultar “Hola”, ¿qué pasa con los demás pixeles? no vamos a recorrer cada pixel sin escribirle nada, y tampoco lo vamos a llenar de ceros.
Mejor ponemos un carácter de terminación; ya que cuando leamos el mensaje oculto tendremos que leer únicamente hasta dicho símbolo. Entonces necesitamos modificar nuestra lista de bits para que regrese los bits de la palabra, y también unos bits de terminación:
caracter_terminacion = [1, 1, 1, 1, 1, 1, 1, 1]
defobtener_representacion_ascii(caracter):
return ord(caracter)
defobtener_representacion_binaria(numero):
return bin(numero)[2:].zfill(8)
defobtener_lista_de_bits(texto):
lista = []
for letra in texto:
representacion_ascii = obtener_representacion_ascii(letra)
representacion_binaria = obtener_representacion_binaria(representacion_ascii)
for bit in representacion_binaria:
lista.append(bit)
for bit in caracter_terminacion:
lista.append(bit)
return lista
print("Hola en bits es")
print(obtener_lista_de_bits("Hola"))
Al ejecutarlo, se ve lo mismo que antes. Pero con la diferencia de que ahora, al final, está nuestro carácter de terminación.
Ahora que ya aprendimos esto, vamos a ir modificando un color por cada bit que haya en nuestra lista.
Antes de juntar todo esto, vamos a ver cómo modificar un color. Recordemos que un pixel tiene 3 colores: Rojo, Verde y Azul. Y cada nivel tiene un valor entre 0 y 255.
Supongamos que nuestro color rojo es 200, y nuestro bit que vamos a ocultar es 1. El código quedaría así:
Es decir, le pasamos una tupla. Y para guardar todo el arreglo de pixeles (es decir, la imagen con el texto oculto) hacemos esto:
imagen.save(ruta_imagen_salida)
Finalmente, quiero explicar que hay un contador para saber si ya terminamos de escribir el mensaje… si ya terminamos, entonces terminamos el ciclo. También sirve para saber si pudimos escribir o no el mensaje completo.
He aquí el código
"""
Ocultar mensaje en una imagen utilizando esteganografía, ocultando un bit en cada nivel de color
Nota: recuerda instalar Pillow:
pip install Pillow
@author parzibyte
@date 06-04-2018
@web https://parzibyte.me/blog/
"""from PIL import Image
import math #Utilizado sólo para redondear hacia abajocaracter_terminacion = [1, 1, 1, 1, 1, 1, 1, 1]
defobtener_representacion_ascii(caracter):
return ord(caracter)
defobtener_representacion_binaria(numero):
return bin(numero)[2:].zfill(8)
defcambiar_ultimo_bit(byte, nuevo_bit):
return byte[:-1] + str(nuevo_bit)
defbinario_a_decimal(binario):
return int(binario, 2)
defmodificar_color(color_original, bit):
color_binario = obtener_representacion_binaria(color_original)
color_modificado = cambiar_ultimo_bit(color_binario, bit)
return binario_a_decimal(color_modificado)
defobtener_lista_de_bits(texto):
lista = []
for letra in texto:
representacion_ascii = obtener_representacion_ascii(letra)
representacion_binaria = obtener_representacion_binaria(representacion_ascii)
for bit in representacion_binaria:
lista.append(bit)
for bit in caracter_terminacion:
lista.append(bit)
return lista
defocultar_texto(mensaje, ruta_imagen_original, ruta_imagen_salida="salida.png"):
print("Ocultando mensaje...".format(mensaje))
imagen = Image.open(ruta_imagen_original)
pixeles = imagen.load()
tamaño = imagen.size
anchura = tamaño[0]
altura = tamaño[1]
lista = obtener_lista_de_bits(mensaje)
contador =0 longitud = len(lista)
for x in range(anchura):
for y in range(altura):
if contador < longitud:
pixel = pixeles[x, y]
rojo = pixel[0]
verde = pixel[1]
azul = pixel[2]
if contador < longitud:
rojo_modificado = modificar_color(rojo, lista[contador])
contador +=1else:
rojo_modificado = rojo
if contador < longitud:
verde_modificado = modificar_color(verde, lista[contador])
contador +=1else:
verde_modificado = verde
if contador < longitud:
azul_modificado = modificar_color(azul, lista[contador])
contador +=1else:
azul_modificado = azul
pixeles[x, y] = (rojo_modificado, verde_modificado, azul_modificado)
else:
breakelse:
continuebreakif contador >= longitud:
print("Mensaje escrito correctamente")
else:
print("Advertencia: no se pudo escribir todo el mensaje, sobraron {} caracteres".format( math.floor((longitud - contador) /8) ))
imagen.save(ruta_imagen_salida)
ocultar_texto("Hola, mundo. Esto es un mensaje oculto desde https://parzibyte.me/blog/", "oveja.png")
Cuando lo ejecuto, sale esto:
En mi caso, la imagen es la de una oveja. Originalmente es esta:
No he probado con imágenes JPG, pero no lo recomiendo, ya que (creo) comprimen los pixeles o algo así, perdiendo nuestros bits ocultos. Por favor prueba sólo con imágenes PNG.
Esto es un poco más sencillo. Ahora no modificamos ningún color, sólo leemos el LSB de cada nivel. Concatenamos todo en un byte; cuando el byte es un byte (es decir, cuando la cadena mide 8) obtenemos su carácter y ese lo concatenamos en una variable llamada mensaje.
Por ejemplo, si mi byte es “01000000”, entonces su representación decimal es 64, y eso, según el código ascii es el arroba @.
Ese arroba lo concatenamos en otra variable, y así vamos formando nuestro mensaje final.
Ahh, otra cosa. Si encontramos el carácter de terminación, nos damos por bien servidos y dejamos de leer los pixeles.
He aquí el código:
"""
Leer mensaje en una imagen utilizando esteganografía, leyendo un bit en cada nivel de color
Nota: recuerda instalar Pillow:
pip install Pillow
@author parzibyte
@date 06-04-2018
@web https://parzibyte.me/blog/
"""from PIL import Image
caracter_terminacion ="11111111"defobtener_lsb(byte):
return byte[-1]
defobtener_representacion_binaria(numero):
return bin(numero)[2:].zfill(8)
defbinario_a_decimal(binario):
return int(binario, 2)
defcaracter_desde_codigo_ascii(numero):
return chr(numero)
defleer(ruta_imagen):
imagen = Image.open(ruta_imagen)
pixeles = imagen.load()
tamaño = imagen.size
anchura = tamaño[0]
altura = tamaño[1]
byte ="" mensaje =""for x in range(anchura):
for y in range(altura):
pixel = pixeles[x, y]
rojo = pixel[0]
verde = pixel[1]
azul = pixel[2]
byte += obtener_lsb(obtener_representacion_binaria(rojo))
if len(byte) >=8:
if byte == caracter_terminacion:
break mensaje += caracter_desde_codigo_ascii(binario_a_decimal(byte))
byte ="" byte += obtener_lsb(obtener_representacion_binaria(verde))
if len(byte) >=8:
if byte == caracter_terminacion:
break mensaje += caracter_desde_codigo_ascii(binario_a_decimal(byte))
byte ="" byte += obtener_lsb(obtener_representacion_binaria(azul))
if len(byte) >=8:
if byte == caracter_terminacion:
break mensaje += caracter_desde_codigo_ascii(binario_a_decimal(byte))
byte =""else:
continuebreakreturn mensaje
mensaje = leer("salida.png")
print("El mensaje oculto es:")
print(mensaje)
En mi caso, la imagen se llama salida.png. Si quieres, descarga la imagen de la oveja modificada, pon el nombre que indico, ejecuta el script y verás que sale el mensaje:
Con eso terminamos. Ya veremos más tarde cómo podemos regar el mensaje, cifrarlo, etcétera.
Si el post ha sido de tu agrado te invito a que me sigas para saber cuando haya escrito un nuevo post, haya
actualizado algún sistema o publicado un nuevo software.
Facebook
| X
| Instagram
| Telegram |
También estoy a tus órdenes para cualquier contratación en mi página de contacto