Archive

Posts Tagged ‘segurança’

[VD03.1] Heap Overflow

July 9th, 2009 Marcos Álvares No comments

O foco desse artigo é explicar a essência da técnica de exploração de overflows baseados na heap, para plataformas Intel x86 32 bits usando como sistema operacional o Linux. Esse artigo ficou muito grande para caber em um único post e resolvi dividi-lo em duas partes. A primeira irá descrever a fundamentação teórica necessária para o entendimento do ataque (meio frustrante, mas extremamente necessária) e a segunda irá descrever mecanismos de bypassing necessários para a exploração em ambientes atualizados. Os exemplos serão baseados na implementação de Doug Lea e Wolfram Gloger presente no pacote da glibc 2.9 [1].
A heap nada mais é do que uma região de memória reservada pelo sistema operacional para cada processo armazenar os dados referentes à variáveis alocadas dinamicamente usando as chamadas de sistema mmap(reserva uma nova área) ou brk(libera uma determinada área) através de funções da glibc como malloc(), calloc(), realloc() e free(). Cada processo mapeado em memória possui a sua região de heap privada. A seguir, podemos observar um exemplo de código que faz uso da heap:

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[]) {
        char *buffer;

        /*
            Cria uma porção de 200 bytes na heap e armazena o endereço
            para esta nova porção na variável buffer. Faz uma chamada de
            sistema mmap.Podemos acompanhar esse fluxo de chamadas
            de sistemas através do comando strace.
       */

        buffer = (char *) malloc (200 * sizeof(char));   

        /*
            Avisa ao sistema operacional que a porção armazenada no endereço
            "buffer" não é mais necessária, através da chamada de sistema brk.
        */
        free(buffer);
        return 0;
}

.
Na literatura e em diversos advisories, quando se fala de “heap overflow“, de uma forma geral, a intenção é se referir a buffer overflow em qualquer porção de memória diferente da pilha, que tenha permissão de escrita. Isso é algo a se levar em consideração já que muitas vezes iremos estudar códigos que atacam regiões diferentes da heap e serão classificados como “heap based vulnerabilities“.

Fig 01 - Organização de um processo no Linux

A motivação para explorar essa categoria de vulnerabilidades consiste em dois argumentos:
.
- Linha do tempo: os ataques à heap são mais recentes em relação aos baseados na pilha, com isso, existem menos mecanismos de proteção internos aos sistemas operacionais (falo quantitativamente e não qualitativamente). A heap é usada por programas que necessitam de uso intensivo de memória, portanto projetar um mecanismo de proteção para essa área nos leva ao dilema de segurança versus performance, pois qualquer verificação de segurança adicionada nessa camada será executada inúmeras vezes, introduzindo um overhead em processos que fazem uso intensivo da heap (banco de dados, servidores web, servidores DNS …).

- Complexidade: com uma busca rápida em qualquer sistema de indexação de vulnerabilidades, percebemos que existem bem mais explorações bem sucedidas e publicadas, usando ataques baseados na pilha do que na heap. Isso acontece devido a complexidade envolvida no processo de busca e exploração de falhas no uso da heap. Essa complexidade é um fator decisivo no tempo de vida do 0-day.

Para iniciarmos nosso estudo é necessário uma visão geral do sistema de gerenciamento de memória alocada dinâmicamente projetado por Doug Lea e Wolfram Gloger, usado na maioria das distribuições de Linux. Esse estudo é necessário, pois as técnicas para exploração de problemas na heap se baseiam justamente na forma como as funções da glibc tratam as porções de memória alocadas.

O sistema de alocação de memória usado no Linux, através da glibc, não é o mais otimizado que existe, porém com certeza ele está entre um dos mais flexíveis, portáveis e aceitáveis em termos de conservação de espaço em memória. Balanceando consistentemente esses fatores, temos um bom alocador, em termos gerais, para uso em programas que requerem um uso intensivo do recurso de alocação dinâmica de memória.

As propriedades do algoritmo de alocação variam em quatro modos de acordo com a quantidade de memória solicitada:

