Todo empezó cuando vi que con una suscripción a Amazon Prime tenemos disponibles 5GB de almacenamiento Amazon Drive e ilimitado para imágenes. A primera vista, el servicio de Amazon no ofrece demasiado espacio en su nube, comparado con sus competidores directos.
Tal como puede verse en la imagen, el espacio que nos ofrece Amazon con su suscripción no es nada del otro mundo, pero hay una gran diferencia, sobre todo a nivel de almacenamiento de imágenes. En Google Photos tenemos, hasta Junio de 2021 almacenamiento ilimitado de imágenes, pero implementa compresión con pérdida.
Google Photos comprime las imágenes que subimos. Tiene también soporte de subida y visualización para el formato de imágenes HEIF, introducido por primera vez por Apple en iOS 11. Aunque no podemos asegurar que sea este el formato que utiliza el servicio de forma nativa, probablemente haga uso de él para el almacenamiento de imágenes. De hecho, aunque corregido a día de hoy, hace algún tiempo podían subirse imágenes desde un dispositivo iOS, que por defecto utiliza HEIC en sus capturas y sobre el que Google Photos no aplicaba ningún tipo de compresión, por lo que es lógico pensar que no lo hacían porque era el formato que ellos ya consideraban comprimido en su propio servicio.
"No tengo pruebas, pero tampoco dudas."
HEIC: Un archivo HEIC es una imagen rasterizada guardada en HEIF, un formato de contenedor de medios con compresión con pérdida que se usa comúnmente para almacenar imágenes. Puede contener una sola imagen, una colección de imágenes, ráfagas o secuencias de imágenes, junto a metadatos que describen cada imagen. De hecho, los fondos dinámicos de macOS, funcionan gracias a este interesante formato de contenedores. Podéis obtener más información sobre ello en el
GitHub de Marcin Czachurski.
En cambio, Amazon Drive, no aplica ningún tipo de compresión a las imágenes que subimos, de hecho, incluso mantiene el nombre original del fichero y sus metadatos EXIF. Esto hizo que se me encendiese la bombilla de ideas, y me hice la pregunta que siempre me lleva a acabar realizando una PoC.
¿Y si pudiera convertirse cualquier archivo en una imagen?
El origen
Una de las técnicas de esteganografía en imágenes más utilizadas es LSB (Less Significant Bit). Podemos ocultar información en el último, o incluso últimos bit de color de cada pixel de una imagen.
En la imagen de arriba, podéis ver el funcionamiento básico de la técnica LSB, pero para la funcionalidad que quería implementar, era impensable desperdiciar los 7 bits restantes de cada uno de los bytes de color de la imagen, ya que se necesitarían muchas más imágenes para guardar la misma cantidad de información. Así que, comencé a investigar sobre librerías de imágenes en Python que me permitiesen generar imágenes a partir de un array de bytes, de esta forma, solventamos dos de los problemas que se nos presentan.
- No necesitamos una imagen inicial en la que codificar datos. Son los propios datos los que generan la imagen al completo.
- Podemos utilizar la totalidad de los bits de color, para almacenar datos.
Una vez que descubrí que
Pillow permitía generar imágenes a partir de bytes, solo necesitaba leer cualquier tipo de fichero en "RAW binary" para poder convertirlo en imágenes PNG. Elegí este formato ya que es un formato de imagen que utiliza un algoritmo de compresión sin pérdida llamado Deflate, que es una combinación entre LZ77 y
codificación Huffman, así ahorramos algo más de espacio. Sin embargo, quedaban algunas cosas que resolver.
El particionado
«Comience pequeño, piense en grande. No te preocupes por muchas cosas a la vez. Para empezar, tome un puñado de cosas simples y luego progrese a otras más complejas. Piensa no solo en el mañana, sino en el futuro. Pon un toque en el universo». Steve Jobs
Aunque inicialmente podríamos haber decidido generar una única imagen, es una idea horrible. No habría problema para archivos pequeños, pero como estaba intentando conseguir almacenamiento ilimitado sería bastante extraño subir una imagen que ocupe 5GB. Por ello, decidí establecer un tamaño máximo de imagen configurable. El número de bytes que caben en una imagen RGB de un tamaño determinado podemos calcularlo fácilmente.
tamaño_imagen_en_bytes = px_altura * px_anchura * 3_bytes_de_color
En una imagen de 2000x2000 píxeles RGB, caben 12.000.000 bytes. Es decir, 12MB. Usaremos este ejemplo para explicarlo de forma más sencilla, aunque como ya he dicho, el tamaño de la imagen que se genera es modificable.
De esta forma durante la codificación se leen bytes del archivo hasta llegar al tamaño máximo establecido. En ese momento, se llama a una función que genera una imagen a partir del array de bytes y la guarda en un directorio, con un nombre correlativo, para facilitar la tarea posterior de restauración del archivo original a partir de las imágenes creadas. Esta tarea se ejecuta en bucle hasta que se llega al final del archivo.
Metadatos y relleno
Hay que tener algo más en cuenta, y es que probablemente, en la última imagen quedará espacio sin utilizar, pues sería prácticamente imposible que el archivo que queremos codificar, ocupe en bytes exactamente un múltiplo de 12MB. Así que debe establecerse algún tipo de padding que podamos eliminar de forma posterior para poder generar la imagen completa. Por lo que, se han utilizado bytes "\x00" para rellenar el resto de la imagen hasta estar completa.
Además, durante el proceso se me ocurrió la posibilidad de utilizar metadatos para almacenar dos cosas.
- El nombre y la extensión original del archivo a partir del cual se generaron las imágenes
- El tamaño original del archivo previo a ser partido. (A continuación entenderéis el porqué de esto)
La recomposición
Cuando subimos la carpeta con todas las imágenes que conforman nuestro archivo codificado, el propio Amazon Photos nos avisa con un pop-up de si queremos crear un álbum para esas fotos. Agradezco al equipo de desarrollo del servicio haber implementado esta funcionalidad, ya que nos facilita bastante el proceso de descarga, pues así podemos descargar un ZIP con nuestro archivo codificado al completo.
Con el archivo de nuevo en nuestro equipo, llamamos al programa con la opción de "merge" y especificamos la carpeta. En ese momento, comienza el proceso de recomposición de las imágenes, extrayendo la información en RAW del RGB. Primero se extraen los metadatos para conocer el nombre del archivo. Y a partir de ese momento, el software extrae los bytes imagen a imagen hasta que llega a la última, donde como ya explicamos, existe un padding.
De ahí el segundo parámetro que extraemos desde los metadatos. Es obvio, que el archivo final ha de tener exactamente los mismos bytes que el original, sobre todo si queremos que la aplicación sea funcional para todo tipo de ficheros, incluyendo binarios y ejecutables. Así que, si con la fórmula de arriba sabemos cuantos bytes de información existen en las imágenes, podemos asumir lo siguiente.
bytes_última_imagen_hasta_padding = (num_imágenes * tamaño_imagen_en_bytes) - tamaño_archivo_original
Esto nos da la información que necesitamos para saber cuantos bytes existen en la última imagen previos al comienzo del relleno con "\x00".
Una vez que el archivo se restaura, podemos utilizar cualquier utilidad para calcular el checksum, en este caso usamos sha256sum para comprobar la integridad del archivo tras el proceso.
Cuando conseguí que la aplicación funcionase, me surgieron bastantes preguntas relativas a ciberseguridad. Es posible subir cualquier tipo de archivo, pero para no violar los términos de uso, simplemente hicimos comprobaciones con un Eicar para ver que, una vez codificado en imagen, ni software antivirus habituales, ni aparentemente Amazon, son capaces de detectar un potencial malware.
Con esta pequeña aplicación, al final, estamos subiendo "imágenes" a Amazon Photos, y como tal, el espacio es ilimitado. También es posible crear links públicos para compartir nuestros archivos a cualquier persona.
Espero que os sirva de utilidad, y recordad, cuando creéis cosas, nunca olvidéis vuestro "Y si...?".
@Nicomda