Esteganografia - como ocultar uma mensagem • Do bit Ao Byte
Djames Suhanko 20 de maio de 2019 7 min read
A palestra em Cuiabá com o tema "Perícia Forense Digital e IoT – Todos prontos?" foi bastante satisfatória para mim, pessoalmente. Já havia anos que eu não palestrava e não tinha o menor interesse, porque sempre fico tenso ao falar com público. Mas dessa vez foi diferente; eu desejei muito fazer essa palestra (o vídeo deverá estar disponível em breve no canal DobitAoByteBrasil e ArduinoBrasil). Um dos temas tratados foi justamente a esteganografia.
Um dos temas abordados foi justamente a esteganografia, que chegou a despertar dúvidas nos presentes. Claro que aconteceria, afinal, é indetectável a olhos nus. E para deixar claro que foi real a demonstração, vou explicar como a "mágica" acontece e deixarei o link de um projeto bem claro e objetivo para que se possa reproduzir o que aconteceu na palestra.
Técnica de esteganografia
Sintaxe e estrutura de uma imagem .jpg
Modificando os bits menos significativos
Modificando os bits mais significativos
Manipulação de bits de uma imagem
Projeto pronto para uso
Aplicação da esteganografia
A esteganografia é a arte de esconder mensagens dentro de um arquivo, por exemplo, de imagem. Não é o mesmo que concatenar um arquivo ao final de uma imagem; esse processo de concatenação pode ser feito em linux através de comandos do shell script, sem precisar programar nada. É eficiente, funcional, mas detectável, como pode ser visto nesse artigo e nesse outro.
A esteganografia não é um padrão absoluto, podendo variar de diversas maneiras; usando uma porção de bits mínima, usando uma porção de bits maior, usando bytes posicionais para compor uma saída etc. Mas o que são esses bits que estou citando? Para entender direitinho de que se trata, vou levantar os conceitos primeiro, então retomo daqui.
A menor unidade de dados é o bit e 1 bit pode ser 0 ou 1. Se não conhece base binária, é bom dar uma estudada, mas basicamente é é 2^n. Na posição 0 vale 1 (porque qualquer número elevado a 0 é 1), na posição 1 vale 2, na posição 2 vale quatro e assim sucessivamente.
O Byte é formado por 8 bits, indo de 0 a 7. O oitavo bit equivale a 128. A soma de todos anteriores equivale ao valor do bit N-1, portanto, 8 bits equivale a 255.
A base hexadecimal vai de 0 a 15. 10 equivale a A, depois 11 equivale a B e assim sucessivamente. Em uma representação de 8 bits (ou 1 Byte), teríamos 0x0F para o valor 15.
A técnica utiliza aqui consiste em substituir os 4 bits menos significativos da imagem aparente pelos 4 bits mais significativos da imagem a esconder. Quanto representa 4 bits? Quão impactante isso é?
O impacto sobre esses 4 bits menos significativos apenas impacta na resolução, como poderá ser visto. 4 bits equivale a 0b00001111, ou 0x0F, ou em decimal, 15. Se 1 Byte equivale a 256 (considerando o 0), esse valor de 15 tem um impacto mínimo. Vamos fazer o teste no Gimp para ver o quanto é impactante sobre o branco:
255 – 15 = 240, portanto vamos baixar o valor do branco para 240 em cada um dos canais R,G e B.
Olhando assim, é de assustar, mas mexendo em todos os pixels, é como se estivéssemos apenas baixando o contraste. A grande questão é que será isso o resultado da imagem recuperada, afinal os 4 bits menos significativos serão colocados com valor 0 quando a imagem for recuperada. Mas na composição é bem mais interessante, uma vez que esses 15 valores possíveis dos 4 bits menos significativos serão variáveis, conforme 4 bits mais significativos da imagem a ser escondida. Se tivéssemos modificado os 4 bits mais significativos do exemplo acima, teria ficado assim:
255 – 240 = 15
Uma imagem JPEG é formada por uma sequência de segmentos, iniciadas com as marcas 0xFF. O início da imagem (ou o header) é formado por 0xFF 0xD8. Aqui vou pegar a imagem disponibilizada em seguida. Quando a imagem contém os dados do exif, então a informação cresce um pouco mais. Na imagem de exemplo temos o exif, que está logo após o inicio da imagem, marcado por 0xFF 0xE2 e seguido pelo payload de comprimento variável. Em alguns casos, podemos encontrar uma sequência de 0xFF, tratando-se de preenchimento de padding, encontrado em imagens com "entropy-coded" ou, na tradução literal, codificados com entropia. Como o artigo não é sobre jpg, não vou me alongar nessa parte do assunto. Talvez uma pesquisa sobre byte stuffing ajude a entender um poco mais sobre essa parte, mas se for mesmo para se aprofundar, considere estudar também transformada discreta de coseno.
Quando chegamos então em 0xFF 0xDA, inicia-se o scan da imagem, até chegar ao footer, assinado com 0xFF 0xD9.
Primeiramente, devemos considerar que os 4 bits da direita são os menos significativos. Esses bits somados resultam no valor máximo de 15 e variarão quando substituídos pelos bits mais significativos da outra imagem, reduzindo mais ainda a percepção da esteganografia.
O código do projeto utiliza a seguinte expressão para exibir o formato binário:
print('{0:08b}'.format(15))
Isto quer dizer que o comprimento é de 8 bits e o valor decimal será convertido.
Os bits mais significativos são os da esquerda, como mostrado na segunda imagem acima. Do mesmo modo, a representação no código do programa está formatada para 8 bits.
E como esses bits são manipulados? – A resposta é simples, assim como o processo e há variações quanto ao procedimento. No exemplo abaixo, leio um valor, desloco ele para a direita 4 bits. Depois leio outro valor e desloco ele também para a direita, 4 bits. Um desses valores considero o mais significativo. Ao empurrar 4 bits para a direita, descartei os bits menos significativos. Agora empurro os bits mais significativos para a direita e somo os 4 bits menos significativos, desse jeito:
Considere que apenas manipulamos 1 Byte acima. Uma imagem jpg colorida possui 3 canais; R,G,B. Cada um dos canais de cores deve ser manipulado, uma vez que estamos ocultando uma imagem dentro de outra e queremos resgatá-la posteriormente. Para fazer a abstração dessa manipulação, lendo a imagem binária de forma transparente, utilizamos a biblioteca Python PIL, importando dela apenas Image.
Nessa pequena porção de código exemplifico a abertura de uma imagem, o carregamento dos pixels e a exibição da tupla RGB na posição x,y 0 e x,y 100. Claro que ambos os indexadores são variáveis, aqui peguei duas posições de exemplo.
Essa imagem lida é a mesma que foi utilizada na palestra em Cuiabá, da placa LoRa da AFEletrônica:
Usando os exemplos de manipulação anterior, agora já fica fácil varrer os pixels da imagem para trocar seus valores e guardá-los em uma nova imagem.
Apenas para que seja possível reproduzir o que foi feito na palestra, disponibilizo também a imagem que foi ocultada durante a apresentação:
Imagens com contraste maior ficam melhor na recuperação, mas como foi essa que utilizei, devo disponibilizá-la aqui.
Para quem quiser experimentar, fiz um fork de um projeto que já está pronto para fazer essa manipulação. É um arquivo único de programa com algumas poucas linhas para obter o resultado e está disponível no meu repositório no github através desse link.
A imagem composta perde um pouco de qualidade, mas continua imperceptível:
Recomendo que antes de qualquer coisa, faça a extração do relógio contido nessa imagem. Não sei como você poderá fazê-lo em Windows, mas no Linux basta seguir esse procedimento, de dentro do diretório do projeto já clonado:
python steganography.py unmerge --img=merged.jpg --output=relogio.jpg
Esteganografia pode ser utilizada para muitos males, realmente não vejo uma aplicação benéfica para seu uso. Imagens com estegano podem conter pedofilia, mensagens de terroristas etc. A análise de imagens na Perícia Forense Digital pode ser bastante complicada nesses casos, pois é possível criar algorítimos que utilizem pixels randômicos da imagem, ou intercalados, menos de 4 bits etc. Além disso, é indetectável, não há pistas aparentes e a adição de exifs elaborados ajudam a ofuscar mais ainda o conteúdo.
Não use para fazer m3rd4, esse é um artigo para fins de conhecimento e se você for pego pela justiça, não adianta dizer que a culpa é minha.