- Para requisições menores que 64 bytes é usado um alocador baseado em cache, para ser possível uma rápida limpeza da memória quando essas não forem mais necessárias;
- Para requisições intermediárias entre 64 bytes e 512 bytes o algoritmo se adapta a situação do ambiente para escolha da melhor solução para a situação.
- Para requisições de porções maiores que 512 bytes é usado um algoritmo best-fit allocator [2] puro e tradicional (será explicado mais na frente).
- Para requisições maiores que 128 Kb, malloc delega a requisição para o gerenciador de memória do sistema (se esse suportar, no caso do Linux é suportado).

Para indexar certa quantidade de memória alocada, malloc adiciona um cabeçalho que contém a informação da quantidade de memória reservada para aquele segmento, esse cabeçalho será usado para identificação, localização e liberação dessa porção de memória.

Dependendo do estado da porção (livre ou em uso), o cabeçalho pode assumir dois formatos:
.
1) Cabeçalho para porções em uso:

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk, if not allocated        | |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk, in bytes                       |M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             User data starts here...                          .
            .                                                               .
            .             (malloc_usable_size() bytes)                      .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk                                     |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

.
2) Cabeçalho para porções livres:

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk, if not allocated        | |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk, in bytes                       |M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Forward Pointer                                   |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Backward Pointer                                  |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            .                                                               .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk                                     |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

.
Em comum, ambas as estruturas possuem o tamanho da porção alocada e a informação do tamanho da porção imediatamente anterior caso essa esteja livre. O cabeçalho de uma porção livre possui mais dois campos que são ponteiros para a porção livre anterior e posterior, formando uma lista duplamente encadeada. Os dois bits menos significativos do segundo byte do cabeçalho corresponde a uma indicação se a porção anterior estar livre (bit “P”) e se a gerencia da porção atual foi delegada ao sistema operacional (bit “M”).

A seguir é apresentada a estrutura de dados usada pela glibc para representar uma porção de memória, nela podemos visualizar os campos usados pelos quatro modos de alocação de memória (nós só iremos trabalhar com porções menores que 512 bytes).
.

struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

.
Lembrando que dentro do campo “size” da estrutura existem os bits de controle da porção. (esses bits são retirados desse campo através de mascaras).

Um ponto chave é a forma como as porções livres são organizadas e re-aproveitadas na heap. Para otimizar a busca por porções livres de memória o sistema organiza essas em forma de lista duplamente encadeada como mostrado na figura 02.

Fig 02 - Visualização da heap

Fig 02 - Visualização da heap

.
A figura 2 mostra cinco porções alocadas na heap. As porções em azul estão em uso e as em cinza são as livres que possuem links em seus cabeçalhos, dando forma a lista encadeada. A porção de endereço 0x80c5090 é o elemento raiz da lista de porções livres, todo o processo ao iniciar a sua região de heap já cria esse elemento.

O processo de alocação é baseado no algoritmo “Best Fit Allocation” [2], através da pesquisa na lista encadeada de qual a menor porção de memória livre que comporta o tamanho solicitado.

O algoritmo de liberação de uma determinada porção de memória é parte fundamental dentro o sistema de gerencia da heap. Esse algoritmo é responsável pela reciclagem da memória de forma eficiente, observando o estado da heap no momento da liberação para realização de otimizações, garantindo um melhor aproveitamento do espaço em memória e preparando o espaço liberado para ser reaproveitado. O processo de liberação de memória é responsável pela execução de “merges“, evitando uma fragmentação desnecessária da heap. Quanto mais fragmentada a heap mais custoso será o processo de alocação de memória (uma lista maior para fazer busca).

Fiz um “artifício técnico” (a.k.a. gambiarra), para melhor visualização da organização da heap usando a própria estrutura da glibc (vou disponibilizar todo o código dos experimentos [aqui]). São basicamente duas funções:

- __pretty_print_chunk(): Recebe por parâmetro a estrutura da porção e imprime na tela o seu cabeçalho.

