Lesson 5


ESCREVENDO EXPLOITS





Para aprender sobre a exploração binária, primeiro você precisa dominar os conceitos básicos de engenharia reversa

Pré Requititos

  • Conhecimento básico sobre programação em C e compiladores GCC - Você é capaz de escrever/compilar um programa de ‘Hello World’ e entender sobre funções e variáveis?
  • Conhecimento sobre Análise Dinâmica com o gdb - Você consegue testar/analisar seu binário do ‘Hello World’ debugando em tempo de execução?
  • Básico de assembly x86 - Muito básico, porque a exploração é uma boa forma de aprender mais e mais sobre linguagem assembly.
  • gdb-peda instalado - PEDA é um adicional do gdb que facilita a análise dinâmica e tarefas de exploração.

Introdução

Exploração binária é um assunto muito complexo, mas nós começaremos do básico. Entenderemos como um programa se comporta em memória e como nós podemos tomar vantagem disso para forçá-lo a fazer coisas que não foi originalmente programado para fazer. Esta é a chave para começar.

Corrupção de memória

Nesta lição, usaremos um layout de um programa em C sendo executado em uma arquitetura x86. Tenha em mente que existem muitas variações, mas de certa forma, todas derivam desta.

O stack frame

/------------------\  endereços de memória mais altos 
|argumentos via cmd|
|e  variáveis      |
|de ambiente       | 
|------------------| 
|      stack       |
|        |         |
|        -         |
|        -         |
|        |         |
|      heap        |
|------------------|
| dados            |
| inicializados e  |
| não inicializados| 
|------------------|  
|       texto      |  
\------------------/  endereços de memórias mais baixos 
Esta é uma representação superficial de como o programa é armazenado na memória em tempo de execução. Em vez de detalhar teoricamente cada sessão, preferimos mostrar como a pilha funciona de forma prática em tempo de execução.

Vejamos o código corruptme.c abaixo:

Este programa lê o argv[1], o primeiro argumento, e não faz nada com ele. Existe uma outra variável, var1 com o valor fixo de 0. Ele compara se o valor desta variável é diferente de 0 e apresenta uma mensagem de sucesso se isso for verdade.

Mas não foi dado nenhuma forma pela qual o usuário pudesse modificar a variável var1. Então, se quisermos modificar o valor da variável var1, é preciso encontrar uma forma de explorar o programa!

Compilando nosso programa em C

Para funcionar tanto em sistemas 32 quanto em 64 bit você precisará passar o parâmetro -m32 para o gcc.

root@kali:~# gcc corruptme.c -o corruptme -m32 -z execstack -fno-stack-protector -no-pie -w

Porque usamos todos estes outros argumentos?
  • -fno-stack-protector - Desabilita a proteção contra o stack smashing para facilitar nosso estudo de caso.
  • -z execstack - Habilita que a área da pilha possa ser executável (será explicado adiante).
  • -no-pie - Não produzir um executável independente de posição. Deverá produzir uma organização de memória amigável no formato 0x80.
  • -w - Esconder mensagens de aviso do gcc enquanto compilando.

Todas as proteções acima (e muitas outras), mesmo quando ativas, podem ser escapadas por meio de alguma técnica mais complexa.

Problema para compilar/executar em uma arquitetura x64?

Verifique se você já tem instalado as bibliotecas de desenvolvimento 32 bit libc.

Nos sistemas baseados em Ubuntu/Debian você pode resolver com os seguintes comandos:

root@kali:~# dpkg --add-architecture i386
root@kali:~# apt-get update
root@kali:~# apt-get install libc6:i386 libc6-dev:i386 libncurses5:i386 libstdc++6:i386 gcc-multilib

..se falhar:

root@kali:~# apt-get install multiarch-support


Verificando informações de proteção do binário

Na maioria dos casos não é você quem define as opções do compilador, você já recebe o binário compilado. Então, antes de começar a procurar por um vetor de exploração, é sempre uma boa ideia conseguir o máximo de informação possível sobre o binário.

Com o binário compilado, execute o comando file:

root@kali:~# file corruptme
corruptme: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=83303dd49a880854ca78f4d69163af3d89649d04, not stripped

