Фобос-НТ
Содержание

Повышение ставок: Находим баги, которые пропустил OSS-Fuzz

Нам зачастую приходится анализировать проекты, которые уже хорошо покрыты фаззинг-тестами на OSS-Fuzz. В данной статье поговорим о том, как мы используем результаты фаззинга на OSS-Fuzz для того, чтобы улучшить имеющиеся обёртки.

Для начала поговорим о том, что из себя представляет OSS-Fuzz. OSS-Fuzz - это проект от Google, который предоставляет свои вычислительные ресурсы для фаззинга наиболее важных и популярных проектов с открытым исходным кодом. Работает это следующим образом:

  1. в проект добавляют фаззинг-тесты в определённом формате. При этом эти фаззинг-тесты обычно пишут либо сами разработчики проекта, либо какие-нибудь внешние контрибьюторы.
  2. после этого проект добавляется в OSS-Fuzz и запускаются написанные фаззинг-тесты.
  3. если обнаруживаются падения при фаззинге, то отчёты о найденных проблемах приватно отправляются разработчикам.
  4. статистика по работе фаззинг-тестов (с задержкой) выкладывается в общий доступ.

Проект OSS-Fuzz показывает невероятную результативность: суммарно было обнаружено более 10000 уязвимостей, а общее число проектов на OSS-Fuzz более 1300.

As of August 2023, OSS-Fuzz has helped identify and fix over 10,000 vulnerabilities and 36,000 bugs across 1,000 projects.

Поэтому неудивительно, что для некоторых стало равносильны утверждения:
проект фаззится на OSS-Fuzz === В проекте нет багов

И действительно, логично предположить, что если какой-то код долгое время был покрыт фаззинг-тестами, то запустив этот же фаззинг-тест вероятность что-то найти крайне мала. Вот и получается, что для нас, как фаззинг-тестировщиков, приходится искать в проекте на OSS-Fuzz хоть какой-нибудь кодик, не покрытый текущими фаззинг-тестами(что, кстати, тоже зачастую приносит свои плоды, но об этом как-нибудь в другой раз).

Но на самом деле не всё так безнадёжно, даже в проектах на OSS-Fuzz можно находить баги. Покажем это на конкретном примере с openpyxl.

Openpyxl - популярная Python библиотека для чтения и записи Excel файлов. Очевидной поверхностью атаки в данном проекте является функционал, отвечающий за чтение и обработку Excel файлов. Ведь эти Excel файлы могут поступать из недоверенного источника, например, загрузка Excel-файлов на вебсайте. Для чтения Excel файлов в openpyxl используется функция load_workbook - отличная цель для фаззинга! Но прежде чем мы перейдём к написанию фаззинг-обёртки, которая будет тестировать данную функцию, неплохо бы убедиться в том, что openpyxl раньше не фаззился или хотя бы не фаззился функционал, связанный с обработкой Excel файлов.

Заходим на платформу со статистикой по фаззингу на OSS-Fuzz и, к нашему сожалению, убеждаемся, что проект openpyxl уже фаззится, причём уже написано 8 фаззинг-обёрток (а мы одну еле выбрали). Но давайте посмотрим на наработанное покрытие, может остались какие-нибудь участки, которые не покрыты текущими фаззинг-тестами. Как мы видим, около 70% кода библиотеки покрыто фаззинг-тестами, неплохой результат. Но кроме отчётов о покрытии по всем фаззинг-обёрткам для всего проекта, oss-fuzz также выкладывает отчёт от fuzz-introspector.

Fuzz-introspector - это утилита для определения фазз-блокеров, то есть таких состояний в программе, которые фаззер не может пройти по определённым причинам. Примером фазз-блокера может являться условие на какую-нибудь переменную окружения: очевидно, что фаззер самостоятельно никогда не установит переменную окружения и не пройдёт условие в программе. Fuzz-introspector определяет подобные места с фазз-блокерами с помощью наложения покрытия кода для отдельной фаззинг-обёртки на весь потенциально достигаемый код для этой фаззинг-обёртки. Таким образом, мы как бы сравниваем, то чего мы добились сейчас (наработанное покрытие) с тем, чего мы могли бы добиться в идеале (весь статически достигаемый код). Например, если мы покрываем только маленькую часть статически доступного кода (например только 30%), то стоит задать вопрос “а почему фаззинг-обёртка покрывает тестами так мало целевого кода?”. Далее можно определить конкретное место фазз-блокера для обёртки, посмотрев на её дерево вызовов с указанной статистикой по покрытию.

Но одно дело, если мы хотя бы покрывали 30% или 50% статически достигаемого кода, но что если мы покрываем примерно 0% доступного фаззинг-обёртке кода?...

Так и обстояли дела с обёрткой для функции load_workbook из openpyxl.

Получается, что этой фаззинг-обёртки как будто и не существовало, она абсолютно ничего не тестировала. Давайте разбираться, почему так происходило.

Начнём с того, что просто посмотрим на саму фаззинг-обёртку:

Как мы видим, в функции TestInput, которая является входной точкой для фаззера, открывается на запись файл test.xlsx и в него записываются данные от фаззера. После этого мы вызываем нашу целевую функцию load_workbook с этим файлом test.xlsx. Также интересно, что мы инструментируем для фаззера модуль zipfile, хотя казалось бы причём он тут... Получается, что логика фаззинг-обёртки довольно простая и даже правильная - мы просто вызываем нашу целевую функцию load_workbook с входными данными от фаззера, тогда в чём же проблема?

