10.2 Fuzzing עם AFL תרגול
תרגול - Fuzzing עם AFL¶
בתרגול הזה תריצו את שרשרת העבודה המלאה של פאזינג מונחה-כיסוי מקצה לקצה: תהדרו parser vulnerable עם אינסטרומנטציה, תבנו קורפוס זרעים, תריצו את afl-fuzz, תמצאו קריסה תוך דקות, ותבצעו לה טריאז' עד לשורש הבאג עם gdb ו-ASAN. עבדו לפי הסדר - כל תרגיל בונה על הקודם. בסביבת לינוקס עם AFL++ מותקן (ראו את הרצאת 0.2 על סביבת העבודה, או השתמשו בקונטיינר aflplusplus/aflplusplus מ-Docker).
הכנה - הקוד vulnerable¶
צרו קובץ בשם parser.c עם התוכן הבא. זהו parser קטן לformat בינארי מומצא: כותרת עם מספר-קסם, ואז רשומות מסוג type-length-value.
// parser.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
/* File format:
[0..3] magic = "PWN!"
[4] version
[5] record count
then records: [type:1][len:1][data:len]
type 0x01 = "name"
*/
static void handle_name(const uint8_t *data, uint8_t len) {
char name[32];
memcpy(name, data, len); /* note: there's no bounds check here at all */
name[len] = '\0';
printf("name = %s\n", name);
}
int parse(const uint8_t *buf, size_t n) {
if (n < 6) return 1;
if (memcmp(buf, "PWN!", 4) != 0) return 1; // entry gate - magic number
uint8_t count = buf[5];
size_t off = 6;
for (uint8_t i = 0; i < count; i++) {
if (off + 2 > n) break;
uint8_t type = buf[off];
uint8_t len = buf[off + 1];
off += 2;
if (off + len > n) len = (uint8_t)(n - off); // limits the read from the input
if (type == 0x01) handle_name(buf + off, len);
off += len;
}
return 0;
}
#ifndef PERSISTENT
int main(int argc, char **argv) {
uint8_t buf[8192];
FILE *f = (argc > 1) ? fopen(argv[1], "rb") : stdin;
if (!f) return 1;
size_t n = fread(buf, 1, sizeof(buf), f);
if (argc > 1) fclose(f);
return parse(buf, n);
}
#endif
תרגיל 1 - קריאת המטרה וניחוש הבאג¶
לפני שמפזזים, מבינים. קראו את parser.c וענו לעצמכם:
- מהו שער-הכניסה שכל קלט חייב לעבור כדי שהקוד המעניין ירוץ בכלל?
- באיזו פונקציה יש buffer על המחסנית, ומה גודלו?
- מה הערך המקסימלי ש-
lenיכול לקבל, ומה קורה אם הוא גדול מגודל הbuffer?
רמז: שדה len הוא בית בודד. השוו את הטווח שלו לגודל של name.
תרגיל 2 - קורפוס זרעים¶
בנו תיקיית זרעים in/ עם לפחות שני קלטים תקינים שעוברים את שער הכניסה ומגיעים ל-handle_name. בלי זרע תקין, ה-fuzzer יבזבז זמן יקר רק כדי לנחש את "PWN!".
צרו לפחות: זרע עם רשומת-שם אחת קצרה, וזרע עם שתי רשומות.
רמז: זרע שמפעיל את מסלול השם צריך שהבית ה-6 (type) יהיה 0x01. השתמשו ב-printf עם escapes בהקסה, למשל printf 'PWN!\x01\x01\x01\x05hello' > in/name1.
תרגיל 3 - הידור והרצה¶
הדרו את המטרה עם אינסטרומנטציה של AFL++ ועם AddressSanitizer, ואז הריצו את afl-fuzz. המטרה: לראות saved crashes גדל מעל 0 תוך כמה דקות.
- הדרו:
AFL_USE_ASAN=1 afl-clang-fast -g -O1 -o parser_afl parser.c - הכינו את המערכת (
core_pattern, מדיניות תדר) כמו שראינו בהרצאה. - הריצו:
afl-fuzz -i in -o out -m none -- ./parser_afl @@ - עקבו אחרי מסך הסטטוס. מהי מהירות ההרצה (
exec speed)? מתי צצה הקריסה הראשונה?
רמז: אם afl-fuzz מסרב לעלות ומתלונן על core_pattern או על מדיניות ה-CPU, בצעו את הכיוונונים מההרצאה. אם אתם בקונטיינר, לפעמים צריך AFL_SKIP_CPUFREQ=1 ו-AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1.
תרגיל 4 - טריאז' עד לשורש הבאג¶
עכשיו החלק החשוב: להבין למה זה קורס.
- בחרו קובץ מ-
out/default/crashes/ושחזרו את הקריסה ידנית. - הדרו בינארי ASAN נקי לטריאז':
clang -g -fsanitize=address -o parser_asan parser.c, והריצו אותו על קובץ הקריסה. איזו שורה ASAN מסמן? איזה סוג שגיאה (stack-buffer-overflow?WRITEאוREAD?)? - מזערו את קובץ-הקריסה עם
afl-tminוראו כמה בתים באמת נחוצים. - פתחו את הבינ' הרגיל ב-gdb על הקובץ המוזער, הריצו, והסתכלו ב-
btוב-info registers.
כתבו במשפט אחד: מהו שורש הבאג, ומהו התיקון הנכון בקוד?
רמז: התמקדו בקריאה ל-memcpy בתוך handle_name. מה מגביל את כמות הבתים שנכתבים ל-name, ומה לא?
תרגיל 5 - בונוס: מצב מתמשך ומילון¶
הריצו שוב, מהר יותר וחכם יותר.
- כתבו רתמת persistent mode (
harness_persist.c) שקוראת ל-parseבתוך__AFL_LOOP, והדרו עם-DPERSISTENT. השוו אתexec speedלמצב הרגיל. - כתבו קובץ מילון
parser.dictעם מספר-הקסם"PWN!"וסוגי הרשומות, והריצו עם-x parser.dict. - אתגר: הריצו מופע ראשי ושני משניים במקביל (
-M/-S) ובדקו כמה הכיסוי גדל מהר יותר.
רמז: זכרו לקרוא את __AFL_FUZZ_TESTCASE_BUF אחרי __AFL_INIT ומחוץ ללולאה.