void __pretty_print_chunk (struct malloc_chunk *p, int chunk_id) {
  printf("::. Manipulando head do chunk_%02d: \n\n", chunk_id);
  printf("\t- Endereço do chunk:                       [%#x]\n", (uint) p);
  printf("\t- Tamanho do elemento anterior (se livre): [%d]\n" , p->prev_size);
  printf("\t- Tamanho do elemento atual:               [%d]\n" , chunksize(p));
  printf("\t- Foward link:                             [%#x]\n", (uint)p->fd);
  printf("\t- Backward link:                           [%#x]\n", (uint)p->bk);
  printf("\n");
}

- __malloc_char_chunk(): Cria uma nova porção na heap.

struct malloc_chunk *__malloc_char_chunk(uint size) {
  char *temp_buffer = (char *)malloc(size * sizeof(char));
  memset(temp_buffer, 'A', size/2);
  return (struct malloc_chunk *)(temp_buffer - 0x8);
}

.
Usando essas funções conseguimos ter uma interface para visualização das porções alocadas na heap. Vamos usar essa interface para criar quatro porções na memória e vamos liberar a primeira e a terceira porção.
.

int main (int argc, char *argv[]) {
  struct malloc_chunk *chunk_01, *chunk_02,*chunk_03,*chunk_04;
  chunk_01 = __malloc_char_chunk(BUFFER_SIZE);
  chunk_02 = __malloc_char_chunk(BUFFER_SIZE);
  chunk_03 = __malloc_char_chunk(BUFFER_SIZE);
  chunk_04 = __malloc_char_chunk(BUFFER_SIZE);
  free((char *)((uint)chunk_01 + 0x8)); // O free() recebe o endereço do inicio
  free((char *)((uint)chunk_03 + 0x8)); // região de dados da porção
  __pretty_print_chunk(chunk_01, 1);
  __pretty_print_chunk(chunk_02, 2);
  __pretty_print_chunk(chunk_03, 3);
  __pretty_print_chunk(chunk_04, 4);
}

.

::. Manipulando head do chunk_01:

        - Endereço do chunk:                       [0x8cd1680]
        - Tamanho do elemento anterior (se livre): [0]
        - Tamanho do elemento atual:               [208]
        - Foward link:                             [0x80c5090]
        - Backward link:                           [0x8cd1820]

::. Manipulando head do chunk_02:

        - Endereço do chunk:                       [0x8cd1750]
        - Tamanho do elemento anterior (se livre): [208]
        - Tamanho do elemento atual:               [208]
        - Foward link:                             [0x41414141]
        - Backward link:                           [0x41414141]

::. Manipulando head do chunk_03:

        - Endereço do chunk:                       [0x8cd1820]
        - Tamanho do elemento anterior (se livre): [0]
        - Tamanho do elemento atual:               [208]
        - Foward link:                             [0x8cd1680]
        - Backward link:                           [0x80c5090]

::. Manipulando head do chunk_04:

        - Endereço do chunk:                       [0x8cd18f0]
        - Tamanho do elemento anterior (se livre): [208]
        - Tamanho do elemento atual:               [208]
        - Foward link:                             [0x41414141]
        - Backward link:                           [0x41414141]

.
Observe a lista duplamente encadeada de porções livres na heap.
.

Fig 03 - Lista encadeada de porções livres

Fig 03 - Lista encadeada de porções livres


O que acontece se nós liberarmos a segunda porção de memória?
.
O algoritmo do free() irá detectar que as porções três e um também estão livre e criará uma única porção que corresponde a soma das três regiões (um, dois e três).

::. Manipulando head do chunk_01:

        - Endereço do chunk:                       [0x8cfe680]
        - Tamanho do elemento anterior (se livre): [0]
        - Tamanho do elemento atual:               [624]
        - Foward link:                             [0x80c5090]
        - Backward link:                           [0x80c5090]

::. Manipulando head do chunk_02:

        - Endereço do chunk:                       [0x8cfe750]
        - Tamanho do elemento anterior (se livre): [208]
        - Tamanho do elemento atual:               [208]
        - Foward link:                             [0x41414141]
        - Backward link:                           [0x41414141]

