[VD01] – Stack Overflow
Vou iniciar uma série de artigos dedicados às técnicas de desenvolvimento voltadas a exploração de vulnerabilidades. O primeiro post será sobre exploração de falhas de programação na hora de usar a pilha alocada para os processos em execução no Sistema Operacional. A motivação da leitura de mais esse tutorial sobre Stack Overflow é que vou tentar uma abordagem que torne o texto o mais didático possível, projetando e escrevendo em português (diferente de uma tradução). Irei mostrar alguns exemplos reais em arquitetura IA 32 bits rodando no sistema operacional Linux.
Frequentemente escuto pessoas perguntarem qual a melhor forma de se inciar um estudo de técnicas de exploração de aplicações e percebo que a maioria delas tem uma idéia de que estudando tecnologias específicas (C ou Assembly para uma arquitetura específica) é a melhor forma de se trilhar essa linha de pesquisa. Na minha opinião, essa é uma forma errada de iniciar tal estudo. Em nossa área, tecnologias específicas se tornam obsoletas rápido (ver lei de Moore [1]), temos que estudar fundamentos para os problemas tratados. Além do erro de codificação na aplicação explorada, a maioria das técnicas de vulnerability development se baseiam em explorar a forma como a arquitetura e o sistema operacional alvo do ataque organizam o software em execução na memória principal da máquina.
Uma forma de iniciar o estudo de técnicas de exploração de vulnerabilidades é estudar arquiteturas e organização de computadores. Esse estudo executado de forma genérica e não focado em tecnologias específicas é a melhor forma de construir uma base para um estudo mais frutífero de técnicas de exploração. Um livro muito bom que aborda todos os conceitos necessários é o Computer Architecture a Quantitative Approach de David A. Patterson [2].
Tendo em vista que o leitor já possui uma boa base de arquitetura de computadores, podemos dar inicio a nossa primeira técnica de exploração de software que é o Stack Overflow.
O Stack Overflow é uma sub-categoria da área principal que é a de Buffer Overflow. Um buffer nada mais é do que uma porção de memória solicitada pelo desenvolvedor da aplicação para armazenamento de alguma informação. Todas as varáveis dentro de uma aplicação podem ser chamadas de buffers, já que o compilador por “baixo dos panos”, gera o código responsável por solicitar a reserva de uma quantidade de memória de tamanho igual ao tamanho necessário para o tipo da variável. Quem efetivamente é responsável por organizar e gerenciar esse processo de alocação de memória é o Sistema Operacional através de chamadas de sistemas (No Linux nós temos malloc, mmap e brk). Quando um processo é executado, o sistema operacional reserva um espaço em memória que possui uma organização específica fornecendo condições para que esse possa realizar suas tarefas. Um exemplo de organização da memória reservada para um processo no sistema operacional Linux é a mostrada na Fig 01.

