5.3 כתיבה שרירותית עם n תרגול
תרגול - כתיבה שרירותית עם n¶
בתרגול הזה תהפכו את חולשת ה-format string ממכונת קריאה למכונת כתיבה. המטרה: לדרוס משתנה גלובלי בערך מדויק שאתם בוחרים, קודם בידיים (סופרים בתים, בונים את מחרוזת הformat, מטפלים בסדר עולה ובoverflow), ורק אחר כך עם fmtstr_payload שיעשה את זה בשורה אחת. אל תדלגו על החלק הידני - הוא מה שיבדיל אתכם ממישהו שרק מעתיק סקריפטים. תבינו מה קורה מתחת למכסה, ואז תרשו לכלי לעשות את זה בשבילכם.
סביבה והנחות¶
הכל רץ מקומית על לינוקס עם python3 ו-pwntools, gcc ו-gdb/pwndbg. הבינארי שלנו מקומפל בלי PIE (כתובת גלובלית קבועה), בלי canary, עם NX, ובלי _FORTIFY_SOURCE (אחרת glibc תחסום את %n על מחרוזת format כתיבה). שמרו כ-writeme.c:
#include <stdio.h>
long secret = 0; // your target
int main(void) {
char buf[256];
setvbuf(stdout, NULL, _IONBF, 0);
printf("secret @ %p\n", (void*)&secret);
fgets(buf, sizeof(buf), stdin);
printf(buf);
printf("\nsecret = %#lx\n", secret);
if (secret == 0x1337c0de)
puts("WIN: secret overwritten exactly!");
return 0;
}
קומפילציה:
בדקו את ההגנות עם checksec --file=./writeme וודאו שאתם רואים No PIE, No canary, NX enabled, Partial RELRO.
תרגיל 1 - סיור ומציאת offset¶
לפני שכותבים, צריך לדעת איפה הקלט שלנו יושב על המחסנית.
המטרה:
- שלפו את כתובת
secretבשתי דרכים: פעם עםnm writeme | grep ' secret', ופעם מהשורה שהתוכנית מדפיסה בריצה. ודאו שהן זהות. - מצאו את ה-
offset- האינדקס שבו הקלט שלכם מתחיל בארגומנטים שלprintf. שלחו קלט כמוAAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%pוזהו באיזה%pמופיע0x4141414141414141. - רשמו לעצמכם את המספר הזה. כל שאר התרגיל תלוי בו.
רמז: ב-x86-64 חמשת הארגומנטים הראשונים מגיעים מרגיסטרים, אז הקלט שעל המחסנית מתחיל בדרך כלל באינדקס 6 ומעלה. אל תניחו - תמדדו.
רמז: אם קשה לספור, תנו לכל %p מפריד ברור עם נקודות, ותשוו את מספר הנקודות עד שאתם רואים את רצף ה-41-ים.
תרגיל 2 - כתיבת בית בודד עם %hhn¶
נתחיל בקטן: לכתוב ערך של בית אחד כדי להרגיש את המנגנון.
המטרה:
- בנו קלט שכותב את הערך
0x41(65 תווים) לבית הנמוך ביותר שלsecret, כלומר לכתובת&secret + 0. - השתמשו בצורה
%<רוחב>c%<אינדקס>$hhn. חשבו: כמה תווים צריך להדפיס לפני ה-%hhn? - ודאו בפלט שהשורה
secret = ...מסתיימת ב-41.
רמז: הכתובת &secret צריכה לשבת בארגומנט שאליו האינדקס בתוך %hhn מצביע. שימו אותה בסוף ה-payload (בגלל ה-null בתים), מרופדת לalignment של 8, וחשבו את האינדקס: offset + מספר ה-qwords של חלק הformat.
רמז: %65c מדפיס 65 תווים. אם צירפתם עוד תווי padding לפני זה, זכרו לספור גם אותם במונה.
תרגיל 3 - כתיבה מלאה בידיים עם %hn¶
עכשיו היעד המלא: לכתוב את הערך 0x1337c0de ל-secret כך שהתוכנית תדפיס WIN. עשו את זה בלי fmtstr_payload.
המטרה:
- פרקו את
0x1337c0deלשני נתחים של 16 ביט: הנתח הנמוך (0xc0de) לכתובת&secret+0, והגבוה (0x1337) לכתובת&secret+2. - מיינו את הנתחים בסדר עולה לפי הערך שרוצים לכתוב. מי צריך להיכתב קודם?
- בנו את מחרוזת הformat: הדפיסו את ההפרש בין נתח לנתח, ואחרי כל הפרש בצעו
%<idx>$hnלכתובת המתאימה. - הצמידו את שתי הכתובות בסוף ה-payload, מרופדות לalignment, וכוונו את האינדקסים.
- הריצו וודאו שאתם רואים
WIN: secret overwritten exactly!.
רמז: 0x1337 (4919) קטן מ-0xc0de (49374), אז הנתח הגבוה צריך להיכתב קודם. אחריו מדפיסים עוד 49374 - 4919 תווים כדי להגיע ל-0xc0de.
רמז: אם אתם מתקשים לייצב את האינדקס, רפדו את חלק הformat לאורך קבוע (כפולה של 8) עם תווי a מיותרים, ואז האינדקס של הכתובת הראשונה הוא בדיוק offset + FMT_QWORDS.
רמז: אחרי שזה עובד, נסו במכוון להחליף את סדר הכתיבות (קודם הנמוך ואז הגבוה) ותראו איך זה נשבר. זה יחדד לכם למה הסדר העולה קריטי.
תרגיל 4 - אותו הדבר עם fmtstr_payload¶
עכשיו כשהבנתם את הכאב, תנו לכלי לעשות את העבודה.
המטרה:
- השיגו את אותה כתיבה של
0x1337c0deל-secretעם שורה אחת שלfmtstr_payload(offset, {addr: value}, write_size='short'). - הריצו גם עם
write_size='byte'וגם עם'short'. השוו את אורך ה-payload שנוצר (len(payload)) ואת מספר התווים שמודפסים. איזה מהם "רועש" יותר? - הדפיסו את ה-payload עצמו (
print(payload)) והשוו אותו למה שבניתם ידנית בתרגיל 3. מה דומה ומה שונה?
רמז: אם התוכנית מדפיסה משהו לפני מחרוזת הformat שלכם (כאן היא לא, אבל באתגרים אחרים כן), העבירו את מספר התווים דרך numbwritten=.
רמז: fmtstr_payload מניח שה-payload שלכם מתחיל בדיוק ב-offset. אם שיניתם את מבנה הקלט (הוספתם prefix), עדכנו את ה-offset בהתאם.
תרגיל 5 (אתגר) - כתיבת כתובת שלמה של 8 בתים¶
עד עכשיו כתבנו ערך "קטן". עכשיו נכתוב כתובת מלאה, כמו שנצטרך כשנדרוס מצביע פונקציה בשיעורים הבאים.
המטרה:
- שנו את
writeme.cכך ש-secretיאותחל למצביע כלשהו (למשל הכתובת שלputsשאתם מדפיסים), והמטרה תהיה לדרוס אותו בערך שרירותי בן 8 בתים כמו0xdeadbeefcafe1234. - עם
fmtstr_payloadו-write_size='short', כתבו את הערך המלא. אמתו עםgdbשכל ארבעת הנתחים נכתבו נכון:x/gx &secret. - עכשיו נסו לבנות את זה בידיים: ארבע כתיבות
%hnלכתובות&secret+0/+2/+4/+6, ממוינות בסדר עולה, עם טיפול בoverflow כשהערך הבא קטן מהמונה.
רמז: הערך 0xdeadbeefcafe1234 מתפרק לנתחים 0x1234, 0xcafe, 0xbeef, 0xdead (לכתובות +0, +2, +4, +6). מיינו אותם לפי הערך בסדר עולה, ותקבלו הפרשים חיוביים בלי צורך בoverflow. overflow נדרשת רק אם שני נתחים דורשים אותו ערך (ההפרש יוצא 0), ואז מדפיסים מחזור שלם של 0x10000.
רמז: כשהמונה כבר גדול והנתח הבא קטן, ההפרש (chunk - printed) % 0x10000 נותן לכם בדיוק כמה עוד להדפיס. אם יצא 0, הדפיסו 0x10000 שלם (מחזור).
רמז: אמתו כל נתח בנפרד ב-gdb. אם בית אחד שגוי, סימן שהמונה או האינדקס של אותו נתח לא מדויק - בדקו את הalignment של בלוק הכתובות.