Fuzzing-тестування – ідеї та приклади

6 хв. читання

Фазинг — техніка тестування програмного забезпечення, а також ще один термін, у якого немає адекватного українського перекладу. Основна ідея полягає у тому, щоб подати на вхід програми велику кількість випадкових даних, сподіваючись, що це спровокує помилку. Класичною реалізацією такої ідеї стала розроблена у 1983 році програма «The Monkey» для перших Macintosh. Вона допомогла знайти багато багів, імітуючи випадкові натиснення клавіатури, кліки мишею тощо.

В загальному випадку фазинг можна застосувати до будь-яких програм, навіть без явної можливості вводу даних. Просто код, що підлягає тестуванню, загортають у тестову обгортку, приєднують до фазингового рушія, що генерує дані, і запускають все це на якомусь окремому сервері. Виконання може займати години, дні або навіть тижні — але рано чи пізно, якщо у коді є помилка, програма впаде.

Є пара речей, які дозволяють пришвидшити та краще організувати процес фазингового тестування:

  • використання санітайзерів (sanitizer, знову проблема перекладу). Це така фіча сучасних компіляторів — вони можуть додавати до програми код, який гарантує падіння програми у випадку виникнення нештатної ситуації, типу переповнення при додаванні цілих чисел, виходу за межі пам'яті або ще чогось.
  • генерування даних за спеціальними правилами. Наприклад, якщо програма працює з даними в XML, її краще перевіряти більш "розумним" фазером, що вміє генерувати дані у цьому форматі.

Для прикладу можна розглянути libFuzzer — бібліотеку, що лінкується з кодом і може використовуватись для перевірки роботи окремих функцій. Під час тестування вона може відслідковувати, яка частина програми спрацювала (за допомогою ще однієї особливості clang) та змінювати вхідні дані таким чином, щоб забезпечити максимальне покриття коду.

На офіційному сайті libFuzzer розказується, як її збирати, але насправді це не потрібно. З деяких пір бібліотека є частиною проекту llvm, тому для її використання досить мати найновішу версію clang. Наступний приклад я перевіряв за допомогою clang 6.0.0, зібраного відповідно до офіційного керівництва. Процес компіляції компілятора зайняв майже дві години; ось тут є рекомендація, як його пришвидшити, однак я не тестував.

Отже, припустимо, що у вас є така функція:

bool FuzzMe(const uint8_t *Data, size_t DataSize) {
  return DataSize >= 3 &&
      Data[0] == 'F' &&
      Data[1] == 'U' &&
      Data[2] == 'Z' &&
      Data[3] == 'Z';  // :‑<
}

Код взято з цієї статті, і там ще багато прикладів, що демонструють усю потужність фазингового тестування.

Для того, щоб libFuzzer змогла з цим працювати, треба написати обгортку:

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  FuzzMe(Data, Size);
  return 0;
}

Після цього файл з цими двома функціями можна скомпілювати таким чином (опції компілятора можуть виглядати трохи інакше в залежності від версії):

clang++ -g -fsanitize=address,fuzzer -fsanitize-coverage=trace-pc-guard ./fuzz_me.cc -o ./ft

Зверніть увагу, функція main() тут не потрібна, фазер додасть її автоматично.

Після запуску ft видасть типове для AddressSanitizer повідомлення про знайдену помилку. Мені здається, краще розібрати такий звіт на іншому прикладі, трохи складнішому.

Припустимо, є така пара функцій:

static void int_to_color(const int v, char* buffer, const size_t buffer_len)
{
    char green_str[] = "green";
    char red_str[] =   "red";    
    //more arrays here ....
    char unrecongized_str[] = "<BadValue>";

    char* str = 0;
    size_t str_len = 0;

    switch (v%10)
    {
        case GREEN_COLOR_INT:
          str = green_str;
          str_len = sizeof(green_str);
          break;

        case RED_COLOR_INT:
          str = red_str;
          str_len = sizeof(green_str);
          break;

        // more cases with colors here ....        

        default:
          str = unrecongized_str;
          str_len = sizeof(unrecongized_str);
    }

    str_len = (buffer_len>=str_len)? str_len : buffer_len;
    memcpy(buffer, str, str_len-1);
    buffer[str_len-1] = '\\0';
}

void int_pair_to_str(const int first, const int second, 
	                char* buffer, size_t buffer_len)
{
    const int MAX_STR_LEN = 40;
    char order_buff[MAX_STR_LEN] ;
    int_to_color(first, order_buff, MAX_STR_LEN);
    
    snprintf(buffer, buffer_len, "Color is %s, number is %d", order_buff, second);
}