Fig 01: Layout da memória de um processo no Linux
Na primeira região mostrada na figura, é armazenada a entrada do programa fornecida pelo usuário através dos parâmetros da função principal (argc e argv). O código assembly que será executado pelo processador será armazenado na sessão “text” o processador irá executar sequencialmente dessa região de memória para isso fará uso de um registrador auxiliar que indica qual a próxima instrução a ser executada chamado “EIP” (Instruction Pointer, normalmente o objetivo do atacante é controlar esse registrador).
Dependendo do tipo de variável em seu programa (global ou local) e da forma como ela foi alocada (estaticamente ou dinamicamente), esse buffer será posicionado em regiões diferentes na memória. As variáveis locais e argumentos passados para funções são armazenadas na pilha já as alocadas dinamicamente, usando malloc e suas variações, são armazenadas na heap e dados estáticos e globais são armazenados na BSS.
O problema de buffer overflow ocorre quando tentamos inserir mais dados do que o buffer foi projetado para suportar. Existem duas principais categorias de buffers overflows, que são a baseada em ataques na Pilha e a baseada em ataques na Heap. Nesse texto vamos explicar os ataques que visam explorar falhas no uso da pilha de processos.
As vulnerabilidades de Stack Overflow são baseadas em exploração de falhas na alocação e uso de memória em variáveis locais (dentro do contexto de um bloco de código). Quando é executada uma instrução de chamada de procedimento como CALL o contexto de qual instrução será executada no retorno da execução desse bloco de código é armazenado na pilha (isso se chama: ponto de retorno ou return point). Dentro do bloco chamado pode haver uma falha no uso da pilha de tal forma que permita ao atacante sobrescrever a região da pilha em que foi armazenada o valor do ponto de retorno para o procedimento anterior ao vulnerável.
É ai que nós podemos induzir o fluxo de execução do software. Podemos fazer um salto incondicional para qualquer região de memória acessível pelo processo atacado, inclusive para código que nós mesmos injetamos no processo através de parâmetros de entrada ou variáveis de ambiente. É assim que a maior parte dos exploits para explorar vulnerabilidades no uso da pilha funcionam. Nesse estado, podemos alterar o fluxo do programa para qualquer endereço em memória que seja visível ao processo, inserir um ponto de retorno válido e executar código com os privilégios do programa vulnerável.
Vamos mostrar agora passo a passo como se controla o EIP do programa vulnerável a um stack overflow e com isso podemos redirecionar o fluxo do código para onde desejarmos dentro do espaço de endereçamento do processo. Para isso iremos fazer uso do GDB [3], que pode ser encontrado na maioria das distribuições Linux, como ferramenta de depuração.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int function(char *arg) {
char buffer[2];
strcpy(buffer, arg);
}
int main (int argc, char *argv[]) {
printf("AQUI\n\n");
function(argv[1]);
return 0;
}
1> Precisamos que o programa seja compilado usando a diretiva no-stack-protector para que o GCC não adicione código para prevenir stacks overflows (canários). Precisamos também desabilitar o ASLR – Adress Space Layout Randomization no kernel para que o espaço de endereçamento não seja randomizado toda vez que o processo for iniciado pelo Kernel. Em outra oportunidade falaremos a respeito das proteções existentes hoje em dia (grande parte dessas são “bypassaveis”), para a finalidade de aprender o fundamento da técnica esse ambiente montado é suficiente.
~# echo 0 > /proc/sys/kernel/randomize_va_space ~$ gcc -fno-stack-protector -ggdb -o bug bug.c
2> Execute o programa passando uma parâmetro de entrada aleatorio e note que o processo receberá sinal de falha de segmentação do sistema operacional. Note que o parâmetro de entrada é copiado diretamente para a variável buffer de tamanho 2 bytes e isso causa a falha de segmentação.
mabj@envy:~/tmp$ ./bug AAAAAAAAAAAAAA AQUI Segmentation fault (core dumped)

