-
Notifications
You must be signed in to change notification settings - Fork 32
Fuzzing golang image (Go) project with sydr fuzz (go fuzz backend) (rus)
В этой статье мы попробуем применить гибридный подход к фаззингу с помощью инструмента sydr-fuzz для проектов на языке Go. Sydr-fuzz сочитает в себе преимущества инструмента динамического символьного выполнения Sydr и фаззера AFLplusplus. Он также поддерживает другой движок фаззинга libFuzzer, который мы будем использовать. Нам повезло, go-fuzz имеет поддержку libFuzzer'а, поэтому мы можем попробовать его использовать как движок libFuzzer'а в sydr-fuzz. Мы сосредоточимся в этом гайде на создании фаззинг-целей для go-fuzz(libFuzzer), Sydr и сбора покрытия. Мы проведём гибридный фаззинг с использованием Sydr и go-fuzz, а потом собирём покрытие по исходному коду. Также мы будем использовать инструмент для сортировки аварийных завершений casr, и проверим предикаты безопасности с помощью Sydr. И конечно, я расскажу об интересных случиях, которые мне встретились во время моего знакомства с фаззингом Go проектов.
Парсеры изображений, такие как golang/image являются хорошими примерами для фаззинга. Перед тем как я начну, есть уже готовый для сборки докер [контейнер] (https://github.com/ispras/oss-sydr-fuzz/tree/master/projects/image-go) со всем необходимым окружением для фаззинга: фазиннг-цели для go-fuzz, Sydr и покрытия по коду.
В репозитории go-fuzz есть подробная инструкция о том, как подготовить фаззинг-цели и начать фаззинг. Также, есть отличный репозиторий go-fuzz-corpus где вы можете найти фаззинг-цели и начальный корпус данных. В этом репозитории есть и фаззинг-цели для проекта golang/image (png, webp, tiff, jpeg, и т.д.). Исходя из гайда по проекту go-fuzz, мы можем собрать фаззинг-цели прям в репозитории go-fuzz-corpus, но давайте возмём их, представим, что разрабатываем их с нуля сами, может что-то поменяем.
Хорошо, давайте напишем фаззинг-цель для декодера webp. Сперва, я клонирую репозиторий golang/image
и создаю модуль fuzz.go
, со следующим содержимым:
package image
import (
"bytes"
"golang.org/x/image/webp"
)
func FuzzWebp(data []byte) int {
cfg, err := webp.DecodeConfig(bytes.NewReader(data))
if err != nil {
return 0
}
if cfg.Width*cfg.Height > 4000000 {
return 0
}
if _, err := webp.Decode(bytes.NewReader(data)); err != nil {
return 0
}
return 1
}
Я немного изменил оригинальную фаззинг-цель используя фаззинг-цель из image-rs webp target:
if cfg.Width*cfg.Height > 4000000 { // originally 1e6
Для сборки фаззинг-цели нам необходимо просто выполнить команду go-fuzz-build
с опцией -libfuzzer
:
go-fuzz-build -libfuzzer -func=FuzzWebp -o webp.a
clang -fsanitize=fuzzer webp.a -o fuzz_webp
Отлично, сейчас мы имеем фаззинг-цель для Go проекта которая выглядит и работает как фаззинг-цель для libFuzzer
. Давайте собирём цель для DSE инструмента (Sydr). Для этого мы создадим исполняемый файл, который получает входные данные из файла и вызывает функцию FuzzWebp
. Создадим файл cmd/sydr_webp/main.go
со следующим содержимым:
package main
import (
"os"
"golang.org/x/image"
)
func main() {
data, _ := os.ReadFile(os.Args[1])
image.FuzzWebp(data)
}
Для сборки DSE-цели (Sydr) нам нужно выполнить следующую команду:
cd cmd/sydr_webp && go build
Прекрасно, у нас есть sydr_webp
бинарник для Sydr. Нам остаётся только собрать бинарник для сбора покрытия по исходному коду. Мне протребовалось немало времи, чтобы разобраться с этим. У нас нет опций компилятора, таких как "-C instrument-coverage"
для компилятора Rust, или "-fprofile-instr-generate -fcoverage-mapping"
для компилятора clang/clang++. В OSS-Fuzz сделано немало работы, но это всё затруднительно использовать не в инфраструктуре OSS-Fuzz. Я обнаружил интересный способ подходяжий мне и подходу гибридного фаззинга, который я использую. Давайте поговорим об этом чуть позже в секции Покрытие. Пред тем, как мы начнём фаззинг, давайте собирём докер контейнер.
Мы начинаем гибридный подход к фаззингу используя sydr-fuzz с Sydr и go-fuzz. Вот конфигурационный файл для sydr-fuzz:
[sydr]
target = "/image/cmd/sydr_webp/sydr_webp @@"
[libfuzzer]
path = "/image/fuzz_webp"
args = "-dict=/webp.dict -rss_limit_mb=8192 /go-fuzz-corpus/webp/corpus"
Давайте начнём фаззинг:
# sydr-fuzz -c webp.toml run
Исходя из логов, через 6 часлов мы нашли первое аварийное завершение, отлично!
[INFO] #5398923 REDUCE ft: 6376 corp: 1094/12459Kb lim: 175231 exec/s: 226 rss: 1516Mb L: 69/175231 MS: 1 EraseBytes-
[INFO] [LIBFUZZER] run time : 0 days, 6 hrs, 36 min, 54 sec
[INFO] [LIBFUZZER] last new find : 0 days, 0 hrs, 0 min, 30 sec
[INFO] #5402010 REDUCE ft: 6376 corp: 1094/12459Kb lim: 175231 exec/s: 226 rss: 1516Mb L: 2450/175231 MS: 5 ChangeByte-ManualDict-InsertRepeatedBytes-EraseBytes-PersAutoDict- DE: "CCIP"-"AIMN"-
[INFO] #5402116 REDUCE ft: 6376 corp: 1094/12459Kb lim: 175231 exec/s: 226 rss: 1516Mb L: 49/175231 MS: 1 EraseBytes-
[INFO] #5402357 REDUCE ft: 6376 corp: 1094/12459Kb lim: 175231 exec/s: 226 rss: 1516Mb L: 56/175231 MS: 1 EraseBytes-
[INFO] [SYDR] execs: 273, sat{opt|sopt|fuzzmem}: 17649{8575|4553|2350}, unsat: 29348, timeout: 94, oom: 1
[INFO] Launching Sydr: "/builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/sydr/sydr" "--no-console-log" "-o" "/builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/corpus" "--optimistic" "--wait-jobs" "-s" "60" "--fuzzmem" "--fuzzmem-models" "-c" "/builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/sydr/cache" "-m" "8192" "-f" "/builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/sydr/seeds/b75bc8d30b99ba6eb03817c79a0d29010dabd3c6" "--flat" "b75bc8d30b99ba6eb03817c79a0d29010dabd3c6" "--log-file" "/builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/sydr/logs/log_b75bc8d30b99ba6eb03817c79a0d29010dabd3c6.txt" "--stats-file" "/builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/sydr/stats/stats_b75bc8d30b99ba6eb03817c79a0d29010dabd3c6.json" "--" "/image/cmd/sydr_webp/sydr_webp" "/builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/sydr/seeds/b75bc8d30b99ba6eb03817c79a0d29010dabd3c6"
[INFO] #5406157 REDUCE ft: 6376 corp: 1094/12459Kb lim: 175231 exec/s: 226 rss: 1516Mb L: 64/175231 MS: 1 EraseBytes-
[INFO] SUMMARY: libFuzzer: deadly signal /builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/crashes/crash-9c8db1037a649c084d5a92e5dd6bb33332113e8a
Также, можно заметить, что лог go-fuzz (libFuzzer)'а немного отличается от оригинального libFuzzer'а. У go-fuzz отсутствует поле cov:
, есть только ft:
. Давайте подождём, пока фаззинг завершится.
[INFO] [SYDR] execs: 423, sat{opt|sopt|fuzzmem}: 20585{9634|4975|3451}, unsat: 32720, timeout: 176, oom: 3
[INFO] [RESULTS] Fuzzing corpus is saved in /builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/corpus
[INFO] [RESULTS] oom/leak/timeout/crash: 37/0/0/81
[INFO] [RESULTS] Fuzzing results are saved in /builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/crashes
После 11-ти часов фаззинг завершился и мы обнаружили 81 аварийное завершение.
Перед тем, как мы двинимся дальше, давайте минимизируем входной corpus:
# sydr-fuzz -c webp.toml cmin
[INFO] Original fuzzing corpus saved as /builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/corpus-old
[INFO] Minimizing corpus /builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/corpus
[INFO] libFuzzer environment: ASAN_OPTIONS=allocator_may_return_null=1
[INFO] Launching libFuzzer: "/image/fuzz_webp" "-merge=1" "-rss_limit_mb=8192" "-artifact_prefix=/builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/crashes/" "-close_fd_mask=3" "-verbosity=2" "-dict=/webp.dict" "/builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/corpus" "/builds/dse/gitlab-jobs/oss-sydr-fuzz/projects/image-go/webp-out/corpus-old"
[INFO] MERGE-OUTER: 4760 files, 0 in the initial corpus, 0 processed earlier
[INFO] MERGE-OUTER: attempt 1
[INFO] MERGE-OUTER: successful in 1 attempt(s)
[INFO] MERGE-OUTER: the control file has 763040 bytes
[INFO] MERGE-OUTER: consumed 0Mb (32Mb rss) to parse the control file
[INFO] MERGE-OUTER: 831 new files with 6460 new features added; 0 new coverage edges
Минимизация сократила число файлов в корпусе с 4760 до 831 файла. Давайте проверим предикаты безопасности!
Идея, которая стоит за предикатами безопасности кратко описана в гайде по фаззингу xlnt. Предикаты безопасности нацелены на поиск ошибок целочисленного переполнения, выхода за границы буфера и деления на ноль. Проверка на истинность найденых ошибок происходит с помощью санитайзеров (ASAN, UBSAN). Часто целочисленное переполнение не приводит к аварийному завершению. Поэтому, сборка целей с UBSAN сильно рекомендуется при использовании предикатов безопасности, но к сожалению, в Go я не обнаружил возможности сборки c UBSAN. Также, не нашлось и специальных опций типа overflow-checks = true
, которые можно обнаружить для проектов на языке Rust. В любом случае, давайте попробуем запустить предикаты безопасности, чтобы обнаружить новые аварийные завершения.
# sydr-fuzz -c web.toml security -j 64
[INFO] [RESULTS] Security predicates results are saved in /fuzz/webp-out/security
[INFO] [RESULTS] Verified errors are saved in /fuzz/webp-out/security-verified
[INFO] [RESULTS] Unique errors are saved in /fuzz/webp-out/security-unique
[INFO] [RESULTS] Security total/verified/unique: 3964/0/0
[INFO] [RESULTS] Unverified intoverflow/bounds/zerodiv/null/negsize: 3938/26/0/0/0
[INFO] [RESULTS] Verified intoverflow/bounds/zerodiv/null/negsize: 0/0/0/0/0
[INFO] [RESULTS] Unique intoverflow/bounds/zerodiv/null/negsize : 0/0/0/0/0
[INFO] [RESULTS] oom/leak/timeout/crash: 37/0/0/81
[INFO] [RESULTS] Crashes are saved in /fuzz/webp-out/crashes
К сожалению, новых аварийных завершений обнаружить, двигаемся дальше.