8.4 יישור זיכרון ו SSE תרגול
תרגול - יישור זיכרון ו-SSE¶
בתרגול הזה נפתור את אתגר memcpy של pwnable.kr. האתגר כולו סובב סביב דבר אחד: memcpy מהיר שמבוסס על הוראות SSE שדורשות alignment ל-16 בתים, ותוכנית שמאכילה אותו בbuffer מ-malloc שאיננו מיושר. המטרה שלנו היא לבחור את הקלטים כך שאף movdqa/movntdq לא ייגע בכתובת לא מיושרת, ואז התוכנית תשרוד עד הסוף ותדפיס את הדגל.
התחברות לאתגר¶
התחברו לשרת האתגר לפי ההוראות בעמוד האתגר באתר pwnable.kr (הם נותנים את שורת ה-ssh או ה-nc המדויקת, וה-flavor הוא בינארי 32 ביט). בתיקיית האתגר נמצא הבינארי memcpy, קוד המקור memcpy.c, וקובץ flag שרק המשתמש המיוחס יכול לקרוא. הריצו קודם cat memcpy.c וקראו אותו מההתחלה עד הסוף - כל מה שצריך כדי לפתור נמצא שם.
כדי לפתח בנוחות, שחזרו את האתגר מקומית. צרו memcpy_demo.c שמכיל את slow_memcpy, את fast_memcpy עם לולאת ה-SSE (כמו בהרצאה), ולולאת main שרצה על order מ-2 עד 21, מקצה dest = malloc(2^order) ו-src = mmap(...), קוראת גודל עם scanf, וקוראת ל-fast_memcpy. קמפלו ב-32 ביט:
אם אין לכם ספריות 32 ביט, התקינו אותן (למשל sudo apt install gcc-multilib libc6-dev-i386).
תרגיל 1 - לשחזר את הקריסה התמימה¶
לפני שפותרים, כדאי לראות את הבעיה במו עינינו. הריצו את הבינארי (המקומי או המרוחק) והזינו בכל סיבוב את alloc_size עצמו כגודל ההעתקה - כלומר 4, ואז 8, ואז 16, וכן הלאה.
המטרה: לזהות באיזה סיבוב בדיוק זה קורס, ולמה דווקא שם.
- רמז: הלולאה המהירה רצה רק כאשר הגודל הוא
64ומעלה. באיזהorderהגודל מגיע ל-64? - רמז: הפעילו את הבינארי תחת gdb / pwndbg. כשזה קורס, הסתכלו על ההוראה שגרמה לחריגה. איזו הוראה זו, ומה הכתובת שהיא ניגשה אליה?
תרגיל 2 - למדוד את הalignment של dest¶
עכשיו נבין למה הכתובת בעייתית. הוסיפו ל-memcpy_demo.c שורת הדפסה זמנית אחרי כל malloc, שמדפיסה את הכתובת ואת השארית שלה מודולו 16:
הריצו וצפו בעמודה האחרונה לאורך כל הסיבובים.
המטרה: להסיק מהו הalignment ש-malloc נותנת במערכת 32 ביט, והאם movdqa על הbuffer הזה יכול אי פעם לעבוד.
- רמז: מה הערך של
MALLOC_ALIGNMENTבמערכת 32 ביט? כמה בתים מפרידים בין בסיס הנתח למצביע שאתם מקבלים? - רמז: בדקו גם את הalignment של
srcשמגיע מ-mmap. האם הוא אי פעם לא מיושר? למה?
תרגיל 3 - למצוא גדלים בטוחים¶
השאלה המרכזית: איזה גודל להזין בכל סיבוב כדי ש-fast_memcpy לא ייכנס בכלל ללולאת ה-SSE, וירד ישר להעתקה הבטוחה בית-בית.
המטרה: לגבש נוסחה לגודל שתזינו בכל order, כך שכל 20 הסיבובים יעברו בלי קריסה.
- רמז: מה התנאי המדויק שמפעיל את לולאת ה-SSE? מה קורה אם הגודל נמצא מתחת לסף הזה?
- רמז: שימו לב שאין בקוד שום קריאה ל-
free, ואין בדיקה שהגודל תואם ל-alloc_size. מה זה אומר לגבי הזנת גודל קטן מהbuffer? ומה לגבי בטיחות של overflow קלה כשאין שחרור? - רמז: הגדירו את הגודל כפונקציה של
order- משהו כמו המינימום בין2^orderלבין ערך קבוע קטן. איזה ערך קבוע מבטיח שלעולם לא נחצה את הסף?
תרגיל 4 - כתיבת פותרן אוטומטי¶
עשרים סיבובים זה הרבה להקליד ביד, וקל לטעות. כתבו סקריפט ב-pwntools שמתחבר, קורא את ההנחיה Order N! : בכל סיבוב, שולח את הגודל שחישבתם בתרגיל 3, ובסוף קורא ומדפיס את הדגל.
המטרה: פותרן שרץ מקצה לקצה ומדפיס את תוכן הדגל.
- רמז: השתמשו ב-
process(['./memcpy_demo'])בזמן הפיתוח, ובחיבור המרוחק לפי עמוד האתגר בזמן ההרצה האמיתית. - רמז: הבינו כל סיבוב עם
io.recvuntil(b': ')ואזio.sendline(str(size).encode()). - רמז: הדגל מודפס אחרי השורה
Well done. השתמשו ב-io.recvall()בסוף כדי לתפוס אותו.
תרגיל 5 (העשרה) - הדרך ה"נכונה" להריץ movdqa¶
בתרגילים הקודמים דילגנו על ה-SSE. בתרגיל הזה נעשה את ההפך: נגרום ל-fast_memcpy להצליח עם הלולאה המהירה, בלי לקרוס.
המטרה: לשנות את הדמו המקומי כך שההעתקה של 64 בתים ומעלה תעבוד תקין, ולהסביר מה השתנה.
- רמז: החליפו את
mallocב-aligned_alloc(16, ...)או ב-posix_memalign. מה קורה עכשיו לalignment שלdest? - רמז: לחלופין, השאירו את
mallocוהחליפו במימוש אתmovdqa/movntdqבגרסאות הלא-מיושרותmovdqu. למה זה גם פותר את הקריסה, ומה המחיר?
בהצלחה. בפתרון תמצאו הליכה מלאה על ההיגיון, את הפקודות ב-gdb, ואת הפותרן השלם ב-pwntools.