Тут int_pair_to_str() створює якесь текстове повідомлення на основі двох цілих чисел. Одне з цих чисел залишається як є, замість іншого у рядок вставляється назва якогось кольору. Приклад штучний, основне завдання — імітація механічної помилки при копіпейсті.

Стандартна функція LLVMFuzzerTestOneInput() для такого випадку стане трохи більшою:

void Fuzz_int_pair_to_str(const uint8_t *Data, size_t DataSize) {
  const size_t MAX_STR_LEN=255;
  char str[MAX_STR_LEN];

  int pair[2];
  const int loops = DataSize / (sizeof(pair));
  const uint8_t* c_ptr = Data;
  for (int i=0; i<loops; i++)
  {
    memcpy(&(pair[0]), c_ptr, sizeof(pair));
    c_ptr += sizeof(pair);

    int_pair_to_str(pair[0], pair[1], str, MAX_STR_LEN);
  }
}

int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  Fuzz_int_pair_to_str(Data, Size);
  return 0;
}

Потрібно правильно зчитати два цілих числа з масиву випадкової довжини. Я це роблю за допомогою масиву та memcpy, це такий типовий для чистого C підхід, хоча, можливо, можна було б зробити щось краще.

Спочатку всі вихідні файли компілюються в об'єктні з опцією -fsanitize=address, а потім лінкуються з -fsanitize=address,fuzzer:

  clang -g -Wall -I"./src" -fsanitize=address -c ./src/func2.c -o "./obj/func2.o"
  clang -g -Wall -I"./src" -fsanitize=address -fsanitize-coverage=trace-pc-guard -c ./src/fuzz2.c -o "./obj/fuzz2.o"
  clang -g -Wall -fsanitize=address,fuzzer "./obj/func2.o" "./obj/fuzz2.o" -o ./bin/-clang-fuzz 

Після цього тестову програму можна запустити таким чином:

./bin/basic-clang-fuzz -artifact_prefix="./crash/" -max_len=16

Тут:

  • -artifactprefix="./crash/ вказує, що дані, які викликали помилку, слід зберігати у теці "./crash/"
  • -maxlen=16 довжина тестового масиву має бути не більше шістнадцяти байт.
  • -runs=50 максимальна кількість тестових запусків. Для даного прикладу 50 вистачає, щоб натрапити на помилку.

Серед інших можливих опцій варто відзначити:

  • -help=1 покаже довідку про опції
  • -seed=<value> задає основу для генерації тестових даних. Наприклад, щоб точно відтворити цей лог, треба запустити програму з -seed=2956761295
  • -jobs використовується для організації багатониткового тестування

Після запуску ви повинні отримати звіт про падіння, схожий на цей. Він інтуїтивно зрозумілий, причину та місце падіння легко ідентифікувати:

==20543==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffee1e96704 at pc 0x000000504cdd bp 0x7ffee1e966b0 sp 0x7ffee1e95e60
READ of size 5 at 0x7ffee1e96704 thread T0
    #0 0x504cdc in __asan_memcpy /home/u1user/proj/clang-b01/llvm/projects/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cc:23
    #1 0x53d063 in int_to_color /home/u1user/proj/ft/af/./src/func2.c:39:5
    #2 0x53cce6 in int_pair_to_str /home/u1user/proj/ft/af/./src/func2.c:48:5
    #3 0x53d40f in Fuzz_int_pair_to_str /home/u1user/proj/ft/af/./src/fuzz2.c:22:3
    #4 0x53d55e in LLVMFuzzerTestOneInput /home/u1user/proj/ft/af/./src/fuzz2.c:26:3

Дані, які викликали помилку, будуть збережені у бінарному вигляді до файлу crash, про це сказано у кінці звіту. Подивитися на них можна командою xxd:

$ xxd ./crash/crash-c8ffc16f826cc69a965a64adf3df26d712d4ceb1
00000000: 2a00 0000 0000 0000 0000 0000 0000 00    *..............

Відтворити падіння можна, просто вказавши цей же файл при запуску програми:

$ ./bin/basic-clang-fuzz ./crash/crash-c8ffc16f826cc69a965a64adf3df26d712d4ceb1

Зверніть увагу, це не теж саме, що буде при вказуванні seed. Коли ви використовуєте опцію seed, програма пройде весь шлях по генерації тестових послідовностей, і це може зайняти багато часу. В даному випадку вона одразу використає для перевірки вказаний масив даних.

На цьому все, більше інформації можна знайти за посиланнями:

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація