ARM64
Fondamentaux
Instructions de base
Ces instructions agissent sur les valeurs des registres. Elles constituent la base du calcul ARM64.
MOV
Déeplacer / charger immédiatement
mov x0, #42
16 bits immédiat, ou registre à registre
MOVK
Déplacer et garder
movk x0, #0xDEAD, lsl #16
Insérer 16 bits à une position décalée
MOVN
Ne pas déplacer
movn x0, #6
Chargement par NON bit à bit : ~6 = -7 (complément à deux)
ADD
Ajouter
add x2, x0, x1
x2 = x0 + x1
SUB
Soustraire
sub x4, x3, x2
x4 = x3 - x2
MUL
Multiplier
mul x7, x5, x6
64 bits inférieurs du produit
AND
ET bit à bit
and x10, x8, x9
Opérations de masquage, extraction de bits
ORR
OU bit à bit
orr x12, x11, x9
Définir des bits spécifiques
EOR
XOR bit à bit
eor x15, x13, x14
Basculer les bits, obfuscation simple
LSL
Décalage logique vers la gauche
lsl x17, x16, #8
Multiplier par une puissance de 2
LSR
Décalage logique vers la droite
lsr x19, x18, #4
Division non signée par une puissance de 2
ASR
Décalage arithmétique vers la droite
asr x1, x0, #3
Division signée (préserve le bit de signature)
NEG
Nier
neg x0, x0
négation du complément à deux
Accès mémoire
L'architecture ARM64 utilise des instructions de chargement/stockage explicites. La compréhension des modes d'adressage est essentielle pour interpréter les crash et les heap operations.
LDR
Registre de chargement (64 bits)
ldr x0, [x1]
Registre de base
LDRB
Octet de chargement (8 bits)
ldrb w0, [x1, #5]
Base + décalage immédiat
LDRH
Load halfword (16 bits)
ldrh w0, [x1, x2]
Base + décalage du registre
LDP
Paire de chargement (2 registres)
ldp x29, x30, [sp], #16
Post-index (charger, puis ajouter 16 à SP)
STR
Registre de stockage (64 bits)
str x0, [x1]
Registre de base
STP
Paire de stockage
stp x29, x30, [sp, #-16]!
Pré-indexation (soustraire 16 de SP, puis stocker)
ADRP
Adresse de la page de 4 KB
adrp x0, label@PAGE
Adresse de page relative au PC
Branchement et flux de contrôle
B
Branche inconditionnelle
b label
Plage relative au PC, ±128 Mo
BL
Branche avec lien
bl function
Définit X30 = adresse de retour, puis branches
BR
Branche à enregistrer
br x16
Branche indirecte (utilisée par les stubs PLT)
BLR
Lien vers la branche pour s'inscrire
blr x8
Appel indirect : X30 = adresse de retour, saut vers X8
RET
Retour
ret
Branches vers X30. Cible ROP.
CBZ
Comparer et brancher si zéro
cbz x0, label
Aucun CMP requis — les tests s'enregistrent directement
CBNZ
Comparer et brancher si non nul
cbnz x1, loop
Motif de bord arrière en boucle commune
B.cond
Branche conditionnelle
b.eq target
Teste les indicateurs NZCV des CMP/TST précédents
CMP
Comparer (définit les indicateurs)
cmp x0, x1
Alias poursubs xzr, x0, x1
TST
Bits de test (ET, définit les indicateurs)
tst x0, #0xFF
Alias pourands xzr, x0, #0xFF
SVC
Appel du superviseur
svc #0x80
Appel système sur macOS/iOS (X16 = numéro d'appel système)
BRK
Point de rupture
brk #0
Déclenche débogage
Conditions
EQ
Égal
Z = 1
Après CMP x0, x1: branchement si x0 == x1
NE
Non égal
Z = 0
Après CMP: branchement si x0 != x1
GT
Supérieur à (signé)
Z=0, N=V
if (a > b)
LT
Inférieur à (signé)
N ≠ V
if (a < b)
GE
Supérieur ou égal (signé)
N = V
if (a >= b)
LE
Inférieur ou égal (signé)
Z=1 ou N!=V
if (a <= b)
HI
Supérieur (non signé)
C=1, Z=0
Comparaison non signée
LO/CC
Inférieur (non signé)
C = 0
Comparaison non signée
MI
Moins (négatif)
N = 1
Le résultat était négatif
PL
Plus (non négatif)
N = 0
Le résultat était positif ou nul.
VS
Débordement
V = 1
Un débordement signé s'est produit
AAPCS64
À quoi sert l’AAPCS64 ?
C’est l’ensemble des règles qui définissent comment les fonctions ARM64 :
reçoivent leurs arguments,
renvoient leurs résultats,
utilisent les registres,
gèrent la pile (stack).
Toutes les fonctions en environnement iOS ARM64 suivent ces règles.
Passage des arguments
Les 8 premiers arguments sont passés dans les registres :
arg1
X0
arg2
X1
arg3
X2
arg4
X3
arg5
X4
arg6
X5
arg7
X6
arg8
X7
Les arguments à partir du 9e sont placés sur la pile.
X8 est utilisé pour certaines structures volumineuses retournées par une fonction.
Valeur de retour
La valeur retournée est placée dans X0.
Pour une valeur de 128 bits, X0 + X1 sont utilisés.
Registres sauvegardés
Caller-saved (X0–X18)
La fonction appelée peut les modifier librement.
Si l’appelant veut conserver leur valeur, il doit les sauvegarder avant l’appel.
Callee-saved (X19–X28, X29, X30)
La fonction appelée doit les restaurer avant de retourner.
C’est pourquoi on voit souvent X29 et X30 sauvegardés au début d’une fonction.
Prologue et épilogue d’une fonction
Au début :
Sauvegarde X29 et X30 sur la pile.
Crée une nouvelle frame.
À la fin :
Restaure X29 et X30.
Retourne à l’appelant via l’adresse contenue dans X30.
Organisation d’une frame de pile
Une frame contient généralement :
La pile doit toujours être alignée sur 16 octets.
Importance pour l’exploitation (exploitation de vulnérabilités)
Lorsqu’un buffer local est situé sur la pile :
un dépassement de tampon (buffer overflow) peut écraser :
les variables voisines,
X29 sauvegardé,
X30 sauvegardé.
Si l’attaquant contrôle X30, alors lors du :
le programme saute vers une adresse choisie par l’attaquant.
C’est l’équivalent ARM64 de l’écrasement de l’adresse de retour sur x86 et la base des attaques ROP (Return-Oriented Programming).
Idée essentielle
Pour analyser un crash ou une vulnérabilité ARM64 :
X0–X7 → arguments de fonction.
X0 → valeur de retour.
X30 → adresse de retour.
X29 → frame pointer.
Le motif
stp x29, x30/ldp x29, x30est le signe classique du début et de la fin d’une fonction.Contrôler le X30 sauvegardé sur la pile permet potentiellement de contrôler le flot d’exécution lors du
RET.
Dispatch Objective-C (objc_msgSend)
objc_msgSend)Contrairement à C++, un appel de méthode Objective-C n'appelle pas directement une fonction.
Code source :
devient approximativement :
Le runtime Objective-C :
lit le pointeur
isade l'objet,trouve sa classe,
cherche la méthode dans le cache ou la table des méthodes,
récupère l'IMP (adresse réelle de la fonction),
saute vers cette fonction.
Registres importants
X0
self (objet)
X1
_cmd (sélecteur)
X2+
arguments de la méthode
Exemple :
devient :
Pourquoi c'est important en debugging ?
Dans LLDB :
Puis examiner :
permet de savoir :
quel objet reçoit le message ;
quelle méthode est appelée.
C'est une technique fondamentale pour comprendre le comportement d'une application iOS.
Intérêt pour l'exploitation
Le pointeur isa détermine quelle classe traite les messages.
Si une corruption mémoire permet de modifier isa :
alors les appels Objective-C peuvent être redirigés vers des structures contrôlées.
Cette famille d'attaques est souvent appelée :
counterfeit object attacks ;
JOP (Jump-Oriented Programming) basé sur le runtime Objective-C.
Format binaire Mach-O
Les exécutables iOS/macOS utilisent le format Mach-O.
Structure simplifiée :
Éléments importants
mach_header_64
Magic, type de processeur, flags (PIE)
flag PIE → ASLR appliqué ; type de processeur confirmé ARM64
__TEXTsegment
Code exécutable, chaînes de caractères littérales
Les gadgets ROP/JOP sont disponibles ici (lecture + exécution)
__TEXT.__stubs
souches de saut équivalentes à PLT
Appels indirects à des fonctions importées
__DATAsegment
Variables globales, métadonnées ObjC
__objc_classlist: toutes les classes ObjC dans le binaire
__DATA.__got
Tableau de décalage global
Pointeur de fonction pour lazy binding ⇒ GOT overwrite attacks
LC_LOAD_DYLIB
Bibliothèques dynamiques liées
Identifie les frameworks chargés (surface d'attaque).
LC_CODE_SIGNATURE
données de signature de code
Doit être valide sur iOS, aucune injection de code sans contournement n'est autorisée
LC_MAIN
Décalage du point d'entrée
L'exécution commence après la configuration de dyld.
Principales protections de sécurité iOS ARM64
Apple combine plusieurs mécanismes.
ASLR + PIE
Randomise les adresses de chargement binaire/bibliothèque
Chaque MH_PIEexécutable reçoit une slide aléatoire
iOS 4.3
W^X
Les pages sont soit modifiables, soit exécutables, jamais les deux.
Les permissions des tables de pages sont appliquées par MMU.
Toujours
Stack Canaries
Détection des débordements de tampon de pile
-fstack-protector: un cookie aléatoire sur la pile
N'est pas lié à une version mais au moment de la compilation
PAC
Codes d'authentification de pointeur
PACIA/PACIB pointeur de signes; AUTIA/AUTIB vérifient
A12+ (iPhone XS)
MTE
Extension d'étiquetage de mémoire
Tags de 4 bits sur les pointeurs et la mémoire ⇒ détection des use-after-free
ARM v8.5
Signature de code
Tout le code doit être signé par Apple ou le développeur.
Le noyau vérifie les hachages de page en cas de défaut
iOS 2.0
PAC sur ARM64: Sur les appareils A12+, l'instruction PACIASPsigne le registre de lien (X30) avec une clé dérivée du pointeur de pile. L'épilogue AUTIASPvérifie la signature avant le return RET. Si le registre de lien enregistré a été altéré, l'authentification échoue et le processus plante. Cela rend l'exécution traditionnelle de ROP nettement plus difficile sur les iPhones modernes, mais pas impossible.
Tagged pointers vs Heap-allocated pointers
La distinction entre les objets à pointeur taggé (tagged pointers) et les objets alloués sur le tas (heap-allocated) est essentielle.
Fonctionnement interne des tagged pointers sur ARM64
Bit 63 (MSB) : ce bit est positionné à 1 pour les tagged pointers sur ARM64. Le runtime vérifie en priorité ce bit afin de déterminer si le pointeur est taggé.
Charge utile obfusquée (payload) : depuis iOS 14 et macOS Big Sur, la valeur contenue dans un tagged pointer est masquée par une opération XOR avec une valeur aléatoire propre au processus. Le pointeur brut paraît donc aléatoire, mais le runtime est capable de reconstruire la valeur d'origine.
Indice de classe (tag class index) : il est encodé dans les bits de poids fort et permet d'identifier le type de l'objet (par exemple
NSNumber,NSDate,NSString, etc.).Absence de pointeur
isa: comme l'objet est directement représenté par la valeur du pointeur, aucune allocation mémoire n'est effectuée et il n'existe donc pas de champisa.
Pourquoi est-ce important pour l'exploitation de vulnérabilités ?
Il est impossible de corrompre le champ
isad'un tagged pointer, car ce champ n'existe tout simplement pas : l'objet est entièrement représenté par la valeur du pointeur.Il est impossible de réaliser une attaque de type use-after-free sur un tagged pointer, puisqu'il n'a jamais été alloué sur le tas et qu'il n'y a donc rien à libérer.
Les tagged pointers sont insensibles aux corruptions des métadonnées du tas, car ils contournent complètement l'allocateur mémoire.
Lors du développement d'exploits ciblant des objets Objective-C, il est donc indispensable de vérifier que l'objet visé est alloué sur le tas et non représenté sous forme de tagged pointer.
Les objets
NSNumbercontenant des entiers, lesNSStringcourtes (environ jusqu'à 7 caractères) ainsi que les objetsNSDatesont fréquemment implémentés sous forme de tagged pointers ; il est généralement préférable d'éviter de les cibler dans ce contexte.
Mis à jour