AFL מבוא לפאזינג עם
- Get link
- X
- Other Apps
כמה מילים על פאזינג ותיאוריה בסיסית
מה זה 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, עושה משהו עם הקלט ומסיימת את פעולתה.
לצורך
ההדגמה, נעבוד על קטע הקוד הבא:
#include <stdio.h>
void func(char*);
char str[20];
int main() {
printf("Give me a string: \n");
fgets(str, 20, stdin);
func(str);
}
void func(char* buffer) {
if (buffer[0] == 'F'){
if (buffer[1] == 'U'){
if (buffer[2] == 'Z'){
if (buffer[3] == 'Z'){
if (buffer[4] == 'M'){
if (buffer[5] == 'E'){
if (buffer[6] == '0'){
printf("Crash! \n");
int* x = 0;
printf("%d\n", *x);
}}}}}}
}
ניתן לראות בקוד שהתוכנית מבקשת קלט מהמשתמש, והקלט מועבר לפונקציה שבודקת
האם המחרוזת מתחילה ב 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 ונראה מה השתנה:
ניתן לראות ב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 זה הפירוט העשיר שהוא נותן אם משהו הולך לא כשורה או כשיש מקום
לשיפור ביצועים וכו'. במקרה הזה יש לנו הוראות בדיוק מה לעשות ולמה. אז אחרי
שתיקנו את זה (מניעת יצירת coredump כדי למנוע דיליי בפאזינג), נפעיל שוב את הפאזר ואם הכל עובד נגיע כעת
למסך הראשי:
השדות די
מסבירים את עצמם, השדה המשמעותי הוא כמובן uniq crashes שמציין כמה קלטים גרמו לקריסה ייחודית של
התוכנית (ייתכן שכמה קלטים שונים יגרמו לקריסה בדיוק באותו מקום ומאותו מצב אז הם
לא ייחודיים). כמו כן, cycle עובר כל פעם שAFL מסיים לעבור על תור הקלטים
והמוטציות ואז הוא חוזר חלילה, total paths מציין לכמה נתיבי קוד שונים הצליח להגיע
הפאזר. תחת Process timing נמצאים טיימרים, ושאר השדות פחות רלוונטיים לדוגמה הפשוטה שלנו
אבל מוזמנים לקרוא עליהם בdocs של AFL.
ניתן לפאזר כמה
דקות ונבדוק שוב מה מצבינו:

נראה שהפאזר מצא קלט שמקריס את התוכנה! בואו ניגש לתיקייה out ששם AFL שומר את המידע
והקלטים שלו, ניגש לcrashes שבתוך התיקייה
ונקרא את הקבצים שם: שם הקובץ הוא 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 כדי לחקור תוכנה אמיתית.
מקורות:
https://en.wikipedia.org/wiki/Fuzzing
- Get link
- X
- Other Apps



Comments
Post a Comment