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:
- המשימה: זהו אילו מהערכים שהודפסו מגיעים מאוגרים ואילו מהמחסנית.
- רמז 1: במערכת 64 ביט, כמה מההמרות הראשונות נשלפות מאוגרים לפני ש-
printfנוגעת במחסנית בכלל? חזרו לטבלה בהרצאה. - רמז 2: ערכי האוגרים לרוב נראים כמו זבל או אפסים, בעוד שערכי המחסנית כוללים כתובות שמתחילות ב-
0x7fff...(מחסנית) או0x7f...(libc). - רמז 3: נסו להחליף
%pב-%xולראות את ההבדל. למה כל%xמראה רק חצי מסלוט?
רשמו לעצמכם: כמה ההמרות הראשונות מגיעות מאוגרים בבנייה שלכם?
תרגיל 2 - מציאת ההיסט של הbuffer¶
זו ליבת התרגול. שלחו סמן ייחודי באורך 8 בתים ואחריו רצף %p, ומצאו באיזה מיקום הסמן צץ:
- המשימה: מצאו את המספר הממוקם 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 בתים?