O resultado da execução do comando mostra que temos um ELF 32-bit executável, dynamically linked (ligado com 'includes' da libc) e not-stripped (significando que contem todas as informações de depuração). Você pode remover estas informações com a opção -g no gcc, mas não faremos isto para deixar o depuração mais fácil.

Agora verifique as proteções com o gdb-peda:

root@kali:~# gdb corruptme
gdb-peda$ checksec


Ótimo, todas as proteções que poderiam dificultar nosso estudo estão desabilitadas!

Buffer Overflow and Stack Smashing

Agora vamos voltar ao nosso objetivo..

> Modificar o valor fixo da variável var1 para forçar o programa a exibir a mensagem de sucesso.

Nesse caso, nós temos o código fonte, e podemos facilmente identificar o vetor de exploração.


O programa aloca 40 bytes para a variável buffer, em seguida strcpy() copia o conteúdo de argv[1] para esta variável. Se você olhar o manual da função strcpy() existe um bug o qual os programadores devem estar atentos:


A função strcpy() copia para o buffer a string byte a byte, até encontrar o byte final 0x0. O buffer de destino deve ser grande suficiente para receber a cópia. Bem, o que acontece se enviarmos mais bytes do que foi alocado?

Devido ao layout da memória, bug da strcpy() e nenhuma proteção no código ou na compilação, nós começaremos a sobrescrever todas as coisas armazenadas na memória logo depois da variável buffer na pilha, incluindo o conteúdo das variáveis!

Este Stack buffer overflow pode ser usado deliberadamente como parte de um ataque conhecido como stack smashing.

root@kali:~# ./corruptme BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
>> Wow! you've changed the variable value to: 0x00000042


Com isso, o binário foi explorado e nós atingimos nosso objetivo. De alterar o valor da variável var1.

Começamos listando as funções do binário com comando info functions, e depois disassemble main() e colocamos um breakpoint (ponto de parada) no endereço de chamada em strcpy().


Execute o programa com algum argumento aleatório r AAAAAAAA, e o programa irá parar quando atingir o breakpoint..


É por isso que recomendo o uso do gdb-peda, ele mostra a pilha, registradores e muitas outras informações úteis sobre o programa em execução de forma amigável.

Use o comando s, uma apreviação de seek para ver a função strcpy() copiando todos os bytes da sua string para a memória, e parando quando encontrar o byte 0x0 null byte, o final da string.


Agora temos AAAAAAAA armazenado no registrador ECX, localizado no endereço de memória 0xffffd7a5.


Mas como podemos encontrar o endereço da variável var1 na memória e ler seu valor?

Olhando no código em assembly da main(), vemos que o valor de var1 está armazenado no endereço ebp-0x1c.



EBP = 0xffffd578
- 0x1c = 0xffffd55c

O gdb pode nos ajudar a ler o valor no endereço da variável diretamente..

gdb-peda$ x/wx $ebp-0x1c



Como podemos ver, o valor continua 0.

Agora coloque um breakpoint no endereço de test eax,eax, a instrução que compara se o valor armazenado em var1 é diferente de 0. Agora envie 41 A's, r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA como argumento, e use o comando c para continuar até o segundo breakpoint, agora leia o valor em var1 novamente


var1 == "A" (0x00000041 in hex), o valor foi modificado, e se voce continuar o programa, verá a mensagem de sucesso.

Endianess

Uma coisa importante à observar, é que a ordem que passamos os dados no payload afeta diretamente como ele será armazenado em memória, veja:


Enviei 40 junks A para preencher o buffer + ABC, e é escrito CBA (0x00434241 in hex) em var1, mas porque está ao contrário? Isso é devido ao endianness da arquitetura x86.
  • No big endian, é armazenado o byte mais significante no menor endereço.
  • No little endian, é armazenado o byte menos significativo no menor endereço.

Veja como funciona:

  • x86 architecture = Little-endian
  • x64 architecture = Big-endian
  • Mais arquiteturas e endianesshere.


Isso significa que se você quiser armazenar ABC nessa arquitetura, você precisa enviar CBA.

root@kali:~# ./corruptme AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBA


Dicas úteis

Existem alguns truques usando Python que podem ser muito úteis para escrever o exploit. Não se preocupe se você não programa em Python. É muito básico.

Pegando o exemplo acima, o mesmo resultado pode ser obtido da seguinte forma:

root@kali:~# ./corruptme $(python -c 'print "A"*40 + "CBA" ')

Se você deseja armazenar decimais como 123 ou caracteres não printáveis, você precisa usar Unicode:

root@kali:~# ./corruptme $(python -c 'print "A"*40 + "\x03\x02\x01" ')

Você pode usar o PIPE para enviar o STDIN em alternativa ao argv..

root@kali:~# python -c 'print "A"*40 + "\x03\x02\x01" ' | ./corruptme

Você pode usar o pipe para um serviço remoto usando o netcat..

root@kali:~# python -c 'print "A"*40 + "\x03\x02\x01" ' | nc 200.112.222.333 1337

No gdb você pode passar passar um script em Python no argv..

root@kali:~# gdb r $(python -c 'print "A"*40 + "\x03\x02\x01" ')

..e no STDIN..

root@kali:~# gdb r <<< $(python -c 'print "A"*40 + "\x03\x02\x01" ')

Exploração Remota

Se você chegou aqui sem maiores problemas, vamos à alguns casos mais próximos do real. Dessa vez, não será dado o código fonte, somente o binário:

Acesse o desafio, inicialize o ambiente e faça download do binário.

Na figura a seguir tentamos executar o binário remotamente e em seguida localmente enviando a string "TEST"


Você precisa explorar o binário para fazê-lo imprimir a mensagem de sucesso contendo a flag.

Escreva o exploit localmente. Quando estiver funcionando na sua máquina, teste-o no servidor.

Nota: Este programa é muito similar ao corruptme.c, mas com alguns detalhes diferentes que você deve descobrir para escrever o exploit e capturar a flag.

Return-oriented-programming (ROP)

Agora que você já sabe como alterar os valores das variáveis armazenadas em memória de programas vulneráveis, o que acha de alterar o valor do EIP para forçar o programa retornar e executar uma função em ouro endereço de memória?

Acesse o desafio, inicialize o ambiente e faça download do binário.

Aqui está o código fonte:



Sabendo que o EIP armazena o endereço que deve ser chamado quando a main() retornar, seu objetivo é explorar o programa, e encontrar uma forma de executar a função get_flag(). Isso vai fazer o programa imprimir a flag.

Temos certeza que você consegue fazer isso sem muito esforço!

Dicas..
  • Esse programa está recebendo a entrada do STDIN e não do argvs.
  • Onde o EIP é armazenado? Como eu o sobrescrevo?
  • Erro de Segmentation fault significa que você quebrou o fluxo do programa. Talvez o EIP tenha sido sobrescrito com um endereço de memória inválido.

Conseguiu a Flag?

Maravilha! Agora você está pronto para iniciar no mundo da exploração. Como você pode ver, começando de um simples vetor vulnerável, nós podemos combinar técnicas para fazer tudo que quisermos. Algumas técnicas mais avançadas são:
  • Você pode escrever um ROP-chain usando funções gadgets, que são armazenadas na memória do programa para, basicamente, criar um novo programa em tempo de execução. Você pode usar algumas funções da libc que também são carregadas em memória. Consegue imaginar as possibilidades?
  • Usar a vulnerabilidade de format_string para remotamente vazar pilhas completas de memória, fazer a extração de informações sensíveis do alvo.
  • Usar a vulnerabilidade de format_string para vazar o endereço de memória do system("/bin/sh","") da libc e usar o ROP para conseguir o shell escapando as proteções de ASLR e NX.

Tenha em mente que esta é apenas uma breve introdução. As coisas podem se tornar muito complexas neste mundo, mas todas as informações estão na internet apenas esperando por você.

Acknowledgments

Nós gostariamos de agradecer ao Danilo Salles (@intrd por escrever e criar o desafios desta lição. E nós recomendamos fortemente visitar o blog dele, que contém ótimas resoluções que vão te ajudar a resolver mais desafios aqui no Shellter.

Leitura Complementar

Share: