לדלג לתוכן

6.4 ret2dlresolve תרגול

תרגול - ret2dlresolve על בינארי ללא leak

בתרגול הזה תנצלו בינארי שבמכוון לא נותן לכם שום leak: אין puts להדפיס איתו את ה-GOT, אין format string, ואין אף כתובת של libc בהישג יד. בכל זאת - תפתחו shell. הכלי היחיד שיעבוד כאן הוא ret2dlresolve: תגרמו לפותר הדינמי למצוא בשבילכם את system ולקרוא לה עם "/bin/sh".

הכנת הסביבה

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

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

void vuln() {
    char buf[64];
    read(0, buf, 0x200);      // generous overflow - no canary
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    write(1, "go\n", 3);
    vuln();
    return 0;
}
gcc -fno-stack-protector -no-pie -z lazy -o vuln vuln.c

לפני שנוגעים בכלום - בדקו מה מותקן:

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

יש כאן Partial RELRO, בלי canary, NX פעיל, בלי PIE. זה בדיוק המצב שבו ret2dlresolve עובד. שימו לב: בקוד אין system ואין "/bin/sh". אל תחפשו אותם בבינארי - הם לא שם, וזו כל הפואנטה.

תרגיל 1 - מדידת ה-offset

מצאו כמה בתים מפרידים בין תחילת buf לבין return address. השתמשו ב-cyclic כדי לייצר קלט, הריצו תחת gdb, וחלצו את ה-offset מה-rsp בזמן הקריסה.

רמז: cyclic(200) פנימה, ובזמן הקריסה cyclic_find(...) על הערך שב-rsp (או ב-pattern שראה pwndbg). הצפוי הוא 72.

תרגיל 2 - איתור הרכיבים בבינארי

אספו את כל מה שהטכניקה דורשת ישירות מהבינארי:

  1. את בסיסי הטבלאות הדינמיות. איזו פקודה מראה את JMPREL, SYMTAB ו-STRTAB?
  2. את הכתובת של PLT0 (הרשומה הראשונה בסקשן .plt).
  3. גאדג'ט pop rdi ; ret להכנת הארגומנט.
  4. אזור כתיב שבו תניחו את המבנים המזויפים (למשל תוך .bss).

רמז: readelf -d ./vuln לטבלאות, objdump -d -j .plt ./vuln ל-PLT0, ו-ROPgadget --binary ./vuln | grep 'pop rdi' לגאדג'ט. את גבולות ה-.bss תראו ב-readelf -S ./vuln.

תרגיל 3 - בניית המבנים המזויפים

זה הלב. אתם צריכים להניח באזור הכתיב שלושה דברים: מחרוזת "system\0", מבנה Elf64_Sym שמצביע אליה, ומבנה Elf64_Rela שמצביע לסמל. אחר כך תחשבו את reloc_arg כך שהוא יצביע מהטבלה המקורית אל ה-Elf64_Rela שלכם.

  • מהם 24 הבתים של Elf64_Rela? מה צריך להיות ב-r_info כדי שהסוג יהיה R_X86_64_JUMP_SLOT (7) והאינדקס יצביע לסמל שלכם?
  • מהם 24 הבתים של Elf64_Sym? איזה שדה מצביע למחרוזת, ומה האופסט שלו מול STRTAB?
  • למה reloc_arg ו-sym_index חייבים לצאת מספרים שלמים אחרי חלוקה ב-24, ואיך מוסיפים padding שיבטיח את זה?

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

תרגיל 4 - הפעלה ופתיחת shell

חברו הכל לchain אחת. בשלב הראשון קראו את המבנים אל האזור הכתיב (איזו פונקציה שכבר מיובאת בבינארי מאפשרת לכם לכתוב קלט לכתובת שתבחרו?). בשלב השני, חזרו ל-PLT0 עם ה-reloc_arg על המחסנית וכש-rdi מצביע על "/bin/sh".

  • באיזה סדר חייבים לקרות שני השלבים, ולמה?
  • איך מוודאים ש-rdx מספיק גדול כדי שה-read יקלוט את כל המבנים?
  • אחרי p.interactive(), האם קיבלתם shell? נסו id ו-ls.

רמז: המבנה של הchain ב-pwntools הוא בדיוק rop.read(0, dlresolve.data_addr, len(dlresolve.payload)) ואז rop.ret2dlresolve(dlresolve). את הבתים המזויפים (dlresolve.payload) שולחים בקריאת ה-read השנייה, אחרי payload הoverflow.

אתגר נוסף - בנייה ידנית לגמרי

אחרי שהצלחתם עם Ret2dlresolvePayload, נסו לבנות את שלושת המבנים ביד, בלי המחלקה - רק עם p64/p32/p8 והחישובים מההרצאה. תיתקלו כנראה בבדיקת הגרסאות של glibc המודרני. חקרו: מה הערך של sym_index שגורם ל-.gnu.version להחזיר אפס, ולמה זה מציל אתכם. זה יעמיק לכם את ההבנה יותר מכל דבר אחר.

שאלות לבדיקה עצמית

  • למה הטכניקה הזו נכשלת לחלוטין תחת Full RELRO? מה בדיוק חסר שם?
  • למה אנחנו יכולים לקרוא ל-system למרות שהיא לא מיובאת בבינארי שלנו בכלל?
  • אם הבינארי היה מקומפל עם PIE, מה היה חסר לנו, ומה היינו צריכים להשיג קודם?