А проблема заключается в том, что Excel-файлы на самом деле представляют собой комплект стандартизованных xml-файлов, запакованных в zip архив. И получается, что для того чтобы добраться до логики обработки самих данных в openpyxl, фаззеру необходимо смутировать такой набор байтов, который будет представлять из себя валидный zip-архив, который openpyxl сможет распаковать и при этом получить определённую структуру xml-файлов. Вероятность такого события минимальна. Поэтому получается, что максимум что делает данная фаззинг-обёртка - это тестирует архиватор zip в Python, но это точно не то, чего мы хотели от неё.

Тогда определимся с тем, что мы хотим улучшить в данной обёртке:

  1. обёртка должна порождать валидные zip архивы.
  2. zip архивы должны распаковываться в заданную структуру xml-файлов.
  3. xml-файлы должны иметь валидную структуру, чтобы проходить этап парсинга xml-файлов (данная тема выходит за рамки данной статьи).

Начнём улучшать данную фаззинг-обёртку. Чтобы порождать на входе всегда валидные zip архивы, мы можем просто использовать zip архиватор, который будет запаковывать данные от фаззера в zip архив.

import zipfile
import atheris

with atheris.instrument_imports():
   import openpyxl

def TestInput(data):
   temp_file = "temp.xml"
   with open(temp_file, "wb") as fd:
       fd.write(data)
  
   with zipfile.ZipFile("test.xlsx", 'w') as zipf:
       zipf.write("temp.xml")
  
   wb2 = openpyxl.load_workbook("test.xlsx")

Сейчас мы имеем обёртку, которая решает первый пункт, то есть всегда проходит стадию распаковки zip архива. При этом стоит заметить, что мы убрали из под инструментации модуль zipfile, поскольку он не является целью нашего тестирования. Теперь мы хотим, чтобы этот zip архив состоял из определённых файлов, а не только из temp.xml, как это реализовано сейчас у нас.

Здесь мы должны решить, какой именно функционал по обработке Excel-файлов мы хотим протестировать. Например, мы хотим сосредоточиться на части кода, которая обрабатывает стили, тогда мы должны мутировать файл xl/styles.xml. Если мы хотим сосредоточиться на обработке конкретного листа, то мы можем мутировать только содержимое файла xl/worksheets/sheet1.xml. На самом деле, мы можем разделять наш набор байтов от фаззера и мутировать сразу несколько xml-файлов. Но в нашем тесте мы сосредоточимся на обработке данных, форматировании и парсинге структуры листа электронной таблицы Excel (файл sheet1.xml). Тогда мы можем модифицировать нашу фаззинг-обёртку следующим образом:

def add_file_to_zip(file_name, zip_path='temp.xlsx'):
   shutil.copyfile("test.xlsx", zip_path)

   # Добавляем файл в архив
   try:
       with zipfile.ZipFile(zip_path, 'a') as zipf:
           zipf.write(file_name, arcname="xl/worksheets/sheet1.xml")
   except Exception as e:
       print(f"Произошла ошибка: {e}")
       return False
  
   return True

def TestInput(data):
   temp_file = "test/xl/worksheets/sheet1.xml"
   with open(temp_file, "wb") as fd:
       fd.write(data)

   if not add_file_to_zip(temp_file):
       return

   wb = openpyxl.load_workbook('temp.xlsx')

При этом файл test.xlsx уже должен существовать и должен быть валидным Excel-файлом. В этом test.xlsx мы будем заменять только файл xl/worksheets/sheet1.xml, а остальное будет оставаться не тронутым. Таким образом, мы решили вторую подзадачу.

Чтобы реализовать третий пункт с более эффективной мутацией xml-файлов, мы могли бы добавить подход structure-aware fuzzing в данную обёртку, чтобы генерировать данные в xml-формате.

structure-aware fuzzing (структурный фаззинг) - это техника фаззинга, при которой фаззеру предоставляется информация о структуре входных данных, что позволяет ему создавать более корректные и осмысленные тесты для выявления ошибок. Подробнее о подходах к структурному фаззинг.

Ведь мы сейчас не очень заинтересованы в тестировании парсера xml, но при нашем подходе фаззер зачастую будет генерировать невалидные xml-файлы, которые не будут проходить стадию парсинга, и поэтому мы зачастую не будем пробиваться до логики самого модуля openpyxl. Но даже при таком подходе мы сразу же обнаруживаем несколько уникальных падений. О найденных падениях мы сообщили разработчикам, но, к сожалению, пока так и не получили ответа.

Далее и эту фаззинг-обёртку можно улучшить, добавив к ней подход structure-aware fuzzing, который позволит нам реализовать пункт 3) из задуманного, а именно чаще генерировать структурно валидные xml-файлы. Но об этом мы поговорим в следующих статьях.

Автор:
Ревякин Леонид Константинович, ведущий специалист - руководитель группы автоматизации фаззинг-тестирования

Ключевые слова

На нашем сайте мы используем cookie файлы, содержащие информацию о предыдущих посещениях веб-сайта. Данные обрабатываются для улучшения качества работы нашего веб-сайта. Если вы не хотите использовать cookie файлы, измените настройки браузера.