Fuzzing binaries using AFL

כמה מילים על פאזינג ותיאוריה בסיסית

פאזינג (Fuzzing) זו שיטה לבדיקת תוכנה, שבבסיסה עומד הרעיון להקריס את התוכנה ע"י מתן קלט לא תקין, קלט אקראי או לא צפוי. באמצעות שיטה זו מוצאים כיום את רוב החולשות בבינארים, והפאזרים חוסכים זמן יקר לחוקרים שיכולים להקדיש אותו לדברים אחרים כמו ניצול החולשות.

אמנם מתן קלט אקראי לגמרי לתוכנה הוא סוג של פאזינג, אך זו לא השיטה שבה עובדים כיום רוב הפאזרים – ניתן לייעל את התהליך:

דרך אחת היא לקחת קלט משתמש תקין ולהתחיל לעוות אותו (להפוך ביט בקלט, אם זה מספר להגדיל אותו או להקטין אותו, לחזור על הקלט וכו’) במקום להתחיל מאפס וליצור קלטים אקראיים לחלוטין.

דרך נוספת היא מעקב אחר הזרימה של התוכנה, לראות לאיזה קוד קלט מסוים מוביל אותנו ואם היינו כבר באזור הזה בקוד או שנחשפנו ל"ענף" חדש שהקלט הוביל אותנו אליו. זה נעשה באמצעות הכנסת הוראות נוספות לתוכנה במקומות אסטרטגיים (לדוגמה לפני או אחרי jmpים, קריאות לפונקציות) שיעזרו לנו להבין איפה אנחנו נמצאים בקוד וישלחו את המידע לכלי שלנו. טכניקה זו נקראת אינסטרומנטציה (instrumentation).

ניתן לשלב את 2 השיטות הללו ביחד באמצעות אלגוריתם גנטי: האלגוריתם לוקח קלט ראשוני, יוצר מספר מסוים של מוטציות לקלט ובודק אם אחד הקלטים המעוותים פתח נתיב קוד חדש. במידה והוא אכן הוביל אותנו לקטע קוד חדש, נוסיף את הקלט הזה לתור (queue) ולאחר מכן נעשה את המוטציות גם עליו. (האלגוריתם נקרא כך משום שהוא מחקה ברירה טבעית – הקלטים המתאימים, אלו שמובילים לנתיב קוד חדש, ממשיכים הלאה לשימוש הפאזר, והאחרים כנראה לא רלוונטיים ולכן לא ממשיכים לעבד אותם).

מה זה AFL ?

American fuzzy lop הוא פאזר חינמי (הקוד שלו נמצא בgithub) שאופן הפעולה שלו הוא באמצעות אלגוריתם גנטי, מונחה ע"י אינסטרומנטציה, והוא מאוד קל לשימוש ולהפעלה ראשונית לכן אני משתמש בו כדרך להדגים תהליך פאזינג.

אופן הפעולה של AFL:

שליפת קלט מהתור (התור מתחיל עם קלט ראשוני שאנו נותנים) --> הקטנת הקלט לגודל הקטן ביותר שיתקבל בתוכנית --> לבצע מוטציות שונות על הקלט --> אם הקלט המעוות יוצר התנהגות חדשה בתוכנית, נשמור אותו בתור ונחזור על התהליך.

AFL יכול לבצע source instrumentation: נעביר את הקוד של התוכנית שלנו בקומפיילר מיוחד של AFL כדי שיזריק לנו קוד אינסטרומנטציה בין הקוד שאנחנו כתבנו, ואז לקמפל אותו לבינארי (והבינארי יכלול אינסטרומנטציה). לAFL יש את היכולת גם לעבוד בלי קוד מקור אלא רק עם הבינארי עצמו – השיטה נקראת binary instrumentation והיא משתמשת בQEMU עם אמולציה לקוד user space. אך השיטה הזאת איטית בהרבה יותר מאשר source instrumentation בגלל שצריך להזריק ולהריץ הוראות בזמן ריצה, ונדרש אימולטור שיוצר overhead.

עכשיו כשאנו מבינים את התיאוריה הבסיסית של הפאזר, הגיע הזמן ללכלך את הידיים ולראות איך הכול עובד.

שימוש ב AFL

שלב ראשון: נוריד ונתקין את הכלי. תהליך התקנה סופר פשוט - נעשה

git clone https://github.com/google/AFL.git

