לדלג לתוכן

10.3 Fuzzing עם libFuzzer וsanitizers תרגול

תרגול - Fuzzing עם libFuzzer וsanitizers

בתרגול הזה תכתבו harness של libFuzzer לספרייה קטנה, תבנו אותה עם AddressSanitizer, ותנו ל-fuzzer למצוא בשבילכם overflow heap אמיתית - בלי לקרוא את הקוד ולחפש את הבאג ביד. אחר כך תלמדו לקרוא את דוח הקריסה, לשחזר אותו, ולמזער אותו. עבדו לפי הסדר; כל תרגיל בונה על הקודם.

הדרישה היחידה לסביבה היא clang בגרסה סבירה (שיש בה libFuzzer, כלומר גרסה 6 ומעלה). בדקו:

clang --version

הכנה - הספרייה הvulnerable

צרו את הקבצים הבאים. זו ספריית ניתוח (parser) לformat בינארי זעיר בשם TinyParse. הformat:

bytes 0..3 : magic number - the characters "TP01"
byte 4      : number of records n
then n records, each one: [byte type][byte len][len bytes value]
// tinyparse.h
#ifndef TINYPARSE_H
#define TINYPARSE_H
#include <stdint.h>
#include <stddef.h>
int tp_parse(const uint8_t *data, size_t size);
#endif
// tinyparse.c
#include "tinyparse.h"
#include <stdlib.h>
#include <string.h>

int tp_parse(const uint8_t *data, size_t size) {
    if (size < 5) return -1;
    if (memcmp(data, "TP01", 4) != 0) return -1;   // the magic gate
    uint8_t n = data[4];
    size_t off = 5;
    for (uint8_t i = 0; i < n; i++) {
        if (off + 2 > size) return -1;
        uint8_t type = data[off];
        uint8_t len  = data[off + 1];
        off += 2;
        if (off + len > size) return -1;           // don't read beyond the input
        if (type == 0x02) {                        // "name" record
            char *name = malloc(16);               // fixed buffer of 16 bytes
            memcpy(name, data + off, len);         // copy len bytes into it
            free(name);
        }
        off += len;
    }
    return 0;
}

שימו לב: הספרייה נזהרת לא לקרוא מעבר לקלט (off + len > size). הבאג הוא במקום אחר, ותמצאו אותו עם ה-fuzzer. אל תנתחו אותו ידנית עדיין - המטרה של התרגול היא לתת לכלי לעבוד.

תרגיל 1 - כתיבת ה-harness

כתבו קובץ harness.c עם הפונקציה LLVMFuzzerTestOneInput שמעבירה את הקלט ל-tp_parse.

  1. כללו את tinyparse.h.
  2. ממשו את LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) שקוראת ל-tp_parse(data, size) ומחזירה 0.
  3. ודאו שהפונקציה לא נוגעת ב-data מעבר ל-size ולא שומרת מצב גלובלי.

רמז: ה-harness כאן הוא בן שלוש שורות תוכן. אל תסבכו אותו; כל העבודה נעשית בתוך tp_parse.

תרגיל 2 - בנייה עם ASan

בנו את המטרה עם libFuzzer ו-AddressSanitizer.

  1. הידרו את harness.c ואת tinyparse.c יחד עם clang ובדגל שמפעיל את libFuzzer ואת ASan.
  2. אל תשכחו את -g (לסמלי ניפוי) ואת -O1.
  3. ודאו שנוצר בינארי הרצה בשם fuzz_tp.

רמז: הדגל המרכזי הוא -fsanitize=fuzzer,address. פקודה בסגנון clang -g -O1 -fsanitize=fuzzer,address -o fuzz_tp harness.c tinyparse.c.

תרגיל 3 - קורפוס זרע והרצה

בלי זרע, ה-fuzzer יבזבז זמן רק כדי לעבור את שער ה-magic. תנו לו התחלה.

  1. צרו תיקיית corpus/ וקובץ זרע אחד שהוא קלט תקין: התווים TP01, ואז בית אחד עם ערך 1 (רשומה אחת), ואז רשומה קצרה ותקינה מסוג 0x02 עם ערך קצר (למשל אורך 3).
  2. הריצו ./fuzz_tp corpus/.
  3. צפו בשדות cov ו-exec/s. תוך שניות בודדות אמורה להופיע קריסה.

רמז: אפשר לייצר את הזרע מ-python: משהו כמו open('corpus/seed','wb').write(b'TP01' + bytes([1]) + bytes([0x02, 3]) + b'abc'). הזרע עוזר ל-fuzzer להבין שאחרי ה-magic יש מבנה של רשומות, ומשם הוא ילמד להגדיל את שדה ה-len.

תרגיל 4 - קריאת הדוח ואיתור השורה

עצרו על הדוח שהתקבל ופענחו אותו.

  1. איזה סוג באג ASan מדווח? (רמז: זו לא UAF).
  2. האם זו כתיבה (WRITE) או קריאה (READ), ובאיזה גודל?
  3. באיזו שורה בקובץ tinyparse.c נמצאת הפעולה הפוגעת, ובאיזו שורה הוקצה הbuffer שנדרס? מצאו את שתי השורות במחסנית הקריאות.
  4. מה גודל האזור שנדרס לפי השורה ... region?

רמז: דלגו על מסגרת ה-memcpy העליונה (זה מיירט של ASan) והסתכלו על המסגרת הבאה. השורה located ... bytes to the right of 16-byte region מקשרת ישירות ל-malloc(16).

תרגיל 5 - שחזור ומזעור

הפכו את הקריסה לדבר יציב שאפשר לחקור.

  1. שחזרו את הקריסה על ידי הרצת הבינארי עם קובץ ה-crash-* כארגומנט.
  2. מזערו את הקלט המפיל למינימום עם -minimize_crash=1.
  3. הסתכלו בקלט הממוזער (למשל עם xxd). מהו הבית שמגדיר את האורך שגורם לoverflow, ומה ערכו המינימלי כדי לחרוג מ-16?

רמז: הפקודה למזעור היא ./fuzz_tp -minimize_crash=1 crash-<hash>. הקלט הממוזער אמור להישאר TP01, בית ספירה, בית type ששווה 0x02, בית len שגדול מ-16, ואז מספיק בתים כדי שהבדיקה off + len > size תעבור.

תרגיל 6 (בונוס) - הוספת UBSan ומילון

חדדו את ההגדרה.

  1. בנו מחדש עם UBSan בנוסף ל-ASan, ועם הדגל שהופך שגיאות סניטייזר לקריסה. הריצו שוב.
  2. צרו מילון (dictionary) שמכיל את מחרוזת ה-magic, והריצו עם -dict=. הריצו פעם אחת גם בלי קורפוס זרע ובלי מילון, ופעם עם המילון, והשוו כמה מהר נמצא הבאג.
  3. חשבו: למה מילון או פרופיל ערכים (-use_value_profile=1) עוזרים כל כך לעבור את שער ה-memcmp(data, "TP01", 4)?

רמז: הדגל להפיכת שגיאות לקריסה הוא -fno-sanitize-recover=all. קובץ מילון הוא שורות בסגנון magic="TP01". בלי עזרה, לנחש 4 בתים מדויקים זה 1 מתוך כ-4 מיליארד; המילון או פרופיל הערכים מפרקים את המחסום הזה כמעט לחלוטין.