Skip to content

Todo para WordPress, el desarrollo web — y mucho más

🔐 Esteganografía de imágenes en PHP: ocultar texto en píxeles con LSB

🔐 Esteganografía de imágenes en PHP: ocultar texto en píxeles con LSB

Haga clic y su encoded.png se verá idéntica a container.jpg. Nadie sospechará que hay un mensaje oculto dentro. La esteganografía no vuelve los datos ilegibles como hace el cifrado; oculta el hecho mismo de la transmisión. Para un desarrollador PHP, esta es una de esas técnicas que vale la pena entender a nivel de bits y píxeles.

La esteganografía LSB en PHP con el módulo GD no es una herramienta de producción, sino un campo de entrenamiento. Un campo de entrenamiento que le enseña a trabajar con representación binaria, máscaras de bits y formatos sin pérdida mejor que una docena de tareas CRUD. Pasará de «cómo se estructura una imagen por dentro» a la línea php decrypt.php que extrae hello de los píxeles, y entenderá por qué JPEG no es adecuado para esto.

💡 Resumen rápido:

  • Desglosamos la teoría: cómo el bit menos significativo del canal azul oculta un mensaje y por qué el ojo no lo ve
  • Escribimos tres archivos PHP: functions.php (utilidades), encrypt.php (codificación), decrypt.php (extracción)
  • Lo ejecutamos en un contenedor JPEG real y obtenemos un resultado PNG visualmente indistinguible
  • Repasamos las limitaciones: compresión, detectabilidad, capacidad y la combinación con cifrado AES
  • Examinamos bibliotecas PHP ya existentes y decidimos cuándo GD es suficiente y cuándo es momento de usar Python

Cómo funciona la esteganografía LSB

LSB (Least Significant Bit, bit menos significativo) es un método donde el mensaje secreto se escribe en los bits menos significativos de los píxeles. El nombre es literal: tomamos el bit menos significativo de un canal de color y lo reemplazamos con un bit del mensaje.

Una imagen digital es una cuadrícula de píxeles. Cada píxel se codifica con tres bytes: rojo (R), verde (G) y azul (B). La combinación de tres valores de 0 a 255 determina el color final. Cambiar el bit menos significativo altera el valor del canal en no más de 1; por ejemplo, RGB(100, 50, 200) se convierte en RGB(100, 50, 201). El ojo humano no puede distinguir la diferencia.

Tomemos un píxel naranja:

  • Rojo: 11001001 (201)
  • Verde: 11111000 (248)
  • Azul: 00000011 (3)

El bit menos significativo del canal azul es uno. Reemplácelo con un bit del mensaje secreto y el color permanecerá visualmente igual. Al procesar los píxeles secuencialmente, escribimos el mensaje completo bit a bit.

Supongamos que necesitamos ocultar la secuencia 1101. Tomamos 4 píxeles y cambiamos el bit menos significativo del canal azul:

Píxel

R

G

B original

LSB reemplazado

Nuevo B

1

11001100

10010001

00101011

1

00101011

2

00011000

11110000

11111110

1

11111111

3

11100010

00100101

01010101

0

01010100

4

11111101

00001010

01000011

1

01000011

La decodificación es el proceso inverso: recolectamos los bits menos significativos del canal azul de los mismos cuatro píxeles y obtenemos 1101. No hay magia, solo operaciones a nivel de bits.

En la práctica, no está limitado solo al canal azul: puede usar los tres (R, G, B), triplicando el volumen de datos ocultos. En este tutorial trabajamos solo con el azul por simplicidad, para que sea más fácil ver qué sucede con cada bit.

Implementación paso a paso en PHP

Necesitará PHP con el módulo GD, que viene preinstalado en prácticamente todos los proveedores de hosting. El código está escrito para PHP 8.0+ y probado hasta la versión 8.4. Cree tres archivos en una carpeta.

Paso 1: Funciones auxiliares, functions.php

Dos utilidades: convertir una cadena a representación binaria y viceversa.

1<?php
2
3function toBin(string $str): string
4{
5 $result = '';
6 $len = strlen($str);
7
8 while ($len--) {
9 $result = str_pad(
10 decbin(ord($str[$len])),
11 8,
12 '0',
13 STR_PAD_LEFT
14 ) . $result;
15 }
16
17 return $result;
18}
19
20function toString(string $binary): string
21{
22 $result = '';
23 foreach (str_split($binary, 8) as $byte) {
24 $result .= chr((int) base_convert($byte, 2, 10));
25 }
26
27 return $result;
28}

toBin() convierte cada carácter de la cadena a una representación de 8 bits mediante decbin(ord(...)) y rellena con ceros a la izquierda para completar un byte. toString() hace lo contrario: divide la cadena binaria en fragmentos de 8 bits, convierte cada fragmento a un número decimal y obtiene el carácter mediante chr().

