לדלג לתוכן

8.3 בינארי זעיר תרגול

תרגול - בינארי זעיר (tiny_easy)

בתרגול הזה תפצחו את tiny_easy מ-pwnable.kr - ELF זערורי ומופשט מסמלים שכל מה שהוא עושה זה לקרוס. הדרך לפצח אותו עוברת דרך הבנה מדויקת של מצב התהליך ברגע הכניסה: מה יש על המחסנית, מה ב-eax, ולאן בדיוק הקוד קופץ. כדי לתרגל בנוחות נבנה קודם בינארי מקומי שמחקה את tiny_easy בול, נפצח אותו, ורק אז נכוון לשרת האמיתי. עבדו לפי הסדר - כל תרגיל בונה על הקודם.

האתגר נמצא בקטגוריה Toddler's Bottle. מתחברים אליו כך:

ssh tiny_easy@pwnable.kr -p2222
# the password appears on the challenge page on the site

בתיקיית הבית תמצאו בינארי בשם tiny_easy (עם ביט setuid) וקובץ flag שרק המשתמש המיוחס יכול לקרוא. שימו לב: אין כאן קובץ מקור .c. הבינארי כל כך קטן שאתם אמורים לפרק אותו ולהבין אותו בעצמכם. עבדו בתיקייה זמנית תחת /tmp כמו שלמדנו ב-0.5.

הכנה - בינארי מקומי שמחקה את tiny_easy

לפני שנוגעים בשרת, נבנה עותק מקומי להתאמן עליו. צרו קובץ אסמבלי tiny.asm שמחקה את נקודת הכניסה של tiny_easy:

; tiny.asm - mimics the entry point of tiny_easy
BITS 32
global _start
_start:
    pop eax        ; eax = argc
    pop eax        ; eax = argv[0]  (pointer to the program name string)
    mov eax, [eax] ; eax = *(argv[0]) = the first four bytes of the program name
    jmp eax        ; jump to the address held in those four bytes

קמפלו וקשרו אותו כבינארי לא-PIE עם מחסנית ניתנת להרצה (אם חסרות ספריות 32 ביט: sudo apt install gcc-multilib nasm):

nasm -f elf32 tiny.asm -o tiny.o
ld -m elf_i386 -z execstack -o tiny tiny.o
strip tiny

כבו ASLR לצורך פיתוח ראשוני:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

תרגיל 1 - סקירה של הבינארי המינימלי

לפני שתוקפים, מבינים מול מה עומדים.

  1. הריצו file tiny, wc -c tiny, ו-checksec --file=tiny. שימו לב לגודל הזערורי, ל-NX disabled, ול-No PIE.
  2. הריצו readelf -h tiny | grep Entry ורשמו את נקודת הכניסה. הריצו readelf -l tiny וראו כמה מקטעי LOAD יש ובאילו הרשאות.
  3. חשבו: מדוע ה-NX מכובה למרות שלא ביקשתם זאת במפורש? מה בקובץ (או מה חסר בו) גורם לכך?

רמז: חפשו אם קיים מקטע GNU_STACK בפלט של readelf -l. אם הוא חסר, הkernel מפעיל READ_IMPLIES_EXEC וכל דף קריא הופך גם לניתן להרצה.

רמז: נקודת הכניסה נמצאת בדיוק אחרי כותרת ה-ELF (52 בתים) וכותרת התוכנית (32 בתים). נסו לחשב את הכתובת ולראות שהיא מתאימה לפלט של readelf.

תרגיל 2 - פירוק הקוד ומעקב אחרי eax

עכשיו נבין מה הבינארי עושה בפועל.

  1. הריצו objdump -d tiny וכתבו לעצמכם את ההוראות בנקודת הכניסה.
  2. עקבו אחרי eax הוראה-הוראה, כשמפת המחסנית בכניסה (argc, argv[], envp[]) לפניכם. מה eax מחזיק אחרי כל אחד משני ה-pop? מה אחרי ה-mov?
  3. ענו במדויק: לאיזו כתובת קופצת ההוראה האחרונה, ומאיפה מגיע הערך של הכתובת הזו?
  4. הריצו ./tiny רגיל וראו את הקריסה. השתמשו ב-dmesg | tail או ב-gdb כדי לראות באיזו כתובת ה-eip קרס. האם הכתובת מתאימה לבתים הראשונים של המחרוזת ./tiny?

רמז: אחרי שני ה-pop, eax מחזיק את המצביע argv[0]. ההוראה mov eax,[eax] קוראת את ארבעת הבתים הראשונים של המחרוזת ש-argv[0] מצביע אליה - כלומר של שם התוכנית.

רמז: ./ti בקידוד little-endian הוא 0x69742e2e. אם הקריסה ב-eip היא בכתובת הזו, הבנתם נכון: יעד הקפיצה נשלט לגמרי מארבעת הבתים הראשונים של שם התוכנית.

תרגיל 3 - מצב התהליך בכניסה ב-gdb

נאשר את מפת המחסנית מההרצאה בעיניים שלנו.

  1. פתחו את הבינארי ב-gdb ועצרו בכניסה עם starti (עוצר על ההוראה הראשונה של _start).
  2. הסתכלו על ראש המחסנית: x/8x $esp. הערך הראשון הוא argc. אחריו מצביעי argv, אחריו NULL, אחריו מצביעי envp.
  3. עקבו אחרי מצביע argv[0] (x/s על הערך שב-$esp+4) וראו שהוא מצביע על מחרוזת שם התוכנית.
  4. הכניסו משתנה סביבה מזהה (למשל AAAA=BBBBCCCC), הריצו שוב, וחפשו אותו על המחסנית: search -s "BBBBCCCC". שימו לב שהוא יושב גבוה, בראש המחסנית.

