לדלג לתוכן

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 ביט:

gcc -m32 -O0 -o memcpy_demo memcpy_demo.c -lm

אם אין לכם ספריות 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:

printf("order=%d dest=%p dest%%16=%lu\n",
       order, (void*)dest, (unsigned long)dest & 0xf);

הריצו וצפו בעמודה האחרונה לאורך כל הסיבובים.

המטרה: להסיק מהו ה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.