Fig 02: Alocação e uso de recursos na pilha
3> Vamos agora iniciar o GDB e fazer a engenharia reversa do binário.
mabj@envy:~/tmp$ gdb bug (gdb) disas main Dump of assembler code for function main: 0x0804840e : lea 0x4(%esp),%ecx 0x08048412 : and $0xfffffff0,%esp 0x08048415 : pushl -0x4(%ecx) 0x08048418 : push %ebp 0x08048419 : mov %esp,%ebp 0x0804841b : push %ebx 0x0804841c : push %ecx 0x0804841d : sub $0x10,%esp 0x08048420 : mov %ecx,%ebx 0x08048422 : movl $0x8048510,(%esp) 0x08048429 : call 0x804832c <puts@plt> 0x0804842e : mov 0x4(%ebx),%eax 0x08048431 : add $0x4,%eax 0x08048434 : mov (%eax),%eax 0x08048436 : mov %eax,(%esp) 0x08048439 : call 0x80483f4 <function> <--- Chamada para a função vulnerável 0x0804843e : mov $0x0,%eax <-- %EIP(0x0804843e) que será salvo na pilha 0x08048443 : add $0x10,%esp 0x08048446 : pop %ecx 0x08048447 : pop %ebx 0x08048448 : pop %ebp 0x08048449 : lea -0x4(%ecx),%esp 0x0804844c : ret End of assembler dump.
(gdb) disas function
Dump of assembler code for function function:
0x080483f4 : push %ebp
0x080483f5 : mov %esp,%ebp
0x080483f7 : sub $0x28,%esp
0x080483fa : mov 0x8(%ebp),%eax
0x080483fd : mov %eax,0x4(%esp)
0x08048401 : lea -0x2(%ebp),%eax
0x08048404 : mov %eax,(%esp)
0x08048407 : call 0x804831c <strcpy@plt> < -- Copia o argv para buffer
^ --- É aqui que o bug se encontra
0x0804840c : leave
0x0804840d : ret
End of assembler dump.
4> Temos que achar em qual endereço na pilha foi armazenado o contexto antes de iniciar o fluxo dentro da função vulnerável. Para isso nós sabemos que o valor do eip salvo será de “0x0804843e” que é o endereço da próxima instrução após a chamada para a função vulnerável. É esse endereço que nós deveremos sobrescrever. Vamos colocar um breakpoint logo no inicio da função vulnerável e fazer um dump da pilha em busca do ponto de retorno (eip).
(gdb) b *(function+3) Breakpoint 2 at 0x80483f7: file teste.c, line 5.
(gdb) r
Starting program: /home/mabj/tmp/bug
AQUI < ---- Imprimindo a string "AQUI" através do printf no código
Breakpoint 2, 0x080483f7 in function (arg=0x0) at teste.c:5
5 int function(char *arg) {
(gdb) i r $ebp <--- Achamos EBP que contém o endereço da base da pilha
ebp 0xbfd082a8 0xbfd082a8
(gdb) x/x $ebp
0xbfd082a8: 0xbfd082c8
(gdb) x/x $ebp+4 <--- Achamos o endereço e o conteúdo do ponto de retorno
0xbfd082ac: 0x0804843e
(gdb) x/20x $esp 0xbfd082a8: [0xbfd082c8] [0x0804843e] 0x00000000 0x08049ff4 0xbfd082b8: 0xbfd082d8 0x08048479 0xbfd082e0 0xb7ed8ff4 0xbfd082c8: 0xbfd08338 0xb7d95685 0x08048460 0x08048340 0xbfd082d8: 0xbfd08338 0xb7d95685 0x00000001 0xbfd08364 0xbfd082e8: 0xbfd0836c 0xb7eedb38 0x00000001 0x00000001
5> Colocamos um breakpoint após o strcpy e vamos vigiar o endereço de memória do ponto de retorno da função. Com isso descobrimos que é necessário 10 Bytes para poder sobrescrever o ponto de retorno por completo.
(gdb) r AAAAAAAAA < --- Primeira tentativa Starting program: /home/mabj/tmp/bug AAAAAAAAA AQUI Breakpoint 9, 0x08048407 in function (arg=0xbfc5d7a9 "AAAAAAAAA") at teste.c:7 7 strcpy(buffer, arg); (gdb) x/x $ebp+4 0xbfc5d1fc: 0x0804843e (gdb) n 8 } (gdb) x/x $ebp+4 0xbfc5d1fc: 0x00414141 <--- Ainda há um octeto que não foi sobrescrito "0x00". (gdb) r AAAAAAAAAA <--- Precisamos colocar mais um "A" na string de entrada. Starting program: /home/mabj/tmp/bug AAAAAAAAAA AQUI Breakpoint 9, 0x08048407 in function (arg=0xbfc5d7a9 "AAAAAAAAAA") at teste.c:7 7 strcpy(buffer, arg); (gdb) x/x $ebp+4 0xbfc5d1fc: 0x0804843e (gdb) n 8 } (gdb) x/x $ebp+4 0xbfc5d1fc: 0x41414141 < --- Agora foi sobrescrito com sucesso ! \O/.Fig 03:Execução do código e estado da pilha.
6> Sobrescrevemos o ponto de retorno com o endereço do código para impressão da string “AQUI” de modo que o programa imprimirá dois “AQUI”s na tela.
(gdb) r $'AAAAAA\x20\x84\x04\x08' <--- "0x08048422" Endereço do codigo do "AQUI" Starting program: /home/mabj/tmp/bug $'AAAAAA\x20\x84\x04\x08' AQUI < --- Primeira passagem AQUI < --- Passagem devido a exploração do Stack Overflow Program received signal SIGSEGV, Segmentation fault. 0x08048434 in main (argc=1091044483, argv=0x2e414141) at teste.c:12 12 function(argv[1]);
Através desses passos, podemos injetar um endereço para execução de código armazenado em qualquer região do espaço de endereçamento reservado para o processo. O que os “Hackers” fazem é preencher a memória com um código malicioso (Shellcode) através de diversos métodos e direcionar o fluxo do programa para a execução desse código.
Agora que a essência do ataque foi mostrada, o leitor pode colocar seus conhecimentos a prova resolvendo o Capture The Flag da uCon Security Conference [4]. Lá temos seis desafios apenas de Stack Overflow. Estou acabando de escrever outro post sobre Format String que será publicado em breve.
[1] http://en.wikipedia.org/wiki/Moore%27s_law
[2] http://www.amazon.com/Computer-Architecture-Quantitative-Approach-Kaufmann/dp/1558605967
[3] http://sourceware.org/gdb/current/onlinedocs/gdb.pdf.gz
[4] http://github.com/mabj/ctf_ucon2/tree/master

Boooaaaa slavio!
fico muito bem escrito.
abraco
Muito bom! \o/
Vo começar a ler… dificil achar algo em PT-BR!