רמז: הפקודה starti ב-pwndbg עוצרת עוד לפני הקפיצה, כשהמחסנית עדיין במצב הכניסה שהkernel בנה. זה הרגע המושלם לראות את argc/argv/envp.

תרגיל 4 - השתלת shellcode בסביבה וכיוון הקפיצה (מקומי)

עכשיו נפרוץ את הבינארי המקומי.

  1. בנו shellcode של 32 ביט שמריץ execve("/bin/sh", 0, 0) (היד-כתוב מ-3.1, או asm(shellcraft.i386.linux.sh())).
  2. הכניסו למשתנה סביבה מזחלת ענקית של \x90 (למשל 0x10000 בתים) ואחריה ה-shellcode.
  3. מצאו ב-gdb כתובת עמוק בתוך המזחלת (search -s "\x90\x90\x90\x90\x90\x90\x90\x90" על ה-[stack]).
  4. הפעילו את הבינארי דרך execve עם argv[0] = p32(addr), כשה-addr הוא כתובת בתוך המזחלת. ודאו שנפתח shell. נסו id.

רמז: השליטה שלכם היא ב-argv[0]. כדי לשים שם בתים גולמיים חייבים execve - ב-pwntools זה process(['./tiny'], executable='./tiny', argv=[p32(addr)], env={...}).

רמז: מכיוון שאתם קוראים רק את ארבעת הבתים הראשונים של argv[0], מספיק ש-p32(addr) יהיה בלי בית אפס. כתובת מחסנית טיפוסית כמו 0xffffd800 נקייה.

רמז: אם זה מפספס, ודאו שלא שיניתם את גודל הסביבה בין החיפוש ב-gdb לבין ההרצה - כל שינוי כזה מזיז את כל המחסנית.

תרגיל 5 - התמודדות עם ASLR ולולאת brute force

עד עכשיו עבדנו עם ASLR כבוי. על השרת האמיתי הוא כנראה פעיל.

  1. הדליקו ASLR מחדש: echo 2 | sudo tee /proc/sys/kernel/randomize_va_space, והריצו את ה-exploit מתרגיל 4. הוא כנראה יפספס, כי כתובת המחסנית זזה בכל הרצה.
  2. עטפו את ההפעלה בלולאה: קבעו כתובת ניחוש אחת בתוך תחום המזחלת, והריצו שוב ושוב עד שנפתח shell. אחרי כל כישלון סגרו את התהליך ונסו שוב.
  3. מדדו כמה ניסיונות לקח. נסו להגדיל את המזחלת ולראות שמספר הניסיונות יורד.

רמז: כל הרצה מגרילה מחסנית חדשה. בסופו של דבר המזחלת הגדולה שלכם "נוחתת" מעל הכתובת שניחשתם, וה-shellcode רץ. ב-32 ביט האנטרופיה נמוכה, אז כמה מאות עד אלפי ניסיונות מספיקים.

רמז: ככל שהמזחלת גדולה יותר, כך היא מכסה יותר כתובות אפשריות, ופחות ניסיונות צריך. מזחלת של 0x10000 בתים ומעלה היא נקודת פתיחה טובה.

תרגיל 6 - פיצוח tiny_easy האמיתי על השרת

עכשיו למטרה האמיתית.

  1. התחברו ל-ssh tiny_easy@pwnable.kr -p2222, העתיקו את הבינארי לתיקייה זמנית ב-/tmp, ופרקו אותו עם objdump -d. ודאו שהמנגנון זהה למה שתרגלתם (אמתו את הבתים - הם עשויים להיות מעט שונים).
  2. התאימו את ה-exploit: הריצו את הבינארי האמיתי דרך execve עם argv[0] נשלט ו-shellcode בסביבה, בלולאת brute force בגלל ה-ASLR.
  3. כשנפתח shell, הריצו cat flag. זכרו שה-shell רץ בהרשאות המשתמש המיוחס בזכות ביט ה-setuid.

רמז: אם אין pwntools על השרת, כתבו תוכנית עטיפה קצרה בפייתון שקוראת ל-os.execve('./tiny_easy', [struct.pack('<I', addr)], env), והריצו אותה בלולאת shell: while true; do python3 wrap.py 2>/dev/null; done.

רמז: אם קשה לזהות מתי נפתח shell בתוך הלולאה, שלחו פקודה כמו echo HIT; cat flag וחפשו את המחרוזת HIT בפלט.

תרגיל 7 (מחשבה) - להכליל את מה שלמדנו

בלי לכתוב קוד, ענו:

  1. מדוע דווקא ELF מינימלי (בלי GNU_STACK) נותן לנו מחסנית ניתנת להרצה בחינם? מה היה משתנה אם הבינארי היה כולל GNU_STACK עם דגל RW בלבד?
  2. הexploit נשען על כך שהקוד קופץ אל ערך שמגיע מ-argv[0]. אילו עוד מקורות קלט "מוקדמים" (לפני main) עשויים להיות בשליטת התוקף בבינארי כזה?
  3. מדוע השתמשנו במשתנה סביבה כדי לאחסן את ה-shellcode ולא, נניח, בתוך argv[0] עצמו? מה המגבלה על argv[0] שמונעת לשים בו את כל ה-shellcode?

רמז: חשבו על הגודל ועל קטיעה. argv[0] צריך שארבעת הבתים הראשונים שלו יהיו כתובת קפיצה - אבל אפשר לשים אחריהם עוד בתים. משתנה סביבה נותן מרחב גדול ונוח למזחלת ול-shellcode בלי לפגוע בכתובת הקפיצה.