::. Manipulando head do chunk_03:

        - Endereço do chunk:                       [0x8cfe820]
        - Tamanho do elemento anterior (se livre): [416]
        - Tamanho do elemento atual:               [208]
        - Foward link:                             [0x41414141]
        - Backward link:                           [0x41414141]

::. Manipulando head do chunk_04:

        - Endereço do chunk:                       [0x8cfe8f0]
        - Tamanho do elemento anterior (se livre): [624]
        - Tamanho do elemento atual:               [208]
        - Foward link:                             [0x41414141]
        - Backward link:                           [0x41414141]

Reparem que na porção quatro diz que existe uma porção livre com 624 bytes (3 * 208) na porção imediatamente anterior. Se obsevarmos o cabeçalho dessa porção vamos verificar que o merge das porções foi realizado com sucesso.

Fig 04 - Região de Merge na heap

Fig 04 - Região de Merge na heap


.
Na Figura 04 mostramos a região em que será realizado o merge. Após a liberação da porção 2 será criada uma nova porção contendo a soma das porções 1, 2 e 3, como mostrado na figura abaixo.
.
Fig 05 - Lista de porções livres após o Merge

Fig 05 - Lista de porções livres após o Merge

.
Nesse tipo de merge, o algoritimo fez alterações na porção livre que estava imediatamente abaixo da porção três para apontar para a nova porção e fez alteração na nova porção para apontar para a porção que era apontada pela porção três.
.

(porção 3)->bd + 8    < -  (porção 3)->fd
(porçao 3)->fd + 12   < -  (porção 3)->bd

.
É justamente explorando essas heurísticas de merge, que conseguimos tirar proveito de falhas no uso da heap. E é ai que a brincadeira começa …

Imagine se pudéssemos manipular o cabeçalho da porção imediatamente após da que nós iríamos liberar (a porção 3 do exemplo anterior)… Isso significa que nós poderíamos manipular os valores dos links usados para ligar a lista de porções livres apos a operação de merge.

Quando é possível preencher uma porção na heap com mais dados do que essa foi inicialmente planejada para comportar, conseguimos sobrescrever regiões da porção de memória conseguinte, colocando um cabeçalho falso nessa seção e criando porções adjacentes falsas, de forma que quando a função free() for chamada recebendo como parâmetro o endereço para essa porção de memória, ela irá buscar informações no header falso e tentar realizar a operação de merge, com isso conseguimos realizar o ataque sobrescrevendo uma posição qualquer na memória que tenha permissão de escrita com um conteúdo especificado (Já que a “porção 3″ será injetada na “porção 2″ através da “porção 1″ que está vulnerável \O/).

É … não é simples de entender mesmo não … leia e releia o parágrafo acima. Desenhe no papel que talvez fique mais fácil. Tudo gira em torno do funcionamento do merge. Eu também pensei a mesma coisa: “O cara que fez isso pela primeira vez devia ter muitas drogas em mente.

Agora, que já temos idéia na prática como o algoritmo de merge funciona, vamos montar um ambiente com dois buffers alocados na heap em que o primeiro está vulnerável e recebe dados direto da linha de comando sem nenhuma espécie de verificação.

Antes de tudo vamos desabilitar algumas proteções existentes no nosso Linux, fazer o bypass desses mecanismos de segurança não faz parte do escopo dessa primeira parte do artigo. Em breve irei escrever um artigo mostrando como funcionam esses mecanismos de proteção e se possível como fazer um bypass de forma eficiente e confiável (não tem quem goste dessas técnicas probabilísticas). Esse artigo sobre as técnicas usadas para proteção da memória, será útil para todos os posts de vulndev publicados aqui no blog.
.
Primeiro vamos desabilitar ASLR (Adress Space Layout Randomization):

root@envy:/# /bin/echo 0 > /proc/sys/kernel/randomize_va_space

.
Habilitar o modo de depuração do mecanismo de proteção em tempo de execução interno a glibc:

export MALLOC_CHECK_=1

.
O código usado como exemplo para todo o resto do artigo será o seguinte:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFFER_SIZE 200