ואז מהתיקייה של הפרויקט שעכשיו על המחשב שלנו, נעשה make. (המכונה שלי היא linux x86_64 ולא נתקלתי בשום קושי בבניית הכלי, ואין צורך בשום dependencies פרט לקומפיילר למיטב הבנתי).
אז נוצרו אצלינו כמה בינארים בתיקייה: afl-as, afl-fuzz, afl-gcc, afl-tmin, afl-analyze.
מה שהכי מעניין אותנו כרגע אלה afl-fuzz ו afl-gcc שתכף נראה מה עושים איתם.

AFL מצפה לתוכנית שקוראת מSTDIN, עושה משהו עם הקלט ומסיימת את פעולתה.
לצורך ההדגמה, נעבוד על קטע הקוד הבא:
image
ניתן לראות בקוד שהתוכנית מבקשת קלט מהמשתמש, והקלט מועבר לפונקציה שבודקת האם המחרוזת מתחילה ב FUZZME0. במידה וכן, התוכנית קורסת באופן מלאכותי ע"י נסיון לעשות dereference ל null pointer והפעולה לא חוקית משום שלא ניתן לגשת לזיכרון בכתובת 0 (NULL). הסיבה לריבוי תנאי ה-if שנראים חסרי משמעות, היא כדי לדמות הרבה נתיבי קוד בתוכנית (למעשה, כל if מתורגם באסמבלי לcmp וconditional jmp וזה בדיוק מה שAFL מחפש – קפיצות וענפים בקוד).

כדי לקמפל את התוכנית שלנו עם הinstrumentation (לא נתעסק כרגע בפאזינג ללא קוד מקור), נשתמש בafl-gcc במקום הקומפיילר הרגיל שהיינו משתמשים בו. זה נראה בערך כך:
./afl-gcc /path/to/file.c -o /path/to/output
אפשר להריץ את הבינארי שיצא לנו ולראות שהוא באמת עובד (ולבדוק אם הקלט FUZZME0 מקריס את התוכנה).
לכאורה הכול נראה אותו דבר כמו שהיינו מקמפלים עם קומפיילר רגיל, אך אם תנסו לזרוק את הבינארי החדש לghidra או כל דיקומפיילר\דיסאסמבלר אחר תראו שנוספו לבינארי שלנו כמה הוראות שלא כתבנו בתוכנית (קריאות לפונקציה __afl_maybe_log וכמה הוראות אסמבלי שמתעסקות במשתנים שקשורים לAFL). אלו אחראים לדווח לAFL מידע לגבי ההימצאות שלנו בקוד ובענפים השונים.

פרט לבינארי, אנחנו צריכים גם קלט ראשוני תקין שישמש את הפאזר ליצירת מוטציות לקלט. ניצור תיקייה חדשה שנקרא לה in, ושם ניצור קובץ (לא משנה איך הקובץ נקרא) ובו נכתוב קלט תקין. לדוגמה: “Hello AFL” זה קלט תקין לתוכנה שלנו, אז נכתוב אותו בקובץ ששמו test1 נניח.

ניצור גם תיקייה נוספת שאקרא לה out, שם הפאזר ישמור את הקלטים שמקריסים את התוכנית, קלטים שמשנים את ההתנהגות שלה ועוד מידע מעניין נוסף.

עכשיו לחלק העיקרי, הרצת הפאזר:
afl-fuzz -i /path/to/in -o /path/to/out /path/to/binary
מה שיפה בAFL זה שהוא מפרט בצורה טובה ומסביר מה לעשות במידה ויש בעיה בכלי או בבינארי.
במידה ואין שום בעיה נגיע למסך הראשי, הפאזר רץ ומדווח לנו בזמן אמת.
השדות במסך די מסבירים את עצמם:

  • uniq crashes שמציין כמה קלטים גרמו לקריסה ייחודית של התוכנית (ייתכן שכמה קלטים שונים יגרמו לקריסה בדיוק באותו מקום ומאותו מצב אז הם לא ייחודיים).
  • cycle עובר כל פעם שAFL מסיים לעבור על תור הקלטים והמוטציות ואז הוא חוזר חלילה
  • total paths מציין לכמה נתיבי קוד שונים הצליח להגיע הפאזר.
  • תחת Process timing נמצאים טיימרים

ושאר השדות פחות רלוונטיים לדוגמה הפשוטה שלנו אבל מוזמנים לקרוא עליהם בdocs של AFL.
ניתן לפאזר כמה דקות ונבדוק שוב מה מצבינו:
image
נראה שהפאזר מצא קלט שמקריס את התוכנה! בואו ניגש לתיקייה out ששם AFL שומר את המידע והקלטים שלו, ניגש לתיקייה crashes שבתוך התיקייה out ונקרא את הקבצים שם:
שם הקובץ הוא id:000000,sig:11,src:000005,op:havoc,rep:2 (ככה AFL קורא לקלטים) והתוכן שלו הוא FUZZME0M
נראה שאכן הפאזר הצליח לעלות על הקריסה בתוכנית שלנו שקורית כאשר הקלט מתחיל בFUZZME0.
עוד תיקייה מעניינת לצד crashes היא queue, שם ממש ניתן לראות את כל הקלטים שהובילו לפתיחת כל נתיב חדש, שלבסוף מוביל לקריסה:

==> id:000000,orig:test1 <==

Hell

==> id:000001,src:000000,op:havoc,rep:8,+cov <==

FU��

==> id:000002,src:000001,op:havoc,rep:2,+cov <==

FUZ�

==> id:000003,src:000002,op:havoc,rep:2,+cov <==

FUZZ

==> id:000004,src:000003,op:havoc,rep:16,+cov <==

FUZZMMZM

==> id:000005,src:000004,op:flip1,pos:5,+cov <==

FUZZMEZM

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

סיכום
הרעיון של פאזינג אמנם לא חדש, אבל הפאזרים משתכללים ונהיים חכמים יותר כל הזמן. הפאזר חוסך המון זמן בחיפוש אחר חולשות, ופאזינג מיושם בבדיקה של כמעט כל תוכנה, החל מבינארים רגילים, מערכות הפעלה וקרנלים (ראו kAFL, syzkaller), ועד מערכות web ואתרי אינטרנט (wfuzz ונוספים). בחרתי להראות את תהליך הפאזינג עם AFL כי הוא כלי מהפכני מבחינת קלות השימוש, השתמשו בו למצוא המון חולשות בשנים האחרונות ופאזרים נוספים כיום מתבססים על רעיונות דומים לשל AFL.
מטרת הפוסט הזה היא לחשוף את הקוראים לפאזינג בכללי ולהראות כמה פשוט השימוש בAFL וכמה עוצמתי הכלי. פוסטים הבאים אולי אתעסק יותר באיך בפועל AFL עושה אינסטרומנטציה, איך מתבצע פאזינג בלי קוד מקור, ואיך משתמשים בAFL כדי לחקור תוכנה אמיתית.

הערות ושאלות מכל סוג שהן יתקבלו בברכה :slightly_smiling_face:

מקורות:
ויקיפדיה
AFL github

10 לייקים

פוסט מעולה :smile: למדתי הרבה
השאלה שלי היא עד כמה יעיל פאזינג עבור תוכניות מורכבות יותר ממה שהצגת? האם ניתן בעזרתו באמת למצוא חולשות “מתוחכמות” יותר? או שעושים איזשהו שילוב בין הפעלת פאזינג לבין ממש לעבור על התוכנית עם דיקומפיילר כלשהו?

לייק 1

אחד הפוסטים פוצל לנושא חדש: המלצה על כלי לביצוע פאזינג

שאלה טובה. הכוח של פאזינג דווקא בא לידי ביטוי עוד יותר בתוכנות מורכבות עם הרבה נתיבים, בגלל שאי אפשר לעבור על כל הנתיבים בקוד בצורה ידנית - זה דורש הרבה זמן, ותשומת לב לדברים קטנים בקוד שאפשר לפספס. בשלב testing בפיתוח תוכנה, זה לא אמור לבוא במקום source code reviewing ו automated testing, אלא ככלי הערכה נוסף שמסוגל באמת למצוא מצבי קיצון חריגים.
פאזר הוא לא כלי קסם שמוצא חולשות במהירות מטורפת, אבל מה שכן הוא מאפשר זה להריץ אותו ברקע, לשכוח ממנו, ולחזור אחרי כמה שעות \ ימים \ אפילו שבועות, וייתכן למצוא קריסות. סופר low maintenance.
אמנם אני לא חוקר חולשות במקצוע אבל אני מאמין שמשלבים גם דיקומפיילינג ועיבוד ידני על תוכנה.
יש לך כאן רשימה של כל מיני critical vulnerabilities בכל מיני תוכנות (עיבוד סרטונים, תמונות, שמע, זיפ, כל מיני פרסרים) שנמצאו ע"י שימוש ב AFL: https://lcamtuf.coredump.cx/afl
אחד המקרים המעניינים והמשמעותיים הוא CVE-2014-6277/6278 חולשה ב bash שמאפשרת הזרקה והרצה של קוד (חלק מחולשות shellshock) ושווה קריאה הפוסט של Michał Zalewski שהוא במקרה גם המפתח של AFL :open_mouth:
https://lcamtuf.blogspot.com/2014/10/bash-bug-how-we-finally-cracked.html

2 לייקים