Stack Alignment - כיצד זה משפיע על Binary Exploitation?

עוד כשהתחלתי בנושא Binary Exploitation, פתרתי אתגרים סבירים, שכל מטרתם היא דריסת ה return value בכתובת של פונקציה הנותנת shell. כאשר פתרתי אתגר בשם GetIt (בינארי לינוקס x64 מ CTF בשם CSAW CTF Qualification Round 2019), האקספלויט שלי קיבל segmentation fault שוב ושוב. לא ידעתי מהי הסיבה כי האקספלויט היה נראה בסדר, אז זרקתי אותו ל GDB. כאשר דיבאגתי, הבינארי כן קפץ לפונקציה שנותנת shell, אך שמתי לב שה segfault נזרק כאשר הפונקציה do_system (פונקציה ש system קוראת לה) מבצעת את הפקודה movaps xmmword ptr [rsp + 0x50], xmm0.

הפקודה movaps

הפקודה עומדת ל " Move Aligned Packed Single-Precision Floating-Point Values". היא משומשת להעברת double quadword המכילים ארבעה מספרים עשרוניים, כאשר שני האופרנדים יכולים להיות אוגרי XMM, או אוגר XMM וזיכרון.
כפי שראיתם, בתיאור הפקודה רשום “Move Aligned”, אך מה זה אומר? הפקודה מחייבת שכתובת הזיכרון, במידה וסופקה כאחד מהאופרנדים, תיהיה 16 byte aligned, כלומר, שהכתובת תתחלק ב 16. אם היא לא, המעבד יזרוק segfault, כפי שראינו באתגר.
קיימות עוד מגוון פקודות הדורשות שהזיכרון יהיה aligned.

למה זה קורה בכלל?

כפי שראינו, הפקודה movaps דורשת שכתובת הזיכרון תיהיה 16B aligned. אנחנו בוודאות יודעים עכשיו שהאתגר נתן segfault בגלל שזה לא היה aligned, אבל למה?
לינוקס משתמשת ב calling convention הנקרא System V Application Binary Interface. ל calling convention הזה קיימת גרסא עבור 32 bit ו 64 bit. מכייון שהבינארי הוא x64, נדבר על ה 64 bit.

בעת קריאה לפונקציה, על המחסנית להיות 16B aligned ברגע שאופקוד ה call לפונקציה עומד להתבצע. אחרי שה call דחף למחסנית את הכתובת של הפקודה הבאה, המחסנית לא תיהיה aligned, ועל הפונקציה לדאוג גם כן שהמחסנית תיהיה aligned בעת שהיא קוראת לפונקציות אחרות. הפונקציה בכל מקרה יוצרת stack frame, וחלק מהיצור שלו, הוא דחיפת האוגר RBP למחסנית, מה שגורם לה להיות שוב aligned.
בואו נדמיין סיטואציה שבה בפונקציה יש משתנה בגודל בית אחד, למשל, char. האם הפונקציה תאלקץ בשביל משתנה זה מקום על ידי חיסור RSP ב 8? התשובה היא לא, הפונקציה תחסרת מ RSP שישה עשר בתים, כדי שהמחסנית תיהיה aligned. זה תלוי בקומפיילר כמובן, מכיוון שקומפיילרים אחרים יכולים באמת לחסר מ RSP שמונה בתים בשביל המשתנה, אך לחסר שוב 8 בתים מ RSP לפני קריאות לפונקציות אחרות.

אוקיי, אבל למה המחסנית היא לא aligned ברגע שקופצים לפונקציה שנותנת shell? מכיוון שאנחנו בעצם לא עושים call לפונקציה הזאת, אלא קופצים אליה על ידי ה ret שבה דרסנו את המחסנית! אנחנו לא דואגים לעשות align למחסנית בשום דרך, והפונקציה מניחה שהמחסנית כן aligned, כאשר היא לא.

איך נסדר את זה?
מכיוון שב 64 bit אנחנו תמיד או מוסיפים או מחסירים 8 מ RSP, ה nibble הנמוך ביותר של RSP תמיד יהיה או 0 (המחסנית aligned - מתחלקת ב 0x10) או 8 (המחסנית לא aligned - לא מתחלקת ב 0x10), נוכל פשוט להוסיף או להחסיר מ RSP שמונה בתים.
אבל איך, אתם בטח שואלים? נוכל לבנות ROP Chain קטן, שבתחילתו נקפוץ ל ret של פונקציה מסויימת, ולאחר מכן, נקפוץ לפונקציה שנותנת shell. מה עשינו כאן בעצם? ברגע שקפצנו שוב ל ret, ה nibble הנמוך ביותר של RSP נהפך ל 0 מ 8. כעת המחסנית aligned, ואנחנו קופצים לפונקציה!

רגע, אבל איך המחסנית aligned ברגע שאנחנו מגיעים ל main?
הפונקציה main היא אינה הפונקציה הראשונה שמתבצעת. בקבצי ELF, קיים מידע רב לגבי אותו הבינארי, ורשומה מהמידע הזה שומרת את הכתובת של הפונקציה הראשונה שאמורה לרוץ - והיא ה entry point (סתאם משהו מגניב - זה ישמור את ה offset של הפונקציה במידה ו PIE דלוק, ואת הכתובת שלה ומידה ולא).
נוכל לבדוק את הכתובת של ה entry point בעזרת readelf:


נבדוק אותה ב IDA:
Annotation 2020-06-28 174047
אם תשימו לב, השורה שעושה 16B align למחסנית היא and rsp, 0FFFFFFFFFFFFFFF0h. לאחר מכן, היא קוראת לפונקציה libc_start_main, ומשם היא קוראת ל main. כך המחסנית היא aligned עוד מההתחלה.

תודה
תודה רבה על הקריאה, מקווה שנהנתם! זהו המאמר הראשון שלי, אשמח מאוד לדעת אם טעיתי איפהשהו :smile:

15 לייקים

תיקון קטן, אבל זה משהו שיכול לבלבל מתחילים - כתבת שהבית הנמוך ביותר של rsp תמיד יהיה 0 או 8. זה נכון לגבי הnibble (חצי בית) הכי פחות משמעותי, כלומר אות hex אחת, לא לגבי הבית כולו. אחלה מאמר חוץ מזה (:

7 לייקים

צודק לגמרי, התבלבלתי בין nibble לבית. תודה על התיקון!

לייק 1