Introdução
Saudações.
Não faz muito tempo, enquanto estudava na universidade, tive um trabalho de curso na disciplina "Métodos de Programação para Proteção de Informações". A tarefa exigia criar um programa que incorporasse mensagens em arquivos no formato GIF. Decidi fazer em Java.
Neste artigo, vou descrever alguns aspectos teóricos, além de como eu criei este pequeno programa.
Parte Teórica
Formato GIF
GIF (do inglês Graphics Interchange Format — formato para troca de imagens) é um formato de armazenamento de imagens gráficas, capaz de armazenar dados comprimidos sem perda de qualidade em um formato com até 256 cores. Este formato foi desenvolvido em 1987 (GIF87a) pela empresa CompuServe para a transmissão de imagens rasterizadas através de redes. Em 1989, o formato foi modificado (GIF89a), adicionando suporte para transparência e animação.
Os arquivos no formato GIF têm uma estrutura em blocos. Esses blocos sempre têm um tamanho fixo (ou dependem de algumas flags), portanto, é praticamente impossível errar onde cada bloco está localizado. A estrutura da imagem GIF não animada mais simples no formato GIF89a é:

De todos os blocos da estrutura, neste caso, nos interessam o bloco da paleta global e os parâmetros que respondem pela paleta:
CT— a presença de uma paleta global. Se esta flag estiver ativada, logo após o descritor da tela lógica deve começar a paleta global.Tamanho— o tamanho da paleta e o número de cores da imagem. Os valores deste parâmetro são:
Tamanho
Número de cores
Tamanho da paleta, bytes
7
256
768
6
128
384
5
64
192
4
32
96
3
16
48
2
8
24
1
4
12
0
2
6
Métodos de criptografia
Os métodos de criptografia de mensagens em arquivos de imagem utilizados serão:
- Método LSB (Least Significant Bit, menor bit significativo)
- Método de complementação da paleta
Método LSB — um método popular de esteganografia. Ele consiste em substituir os últimos bits significativos no contêiner (neste caso, os bytes da paleta global) pelos bits da mensagem ocultada.
O programa utilizará, no âmbito deste método, os dois últimos bits dos bytes da paleta global. Isso significa que, para uma imagem de 24 bits, onde a cor da paleta é representada por três bytes para as cores vermelha, azul e verde, após a incorporação da mensagem, cada componente de cor mudará no máximo 3/255 graduações. Essa mudança, por um lado, será imperceptível ou pouco perceptível para o olho humano, e, por outro lado, não será distinta em dispositivos de saída de baixa qualidade.
A quantidade de informação dependerá diretamente do tamanho da paleta da imagem. Como o tamanho máximo da paleta é de 256 cores e, se forem gravados dois bits da mensagem em cada componente de cor, a máxima extensão da mensagem (com a paleta máxima na imagem) é de 192 bytes. Após a incorporação da mensagem na imagem, o tamanho do arquivo não muda.
Método de expansão de paleta, funcionando apenas para a estrutura GIF. Ele será mais eficaz em imagens com paletas de tamanhos pequenos. Sua essência é que ele aumenta o tamanho da paleta, proporcionando assim espaço adicional para gravar os bytes necessários no lugar dos bytes de cor. Considerando que o tamanho mínimo da paleta é de 2 cores (6 bytes), o tamanho máximo da mensagem incorporada pode ser 256×3–6=762 bytes. A desvantagem é a baixa segurança criptográfica; a mensagem incorporada pode ser lida por qualquer editor de texto, se não tiver sido sujeita a criptografia adicional.
Parte prática
Projeto do programa
Todas as ferramentas necessárias para a implementação dos algoritmos de criptografia e descriptografia estarão no pacote com.tsarik.steganography. Este pacote inclui a interface Encryptor com os métodos encrypt e decrypt, a classe Binary, que fornece a possibilidade de trabalhar com arrays de bits, além das classes de exceção UnableToEncryptException e UnableToDecryptException, que devem ser usadas nos métodos da interface Encryptor em caso de erros de codificação e decodificação, respectivamente.
O pacote principal do programa com.tsarik.programs.gifed incluirá a classe executável do programa com um método estático main, permitindo executar o programa; a classe que armazena os parâmetros do programa; e pacotes com outras classes.
A implementação dos próprios algoritmos será apresentada no pacote com.tsarik.programs.gifed.gif classes GIFEncryptorByLSBMethod e GIFEncryptorByPaletteExtensionMethod. Ambas essas classes implementarão a interface Encryptor.
Com base na estrutura do formato GIF, pode-se elaborar um algoritmo geral para injetar uma mensagem na paleta da imagem:

