5.2 קריאה שרירותית מהזיכרון תרגול
תרגול - קריאה שרירותית מהזיכרון¶
בתרגול הזה תבנו בעצמכם בינארי vulnerable ל-format string, ותשתמשו בקריאה שרירותית כדי לדלוף שני דברים: את תוכנו של גלובל מסוים, ואת כתובת libc האמיתית - הכל דרך קריאות %s ו-%p בלבד, בלי overflow של buffer ובלי כתיבה. זו המיומנות שמאפשרת לשבור ASLR, והיא הבסיס לשיעורי הכתיבה שאחריו. עבדו לפי הסדר, כל תרגיל בונה על הקודם.
הכנה - הבינארי¶
צרו את קובץ המקור הבא:
// leakme.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
char flag_store[64] = "FLAG{replace_me_at_runtime}"; // the global we want to leak
void vuln() {
char buf[128];
memset(buf, 0, sizeof(buf));
read(0, buf, 127);
printf(buf); // format string vulnerability
putchar('\n');
}
int main() {
setvbuf(stdout, NULL, _IONBF, 0);
puts("send format:");
vuln();
return 0;
}
קמפלו אותו בלי PIE ובלי canary, כדי שהכתובות של הגלובל ושל ה-GOT יהיו קבועות:
ודאו את מצב ההגנות והשאירו את ASLR של המערכת פעיל (זה מה שהופך את דליפת libc למשמעותית):
הנחות: אין PIE (כתובות הבינארי קבועות), אין canary, NX פעיל, ASLR של המערכת פעיל (libc זזה בכל הרצה).
תרגיל 1 - מציאת האינדקס של הbuffer¶
לפני כל דליפה, צריך לדעת באיזה אינדקס פוזיציוני יושב buf[0].
- שלחו סמן ידוע של 8 בתים ואחריו כמה
%pפוזיציוניים, למשלAAAAAAAA.%6$p.%7$p.%8$p.%9$p.%10$p. - מצאו באיזה
%N$pמופיע הערך0x4141414141414141. - רשמו את המספר הזה בתור
BUF_INDEX. הסיקו מהו האינדקס שלbuf[8..15].
רמז: הכי נוח לשלוח את הקלט עם python3 -c '...' | ./leakme, או ישר מ-pwntools עם process ו-sendline. שימו לב שהתוכנית מדפיסה send format: לפני שהיא קוראת, אז סנכרנו עם recvuntil(b'send format:').
תרגיל 2 - דליפת תוכן הגלובל¶
עכשיו דלפו את תוכן flag_store דרך קריאה שרירותית בלבד.
- מצאו את הכתובת של
flag_store(objdump -t leakme | grep flag_storeאוnm). - בנו payload: ספציפייר
%sשמכוון לסלוט שאחריbuf[0], padding לalignment, ואזp64של הכתובת. - שימו לב לalignment - אורך הקידומת חייב להיות כפולה של 8. נסו בכוונה בלי padding וראו שזה קורס, ואז תקנו.
- חתכו את הפלט וּודאו שאתם רואים את תוכן הגלובל.
רמז: אם BUF_INDEX יצא 6, שימו את הכתובת בסלוט 7 (כלומר %7$s) ורפדו את הקידומת ל-8 בתים בדיוק. הוסיפו בית סמן כמו # מיד אחרי הספציפייר כדי שיהיה קל לחתוך את הleak עם recvuntil(b'#').
תרגיל 3 - דליפת כתובת libc¶
כאן הלב. דלפו את הכתובת האמיתית של פונקציית libc מתוך ה-GOT, וחשבו את בסיס libc.
- בחרו פונקציה שכבר נקראה לפני ה-
printf(למשלreadאוprintfעצמה) - רק אז הרשומה ב-GOT מצביעה לכתובת האמיתית ב-libc ולא ל-PLT stub. - השתמשו באותה פונקציית
read_atמהתרגיל הקודם, הפעם עלelf.got['read']. - פרשו את הבתים שדלפו ככתובת:
u64(leak.ljust(8, b'\x00')). למה משלימים ב-null? כי%sעוצר בבית האפס והכתובת ב-64 ביט היא בפועל 6 בתים משמעותיים. - חשבו
libc.address = leaked - libc.symbols['read'], והדפיסו אתlibc.symbols['system']ואת הכתובת של/bin/sh.
רמז: השתמשו באותו קובץ libc שרץ אצלכם: libc = ELF('/lib/x86_64-linux-gnu/libc.so.6'). אם אתם לא בטוחים איזו גרסה, strings libc.so.6 | grep 'GNU C Library'. הריצו כמה פעמים וּודאו שבסיס libc משתנה בין הרצות (בגלל ASLR) אבל תמיד יוצא כתובת שמסתיימת ב-000 (aligned לעמוד).
תרגיל 4 - סקריפט דליפה שלם¶
אחדו את הכל לסקריפט אחד נקי.
- כתבו פונקציית
read_at(io, addr)כללית שמקבלת כתובת ומחזירה את התוכן. - הדליפו ברצף גם את
flag_storeוגם את בסיס libc, והדפיסו את שניהם יפה עםlog.success. - הוסיפו תמיכה ב-
args.REMOTEכדי שאותו סקריפט ירוץ מקומית ומול שרת. הריצו את הבינארי כשירות עםsocat TCP-LISTEN:1337,reuseaddr,fork EXEC:./leakmeותקפו את עצמכם.
רמז: שימו לב ל"היגיינת קלט" - כל leak שולחת payload ומקבלת שורת פלט. ודאו שאתם מסנכרנים נכון בין השליחות (עם recvuntil על סמן קבוע) כדי שהleak הראשונה לא תבלע חלק מהפלט של השנייה.
תרגיל 5 (בונוס) - הcanary ו-PIE¶
הרחיבו את היכולת לשני מקרים מציאותיים יותר.
- קמפלו גרסה עם canary:
gcc -fstack-protector-all -no-pie -o leakme_canary leakme.c. מצאו את האינדקס הפוזיציוני של הcanary (ערך אקראי שמסתיים ב-00) ודלפו אותו עם%N$p. למה כאן משתמשים ב-%pולא ב-%s? - קמפלו גרסת PIE:
gcc -fstack-protector-all -pie -o leakme_pie leakme.c. הפעם כתובת ה-GOT לא קבועה. דלפו קודם מצביע קוד מהמחסנית (כתובת שמתחילה ב-0x55/0x56), חשבו אתelf.address, ורק אז נסו לדלוף libc דרךelf.got['read'].
רמז: הcanary לא יושב בכתובת שאפשר לכוון עליה %s, אבל הוא כן יושב על המחסנית עצמה, אז קוראים אותו כערך של סלוט עם %p. ב-PIE: את ההיסט הקבוע של מצביע הקוד תמצאו פעם אחת עם ASLR כבוי (echo 0 | sudo tee /proc/sys/kernel/randomize_va_space), וההיסט הזה נשאר קבוע גם כשמדליקים ASLR בחזרה.