int main (int argc, char *argv[]) {
  char *buffer_01, *buffer_02;

  buffer_01 = (char *) malloc(BUFFER_SIZE*sizeof(char));
  buffer_02 = (char *) malloc(BUFFER_SIZE*sizeof(char));

  strcpy(buffer_01, argv[1]);

  free(buffer_02);

  return 1;
}

Podemos sobrescrever o “buffer_02” através da vulnerabilidade no “buffer_01“, com isso nós iremos criar duas porções de memória falsas ao redor de “buffer_02“, de forma que quando dermos um free() nessa porção possamos induzir o merge dessas três áreas (fake_01, buffer_02 e fake_02) e com isso explorarmos a falha. O layout do nosso exploit irá ficar como na figura a seguir.

Fig 06 - Layout da heap após a execução do exploit

Fig 06 - Layout da heap após a execução do exploit

Não se esquecer que devemos sobrescrever o cabeçalho da porção dois para sinalizarmos que essa possui uma porção anterior livre, através da flag “P” e ajustarmos o campo de tamanho para a porção terminar no inicio da segunda porção falsa (a que irá conter os endereços). Sem isso, o processo de merge não será iniciado.

Com uma pequena ajuda do interpretador Python podemos construir o nosso exploit.
.

./heap `python -c 'print "A"*184    +    "\xff"*8+"A"*8    +
  "\xf8\xff\xff\xff"+"\xf0\xff\xff\xff"    +    "\xff"*8 + "AAAA" + "BBBB"'`

.
Com o código acima nós conseguiríamos sobrescrever porções específicas do nosso interesse na memória (return pointer, locks, global offset table … ): *(AAAA + 12) = BBBB
.
Quando executamos ele temos:

mabj@envy:~/Documents/project/heap_based_overflow$ ./heap `python -c 'print "A"*188+"\xff"*4+"A"*8+"\xf8\xff\xff\xff"+"\xf0\xff\xff\xff"+"\xff"*8 + "AAAA" + "BBBB"'`
malloc: using debugging hooks
buffer_01 (0x80c8690) - size: [0x15] previous [0xffffffd1]
buffer_02 (0x80c8760)- size: [0xfffffff8] previous [0xfffffff0]

*** glibc detected *** ./heap: free(): invalid pointer: 0x080c8760 ***


.
Essa mensagem ocorre devido a seguinte verificação de segurança interna a glibc:
.

  /* Little security check which won't hurt performance: the
     allocator never wrapps around at the end of the address space.
     Therefore we can exclude some size values which might appear
     here by accident or by "design" from some intruder.  */
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
    || __builtin_expect (misaligned_chunk (p), 0))
  {
    errstr = "free(): invalid pointer";
  errout:
    malloc_printerr (check_action, errstr, mem);
    return;
  }

.
Pronto. É aqui que chegamos ao fim da primeira etapa no nosso estudo a respeito de vulnerabilidades baseadas na heap. Já sabemos o que é a heap, como funciona os mecanismos de gerencia da heap e onde estão os pontos de exploração. No próximo passo, vamos focar no estudo dos mecanismos de proteção e na exploração.

<frustation mode=”on”>
Tudo que foi mostrado até agora foram os fundamento para exploração de vulnerabilidades baseadas na heap. Todos os exploit writers e heap lovers já passaram por tudo o que foi descrito acima até chegar no estado que vamos mostrar no segundo tempo do jogo (VD03.2). A quatro anos atrás o conhecimento acumulado até agora seria o suficiente para a nossa exploração. Hoje em dia, a glibc mais recente possui mecanismos de proteção, como o mostrado acima, que impede nosso ataque. Mas não desanimem! O ‘#’ já está mais perto do que vocês imaginam.
</frustation>
.
Em balanço está a Força. A Escuridão e a Luz. Sem um, não há o outro. O Lado Negro tentador é. Rápido, fácil no início, mas uma armadilha é o Lado Negro. Mau, corrupto. Uma vez que se começa no Lado Negro, para sempre dominará seu destino. Para o caminho da Luz, paciência você precisa. Controle. Paz e harmonia ele é.”

