לדלג לתוכן

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 וענו לעצמכם:

  1. מהו שער-הכניסה שכל קלט חייב לעבור כדי שהקוד המעניין ירוץ בכלל?
  2. באיזו פונקציה יש buffer על המחסנית, ומה גודלו?
  3. מה הערך המקסימלי ש-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 תוך כמה דקות.

  1. הדרו: AFL_USE_ASAN=1 afl-clang-fast -g -O1 -o parser_afl parser.c
  2. הכינו את המערכת (core_pattern, מדיניות תדר) כמו שראינו בהרצאה.
  3. הריצו: afl-fuzz -i in -o out -m none -- ./parser_afl @@
  4. עקבו אחרי מסך הסטטוס. מהי מהירות ההרצה (exec speed)? מתי צצה הקריסה הראשונה?

רמז: אם afl-fuzz מסרב לעלות ומתלונן על core_pattern או על מדיניות ה-CPU, בצעו את הכיוונונים מההרצאה. אם אתם בקונטיינר, לפעמים צריך AFL_SKIP_CPUFREQ=1 ו-AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1.

תרגיל 4 - טריאז' עד לשורש הבאג

עכשיו החלק החשוב: להבין למה זה קורס.

  1. בחרו קובץ מ-out/default/crashes/ ושחזרו את הקריסה ידנית.
  2. הדרו בינארי ASAN נקי לטריאז': clang -g -fsanitize=address -o parser_asan parser.c, והריצו אותו על קובץ הקריסה. איזו שורה ASAN מסמן? איזה סוג שגיאה (stack-buffer-overflow? WRITE או READ?)?
  3. מזערו את קובץ-הקריסה עם afl-tmin וראו כמה בתים באמת נחוצים.
  4. פתחו את הבינ' הרגיל ב-gdb על הקובץ המוזער, הריצו, והסתכלו ב-bt וב-info registers.

כתבו במשפט אחד: מהו שורש הבאג, ומהו התיקון הנכון בקוד?

רמז: התמקדו בקריאה ל-memcpy בתוך handle_name. מה מגביל את כמות הבתים שנכתבים ל-name, ומה לא?

תרגיל 5 - בונוס: מצב מתמשך ומילון

הריצו שוב, מהר יותר וחכם יותר.

  1. כתבו רתמת persistent mode (harness_persist.c) שקוראת ל-parse בתוך __AFL_LOOP, והדרו עם -DPERSISTENT. השוו את exec speed למצב הרגיל.
  2. כתבו קובץ מילון parser.dict עם מספר-הקסם "PWN!" וסוגי הרשומות, והריצו עם -x parser.dict.
  3. אתגר: הריצו מופע ראשי ושני משניים במקביל (-M/-S) ובדקו כמה הכיסוי גדל מהר יותר.

רמז: זכרו לקרוא את __AFL_FUZZ_TESTCASE_BUF אחרי __AFL_INIT ומחוץ ללולאה.