Stop The Voices - UmassCTF2024
Reto de Esteganografía basado en la operación de 400 imágenes para aproximar una función gaussiana.
Autor del reto: unknown
Dificultad: Media
Enunciado
“Patrick’s been trying to remember the flag, but his vision seems a little blurry and the voices just don’t stop…”
Archivos
En este reto, tenemos los siguientes archivos.
chall.zip --> samples/
: Carpeta que contiene 400 imágenes png.chall.zip --> generator.py
: Contiene la lógica del programa.
Archivos utilizados aquí.
Analizando el código
Analizando el script generator.py
, podemos ver lo siguiente.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from PIL import Image
import numpy as np
img = Image.open('FLAG.png').convert('L')
arr = np.asanyarray(img)
def normalize(mat):
return (mat - mat.min()) / (mat.max() - mat.min()) * 255
for i in range(400):
noise = np.random.normal(arr, 200)
noise = normalize(noise)
noise = noise.astype(np.uint8)
im = Image.fromarray(noise)
im.save(f"./samples/{i}.png")
Solución
En este código generator.py
, básicamente lo que esta haciendo, es generar 400 imágenes con diferentes patrones de ruido agregado a partir de la imagen original Flag.png
.
De forma más detallada el código hace lo siguiente.
- Abre la imagen
Flag.png
y la convierte a escala de grises con el parámetro('L')
. - Convierte la imagen en una matriz usando `np.asanyarray(), esto crea una matriz donde cada pixel de la imagen esta representado por un valor numérico.
- Se define una función llamada
normalice(mat)
que normaliza una matriz de entrada entre el valor 0 255. - Iniciamos el bucle que se ejecuta 400 veces.
4.1 Dentro del bucle, genera ruido gaussiano aleatorio con la misma forma que la matriz de la imagen original
(arr)
usando la librerianp.random.normal()
. Importante, el segundo argumento 200 es ladesviación estándar
del ruido, esto es lo que controla la “cantidad” de ruido añadido a las imágenes.- 4.2 Normaliza la matriz de ruido usando la función definida anteriormente
normalize()
. - 4.3 Convierte la matriz de ruido normalizado en un array de enteros sin signo de 8 bits
(np.uint8)
para asegurar que los valores estén en el rango apropiado para los píxeles de la imagen. - 4.4 Crea una nueva imagen a partir del array de pixeles utilizando
Image.fromarray()
. - 4.5 Por último, guarda la imagen resultante en la ubicación
/samples/
.
Llegados a este punto, ya entendemos en profundidad que es lo que hace el código pero ¿Cómo lo resolvemos?
Como hemos comentado, el ruido de cada imagen se genera a partir del ruido gaussiano introducido con la funcion np.random.normal()
. Dicho generador de ruido funciona siguiendo una distribución normal o en otras palabras, siguiendo una función gaussiana.
Como tenemos las 400 imágenes resultantes y queremos recuperar la Flag.png
lo que tenemos que hacer para obtener la imagen original es aproximarnos a dicha función gaussiana a partir de la suma de todos los 400 valores muestrales de las imaágenes, ya que si tenemos una cantidad suficiente de valores muestrales (400 imágenes) a lo largo de la curva de esta función y luego sumamos dichas imágenes, obtendremos una aproximación de dicha función original (En este caso aproximarnos a la funcion original significa recuperar la imagen original Flag.png
)
Entrando más al campo de la estadística, la función gaussiana es una distribución de probabilidad continua que sigue la siguiente forma.
La función gaussiana es una distribución de probabilidad continua, definida por la fórmula:
\[f(x) = \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(x-\mu)^2}{2\sigma^2}\right)\]Donde:
- $\mu$ es la media de la distribución.
- $\sigma$ es la desviación estándar de la distribución.
Por lo tanto, tenemos que hacer un script donde vayamos sumando las 400 imágenes con ruido resultantes en una sola y dicha imagen, que será suma de todas será Flag.png
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from PIL import Image
import numpy as np
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
paths = []
for i in range(400):
paths.append(os.path.join(script_dir, f"{i}.png"))
def normalize(mat):
return ((mat - mat.min()) / (mat.max() - mat.min()) * 255).astype(np.uint8)
flag = np.zeros((450, 450), dtype=np.float64)
for path in paths:
img = Image.open(path)
arr = np.array(img, dtype=np.float64)
flag += arr
flag_normalized = normalize(flag)
im = Image.fromarray(flag_normalized)
output_path = os.path.join(script_dir, "flag.png")
im.save(output_path)
print(f"Flag image saved to: {output_path}")
De forma mucho más detallada, el script hace lo siguiente.
- Importamos las bibliotecas necesarias.
Image
dePIL
: Para manipular imágenes.numpy as np
: Para realizar operaciones numéricas en matrices.os
: Para interactuar con el sistema operativo y manejar rutas de archivos.
script_dir
almacena la ruta del directorio donde se encuentra el script actual.En un bucle
for
generamos 400 rutas de archivos correspondientes a las imágenes en la carpeta/samples
numeradas del 0 al 399 y guardamos dichas rutas en la listapaths
.Rescatamos del script original la función
normalize()
que recordemos que toma una matriz como entrada y normaliza sus valores en el rango de 0 a 255.Inicializamos una matriz nula que sera nuestra
flag
con la forma (450,450) siendo el tipo de datosfloat64
la cual usaremos para acumular los valores de píxeles de las imágenes.- Recorremos en un bucle todas las rutas de archivos de
paths
- 6.1 Se abre cada imagen utilizando PIL.
- 6.2 Se convierte dicha imagen en una matriz numpy de tipo
float64
. - 6.3 Se suma dicha matriz a nuestra matriz
flag
donde se irán sumando todos los valores.
Después de que todas las imágenes se sumaran a la matriz
flag
, aplicamos la funciónnormalize()
para normalizar los valores de píxeles en el rango de 0 a 255.Con dichos valores resultantes creamos una imagen utilizando
Image.fromarray()
.Guardamos dicha imagen resultante en el directorio del script con el nombre
flag.png
.- Por último, imprimimos la ubicación donde se guardó la imagen.
NOTA
El script debe estar dentro de la carpeta samples/
.
Flag
UMASS{#id31n9_L1k3_@_c#Am3_leOn}