Para determinar a presença de uma mensagem na imagem, é necessário adicionar ao início da mensagem uma sequência de bits que o decodificador lê primeiro e verifica a correção. Se não coincidir, considera-se que não há mensagem oculta na imagem. Em seguida, deve-se indicar o comprimento da mensagem. Depois, o próprio texto da mensagem.
Diagrama de classes de toda a aplicação:

Implementação do programa
A implementação do programa pode ser dividida em duas partes: a implementação dos métodos de criptografia e descriptografia da interface Encryptor, nas classes GIFEncryptorByLSBMethod e GIFEncryptorByPaletteExtensionMethod, e a implementação da interface do usuário.
Vamos analisar a classe GIFEncryptorByLSBMethod.

Campos firstLSBit e secondLSBit contêm os índices dos bits de cada byte da imagem, onde a mensagem deve ser inserida e de onde deve ser lida. O campo checkSequence armazena a sequência de bits de verificação para garantir o reconhecimento da mensagem embutida. O método estático getEncryptingFileParameters retorna os parâmetros do arquivo especificado e as características da mensagem potencial.
O algoritmo do método encrypt classe GIFEncryptorByLSBMethod:

E seu código:
@Override
public void encrypt(File in, File out, String text) throws UnableToEncodeException, NullPointerException, IOException {
if (in == null) {
throw new NullPointerException("O arquivo de entrada é nulo");
}
if (out == null) {
throw new NullPointerException("O arquivo de saída é nulo");
}
if (text == null) {
throw new NullPointerException("O texto é nulo");
}
// ler bytes do arquivo de entrada
byte[] bytes = new byte[(int)in.length()];
InputStream is = new FileInputStream(in);
is.read(bytes);
is.close();
// verificar formato
if (!(new String(bytes, 0, 6)).equals("GIF89a")) {
throw new UnableToEncodeException("O arquivo de entrada tem formato GIF incorreto");
}
// ler a propriedade do tamanho da paleta dos três primeiros bits no décimo byte do arquivo
byte[] b10 = Binary.toBitArray(bytes[10]);
byte bsize = Binary.toByte(new byte[] {b10[0], b10[1], b10[2]});
// calcular contagem de cores e comprimento possível da mensagem
int bOrigColorCount = (int)Math.pow(2, bsize+1);
int possibleMessageLength = bOrigColorCount*3/4;
int possibleTextLength = possibleMessageLength-2; // um byte para verificação e um byte para o comprimento da mensagem
if (possibleTextLength < text.length()) {
throw new UnableToEncodeException("O texto é muito grande");
}
int n = 13;
// escrever sequência de verificação
for (int i = 0; i < checkSequence.length/2; i++) {
byte[] ba = Binary.toBitArray(bytes[n]);
ba[firstLSBit] = checkSequence[2*i];
ba[secondLSBit] = checkSequence[2*i+1];
bytes[n] = Binary.toByte(ba);
n++;
}
// escrever comprimento do texto
byte[] cl = Binary.toBitArray((byte)text.length());
for (int i = 0; i < cl.length/2; i++) {
byte[] ba = Binary.toBitArray(bytes[n]);
ba[firstLSBit] = cl[2*i];
ba[secondLSBit] = cl[2*i+1];
bytes[n] = Binary.toByte(ba);
n++;
}
// escrever mensagem
byte[] textBytes = text.getBytes();
for (int i = 0; i < textBytes.length; i++) {
byte[] c = Binary.toBitArray(textBytes[i]);
for (int ci = 0; ci < c.length/2; ci++) {
byte[] ba = Binary.toBitArray(bytes[n]);
ba[firstLSBit] = c[2*ci];
ba[secondLSBit] = c[2*ci+1];
bytes[n] = Binary.toByte(ba);
n++;
}
}
// escrever arquivo de saída
OutputStream os = new FileOutputStream(out);
os.write(bytes);
os.close();
}
Algoritmo e código fonte do método decrypt classe GIFEncryptorByLSBMethod:

