For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 :

Argument
Registre

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 :

  1. les variables voisines,

  2. X29 sauvegardé,

  3. 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, x30 est 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)

Contrairement à C++, un appel de méthode Objective-C n'appelle pas directement une fonction.

Code source :

devient approximativement :

Le runtime Objective-C :

  1. lit le pointeur isa de l'objet,

  2. trouve sa classe,

  3. cherche la méthode dans le cache ou la table des méthodes,

  4. récupère l'IMP (adresse réelle de la fonction),

  5. saute vers cette fonction.

Registres importants

Registre
Contenu

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 champ isa.

Pourquoi est-ce important pour l'exploitation de vulnérabilités ?

  • Il est impossible de corrompre le champ isa d'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 NSNumber contenant des entiers, les NSString courtes (environ jusqu'à 7 caractères) ainsi que les objets NSDate sont 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