yoda
.

TO BE CONTINUED …

Referências:

[1] http://www.gnu.org/software/libc/
[2] http://g.oswego.edu/dl/html/malloc.html
[3] The Shellcoder’s handbook, segunda edição, capitulo 5 (Gera, FX e Chris Anley)
[4] http://www.w00w00.org/files/articles/heaptut.txt
[5] http://www.phrack.org/issues.html?issue=61&id=6&mode=txt
[6] http://www.phrack.org/issues.html?issue=57&id=8&mode=txt
[7] http://www.phrack.org/issues.html?issue=57&id=9&mode=txt
[8] http://sourceware.org/glibc/

Processamento Digital de Imagens e Esteganografia

January 11th, 2009 Marcos Álvares 3 comments

Esse post tem por objetivo falar um pouco sobre processamento digital de imagens, esteganografia e segurança da informação. Será apresentado uma introdução a PDI, como se faz esteganografia em imagens na prática e quais os impactos da popularização do uso dessas técnicas em segurança da informação.

Com a evolução das tecnologias, hoje temos dispositivos como impressoras laser, que conseguem representar imagens com altas resoluções, só para se ter uma idéia tais impressoras conseguem representar mais de 1800 pontos por polegada (DPI – Dots Per Intch) [1]. Cada ponto na matriz que representa a imagem pode assumir um valor entre um universo da ordem de milhões de cores. Numa imagem de 1600×1200, gerada por equipamentos simples como maquinas digitais, temos 1.920.000 pontos que usam quatro bytes para representar uma quantidade razoável de cores, com isso formamos um arquivo digital e serializado em disco de aproximadamente 7.6 MB.

Esse valor é uma quantidade absurda de informação em que nosso cérebro, diferente dos computadores, não é capaz de processar nos mínimos detalhes. O que nós fazemos ao nos deparar com uma imagem com essa quantidade de informação é ir captando detalhes de alto nível, gradativamente, até coletarmos a informação macro que aquela figura representa.

Exemplo:

Homem velho

Homem velho

É provável que você tenha percebido que a imagem se trata de um homem velho, que possui barba e que possivelmente está num lugar humilde. Mas acredito que pouca gente percebeu o sinal que o homem tem na bochecha esquerda, a textura da porta ou a mancha verde que está pintada na parede acima da cabeça do velho. A observação superficial e gradativa é perfeitamente normal e saudável. Imaginem o que aconteceria se nosso cérebro tentasse captar todos os detalhes da foto como: cromancia, iluminação, texturas ou falhas. Na verdade captamos informações mais relevantes aos poucos até conseguirmos retirar a mensagem principal transmitida através da imagem.

Tendo em vista essa quantidade de informação acima da capacidade de processamento humana, podemos explorar esse detalhe para esconder informação dentro da imagem de maneira que um humano, normal, não suspeite de que há algo por “baixo dos panos“. O resto do artigo irá explicar alguns detalhes de processamento digital de imagem e como esconder informações usando as algumas técnicas e matemática.

Primeiramente vamos a definições de termos (Essa parte é sempre importante para evitar ambigüidades), Computação gráfica é a área responsável por transformar uma informação de uma imagem (analógica) oferecida pela natureza em uma representação computacional, é também sua atribuição manipulações e transformações na imagem criada. Visão computacional é o caminho inverso da imagem para a informação, só que isso feito pela máquina ao invés do cérebro, área amplamente estudada em inteligência artificial com redes neurais e detecção de padrões.

Uma imagem digital nada mais é do que uma discretização de um sinal, formando uma matriz. O processo de manipulação de imagens, hoje realizado pelas máquinas, passa pelas seguintes etapas:

Sinal contínuo -> Sinal discreto                  -> Sinal codificado
(imagem real)  -> (Imagem digitalizada) -> (Imagem serializada)

Nós enxergamos as imagens como um sinal contínuo e o computador como uma matriz de pixels [2]. Essa matriz é armazenada em disco, para consultas posteriores, através de algum algoritmo de codificação como: BMP, JPEG, PNG, GIF.

