Skip to content

Fuzzing ruamel yaml (Python) project with sydr fuzz (Atheris backend) (rus)

Savidov Georgy edited this page Jul 12, 2023 · 7 revisions

Введение

В этой статье я бы хотел поделиться своим опытом фаззинга проектов на языке Python. Для этих целей я буду использовать sydr-fuzz с поддержкой Atheris. Sydr-fuzz изначально разрабатывался как гибридный фаззер, который комбинирует инструмент динамического символьного выполнения Sydr и современные инструменты фаззинга AFLplusplus и libFuzzer. Также sydr-fuzz поддерживает несколько полезных фич, таких как сортировка аварийных завершений с помощью casr, возможность проверки предикатов безопасности, удобные команды для минимизации корпуса и сбора покрытия по исходному коду. Atheris — это движок фаззинга с обратной связью по покрытию для Python языка. Он поддерживает фаззинг кода на языке Python, кроме того и фаззинг нативных расширений для CPython. Atheris основан на libFuzzer. Atheris выглядит и работает как libFuzzer, поэтому мы решили поддержать возможность работы с ним через sydr-fuzz, почему нет? Хотя у нас нет динамического символьного выполнения для кода на языке Python, но мы можем фаззить, делать сортировку аварийных завершений, минимизировать корпус и собирать покрытие по исходному коду через удобный интерфейс sydr-fuzz.

Подготовка фаззинг-цели

Страница Atheris на GitHub предоставляет отличную инструкцию о том, как его установить и использовать. Мы будем фаззить yaml проект из его примеров. У нас уже есть подготовленный к сборке docker контейнер с необходимым окружением для фаззинга. Я буду его использовать в своих экспериментах, но сейчас давайте рассмотрим фаззинг цель и скрипт сборки.

import atheris

with atheris.instrument_imports():
  from ruamel import yaml as ruamel_yaml
  import sys
  import warnings

# Suppress all warnings.
warnings.simplefilter("ignore")

ryaml = ruamel_yaml.YAML(typ="safe", pure=True)
ryaml.allow_duplicate_keys = True


@atheris.instrument_func
def TestOneInput(input_bytes):
  fdp = atheris.FuzzedDataProvider(input_bytes)
  data = fdp.ConsumeUnicode(sys.maxsize)

  try:
    iterator = ryaml.load_all(data)
    for _ in iterator:
      pass
  except ruamel_yaml.error.YAMLError:
    return

  except Exception:
    input_type = str(type(data))
    codepoints = [hex(ord(x)) for x in data]
    sys.stderr.write(
        "Input was {input_type}: {data}\nCodepoints: {codepoints}".format(
            input_type=input_type, data=data, codepoints=codepoints))
    raise


def main():
  atheris.Setup(sys.argv, TestOneInput)
  atheris.Fuzz()


if __name__ == "__main__":
  main()

Сперва, нам нужно определить, какие модули мы хотим инструментировать. Также имеется возможность инструментировать все модули: atheris.instrument_all(). Это может быть полезным, если у нас большой проект с множеством зависимостей. Затем мы должны реализовать функцию def TestOneInput(input_bytes):, подобно тому, как это делается для int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) на языках C/C++. Важно обрабатывать исключения, которые выкидывает целевая функция. Но мы должны ловить только те исключения, которые указаны разработчиками или их явно выкидывает эта функция. Например, IndexError, не нужно обрабатывать, если об этом явно не сказано в документации. Atheris сам поймает это исключение и сохранит, как аварийное завершение. Наконец, мы должны написать ещё пару строчек кода, чтобы запустить процесс фаззинга:

atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()

Что касается сборки, то мы можем просто выполнить команду pip install . в директории и установить инструментированный проект в наше фаззинг окружение. Хорошо, давайте соберём docker контейнер и начнём фаззинг!

Фаззинг

Перд тем как мы начнём, давайте посмотрим на yaml_fuzzer.toml:

exit-on-time = 3600

[atheris]
path = "/yaml_fuzzer.py"
args = "/corpus -dict=yaml.dict -jobs=1000 -workers=4"

Он очень простой.

exit-on-time — опциональный параметр, принимающий время в секундах. Если в течение этого времени (1 час в нашем случае) покрытие не увеличилось, то фаззинг завершается автоматически.