@Override
public String decrypt(File in) throws UnableToDecodeException, NullPointerException, IOException {
if (in == null) {
throw new NullPointerException("O arquivo de entrada é nulo");
}
// ler bytes do arquivo de entrada
byte[] bytes = new byte[(int)in.length()];
InputStream is = new FileInputStream(in);
is.read(bytes);
is.close();
// verificar formato
if (!(new String(bytes, 0, 6)).equals("GIF89a")) {
throw new UnableToDecodeException("O arquivo de entrada tem um formato GIF errado");
}
// ler propriedade do tamanho da paleta nos primeiros três bits no 10º byte do arquivo
byte[] b10 = Binary.toBitArray(bytes[10]);
byte bsize = Binary.toByte(new byte[] {b10[0], b10[1], b10[2]});
// calcular contagem de cores e possível comprimento da mensagem
int bOrigColorCount = (int)Math.pow(2, bsize+1);
int possibleMessageLength = bOrigColorCount*3/4;
int possibleTextLength = possibleMessageLength-2; // um byte para verificação e um byte para comprimento da mensagem
int n = 13;
// ler sequência de verificação
byte[] csBits = new byte[checkSequence.length];
for (int i = 0; i < 4; i++) {
byte[] ba = Binary.toBitArray(bytes[n]);
csBits[2*i] = ba[firstLSBit];
csBits[2*i+1] = ba[secondLSBit];
n++;
}
byte cs = Binary.toByte(csBits);
if (cs != Binary.toByte(checkSequence)) {
throw new UnableToDecodeException("Não há mensagem criptografada na imagem (A sequência de verificação está incorreta)");
}
// ler comprimento do texto
byte[] cl = new byte[8];
for (int i = 0; i < 4; i++) {
byte[] ba = Binary.toBitArray(bytes[n]);
cl[2*i] = ba[firstLSBit];
cl[2*i+1] = ba[secondLSBit];
n++;
}
byte textLength = Binary.toByte(cl);
if (textLength < 0) {
throw new UnableToDecodeException("O comprimento do texto decifrado é menor que 0");
}
if (possibleTextLength < textLength) {
throw new UnableToDecodeException("Não existem mensagens (O comprimento da mensagem decifrada (" + textLength + ") é menor que o comprimento possível da mensagem (" + possibleTextLength + "))");
}
// ler bits de texto e criar bytes de texto
byte[] bt = new byte[textLength];
for (int i = 0; i < bt.length; i++) {
byte[] bc = new byte[8];
for (int bci = 0; bci < bc.length/2; bci++) {
byte[] ba = Binary.toBitArray(bytes[n]);
bc[2*bci] = ba[firstLSBit];
bc[2*bci+1] = ba[secondLSBit];
n++;
}
bt[i] = Binary.toByte(bc);
}
return new String(bt);
}
Implementação da classe GIFEncryptorByPaletteExtensionMethod será análoga, apenas difere no método de salvar/leitura de informações.
Na classe MainFrame são descritos métodos "wrapper": encryptImage(Encryptor encryptor) e decryptImage(Encryptor encryptor), que processam os resultados dos métodos da interface Encryptor e realizam a interação com o usuário, ou seja, abrem caixas de diálogo para seleção de arquivos, mostram mensagens de erro, etc.; além de outros métodos: openImage(), que permite ao usuário escolher uma imagem, exit(), que realiza a saída do aplicativo. Esses métodos são chamados a partir de Açõesdos respectivos itens de menu. Nesta classe também são implementados métodos auxiliares: createComponents() — criação de componentes do formulário, loadImageFile(File f) — carregamento de imagem em um componente especial a partir de um arquivo. A implementação da classe GIFEncryptorByPaletteExtensionMethod é análoga à implementação da classe GIFEncryptorByLSBMethod, a principal diferença está na forma de gravação e leitura dos bytes da mensagem da paleta.
Execução do programa
O método LBS
Suponha que exista tal imagem:

Nesta imagem, a paleta consiste em 256 cores (conforme preserva o Paint). As primeiras quatro cores: branco, preto, vermelho, verde. As demais cores são pretas. A sequência de bits da paleta global será a seguinte:
11111111 11111111 11111111 00000000 00000000 00000000 11111111 00000000 00000000 00000000 11111111 00000000…

Após a inserção da mensagem, os bits sublinhados serão substituídos pelos bits da mensagem. A imagem resultante quase não difere da original.
Original
Imagem com a mensagem inserida

![]()
Método de expansão de paleta
Ao abrir a imagem onde a mensagem foi colocada utilizando este método, pode-se encontrar a seguinte situação:

É claro que para atividades de espionagem completas, este método não servirá, e pode exigir alguma criptografia adicional da mensagem.
A criptografia/descriptografia em imagens animadas funciona da mesma forma que em imagens estáticas, sem que a animação seja comprometida.
Fontes utilizadas:
Baixar:
Fonte: habr.com