A área que estuda como ocultar informações dentro outra informação é chamada de Esteganografia (do grego “escrita escondida”). Isso se aplica não só a imagens, mas também a áudios, vídeos e outros meios. Esse artigo apresenta o funcionamento de esteganografia usando imagens, iremos mostrar gradualmente como é possível esconder uma mensagem dentro de uma imagem de forma que o observador da imagem não consiga perceber a informação ali escondida.

A primeira e mais simples aplicação explora o fato das pessoas não conseguirem enxergar diferenças entre tons próximos. Veja a seguinte imagem:

Aplicando um filtro que aumente o contraste da figura, nós transformamos a imagem acima em:

Reparem como em um experimento simples, podemos esconder informação de forma eficaz. Imagine se ao invés de usar uma área tão grande usássemos apenas alguns pixels para esconder nossa mensagem segredo. Nesse exemplo a imagem ofuscou uma informação que também está na forma de imagem o que requer muito mais espaço.

O Exemplo anterior a informação escondida ocupa muito espaço na imagem, além da imagem em si não conter nenhuma informação que retire a atenção do observador dos detalhes. O gamute [3] da imagem é de apenas duas cores (#000000 e #010101).

Vamos mostrar um exemplo da mesma mensagem escondida dentro da imagem do velho. Dessa vez usando apenas a quantidade de dados suficiente para representar a informação: dois pixels.

Acreditem se quiser, no pixel [185, 212] e no [185, 213] existe a mensagem “teste” escondida na formação da cor.

Pixel 1 = (0×54 = ‘T‘, 0×45 = ‘E‘, 0×43 = ‘S‘)
Pixel 2 = (0×54 = ‘T‘, 0×45 = ‘E‘, 0×00 ‘\0‘)

Com apenas dois pixels conseguimos esconder nossa mensagem. Além de ser difícil perceber visualmente, é complicado achar a posição dos pixels quem contêm a informação escondida.

Ainda estamos escondendo uma quantidade de informação relativamente pequena vamos agora trocar nossa mensagem “teste” por um texto com mais informações:

Through the wire screen, the eyes of those standing outside looked in
At her as into the cage of some rare creature in a zoo.
In the hand of one of the assistants she saw the same instrument
Which they had that morning inserted deep into her body. she shuddered
Instinctively. no life at all in the house of dolls.
No love lost. no love lost.

Esse texto possui apenas 324 bytes para serem escondidos em uma imagem de 7MB (56.000.000 de bytes), essa informação ainda ocupa muito pouco espaço na imagem portadora. Outro problema a ser abordado é a função que irá gerar a disposição da informação na imagem, pois se colocarmos todos os pixels seqüencialmente possivelmente o aspecto da imagem irá ser alterado e pode levantar suspeitas em nosso observador de que a imagem contém alguma informação oculta. Vamos usar um espiral logaritmo partindo do centro da figura como função para disposição da nossa informação.  Em coordenadas polares (r,θ) temos:

logarithmic spiral equation

Traduzindo a nossa mensagem para hexadecimal temos:

—–
65 20 63 61 67 65 20 6F 66 20 73 6F 6D 65 20 72 61 72 65 20 63
72 65 61 74 75 72 65 20 69 6E 20 61 20 7A 6F 6F 2E 0D 0A 49 6E
20 74 68 65 20 68 61 6E 64 20 6F 66 20 6F 6E 65 20 6F 66 20 74
68 65 20 61 73 73 69 73 74 61 6E 74 73 20 73 68 65 20 73 61 77
20 74 68 65 20 73 61 6D 65 20 69 6E 73 74 72 75 6D 65 6E 74 0D
0A 57 68 69 63 68 20 74 68 65 79 20 68 61 64 20 74 68 61 74 20
6D 6F 72 6E 69 6E 67 20 69 6E 73 65 72 74 65 64 20 64 65 65 70
20 69 6E 74 6F 20 68 65 72 20 62 6F 64 79 2E 20 73 68 65 20 73
68 75 64 64 65 72 65 64 0D 0A 49 6E 73 74 69 6E 63 74 69 76 65
6C 79 2E 20 6E 6F 20 6C 69 66 65 20 61 74 20 61 6C 6C 20 69 6E
20 74 68 65 20 68 6F 75 73 65 20 6F 66 20 64 6F 6C 6C 73 2E 0D
0A 4E 6F 20 6C 6F 76 65 20 6C 6F 73 74 2E 20 6E 6F 20 6C 6F 76
65 20 6C 6F 73 74 2E
—–

Podemos representar três números hexadecimais em um pixel. Isso nos dá aproximadamente 100 pixels. A nossa imagem tem 480×720 o centro dela fica em 240×360.

Homem velho e espira

Homem velho e espira Disposição

Mesmo com uma mensagem com muito mais informação, a imagem ainda continua indistinguível da original quando observada a olho nu.

Homem velho e mensagem espiral

Homem velho e mensagem espiral

Podemos ainda complicar mais nosso algoritmo para esconder mensagens, usando apenas um espectro de cor da imagem escolhida. De acordo com o padrão RGB toda imagem digitalizada e colorida pode ser representada pela combinação de três matrizes representando as cores primárias R = Vermelho, G = Verde, B = Azul. Com algum software de processamento digital de imagens como o matlab [4] ou o scilab [5] pode separar as matrizes primárias de uma imagem e modificar apenas a correspondente a uma cor. Isso diminui a quantidade de dados armazenada por pixel, porém torna a possibilidade de detecção de mensagem escondida muito menor.

Cada cor é representada por uma matriz com elementos que possuem valores que mostra a intensidade da cor representada em um byte (0 à 255). A matriz de cada cor pode ser exibida como uma imagem em tons de cinza e a junção das três matrizes nos resulta em uma imagem colorida.

Homem velho RGB

Homem velho RGB

Podemos inserir a informação dentro de uma matriz de uma cor específica a diferença agora é que temos apenas um byte por pixel em toda a matriz e a informação segredo ficará mais espalhada pela imagem final. No nosso experimento inserimos a mensagem dentro da matriz referente à cor vermelha, isso com a disposição apontada pela função espiral usada no exemplo anterior. Notamos que houve uma alteração quase que imperceptível na imagem experimento.

Homem velho com a matrix de vermelho com a mensagem repetida quatro vezes.

Homem velho com a matriz de vermelho com a mensagem repetida vinte vezes.

Podemos alternar a matriz em que inserimos os dados numa ordem pseudo-aleatória em que o receptor sabe a semente para determinar a sequencia. Em fim, existem muitas técnicas ainda não exploradas e que podem se usadas para usar uma informação como portadora para algum segredo. Também é possível antes de inserir os dados na imagem cifra-los com algum algorítimo assimétrico o que dificulta o processo reverso para tentar achar o segredo escondido na informação.

Na minha opinião, as técnicas de estaganografia apresentam um grande desafio para a segurança da informação. Pois permitem atacantes esconder dados e fazer túneis explorando limitações físicas humanas (Brain Hacking) e que dificulta muito a análise forense de um conteúdo comprometido. Acho que à medida quse torna popular os recursos multimídia na rede como o Youtube, Google vídeos ou Picasa, uma nova geração de ferramentas estão sendo criadas para fazer uso de técnicas de esteganografia para ocultar informações e distribuí-las por um canal popular e de difícil detecção. A demanda por profissionais de segurança especialistas nessa área tende a crescer, ajudando a criar mecanismos de controle de integridade de conteúdo e ferramentas para auxílio a análise forense.

Referências:
[1] DPI: Dot per inches = Pontos por polegada. Uma polegada possui 2,5 cm. Unidade de densidade de resolução.
[2] Pixels: Uma definição bem simples e suficiente para os nossos experimentos é a de pixel como um pequeno quadrado formado por uma única cor.
[3] Gamute: Quantidade de cores diferentes de uma imagem.
[4] Matlab – http://www.mathworks.com/
[5] Scilab – http://www.scilab.org/