El procesamiento bloque por bloque en toString() no es un capricho. El enfoque popular en tutoriales que usa pack('H*', base_convert($binary, 2, 16)) falla con mensajes de más de 31 bits: la función base_convert opera con enteros y, al exceder PHP_INT_MAX, devuelve basura. Dividir por bytes resuelve el problema para mensajes de cualquier longitud.

Paso 2: Codificación, encrypt.php

Lee un contenedor JPEG, incrusta el mensaje en los bits menos significativos del canal azul y guarda el resultado como PNG. ¿Por qué no JPEG? Un formato sin pérdida es obligatorio: la compresión JPEG destruirá los bits ocultos y el mensaje se convertirá en ruido.

1<?php
2
3require_once 'functions.php';
4
5$message = 'hello';
6$binary = toBin($message);
7$msgLen = strlen($binary);
8
9$src = 'container.jpg';
10$img = imagecreatefromjpeg($src);
11
12for ($x = 0; $x < $msgLen; $x++) {
13 $y = $x;
14 $rgb = imagecolorat($img, $x, $y);
15
16 $r = ($rgb >> 16) & 0xFF;
17 $g = ($rgb >> 8) & 0xFF;
18 $b = $rgb & 0xFF;
19
20 $blueBits = str_pad(decbin($b), 8, '0', STR_PAD_LEFT);
21 $blueBits[7] = $binary[$x];
22 $newB = bindec($blueBits);
23
24 $newColor = imagecolorallocate($img, $r, $g, $newB);
25 imagesetpixel($img, $x, $y, $newColor);
26}
27
28imagepng($img, 'encoded.png');
29
30echo "Закодировано {$msgLen} бит — результат в encoded.png\n";

imagecolorat() obtiene el color del píxel en las coordenadas (x, y). Los desplazamientos de bits >>16 y >>8 extraen R, G, B. El canal azul se expande a una cadena de 8 bits, el bit menos significativo (índice 7) se reemplaza con el siguiente bit del mensaje y el nuevo color se escribe mediante imagesetpixel().

El mensaje se codifica en diagonal (y = x), lo que facilita seguir la lógica en un ejemplo pequeño. En un proyecto real, itere por todos los píxeles fila por fila y añada un encabezado de 32 bits con la longitud del mensaje: sin él, el decodificador no sabe dónde detenerse.

Paso 3: Decodificación, decrypt.php

Extrae el mensaje oculto: recorre los mismos píxeles y recolecta los bits menos significativos del canal azul.

1<?php
2
3require_once 'functions.php';
4
5$src = 'encoded.png';
6$img = imagecreatefrompng($src);
7
8$bits = '';
9
10for ($x = 0; $x < 40; $x++) {
11 $y = $x;
12 $rgb = imagecolorat($img, $x, $y);
13
14 $b = $rgb & 0xFF;
15 $blueBits = str_pad(decbin($b), 8, '0', STR_PAD_LEFT);
16 $bits .= $blueBits[7];
17}
18
19echo toString($bits) . "\n";

Aquí 40 es un número fijo de bits, exactamente 5 bytes para la palabra hello. En una versión de producción, reemplace este número con la longitud del encabezado del mensaje que escribió en los primeros 32 bits durante la codificación.

Paso 4: Ejecución y verificación

Coloque container.jpg en la carpeta con los scripts. El procedimiento:

  • php encrypt.php muestra en la consola: "Encoded 40 bits, result in encoded.png".
  • php decrypt.php muestra en la consola: hello.
  • Abra container.jpg y encoded.png una al lado de la otra; no hay diferencia. Son visualmente indistinguibles.

Puede comparar byte a byte usando ImageMagick: magick compare container.jpg encoded.png diff.png resaltará las diferencias en los bits menos significativos, prácticamente invisibles al ojo. Pero la forma más fiable de verificar es ejecutar decrypt.php y leer el mensaje.

Limitaciones del método