Я буду использовать 4 потока для фаззинга пока фаззер не найдёт 1000 аварийных завершений или не сработает exit-on-time. Начинаем фаззинг следующей командой:

# sydr-fuzz -c yaml_fuzzer.toml run
[2023-01-11 17:22:47] [INFO] #3582      RELOAD cov: 1178 ft: 5252 corp: 478/64Kb lim: 487 exec/s: 275 rss: 713Mb                                                     
[2023-01-11 17:22:48] [INFO] Uncaught Python exception: KeyError: (0, 1) /fuzz/yaml_fuzzer-out/crashes/crash-a0acd109aef7675ce2268eec4e0901759f4e1edc                      
[2023-01-11 17:22:50] [INFO] #17540     REDUCE cov: 1178 ft: 5257 corp: 511/86Kb lim: 481 exec/s: 343 rss: 677Mb L: 13/481 MS: 2 CrossOver-EraseBytes-                                                     
[2023-01-11 17:22:50] [INFO] #17573     REDUCE cov: 1178 ft: 5257 corp: 511/86Kb lim: 481 exec/s: 344 rss: 677Mb L: 58/481 MS: 3 ChangeBit-ManualDict-EraseBytes- DE: "'"- 
[2023-01-11 17:22:50] [INFO] Uncaught Python exception: KeyError: (1, 5) /fuzz/yaml_fuzzer-out/crashes/crash-4230d57dcf9dce49804ffd9abbc43a751068c6a2                                                          
[2023-01-11 17:22:55] [INFO] #1024      pulse  cov: 1171 ft: 4926 corp: 413/36Kb exec/s: 204 rss: 710Mb                                                                                                            
[2023-01-11 17:22:55] [INFO] [ATHERIS]         run time : 0 days, 0 hrs, 0 min, 57 sec                                                                                                                             
[2023-01-11 17:22:55] [INFO] [ATHERIS]    last new find : 0 days, 0 hrs, 0 min, 8 sec                                                                                                                              
[2023-01-11 17:22:57] [INFO] #1268      INITED cov: 1178 ft: 5265 corp: 477/60Kb exec/s: 181 rss: 710Mb

Через некоторое время мы нашли несколько аварийных завершений. Давайте подождём, когда фаззинг закончится.

[2023-01-11 19:21:27] [INFO] Uncaught Python exception: KeyError: (2, 1) /fuzz/yaml_fuzzer-out/crashes/crash-1f71bdb8cbba856923a45f50c7873bdb7ef64e2d
[2023-01-11 19:21:28] [INFO] Uncaught Python exception: KeyError: (1, 5) /fuzz/yaml_fuzzer-out/crashes/crash-c3152f019e73c4c01925ae1533f47583fe3006df
[2023-01-11 19:21:28] [INFO] Uncaught Python exception: KeyError: (2, 1) /fuzz/yaml_fuzzer-out/crashes/crash-fed9747bec6197c9c8cbc0cf10c051c8f807d407
[2023-01-11 19:21:28] [INFO] Uncaught Python exception: KeyError: (1, 0) /fuzz/yaml_fuzzer-out/crashes/crash-01ae8831870d95a5be5898dd17457235b851bfdf
[2023-01-11 19:21:41] [INFO] EXIT_ON_TIME: No new coverage (cov) for 3600 secs.
[2023-01-11 19:21:42] [INFO] EXIT_ON_TIME: No new coverage (cov) for 3600 secs.
[2023-01-11 19:21:42] [INFO] [RESULTS] Fuzzing corpus is saved in /fuzz/yaml_fuzzer-out/corpus
[2023-01-11 19:21:42] [INFO] [RESULTS] oom/leak/timeout/crash: 0/0/0/407
[2023-01-11 19:21:42] [INFO] [RESULTS] Fuzzing results are saved in /fuzz/yaml_fuzzer-out/crashes

Отлично, наш эксперимент закончился по exit-on-time. Итого мы получили 407 аварийных завершений. Это работа для casr.

Но сперва давайте минимизируем корпус:

# sydr-fuzz -c yaml_fuzzer.toml cmin
[2023-01-11 20:30:08] [INFO] Original fuzzing corpus saved as /fuzz/yaml_fuzzer-out/corpus-old
[2023-01-11 20:30:08] [INFO] Minimizing corpus /fuzz/yaml_fuzzer-out/corpus
[2023-01-11 20:30:08] [INFO] Using LD_PRELOAD="/usr/local/lib/python3.8/dist-packages/asan_with_fuzzer.so"
[2023-01-11 20:30:08] [INFO] ASAN_OPTIONS="abort_on_error=1,detect_leaks=0,malloc_context_size=0,symbolize=0,allocator_may_return_null=1"
[2023-01-11 20:30:08] [INFO] Launching atheris: "/yaml_fuzzer.py" "-merge=1" "-artifact_prefix=/fuzz/yaml_fuzzer-out/crashes/" "-close_fd_mask=2" "-verbosity=2" "-detect_leaks=0" "-dict=/fuzz/yaml.dict" "/fuzz/yaml_fuzzer-out/corpus" "/fuzz/yaml_fuzzer-out/corpus-old"
[2023-01-11 20:30:10] [INFO] MERGE-OUTER: 8719 files, 0 in the initial corpus, 0 processed earlier
[2023-01-11 20:30:10] [INFO] MERGE-OUTER: attempt 1
[2023-01-11 20:31:04] [INFO] MERGE-OUTER: successful in 1 attempt(s)
[2023-01-11 20:31:04] [INFO] MERGE-OUTER: the control file has 982127 bytes
[2023-01-11 20:31:04] [INFO] MERGE-OUTER: consumed 0Mb (120Mb rss) to parse the control file
[2023-01-11 20:31:04] [INFO] MERGE-OUTER: 913 new files with 7301 new features added; 1249 new coverage edges

Мы смогли сократить выходной корпус с 8719 файлов до 913 файлов, неплохо. Давайте соберём покрытие по исходному коду!

Покрытие

Для сбора покрытия мы будем использовать известный модуль coverage и инструкцию из репозитория Atheris. Конечно, это всё будет обёрнуто в удобную команду sydr-fuzz pycov. Давайте получим html отчёт о покрытии:

# sydr-fuzz -c yaml_fuzzer.toml pycov html
[2023-01-11 20:37:47] [INFO] Running pycov html "/fuzz/yaml_fuzzer.toml"
[2023-01-11 20:37:47] [INFO] Collecting coverage data for each file in corpus: /fuzz/yaml_fuzzer-out/corpus
[2023-01-11 20:37:47] [INFO] Saving coverage data to /fuzz/yaml_fuzzer-out/coverage/html/.coverage
[2023-01-11 20:37:47] [INFO] Using LD_PRELOAD="/usr/local/lib/python3.8/dist-packages/asan_with_fuzzer.so"
[2023-01-11 20:37:47] [INFO] ASAN_OPTIONS="abort_on_error=1,detect_leaks=0,malloc_context_size=0,symbolize=0,allocator_may_return_null=1"
[2023-01-11 20:37:47] [INFO] Collecting coverage: "coverage" "run" "/yaml_fuzzer.py" "-atheris_runs=914"
[2023-01-11 20:37:51] [INFO] Running coverage html: "coverage" "html" "-d" "/fuzz/yaml_fuzzer-out/coverage/html" "--data-file=/fuzz/yaml_fuzzer-out/coverage/html/.coverage"
Wrote HTML report to /fuzz/yaml_fuzzer-out/coverage/html/index.html

Прекрасно, теперь у нас есть покрытие по исходному коду, давайте на него посмотрим и двинемся дальше!

cov-html

Сортировка аварийных завершений

Как я говорил ранее, я буду использовать casr через команду sydr-fuzz casr для сортировки аварийных завершений:

# sydr-fuzz -c yaml_fuzzer.toml casr

Вы можете узнать больше о casr из репозитория casr или из другого моего гайда.

Давайте посмотрим, что нам выдал casr:

