לדלג לתוכן

4.2 ret2libc קלאסי תרגול

תרגול - ret2libc קלאסי

בתרגול הזה תבנו בעצמכם בינארי 32 ביט vulnerable שאין בו שום פונקציית win, ותשיגו ממנו shell בטכניקת ret2libc - חזרה לתוך system שבתוך libc. זה הבסיס לחצי מאתגרי ה-pwn הקלאסיים, וזה מה שיאפשר לכם בהמשך הפרק לעקוף ASLR אמיתי (4.4). עבדו לפי הסדר: כל תרגיל בונה על הקודם, ומ-recon ועד shell.

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

צרו את קובץ המקור הבא:

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

void vuln() {
    char buf[80];
    puts("give me your bytes:");
    read(0, buf, 400);   // overflow: 400 bytes into an 80-byte buffer
}

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

קמפלו ל-32 ביט, בלי canary ובלי PIE (ה-NX נשאר פעיל - זו כל הנקודה). אם חסרות ספריות 32 ביט: sudo apt install gcc-multilib.

gcc -m32 -fno-stack-protector -no-pie -o ret2libc ret2libc.c

כבו ASLR לזמן התרגול, כדי שהכתובות יהיו יציבות ותוכלו להתרכז במכניקה:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

זכרו להחזיר אותו ל-2 כשתסיימו (או פשוט לרבט). שימו לב שאין כאן פונקציה שקופצים אליה בתוך הבינארי - כל הפתרון חייב להגיע מ-libc.

תרגיל 1 - סיור ומדידה

לפני שתוקפים, מיפוי.

  1. הריצו checksec --file=./ret2libc וּודאו שאתם רואים NX enabled, No canary, No PIE. הבינו למה כל אחת מההגנות (או היעדרה) חשובה למתקפה שלנו.
  2. הריצו ldd ./ret2libc. איזה קובץ libc נטען, ומה הנתיב שלו? רשמו אותו - נצטרך אותו.
  3. מצאו את ה-offset מתחילת הbuffer עד הreturn address, בשיטת ה-cyclic מ-2.2. שלחו cyclic 200, תפסו את הקריסה, והשתמשו ב-cyclic_find על הערך שדרס את מצביע ההוראות.

רמז: ב-32 ביט saved ebp הוא 4 בתים, אבל המהדר עלול להוסיף padding alignment מעבר לגודל הbuffer. אל תניחו ש-offset = 80 + 4. תמדדו את הערך האמיתי.

תרגיל 2 - מציאת הכתובות ב-libc

עכשיו נאסוף את שלוש הכתובות שנצטרך.

  1. פתחו את הבינארי ב-gdb, עצרו ב-main והריצו (חשוב לעצור אחרי ש-libc נטענה). מצאו את הכתובת של system עם p system ואת זו של exit עם p exit.
  2. מצאו את הכתובת של המחרוזת "/bin/sh" בתוך libc. עם pwndbg: search -t string "/bin/sh". בלי pwndbg: find &system, +9999999, "/bin/sh".
  3. הריצו את הבינארי פעמיים ובדקו שהכתובות זהות בין ההרצות. למה הן יציבות עכשיו, ומה היה קורה אילו ASLR היה פעיל?

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

תרגיל 3 - exploit ידני ל-shell

הרכיבו את הpayload עם הכתובות שמצאתם ידנית.

  1. סדרו את שלושת הערכים לפי המבנה: [system] [return address מזויפת] [מצביע ל-"/bin/sh"], אחרי padding באורך ה-offset.
  2. בסלוט החזרה המזויף שימו את הכתובת של exit (כדי שהיציאה מה-shell תהיה נקייה).
  3. הריצו וּודאו שקיבלתם shell. הריצו id ו-ls כדי לאשר שהוא אינטראקטיבי.

רמז: השתמשו ב-flat({offset: [system, exit_a, binsh]}). מכיוון שהקלט נקרא עם read, בתים אפסיים בכתובות ה-libc לא ייחתכו - אין צורך בטריקים.

תרגיל 4 - אותו exploit בלי כתובות קשיחות

עכשיו נהפוך את הסקריפט לנקי ומקצועי, בלי אף כתובת שכתובה ביד.

  1. טענו את ה-libc דרך pwntools: libc = elf.libc.
  2. כשה-ASLR כבוי, שלפו את בסיס ה-libc של התהליך והציבו ב-libc.address. רמז: p.libs() מחזיר מילון של נתיב-לבסיס.
  3. חשבו את הכתובות מתוך האובייקט: libc.symbols['system'], libc.symbols['exit'], ו-next(libc.search(b'/bin/sh\x00')). הרכיבו את אותו payload כמו בתרגיל 3.
  4. הדפיסו את שלוש הכתובות והשוו לאלה שמצאתם ידנית ב-gdb. הן צריכות להיות זהות.

רמז: ברגע שתציבו libc.address, גם libc.symbols וגם libc.search יחזירו כתובות ריצה אמיתיות ולא היסטים. זה בדיוק הסקריפט שתרחיבו ב-4.4, רק ששם את הבסיס תחשבו מדליפה במקום מ-p.libs().

תרגיל 5 - מה קורה כשמשמיטים דברים

הפעם נשבור בכוונה כדי להבין.

  1. הסירו את סלוט החזרה המזויף (שלחו [system] [binsh] בלבד אחרי הpadding). הריצו תחת gdb, צפו בקריסה, והסבירו במדויק למה system נכשלה.
  2. החזירו את הסלוט, אבל שנו את ה-offset ב-4 בתים (למעלה ולמטה). מה קורה לכל אחד מהמקרים, ולמה?
  3. הפעילו מחדש את ASLR (echo 2), הריצו את ה-exploit של תרגיל 3 (עם הכתובות הקשיחות) כמה פעמים ברצף. מה אחוז ההצלחה, ולמה?

רמז: בסעיף 3 אתם אמורים לראות כישלונות כמעט תמיד. זו בדיוק ההמחשה לכך ש-ret2libc עם כתובות קבועות תלוי ב-ASLR כבוי - הגשר לשיעור הleak.

תרגיל 6 (בונוס) - מחרוזת /bin/sh משלכם והכללה לשרת

  1. במקום המחרוזת שבתוך libc, הכניסו "/bin/sh" דרך משתנה סביבה: process('./ret2libc', env={'BINSH':'/bin/sh'}). מצאו את הכתובת שלה על המחסנית עם search -t string "/bin/sh" ב-gdb, והשתמשו בה בpayload. מתי הטריק הזה שימושי, ומתי הוא שביר?
  2. הפכו את ה-exploit של תרגיל 4 לגמיש עם args.REMOTE. הריצו את הבינארי כשירות עם socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./ret2libc ותקפו את עצמכם דרך remote('127.0.0.1', 1337).

רמז: כתובת משתנה הסביבה על המחסנית זזה אם משתנה אורך שם התוכנית או מספר משתני הסביבה, ולכן היא שברירית יותר מהמחרוזת שבתוך libc. מול שרת אמיתי עם ASLR פעיל היא כבר לא דטרמיניסטית - ואז חוזרים למחרוזת שב-libc, שכתובתה נגזרת מדליפה.