-
Notifications
You must be signed in to change notification settings - Fork 36
Fuzzing ruamel yaml (Python) project with sydr fuzz (rus)
В этой статье я бы хотел поделиться своим опытом фаззинга проектов на языке Python. Для этих целей я буду использовать sydr-fuzz с поддержкой Atheris и python-afl. Sydr-fuzz изначально разрабатывался как гибридный фаззер, который комбинирует инструмент динамического символьного выполнения Sydr и современные инструменты фаззинга AFLplusplus и libFuzzer. Также sydr-fuzz поддерживает несколько полезных фич, таких как сортировка аварийных завершений с помощью casr, возможность проверки предикатов безопасности, удобные команды для минимизации корпуса и сбора покрытия по исходному коду. Atheris и python-afl — это движки фаззинга с обратной связью по покрытию для Python языка. Они поддерживает фаззинг кода на языке Python, кроме того и фаззинг нативных расширений для CPython. Atheris основан на libFuzzer, a python-afl на AFL++. Они выглядит и работают как libFuzzer и AFL++, поэтому мы решили поддержать возможность работы с ним через sydr-fuzz, почему нет? Хотя у нас нет динамического символьного выполнения для кода на языке Python, но мы можем фаззить, делать сортировку аварийных завершений, минимизировать корпус и собирать покрытие по исходному коду через удобный интерфейс sydr-fuzz.
Страница Atheris на GitHub предоставляет отличную инструкцию о том, как его установить и использовать. Мы будем фаззить yaml проект из его примеров. У нас уже есть подготовленный к сборке docker контейнер с необходимым окружением для фаззинга. Я буду его использовать в своих экспериментах, но сейчас давайте рассмотрим фаззинг цель и скрипт сборки.
#!/usr/bin/env python3
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 требуется определить модули, которые требуется проинструментировать для сбора метрик фаззинга внутри них. Это подробно описано в (https://github.com/google/atheris/tree/master). Мы воспользуемся with atheris.instrument_imports():
для инструментации перечисленных модулей, а также @atheris.instrument_func для инструментации инструкций внутри функции.
Также имеется возможность инструментировать все модули: atheris.instrument_all()
. Это может быть полезным, если у нас большой проект с множеством зависимостей.
Затем мы должны реализовать функцию def TestOneInput(input_bytes):
, подобно тому, как это делается для int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
на языках C/C++. Важно обрабатывать исключения, которые выкидывает целевая функция. Но мы должны ловить только те исключения, которые указаны разработчиками или их явно выкидывает эта функция. Например, IndexError, не нужно обрабатывать, если об этом явно не сказано в документации. Atheris и python-afl сами поймают это исключение и сохранят, как аварийное завершение.
Для фаззинга при помощи python-afl я внесу необходимые правки, о которых можно подробнее прочитать в литературе по python-afl
#!/usr/bin/python3
import afl, sys, os
from ruamel import yaml as ruamel_yaml
import warnings
# Suppress all warnings.
warnings.simplefilter("ignore")
def _ConsumeString(res_len, data):
...
def TestOneInput(input_bytes):
ryaml = ruamel_yaml.YAML(typ="safe", pure=True)
ryaml.allow_duplicate_keys = True
data = _ConsumeString(sys.maxsize, input_bytes)
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():
# выбираем откуда считывать stdin
try:
# Python 3:
stdin_compat = sys.stdin.buffer
except AttributeError:
# There is no buffer attribute in Python 2:
stdin_compat = sys.stdin
while afl.loop(10000):
TestOneInput(stdin_compat.read())
sys.stdin.seek(0) # очистка stdin между запусками
os._exit(0)
if __name__ == "__main__":
main()
Может возникнуть вопрос — почему shebang (#!/usr/bin/python3) был изменён — это не случайность, у python-afl c asan при использовании run/cmin есть вероятность столкнуться с множеством ошибок вида "[CMIN] du: cannot access ..." или же "Fork server handshake failed ...", и исправлением (при условии, что всё остальное настроено правильно) обнаруженных мной причин является использование shebang без env — так что будьте аккуратны при использовании python-afl.
(но подобные ошибки могут и просто означать, что таргет некорректен (не компилится/...))
Аналогично, пренеприятная проблема с артефактами вида зависающего cmin/сборки покрытия может произойти в очень небольшом проценте систем при использовании LD_PRELOAD — у неё есть решение (https://github.com/actions/runner-images/issues/9524), но простой путь — перейти на python-afl.
Наконец, мы должны написать ещё пару строчек кода, чтобы запустить процесс фаззинга:
В случае Atheris:
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
В случае python-afl:
while afl.loop(10000):
TestOneInput(stdin_compat.read())
sys.stdin.seek(0) # очистка stdin между запусками
os._exit(0)
Выше мы инициализируем python-afl в рекомендованном Persistent mode — он работает на порядки быстрее инициализации приведённой ниже, но глобальные/статические переменные могут повлиять на успешность фаззинга. Важно знать, что он будет вызывать ошибки при работе с целями, интрументированными afl (например msgspec с Cython внутри, который можно дополнительно инструментировать). В таком случае можно использовать стандартный режим:
afl.init()
TestOneInput(stdin_compat.read())
os._exit(0)
Либо, при желании, можно применить ухищрение и импортировать интрументированные библиотеки внутри TestOneInput - таким образом мы останемся в Persistent mode, но уменьшим stability (и возможно фаззинг будет не совсем корректным), но будем работать значительно быстрее стандартного режима.
В нашем случае этого не требуется (так как нет внутренностей, которые мы могли бы дополнительно проинструментировать), но хочется продемонстрировать вышесказанное ухищрение:
def TestOneInput(input_bytes):
# тот самый импорт, о котором шла речь
from ruamel import yaml as ruamel_yaml
ryaml = ruamel_yaml.YAML(typ="safe", pure=True)
ryaml.allow_duplicate_keys = True
# ... идентичный код
while afl.loop(10000):
TestOneInput(stdin_compat.read())
sys.stdin.seek(0)
os._exit(0)
Так же мы добавили строчки получения stdin для различных версий Python и _ConsumeString принимающую байты от фаззинга и конвертирующую их в строки Python(так как для python-afl на данный момент нет готовых удобных конвертаций - можно использовать конвертации от atheris, но, возможно фаззинг немного замедлится), но они нам не так интересны.
Что касается сборки, то мы можем просто выполнить команду pip install .
в директории и установить инструментированный проект в наше фаззинг окружение. Хорошо, давайте соберём docker контейнер и начнём фаззинг!
В случае сборки для python-afl библиотек содержащих C/C++ могут возникнуть определённые трудности — возможно вам помогут примеры сборок из нами поддержанных target-ов в (https://github.com/ispras/oss-sydr-fuzz). На момент написания wiki это msgspec и ultrajson.
Перед тем как мы начнём, давайте посмотрим на yaml_fuzzer-atheris.toml:
exit-on-time = 3600
[atheris]
path = "/yaml_fuzzer-atheris.py"
args = "/corpus -dict=yaml.dict -jobs=1000 -workers=4"
А так же на yaml_fuzzer-pyafl.toml:
exit-on-time = 3600
[pyafl]
args = "-i /corpus"
target = "/fuzz/yaml_fuzzer_pyafl.py"
jobs = 4
Они очень простые.
exit-on-time — опциональный параметр, принимающий время в секундах. Если в течение этого времени (1 час в нашем случае) покрытие не увеличилось, то фаззинг завершается автоматически.
Я буду использовать 4 потока для фаззинга пока фаззер не найдёт 1000 аварийных завершений или не сработает exit-on-time. Начинаем фаззинг с помощью Atheris следующей командой:
# sydr-fuzz -c yaml_fuzzer-atheris.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-atheris.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-atheris.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-atheris.toml pycov html
[2023-01-11 20:37:47] [INFO] Running pycov html "/fuzz/yaml_fuzzer-atheris.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-atheris.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
Прекрасно, теперь у нас есть покрытие по исходному коду, давайте на него посмотрим и двинемся дальше!
Как я говорил ранее, я буду использовать casr через команду sydr-fuzz casr
для сортировки аварийных завершений:
# sydr-fuzz -c yaml_fuzzer-atheris.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
):
Необработанное исключение, вызванное ошибкой конвертации строки в число. Выглядит как потенциальная ошибка:).
Теперь сделаем аналогичные шаги для python-afl.
Я буду использовать 4 потока для фаззинга пока не сработает exit-on-time. Начинаем фаззинг с помощью python-afl следующей командой:
# sydr-fuzz -c yaml_fuzzer-pyafl.toml run
[2025-08-15 14:05:47] [INFO] [AFL++] [*] Attempting dry run with 'id:000009,time:0,execs:0,orig:yaml-version.yaml'...
[2025-08-15 14:05:47] [INFO] [AFL++] len = 24, map size = 1570, exec speed = 5423 us, hash = 26807c104c8188ff
[2025-08-15 14:05:47] [INFO] [AFL++] [!] WARNING: Instrumentation output varies across runs.
[2025-08-15 14:05:47] [INFO] [AFL++] [+] All test cases processed.
[2025-08-15 14:05:47] [INFO] [AFL++] [!] WARNING: The target binary is pretty slow! See /usr/local/share/doc/afl/fuzzing_in_depth.md#i-improve-the-speed
[2025-08-15 14:05:47] [INFO] [AFL++] [+] Here are some useful stats:
[2025-08-15 14:05:47] [INFO] [AFL++] Test case count : 8 favored, 4 variable, 0 ignored, 10 total
[2025-08-15 14:05:47] [INFO] [AFL++] Bitmap range : 1160 to 2057 bits (average: 1525.50 bits)
[2025-08-15 14:05:47] [INFO] [AFL++] Exec timing : 2685 to 48.2k us (average: 12.7k us)
[2025-08-15 14:05:47] [INFO] [AFL++]
[2025-08-15 14:05:47] [INFO] [AFL++] [*] -t option specified. We'll use an exec timeout of 2000 ms.
[2025-08-15 14:05:47] [INFO] [AFL++] [+] All set and ready to roll!
[2025-08-15 14:05:47] [INFO] [AFL++] [*] Entering queue cycle 1
[2025-08-15 14:05:47] [INFO] [AFL++]
[2025-08-15 14:05:47] [INFO] [AFL++] [*] Fuzzing test case #1 (10 total, 0 crashes saved, state: started :-), mode=explore, perf_score=100, weight=1, favorite=1, was_fuzzed=0, exec_us=2685, hits=0, map=1360, ascii=0, run_time=0:00:00:00)...
[2025-08-15 14:06:16] [INFO] Found crash /fuzz/yaml_fuzzer_pyafl-out/crashes/crash-bb4d05f035d758b66070c9250124988e81a168d7
Через некоторое время мы нашли несколько аварийных завершений. Давайте подождём, когда фаззинг закончится.
[2025-08-17 09:45:51] [INFO] [AFL++] +++ Testing aborted programmatically +++
[2025-08-17 09:45:51] [INFO] [AFL++] [!]
[2025-08-17 09:45:51] [INFO] [AFL++] Performing final sync, this make take some time ...
[2025-08-17 09:45:51] [INFO] [AFL++] [!] Done!
[2025-08-17 09:45:51] [INFO] [AFL++] [*] Writing /fuzz/yaml_fuzzer_pyafl-out/aflplusplus/afl_main-worker/fastresume.bin ...
[2025-08-17 09:45:51] [INFO] [AFL++] [+] Written fastresume.bin with 2431192 bytes!
[2025-08-17 09:45:51] [INFO] [AFL++] [+] We're done here. Have a nice day!
[2025-08-17 09:45:51] [INFO] [AFL++]
[2025-08-17 09:45:51] [INFO] [RESULTS] Fuzzing corpuses are saved in workers queue directories. Run sydr-fuzz cmin subcommand to gather full corpus at "/fuzz/yaml_fuzzer_pyafl-out/corpus-old" and minimized corpus at "/fuzz/yaml_fuzzer_pyafl-out/corpus".
[2025-08-17 09:45:51] [INFO] [RESULTS] [afl_main] 1675 new corpus items found, 9.46% coverage achieved, 197 crashes saved, 131 timeouts saved, total runtime 1 days, 15 hrs, 36 min, 53 sec
[2025-08-17 09:45:51] [INFO] [RESULTS] execs done: 27544322, execs/s: 193.14, edges found: 6202/65536, stability: 66.01%
[2025-08-17 09:45:51] [INFO] [RESULTS] [afl_s01] 1422 new corpus items found, 9.35% coverage achieved, 168 crashes saved, 128 timeouts saved, total runtime 1 days, 15 hrs, 36 min, 57 sec
[2025-08-17 09:45:51] [INFO] [RESULTS] execs done: 15183330, execs/s: 106.46, edges found: 6126/65536, stability: 67.37%
[2025-08-17 09:45:51] [INFO] [RESULTS] [afl_s02] 1470 new corpus items found, 9.50% coverage achieved, 218 crashes saved, 111 timeouts saved, total runtime 1 days, 15 hrs, 36 min, 58 sec
[2025-08-17 09:45:51] [INFO] [RESULTS] execs done: 16792546, execs/s: 117.74, edges found: 6226/65536, stability: 83.99%
[2025-08-17 09:45:51] [INFO] [RESULTS] [afl_s03] 1359 new corpus items found, 9.41% coverage achieved, 203 crashes saved, 118 timeouts saved, total runtime 1 days, 15 hrs, 36 min, 55 sec
[2025-08-17 09:45:51] [INFO] [RESULTS] execs done: 16465514, execs/s: 115.45, edges found: 6168/65536, stability: 66.97%
[2025-08-17 09:45:51] [INFO] [RESULTS] timeout/crash: 488/785
Перед сбором покрытия давайте минимизируем корпус:
# sydr-fuzz -c yaml_fuzzer-pyafl.toml cmin
[2025-08-18 11:42:24] [INFO] [CMIN] [+] Found 28239 unique tuples across 10970 files.
[2025-08-18 11:42:24] [INFO] [CMIN] [+] Narrowed down to 734 files, saved in '/fuzz/yaml_fuzzer_pyafl-out/corpus'.
Для сбора покрытия мы будем использовать известный модуль [coverage] аналогично Atheris, но так как покрытие в формате html уже получили , то посмотрим в формате консольного вывода:
# sydr-fuzz -c yaml_fuzzer-pyafl.toml pycov report
[2025-09-29 16:04:48] [INFO] Configured to run with AFL_PRELOAD SET
[2025-09-29 16:04:48] [INFO] Running pycov report "/fuzz/yaml_fuzzer_pyafl.toml"
[2025-09-29 16:04:48] [INFO] Collecting coverage data for each file in corpus: /fuzz/yaml_fuzzer_pyafl-out/corpus
[2025-09-29 16:04:48] [INFO] Saving coverage data to /fuzz/yaml_fuzzer_pyafl-out/coverage/report/.coverage
[2025-09-29 16:04:48] [INFO] pycov environment: AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 AFL_SYNC_TIME=1 ASAN_OPTIONS=abort_on_error=1,allocator_may_return_null=1,detect_leaks=0,hard_rss_limit_mb=2048,malloc_context_size=0,symbolize=0,verify_asan_link_order=0 PYTHON_AFL_PERSISTENT=1 UBSAN_OPTIONS=abort_on_error=0,allocator_may_return_null=1,halt_on_error=0,malloc_context_size=0
[2025-09-29 16:04:48] [INFO] Collecting coverage: cd "/fuzz/yaml_fuzzer_pyafl-out/coverage/report" && AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES="1" AFL_SYNC_TIME="1" ASAN_OPTIONS="abort_on_error=1,allocator_may_return_null=1,detect_leaks=0,hard_rss_limit_mb=2048,malloc_context_size=0,symbolize=0,verify_asan_link_order=0" PYTHON_AFL_PERSISTENT="1" UBSAN_OPTIONS="abort_on_error=0,allocator_may_return_null=1,halt_on_error=0,malloc_context_size=0" "/bin/bash" "-c" "coverage run --include=*/site-packages/*,*/dist-packages/*,/fuzz/yaml_fuzzer_pyafl.py --omit=*/coverage/* /fuzz/yaml_fuzzer_pyafl-out/coverage/pyAflCovWrapper.py /fuzz/yaml_fuzzer_pyafl-out/corpus"
[2025-09-29 16:04:49] [INFO] Running coverage report: AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES="1" AFL_SYNC_TIME="1" ASAN_OPTIONS="abort_on_error=1,allocator_may_return_null=1,detect_leaks=0,hard_rss_limit_mb=2048,malloc_context_size=0,symbolize=0,verify_asan_link_order=0" PYTHON_AFL_PERSISTENT="1" UBSAN_OPTIONS="abort_on_error=0,allocator_may_return_null=1,halt_on_error=0,malloc_context_size=0" "coverage" "report" "--data-file=/fuzz/yaml_fuzzer_pyafl-out/coverage/report/.coverage" "--ignore-errors"
Name Stmts Miss Cover
----------------------------------------------------------------------------------------
/usr/local/lib/python3.9/dist-packages/_distutils_hack/__init__.py 101 96 5%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/__init__.py 9 2 78%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/anchor.py 10 4 60%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/comments.py 777 552 29%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/compat.py 154 87 44%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/composer.py 125 23 82%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py 1062 767 28%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/cyaml.py 42 25 40%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/dumper.py 29 16 45%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/emitter.py 1136 1046 8%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/error.py 163 103 37%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/events.py 81 4 95%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/loader.py 43 28 35%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/main.py 828 609 26%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/nodes.py 52 18 65%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/parser.py 466 161 65%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/reader.py 180 89 51%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/representer.py 778 634 19%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/resolver.py 220 122 45%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/scalarbool.py 23 15 35%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/scalarfloat.py 69 50 28%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/scalarint.py 67 44 34%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/scalarstring.py 75 42 44%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/scanner.py 1327 613 54%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/serializer.py 141 118 16%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/timestamp.py 33 26 21%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/tokens.py 236 117 50%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/util.py 142 121 15%
yaml_fuzzer_pyafl.py 64 26 59%
----------------------------------------------------------------------------------------
TOTAL 8433 5558 34%
Снова воспользуемся sydr-fuzz casr
для сортировки аварийных завершений:
# sydr-fuzz -c yaml_fuzzer-pyafl.toml casr
Давайте посмотрим, что нам выдал casr:
[2025-08-18 13:08:49] [INFO] [CASR] Progress: 785/786
[2025-08-18 13:08:50] [INFO] [CASR] Deduplicating CASR reports...
[2025-08-18 13:08:53] [INFO] [CASR] Number of reports before deduplication: 586. Number of reports after deduplication: 32
[2025-08-18 13:08:53] [INFO] [CASR] Clustering CASR reports...
[2025-08-18 13:08:54] [INFO] [CASR] Number of clusters: 11
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl1>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl1/id:000140,sig:10,src:000473+001776,time:97076090,execs:8677884,op:splice,rep:1
[2025-08-18 13:08:54] [INFO] [CASR] casrep: NOT_EXPLOITABLE: OverflowError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/scanner.py:1471
[2025-08-18 13:08:54] [INFO] [CASR] Similar crashes: 1
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> OverflowError: 1
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl2>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl2/id:000094,sig:10,src:000199+002204,time:28614928,execs:2898551,op:splice,rep:4
[2025-08-18 13:08:54] [INFO] [CASR] casrep: NOT_EXPLOITABLE: TypeError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:273
[2025-08-18 13:08:54] [INFO] [CASR] Similar crashes: 2
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> TypeError: 2
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl3>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl3/id:000017,sig:10,src:001539,time:1026063,execs:292379,op:havoc,rep:3
[2025-08-18 13:08:54] [INFO] [CASR] casrep: NOT_EXPLOITABLE: TypeError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:273
[2025-08-18 13:08:54] [INFO] [CASR] Similar crashes: 2
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> TypeError: 2
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl4>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl4/id:000017,sig:10,src:001221+000530,time:517518,execs:247659,op:splice,rep:1
[2025-08-18 13:08:54] [INFO] [CASR] casrep: NOT_EXPLOITABLE: TypeError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:273
[2025-08-18 13:08:54] [INFO] [CASR] Similar crashes: 3
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> TypeError: 3
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl5>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl5/id:000006,sig:10,src:000827,time:221156,execs:128170,op:havoc,rep:1
[2025-08-18 13:08:54] [INFO] [CASR] casrep: NOT_EXPLOITABLE: KeyError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/resolver.py:361
[2025-08-18 13:08:54] [INFO] [CASR] Similar crashes: 4
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> KeyError: 4
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl6>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl6/id:000043,sig:10,src:001293,time:1646571,execs:573887,op:havoc,rep:1
[2025-08-18 13:08:54] [INFO] [CASR] casrep: NOT_EXPLOITABLE: TypeError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:266
[2025-08-18 13:08:54] [INFO] [CASR] Similar crashes: 1
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl6/id:000000,sig:10,src:000004,time:59104,execs:44738,op:havoc,rep:2
[2025-08-18 13:08:54] [INFO] [CASR] casrep: NOT_EXPLOITABLE: TypeError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:273
[2025-08-18 13:08:54] [INFO] [CASR] Similar crashes: 1
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> TypeError: 2
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl7>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl7/id:000000,sig:10,src:000009,time:38807,execs:31142,op:havoc,rep:1
[2025-08-18 13:08:54] [INFO] [CASR] casrep: NOT_EXPLOITABLE: KeyError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/resolver.py:361
[2025-08-18 13:08:54] [INFO] [CASR] Similar crashes: 4
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> KeyError: 4
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl8>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl8/id:000006,sig:10,src:000534+000049,time:354776,execs:170563,op:splice,rep:7
[2025-08-18 13:08:54] [INFO] [CASR] casrep: NOT_EXPLOITABLE: TypeError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:273
[2025-08-18 13:08:54] [INFO] [CASR] Similar crashes: 4
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> TypeError: 4
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl9>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl9/id:000085,sig:10,src:001266,time:12587845,execs:3097022,op:havoc,rep:2
[2025-08-18 13:08:54] [INFO] [CASR] casrep: NOT_EXPLOITABLE: OverflowError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/scanner.py:1471
[2025-08-18 13:08:54] [INFO] [CASR] Similar crashes: 2
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> OverflowError: 2
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl10>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl10/id:000021,sig:10,src:000128+000228,time:1422306,execs:403476,op:splice,rep:2
[2025-08-18 13:08:54] [INFO] [CASR] casrep: NOT_EXPLOITABLE: ValueError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:498
[2025-08-18 13:08:54] [INFO] [CASR] Similar crashes: 3
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> ValueError: 3
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl11>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl11/id:000005,sig:10,src:000833,time:218012,execs:126600,op:havoc,rep:2
[2025-08-18 13:08:54] [INFO] [CASR] casrep: NOT_EXPLOITABLE: KeyError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/resolver.py:361
[2025-08-18 13:08:54] [INFO] [CASR] Similar crashes: 5
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> KeyError: 5
[2025-08-18 13:08:54] [INFO] [CASR] SUMMARY -> KeyError: 13 OverflowError: 3 TypeError: 13 ValueError: 3
[2025-08-18 13:08:54] [INFO] [CASR] 200 timeout seeds are saved to "/fuzz/yaml_fuzzer_pyafl-out/casr/timeout"
[2025-08-18 13:08:54] [INFO] Crashes and Casr reports are saved in /fuzz/yaml_fuzzer_pyafl-out/casr
Так же хочется обратить внимание на недавно добавленную поддержку виртуальных окружений в Atheris и python-afl — работа с ними демонстрируется в фаззинге msgspec.
С ними возможно фаззить Python таргеты при помощи Atheris и python-afl без ребилда инструментируемых библиотек. build скрипт в msgspec на момент написания wiki с небольшими упрощениями выглядит так:
... # do some msgspec scpefic job
python3 -m venv --system-site-packages /atherisVenv
python3 -m venv --system-site-packages /pyAflVenv
# prepare python-afl venv
source /pyAflVenv/bin/activate
pip install python-afl --ignore-installed
pip install coverage --ignore-installed
... # do some msgspec scpefic job
cd /msgspec
MSGSPEC_DEBUG=1 CC=afl-clang-fast CFLAGS="-fsanitize=address -Wl,-rpath=/usr/lib/clang/14.0.6/lib/linux/" LDFLAGS="/usr/local/lib/afl/afl-compiler-rt.o /usr/lib/clang/14.0.6/lib/linux/libclang_rt.asan-x86_64.so" LDSHARED="clang -shared" pip3 install --ignore-installed .
rm -rf build
deactivate
# Prepare Atheris venv
source /atherisVenv/bin/activate
pip install atheris --ignore-installed
pip install coverage --ignore-installed
... # do some msgspec scpefic job
cd /msgspec
MSGSPEC_DEBUG=1 CC=clang CFLAGS="-g -fsanitize=fuzzer-no-link,address" LDSHARED="clang -shared" pip3 install --ignore-installed .
deactivate
А конфигурационные toml файлы для таргетов дополнительно содержат поле
venv = "/atherisVenv/"
или
venv = "/pyAflVenv/"
В заключении я бы хотел сказать, что Atheris и python-afl — хорошие фаззеры для Python кода. Интерфейс sydr-fuzz делает фаззинг более удобным и приятным. Ну и конечно же casr, куда же без него! Сортировка аварийных завершений для Python сильно упрощает процесс работы.
Андрей Федотов и Алексей Маренков