[2023-01-11 20:47:14] [INFO] Casr-cluster: deduplication of casr reports...
[2023-01-11 20:47:16] [INFO] Reports before deduplication: 407; after: 16
[2023-01-11 20:47:16] [INFO] Casr-cluster: clustering casr reports...
[2023-01-11 20:47:16] [INFO] Reports before clustering: 16. Clusters: 8
[2023-01-11 20:47:16] [INFO] Copying inputs...
[2023-01-11 20:47:16] [INFO] Done!
[2023-01-11 20:47:16] [INFO] ==> <cl1>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl1/crash-bf5829959ccf0211640314bb30de19bc9bafdeb3
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: KeyError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/resolver.py:361
[2023-01-11 20:47:16] [INFO]   Similar crashes: 1
[2023-01-11 20:47:16] [INFO] Cluster summary -> KeyError: 1
[2023-01-11 20:47:16] [INFO] ==> <cl2>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl2/crash-e126eb63b0bc1aefac72c3f56dea8484577f1007
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: RecursionError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/events.py:78
[2023-01-11 20:47:16] [INFO]   Similar crashes: 1
[2023-01-11 20:47:16] [INFO] Cluster summary -> RecursionError: 1
[2023-01-11 20:47:16] [INFO] ==> <cl3>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl3/crash-017ee5d1bb2bee51263f083eb12a60711a3c84f1
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: KeyError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/resolver.py:361
[2023-01-11 20:47:16] [INFO]   Similar crashes: 4
[2023-01-11 20:47:16] [INFO] Cluster summary -> KeyError: 4
[2023-01-11 20:47:16] [INFO] ==> <cl4>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl4/crash-3637416d80df3c5961e05b0bd459b79009e2a182
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: KeyError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/resolver.py:361
[2023-01-11 20:47:16] [INFO]   Similar crashes: 2
[2023-01-11 20:47:16] [INFO] Cluster summary -> KeyError: 2
[2023-01-11 20:47:16] [INFO] ==> <cl5>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl5/crash-01ae8831870d95a5be5898dd17457235b851bfdf
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: KeyError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/resolver.py:361
[2023-01-11 20:47:16] [INFO]   Similar crashes: 4
[2023-01-11 20:47:16] [INFO] Cluster summary -> KeyError: 4
[2023-01-11 20:47:16] [INFO] ==> <cl6>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl6/crash-0ea90a02b95f99e850b036e49419a43103a54149
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: ValueError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/constructor.py:533
[2023-01-11 20:47:16] [INFO]   Similar crashes: 1
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl6/crash-988f305721849b6a75af3b3f424b4593901630c3
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: ValueError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/constructor.py:498
[2023-01-11 20:47:16] [INFO]   Similar crashes: 1
[2023-01-11 20:47:16] [INFO] Cluster summary -> ValueError: 2
[2023-01-11 20:47:16] [INFO] ==> <cl7>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl7/crash-3f369c580ac61eded9d05eb06bc1ad6d0e90bfe1
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: ValueError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/constructor.py:498
[2023-01-11 20:47:16] [INFO]   Similar crashes: 1
[2023-01-11 20:47:16] [INFO] Cluster summary -> ValueError: 1
[2023-01-11 20:47:16] [INFO] ==> <cl8>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl8/crash-05451dc00f42aa97a064d2e08153bb84af113717
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: TypeError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/constructor.py:273
[2023-01-11 20:47:16] [INFO]   Similar crashes: 1
[2023-01-11 20:47:16] [INFO] Cluster summary -> TypeError: 1
[2023-01-11 20:47:16] [INFO] SUMMARY -> RecursionError: 1 KeyError: 11 ValueError: 3 TypeError: 1
[2023-01-11 20:47:16] [INFO] Crashes and Casr reports are saved in /fuzz/yaml_fuzzer-out/casr

После дедупликации у нас 16 аварийных завершений разбитых на 8 кластеров. Отлично, приемлемо для ручного анализа. Давайте посмотрим на какой-нибудь отчёт, например из 6ого кластера (cl6): casrep Необработанное исключение, вызванное ошибкой конвертации строки в число. Выглядит как потенциальная ошибка:).

Заключение

В заключении я бы хотел сказать, что Atheris хороший фаззер для Python кода. Интерфейс sydr-fuzz делает фаззинг боле удобным и приятным. Ну и конечно же casr, куда же без него! Сортировка аварийных завершений для Python сильно упрощает процесс работы.


Андрей Федотов