לדלג לתוכן

4.4 דליפת libc ועקיפת ASLR תרגול

תרגול - libc leak ועקיפת ASLR

בתרגול הזה תבנו בעצמכם בינארי vulnerable 64 ביט עם NX ובלי PIE, תשאירו את ASLR דלוק, ותכתבו תוקפה בשני שלבים: קודם דליפת כתובת libc אמיתית וחישוב הבסיס, ואז ret2libc בכתובות המחושבות עד shell. זה בדיוק דפוס העבודה של רוב אתגרי ה-pwn האמצעיים. עבדו לפי הסדר, כל תרגיל בונה על הקודם.

הכנה - הבינארי והסביבה

צרו קובץ מקור אחד עם overflow קלאסית ועם main שאפשר לחזור אליה:

// leak.c
#include <stdio.h>
#include <unistd.h>

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

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

קמפלו בינארי 64 ביט, לא PIE, בלי canary, עם NX:

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

השאירו את ASLR דלוק - זה כל האתגר. ודאו:

cat /proc/sys/kernel/randomize_va_space   # should be 2

הערה על ה-libc: לצורך התרגול, ה-libc של המטרה הוא ה-libc של המערכת שלכם. מצאו את הנתיב שלו עם ldd ./leak והשתמשו בו בסקריפט (למשל /lib/x86_64-linux-gnu/libc.so.6).

תרגיל 1 - להוכיח ש-ASLR באמת מזיז את libc

לפני שתוקפים, תרגישו את הבעיה.

  1. הריצו ldd ./leak | grep libc שלוש פעמים ברצף. שימו לב שכתובת הבסיס של libc שונה בכל פעם.
  2. הריצו checksec --file=./leak וּודאו: NX enabled, No canary, No PIE.
  3. מצאו את ה-offset עד הreturn address של vuln בשיטת ה-cyclic מ-2.2.
  4. שאלת מחשבה: אם ה-offset של system בתוך libc קבוע, למה בכל זאת אי אפשר לכתוב את כתובת system ישירות בchain?

רמז: הבסיס זז, ה-offsets קבועים. את הבסיס של ההרצה הנוכחית אפשר לגלות רק בזמן ריצה, ולשם כך נועדה הleak.

תרגיל 2 - מיפוי ה-GOT וה-PLT

מטרת התרגיל: להבין מה בדיוק תדליפו.

  1. מצאו את הכתובות של puts@plt ו-puts@got. פעם עם objdump -d leak | grep 'puts' ו-objdump -R leak (מציג את ה-GOT relocations), ופעם עם elf.plt['puts'] ו-elf.got['puts'] ב-pwntools.
  2. הדליפו את הבינארי ב-gdb/pwndbg, הריצו עד אחרי הקריאה הראשונה ל-puts (ה-puts("overflow me:")), והדפיסו את התוכן של puts@got עם x/gx <כתובת>. שימו לב שהיא מצביעה לתוך libc.
  3. עצרו הפעם לפני הקריאה הראשונה ל-puts והשוו את הערך ב-puts@got. מה ההבדל, ולמה זה משנה לבחירת הכניסה שנדליף?

רמז: בקישור עצל הכניסה ב-GOT מכילה כתובת אמיתית של libc רק אחרי הקריאה הראשונה. puts בטוח כבר נקראה כי main השתמשה בה.

תרגיל 3 - שלב הleak וחישוב הבסיס

כתבו את החצי הראשון של התוקפה.

  1. בנו ROP chain שקוראת ל-puts(&puts@got) ואז חוזרת ל-main. השתמשו ב-rop.call('puts', [elf.got['puts']]) ואז rop.call('main').
  2. שלחו, קראו את שורת הleak, ופענחו: u64(p.recvline().strip().ljust(8, b'\x00')).
  3. חשבו את הבסיס: libc.address = leaked - libc.symbols['puts'].
  4. בדיקת שפיות: הדפיסו את הבסיס וּודאו שהוא נגמר ב-000 (aligned לעמוד). אם לא - משהו שגוי.

רמז: זכרו לסנכרן את הפלט. יש puts("overflow me:") לפני ה-read, אז p.recvuntil(b'overflow me:\n') לפני השליחה.

תרגיל 4 - זיהוי גרסת ה-libc מהleak

גם אם יש לכם את ה-libc מקומית, תרגלו את הזיהוי - זו המיומנות שתצטרכו מול שרת אמיתי.

  1. קחו את הכתובת שדלפה בתרגיל 3 והסתכלו רק על שלוש הספרות ההקסדצימליות התחתונות שלה. הריצו כמה פעמים וּודאו שהן קבועות בין הרצות למרות ASLR. הסבירו למה.
  2. הדליפו סמל שני (למשל חזרו על התרגיל עם printf במקום puts) כדי לקבל טביעת אצבע חזקה יותר.
  3. הכניסו את שני הסמלים ל-libc.rip (או ./find puts XXX printf YYY ב-libc-database) וּודאו שהוא מזהה את ה-libc שלכם.

רמז: ASLR מערבב ברמת עמוד (0x1000), ולכן שלוש הספרות התחתונות של כל סמל קבועות. הן טביעת האצבע.

תרגיל 5 - שלב התקיפה ו-shell

השלימו את התוקפה.

  1. אחרי חישוב הבסיס, בנו chain שנייה: system("/bin/sh") בכתובות המחושבות. השתמשו ב-libc.symbols['system'] וב-next(libc.search(b'/bin/sh\x00')).
  2. שלחו את הpayload השני בסבב השני (אחרי שחזרתם ל-main, vuln רצה שוב).
  3. אם ה-shell לא נפתח והתוכנית קורסת בתוך libc - זו כמעט בוודאות מלכודת ה-movaps. הוסיפו גאדג'ט ret בודד לפני הקריאה ל-system.
  4. קבלו shell וּודאו שהוא אינטראקטיבי (id, cat).

רמז: rop2 = ROP(libc) אחרי שהצבתם libc.address, ואז rop2.find_gadget(['ret']) לalignment ו-rop2.call(libc.symbols['system'], [binsh]).

תרגיל 6 (בונוס) - הקשחה ושרת מרוחק

  1. אחדו את שני השלבים לסקריפט אחד גמיש עם args.REMOTE. הריצו את הבינארי דרך socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./leak ותקפו את עצמכם עם python3 exploit.py REMOTE.
  2. הוסיפו assert libc.address & 0xfff == 0 אחרי חישוב הבסיס, כדי שהסקריפט ייכשל בקול אם ה-libc לא תואם.
  3. אתגר: החליפו את הקריאה ל-system ב-one_gadget. מצאו כתובת מתאימה עם one_gadget ./libc.so.6, בדקו את התנאים שלה (ערכי אוגרים), והשתמשו בה במקום chain ה-system. (נרחיב על זה ב-4.5, אבל שווה לנסות כבר עכשיו.)

רמז: one_gadget מריץ execve("/bin/sh", 0, 0) בקפיצה אחת, אבל רק אם מתקיים תנאי מסוים על האוגרים ברגע הקפיצה. אם אחת לא עובדת, נסו את הבאה ברשימה.