Skip to content

Fuzzing golang image (Go) project with sydr fuzz (go fuzz backend) (rus)

Andrey Fedotov edited this page Mar 6, 2023 · 11 revisions

Введение

В этой статье мы попробуем применить гибридный подход к фаззингу с помощью инструмента 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

К сожалению, новых аварийных завершений обнаружить, двигаемся дальше.