La esteganografía LSB es una herramienta de aprendizaje, no protección criptográfica. Esto es lo que debe tener en cuenta antes de usar este enfoque en un proyecto real:

  • La compresión destruye los datos. JPEG recalcula bloques de 8×8 píxeles con cuantificación y los bits menos significativos no sobreviven. Por eso encrypt.php guarda el resultado estrictamente como PNG (sin pérdida). Un solo paso por compresión JPEG y los bits ocultos se convertirán en ruido. Formatos alternativos: BMP, TIFF sin compresión.

  • Detectabilidad. El relleno LSB deja artefactos estadísticos: el histograma del canal azul cambia de forma predecible. El análisis RS y la prueba χ² detectan un mensaje oculto en segundos. Sin cifrado, LSB es ocultación, no protección: la presencia de un mensaje puede detectarse incluso si el contenido no puede leerse.

  • Volumen de datos. Cada píxel cede un bit en el canal azul. Una imagen de 1000×1000 píxeles puede contener hasta 125 KB de texto oculto; este es el límite matemático que se deriva de la definición del método LSB. En la práctica, tras la compresión PNG, el volumen útil es menor, más algunos bits que se destinan al encabezado con la longitud.

  • No es criptografía. La esteganografía no reemplaza el cifrado; lo complementa. El flujo correcto: cifre el mensaje (AES-256), luego oculte el texto cifrado en la imagen. Así, incluso detectar el canal oculto no revelará el contenido.

  • PHP GD es una herramienta básica. GD proporciona acceso de bajo nivel a los píxeles y, para aprender, esto es más que suficiente. Pero el ecosistema Python para esteganografía es notablemente más rico: la biblioteca stegano (LSB + cifrado), Stegano (un conjunto de métodos) y steghide (C++, pero existen bindings para Python). En PHP puro existen varias bibliotecas: kzykhys/Steganography (una implementación LSB mínima para PNG), seba/steganography (una biblioteca moderna con manipulación LSB) y pateon/steganography (PHP 8.5+ con soporte para GD e Imagick). Pero no hay soluciones de grado producción al nivel de steghide en PHP a fecha de 2026.

⁉️🤔 Preguntas frecuentes

¿Se puede ocultar texto directamente en JPEG?

No. La compresión JPEG divide la imagen en bloques de 8×8 y aplica cuantificación; los bits menos significativos no sobreviven. Para ocultar datos en JPEG se usa un enfoque diferente: los datos se ocultan en los coeficientes de la transformada discreta del coseno (DCT), no en los píxeles. Algoritmos: F5, JSteg, OutGuess. Prácticamente no hay bibliotecas PHP listas para esto; es más fácil usar Python.

¿En qué se diferencia LSB de la esteganografía en metadatos?

LSB modifica los datos del píxel; el mensaje se oculta «dentro» de la imagen. Los metadatos (EXIF, XMP, fragmentos tEXt de PNG) se encuentran en las cabeceras del archivo y pueden ser leídos por cualquier visor de propiedades. Los metadatos son más fáciles de extraer, pero sobreviven a la compresión JPEG, mientras que LSB no puede.

¿Cómo puedo verificar que encoded.png contiene realmente un mensaje oculto?

Visualmente, no puede. Compare los archivos byte a byte: magick compare container.jpg encoded.png diff.png (ImageMagick) mostrará diferencias en los bits menos significativos. Son prácticamente invisibles al ojo, pero la utilidad resaltará los píxeles modificados. La forma más fiable es ejecutar php decrypt.php y leer el mensaje.

¿Qué tan seguro es esto en general?

Sin cifrado, no es seguro. LSB sin cifrado AES previo es ocultación, no protección. El hecho de la transmisión de datos ocultos se detecta mediante pruebas estadísticas en segundos. La combinación de «cifrado AES + ocultación LSB» proporciona un nivel aceptable para proyectos personales, pero no para datos sensibles.

¿Existen bibliotecas PHP listas para esteganografía?

Sí, pero todas son educativas o ligeras. kzykhys/Steganography es una implementación LSB mínima que funciona solo con PNG. seba/steganography es más reciente, con una API limpia para codificar/decodificar texto en imágenes. pateon/steganography es para PHP 8.5+ y soporta tanto GD como Imagick. Para tareas serias, mire hacia Python: stegano (LSB + cifrado) y Stegano (un conjunto de métodos, incluyendo un generador de estego-imágenes).

Esteganografía en PHP: ¿base de aprendizaje o herramienta lista para usar?

La esteganografía LSB en PHP con GD es un excelente campo de entrenamiento para entender cómo se ocultan los datos a nivel de píxel. Escribió un ciclo de codificación/decodificación funcional, comprendió las operaciones a nivel de bits y vio en la práctica por qué un cambio «invisible» deja rastros en las estadísticas de la imagen.

Para una demostración de aprendizaje, un proyecto personal o un desafío CTF, este conjunto de herramientas es más que suficiente. Para la protección de datos reales, añada cifrado antes de la ocultación y explore las bibliotecas de Python, que están muy por encima de sus contrapartes PHP en capacidades y resistencia a la detección.

Si desea profundizar, implemente la codificación en los tres canales de color a la vez y añada un encabezado de 32 bits con la longitud del mensaje. Obtendrá una mini-herramienta que es 3 veces más eficiente que la versión básica y ya califica como proyecto de curso. Publique el código fuente en GitHub para que otros puedan aprender de su ejemplo.