10.3 Fuzzing עם libFuzzer וsanitizers תרגול
תרגול - Fuzzing עם libFuzzer וsanitizers¶
בתרגול הזה תכתבו harness של libFuzzer לספרייה קטנה, תבנו אותה עם AddressSanitizer, ותנו ל-fuzzer למצוא בשבילכם overflow heap אמיתית - בלי לקרוא את הקוד ולחפש את הבאג ביד. אחר כך תלמדו לקרוא את דוח הקריסה, לשחזר אותו, ולמזער אותו. עבדו לפי הסדר; כל תרגיל בונה על הקודם.
הדרישה היחידה לסביבה היא clang בגרסה סבירה (שיש בה libFuzzer, כלומר גרסה 6 ומעלה). בדקו:
הכנה - הספרייה ה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.
- כללו את
tinyparse.h. - ממשו את
LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)שקוראת ל-tp_parse(data, size)ומחזירה 0. - ודאו שהפונקציה לא נוגעת ב-
dataמעבר ל-sizeולא שומרת מצב גלובלי.
רמז: ה-harness כאן הוא בן שלוש שורות תוכן. אל תסבכו אותו; כל העבודה נעשית בתוך tp_parse.
תרגיל 2 - בנייה עם ASan¶
בנו את המטרה עם libFuzzer ו-AddressSanitizer.
- הידרו את
harness.cואתtinyparse.cיחד עם clang ובדגל שמפעיל את libFuzzer ואת ASan. - אל תשכחו את
-g(לסמלי ניפוי) ואת-O1. - ודאו שנוצר בינארי הרצה בשם
fuzz_tp.
רמז: הדגל המרכזי הוא -fsanitize=fuzzer,address. פקודה בסגנון clang -g -O1 -fsanitize=fuzzer,address -o fuzz_tp harness.c tinyparse.c.
תרגיל 3 - קורפוס זרע והרצה¶
בלי זרע, ה-fuzzer יבזבז זמן רק כדי לעבור את שער ה-magic. תנו לו התחלה.
- צרו תיקיית
corpus/וקובץ זרע אחד שהוא קלט תקין: התוויםTP01, ואז בית אחד עם ערך1(רשומה אחת), ואז רשומה קצרה ותקינה מסוג0x02עם ערך קצר (למשל אורך 3). - הריצו
./fuzz_tp corpus/. - צפו בשדות
covו-exec/s. תוך שניות בודדות אמורה להופיע קריסה.
רמז: אפשר לייצר את הזרע מ-python: משהו כמו open('corpus/seed','wb').write(b'TP01' + bytes([1]) + bytes([0x02, 3]) + b'abc'). הזרע עוזר ל-fuzzer להבין שאחרי ה-magic יש מבנה של רשומות, ומשם הוא ילמד להגדיל את שדה ה-len.
תרגיל 4 - קריאת הדוח ואיתור השורה¶
עצרו על הדוח שהתקבל ופענחו אותו.
- איזה סוג באג ASan מדווח? (רמז: זו לא UAF).
- האם זו כתיבה (WRITE) או קריאה (READ), ובאיזה גודל?
- באיזו שורה בקובץ
tinyparse.cנמצאת הפעולה הפוגעת, ובאיזו שורה הוקצה הbuffer שנדרס? מצאו את שתי השורות במחסנית הקריאות. - מה גודל האזור שנדרס לפי השורה
... region?
רמז: דלגו על מסגרת ה-memcpy העליונה (זה מיירט של ASan) והסתכלו על המסגרת הבאה. השורה located ... bytes to the right of 16-byte region מקשרת ישירות ל-malloc(16).
תרגיל 5 - שחזור ומזעור¶
הפכו את הקריסה לדבר יציב שאפשר לחקור.
- שחזרו את הקריסה על ידי הרצת הבינארי עם קובץ ה-
crash-*כארגומנט. - מזערו את הקלט המפיל למינימום עם
-minimize_crash=1. - הסתכלו בקלט הממוזער (למשל עם
xxd). מהו הבית שמגדיר את האורך שגורם לoverflow, ומה ערכו המינימלי כדי לחרוג מ-16?
רמז: הפקודה למזעור היא ./fuzz_tp -minimize_crash=1 crash-<hash>. הקלט הממוזער אמור להישאר TP01, בית ספירה, בית type ששווה 0x02, בית len שגדול מ-16, ואז מספיק בתים כדי שהבדיקה off + len > size תעבור.
תרגיל 6 (בונוס) - הוספת UBSan ומילון¶
חדדו את ההגדרה.
- בנו מחדש עם UBSan בנוסף ל-ASan, ועם הדגל שהופך שגיאות סניטייזר לקריסה. הריצו שוב.
- צרו מילון (dictionary) שמכיל את מחרוזת ה-magic, והריצו עם
-dict=. הריצו פעם אחת גם בלי קורפוס זרע ובלי מילון, ופעם עם המילון, והשוו כמה מהר נמצא הבאג. - חשבו: למה מילון או פרופיל ערכים (
-use_value_profile=1) עוזרים כל כך לעבור את שער ה-memcmp(data, "TP01", 4)?
רמז: הדגל להפיכת שגיאות לקריסה הוא -fno-sanitize-recover=all. קובץ מילון הוא שורות בסגנון magic="TP01". בלי עזרה, לנחש 4 בתים מדויקים זה 1 מתוך כ-4 מיליארד; המילון או פרופיל הערכים מפרקים את המחסום הזה כמעט לחלוטין.