
🔐 Esteganografia de imagem em PHP: esconder texto em pixels com LSB
Clique, e o seu encoded.png parece idêntico ao container.jpg. Ninguém vai suspeitar que há uma mensagem escondida lá dentro. A esteganografia não torna os dados ilegíveis como a encriptação faz; ela esconde o próprio facto da transmissão. Para um programador PHP, esta é uma daquelas técnicas que vale a pena compreender ao nível dos bits e dos píxeis.
A esteganografia LSB em PHP com o módulo GD não é uma ferramenta de produção, mas sim um campo de treino. Um campo de treino que o ensina a trabalhar com representação binária, máscaras de bits e formatos sem perdas melhor do que uma dúzia de tarefas CRUD. Vai passar de «como é que uma imagem é estruturada por dentro» à linha php decrypt.php que extrai hello dos píxeis, e vai perceber porque é que o JPEG não é adequado para isto.
💡 Visão geral rápida:
- Explicamos a teoria: como o bit menos significativo do canal azul esconde uma mensagem e porque é que o olho não o vê
- Escrevemos três ficheiros PHP:
functions.php(utilitários),encrypt.php(codificação),decrypt.php(extração) - Executamo-lo num contentor JPEG real e obtemos um resultado PNG visualmente indistinguível
- Analisamos as limitações: compressão, detetabilidade, capacidade e a combinação com encriptação AES
- Analisamos bibliotecas PHP prontas a usar e decidimos quando o GD é suficiente e quando é altura de usar Python
Como funciona a esteganografia LSB
LSB (Least Significant Bit) é um método em que a mensagem secreta é escrita nos bits menos significativos dos píxeis. O nome é literal: pegamos no bit menos significativo de um canal de cor e substituímo-lo por um bit da mensagem.
Uma imagem digital é uma grelha de píxeis. Cada píxel é codificado com três bytes: vermelho (R), verde (G) e azul (B). A combinação de três valores de 0 a 255 determina a cor final. Alterar o bit menos significativo muda o valor do canal no máximo em 1; por exemplo, RGB(100, 50, 200) passa a RGB(100, 50, 201). O olho humano não consegue distinguir a diferença.
Vejamos um píxel laranja:
- Vermelho:
11001001(201) - Verde:
11111000(248) - Azul:
00000011(3)
O bit menos significativo do canal azul é um. Substitua-o por um bit da mensagem secreta e a cor permanecerá visualmente a mesma. Ao processar os píxeis sequencialmente, escrevemos a mensagem completa bit a bit.
Suponha que precisamos de esconder a sequência 1101. Pegamos em 4 píxeis e alteramos o bit menos significativo do canal azul:
Píxel | R | G | B original | LSB substituído | Novo B |
|---|---|---|---|---|---|
1 |
|
|
| → |
|
2 |
|
|
| → |
|
3 |
|
|
| → |
|
4 |
|
|
| → |
|
A descodificação é o processo inverso: recolhemos os bits menos significativos do canal azul dos mesmos quatro píxeis e obtemos 1101. Nada de magia, apenas operações bit a bit.
Na prática, não está limitado apenas ao canal azul: pode usar os três (R, G, B), triplicando o volume de dados escondidos. Neste tutorial, trabalhamos apenas com o azul por simplicidade, para ser mais fácil ver o que acontece com cada bit.
Implementação passo a passo em PHP
Vai precisar do PHP com o módulo GD, que está pré-instalado em praticamente todos os fornecedores de alojamento. O código está escrito para PHP 8.0+ e testado até à versão 8.4. Crie três ficheiros numa pasta.
Passo 1: Funções auxiliares, functions.php
Dois utilitários: converter uma string para representação binária e vice-versa.
1 <?php 2 3 function 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 20 function 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() converte cada caractere da string para uma representação de 8 bits através de decbin(ord(...)) e preenche com zeros à esquerda para formar um byte completo. toString() faz o oposto: fatia a string binária em pedaços de 8 bits, converte cada pedaço para um número decimal e obtém o caractere através de chr().
O processamento bloco a bloco em toString() não é um capricho. A abordagem popular em tutoriais que usa pack('H*', base_convert($binary, 2, 16)) falha em mensagens com mais de 31 bits: a função base_convert opera com inteiros e, ao exceder PHP_INT_MAX, devolve lixo. A divisão por bytes resolve o problema para mensagens de qualquer comprimento.
Passo 2: Codificação, encrypt.php
Lê um contentor JPEG, incorpora a mensagem nos bits menos significativos do canal azul e guarda o resultado como PNG. Porquê não JPEG? Um formato sem perdas é obrigatório: a compressão JPEG destrói os bits escondidos e a mensagem transforma-se em ruído.
1 <?php 2 3 require_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 12 for ($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 28 imagepng($img, 'encoded.png'); 29 30 echo "Закодировано {$msgLen} бит — результат в encoded.png\n";
imagecolorat() obtém a cor do píxel nas coordenadas (x, y). Os deslocamentos de bits >>16 e >>8 extraem R, G, B. O canal azul é expandido para uma string de 8 bits, o bit menos significativo (índice 7) é substituído pelo próximo bit da mensagem e a nova cor é escrita através de imagesetpixel().
A mensagem é codificada diagonalmente (y = x), o que facilita o acompanhamento da lógica num exemplo pequeno. Num projeto real, itere por todos os píxeis linha a linha e adicione um cabeçalho de 32 bits com o comprimento da mensagem: sem ele, o descodificador não sabe onde parar.
Passo 3: Descodificação, decrypt.php
Extrai a mensagem escondida: percorre os mesmos píxeis e recolhe os bits menos significativos do canal azul.
1 <?php 2 3 require_once 'functions.php'; 4 5 $src = 'encoded.png'; 6 $img = imagecreatefrompng($src); 7 8 $bits = ''; 9 10 for ($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 19 echo toString($bits) . "\n";
Aqui 40 é um número de bits fixo, exatamente 5 bytes para a palavra hello. Numa versão de produção, substitua este número pelo comprimento do cabeçalho da mensagem que escreveu nos primeiros 32 bits durante a codificação.
Passo 4: Execução e verificação
Coloque container.jpg na pasta com os scripts. O procedimento:
php encrypt.phpescreve na consola: "Encoded 40 bits, result in encoded.png".php decrypt.phpescreve na consola:hello.- Abra
container.jpgeencoded.pnglado a lado; não há diferença. São visualmente indistinguíveis.
Pode comparar byte a byte usando o ImageMagick: magick compare container.jpg encoded.png diff.png irá destacar diferenças nos bits menos significativos, praticamente invisíveis a olho nu. Mas a forma mais fiável de verificar é executar decrypt.php e ler a mensagem.
Limitações do método
A esteganografia LSB é uma ferramenta de aprendizagem, não uma proteção criptográfica. Eis o que precisa de ter em mente antes de usar esta abordagem num projeto real:
A compressão destrói os dados. O JPEG recalcula blocos de 8×8 píxeis com quantização e os bits menos significativos não sobrevivem. É por isso que o
encrypt.phpguarda o resultado estritamente como PNG (sem perdas). Uma passagem pela compressão JPEG e os bits escondidos transformam-se em ruído. Formatos alternativos: BMP, TIFF sem compressão.Detetabilidade. O preenchimento LSB deixa artefactos estatísticos: o histograma do canal azul muda de uma forma previsível. A análise RS e o teste χ² detetam uma mensagem escondida em segundos. Sem encriptação, o LSB é ocultação, não proteção: a presença de uma mensagem pode ser detetada mesmo que o conteúdo não possa ser lido.
Volume de dados. Cada píxel cede um bit no canal azul. Uma imagem de 1000×1000 píxeis pode conter até 125 KB de texto escondido; este é o limite matemático decorrente da definição do método LSB. Na prática, após a compressão PNG, o volume útil é menor, além de que alguns bits vão para o cabeçalho com o comprimento.
Não é criptografia. A esteganografia não substitui a encriptação; complementa-a. O pipeline correto: encripte a mensagem (AES-256) e depois esconda o texto cifrado na imagem. Assim, mesmo que o canal escondido seja detetado, o conteúdo não será revelado.
O PHP GD é uma ferramenta básica. O GD fornece acesso de baixo nível aos píxeis e, para aprendizagem, isto é mais do que suficiente. Mas o ecossistema Python para esteganografia é visivelmente mais rico: a biblioteca
stegano(LSB + encriptação),Stegano(um conjunto de métodos) e osteghide(C++, mas existem bindings Python). Em PHP puro, existem várias bibliotecas:kzykhys/Steganography(uma implementação LSB mínima para PNG),seba/steganography(uma biblioteca moderna com manipulação LSB) epateon/steganography(PHP 8.5+ com suporte a GD e Imagick). Mas não existem soluções de nível profissional ao nível dosteghideem PHP, à data de 2026.
⁉️🤔 Perguntas frequentes
É possível esconder texto diretamente em JPEG?
Não. A compressão JPEG divide a imagem em blocos de 8×8 e aplica quantização; os bits menos significativos não sobrevivem. Para esconder dados em JPEG, usa-se uma abordagem diferente: os dados são escondidos nos coeficientes da transformada discreta de cosseno (DCT), não nos píxeis. Algoritmos: F5, JSteg, OutGuess. Praticamente não existem bibliotecas PHP prontas para isto; é mais fácil usar Python.
Qual é a diferença entre LSB e esteganografia em metadados?
O LSB altera os dados dos píxeis; a mensagem fica escondida «dentro» da imagem. Os metadados (EXIF, XMP, blocos tEXt do PNG) estão localizados nos cabeçalhos do ficheiro e podem ser lidos por qualquer visualizador de propriedades. Os metadados são mais fáceis de extrair, mas sobrevivem à compressão JPEG, ao contrário do LSB.
Como posso verificar se o encoded.png contém realmente uma mensagem escondida?
Visualmente, não pode. Compare os ficheiros byte a byte:
magick compare container.jpg encoded.png diff.png(ImageMagick) mostrará diferenças nos bits menos significativos. São praticamente invisíveis a olho nu, mas o utilitário destacará os píxeis alterados. A forma mais fiável é executarphp decrypt.phpe ler a mensagem.
Quão seguro é isto, no geral?
Sem encriptação, não é seguro. O LSB sem encriptação AES prévia é ocultação, não proteção. O facto da transmissão de dados escondidos é detetado por testes estatísticos em segundos. A combinação «encriptação AES + ocultação LSB» fornece um nível aceitável para projetos pessoais, mas não para dados sensíveis.
Existem bibliotecas PHP prontas para esteganografia?
Sim, mas são todas educativas ou leves. A
kzykhys/Steganographyé uma implementação LSB mínima que funciona apenas com PNG. Aseba/steganographyé mais recente, com uma API limpa para codificar/descodificar texto em imagens. Apateon/steganographyé para PHP 8.5+ e suporta tanto GD como Imagick. Para tarefas sérias, olhe para Python:stegano(LSB + encriptação) eStegano(um conjunto de métodos, incluindo um gerador de estego-imagens).
Esteganografia em PHP: uma base de aprendizagem ou uma ferramenta pronta a usar?
A esteganografia LSB em PHP com GD é um excelente campo de treino para compreender como os dados são escondidos ao nível do píxel. Escreveu um ciclo de codificação/descodificação funcional, percebeu as operações bit a bit e viu na prática porque é que uma alteração «invisível» deixa rastos nas estatísticas da imagem.
Para uma demonstração de aprendizagem, um projeto pessoal ou um desafio CTF, este conjunto de ferramentas é mais do que suficiente. Para proteção de dados real, adicione encriptação antes da ocultação e explore as bibliotecas Python, que estão muito acima das equivalentes PHP em capacidades e resistência à deteção.
Se quiser aprofundar, implemente a codificação nos três canais de cor em simultâneo e adicione um cabeçalho de 32 bits com o comprimento da mensagem. Obterá uma mini-ferramenta 3 vezes mais eficiente do que a versão básica e que já se qualifica como um projeto de curso. Publique o código-fonte no GitHub para que outros possam aprender com o seu exemplo.



