Skip to content

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

PaDarochek edited this page Aug 23, 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 являются хорошими примерами для фаззинга. Перед тем как я начну, есть уже готовый для сборки докер контейнер со всем необходимым окружением для фаззинга: фазиннг-цели для 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

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

Покрытие

Как я уже говорил ранее, сбор покрытия по исходному коду то ещё приключение, когда ты фаззишь проекты на Go с помощью go-fuzz libFuzzer mode. Для поддержки сбора покрытия в OSS-Fuzz написано немало кода вокруг go-fuzz, но чтобы этим пользоваться, нам нужна инфраструктура OSS-Fuzz (докер образы, скрипты, и т.д.). Сбор покрытия отлично работает для тестов на Go (go test) и для фаззинга через gofuzz. Я некоторое время пытался найти решения конкретно для моего случая: у меня есть корпус данных после фаззинга через go-fuzz (libFuzzer), и я хочу собрать покрытие на этом корпусе. Я заметил, что go-fuzz имеет интересную опцию -dumpcover. Эта опция обновляет файл с покрытием на каждом найденном новом входе (который приносит покрытие в терминах go-fuzz) во время фаззинга. Что будет, если мы наш выходной корпус после фаззинга с помощью go-fuzz (libFuzzer) будем использовать как входной корпус для фаззинга через go-fuzz и попросим dumpcover? Давайте попробуем!

Сперва я подготовлю изначальный корпус для go-fuzz в корневой директории репозитория golang/image:

# cp -r /fuzz/webp-out/corpus /image/corpus

Затем я соберу фаззинг-цель для go-fuzz:

# go-fuzz-build -func=FuzzWebp -o fuzz_webp.zip

И, наконец, мы начнём фаззинг:

# go-fuzz -bin=fuzz_webp.zip  -dumpcover
2023/03/03 17:08:55 workers: 12, corpus: 831 (3s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s
2023/03/03 17:08:58 workers: 12, corpus: 831 (6s ago), crashers: 0, restarts: 1/21, execs: 42 (7/sec), cover: 1224, uptime: 6s
2023/03/03 17:09:01 workers: 12, corpus: 831 (9s ago), crashers: 0, restarts: 1/23, execs: 276 (31/sec), cover: 1262, uptime: 9s
2023/03/03 17:09:04 workers: 12, corpus: 831 (12s ago), crashers: 0, restarts: 1/67, execs: 815 (68/sec), cover: 1295, uptime: 12s
2023/03/03 17:09:07 workers: 12, corpus: 834 (1s ago), crashers: 0, restarts: 1/153, execs: 1840 (123/sec), cover: 1309, uptime: 15s
2023/03/03 17:09:10 workers: 12, corpus: 834 (4s ago), crashers: 1, restarts: 1/2544, execs: 71235 (3954/sec), cover: 1309, uptime: 18s
2023/03/03 17:09:13 workers: 12, corpus: 834 (7s ago), crashers: 1, restarts: 1/3345, execs: 167258 (7958/sec), cover: 1309, uptime: 21s
2023/03/03 17:09:16 workers: 12, corpus: 834 (10s ago), crashers: 1, restarts: 1/3877, execs: 294689 (12269/sec), cover: 1309, uptime: 24s
2023/03/03 17:09:19 workers: 12, corpus: 834 (13s ago), crashers: 1, restarts: 1/4534, execs: 399020 (14769/sec), cover: 1309, uptime: 27s
2023/03/03 17:09:22 workers: 12, corpus: 834 (16s ago), crashers: 1, restarts: 1/4965, execs: 466755 (15548/sec), cover: 1309, uptime: 30s
2023/03/03 17:09:25 workers: 12, corpus: 834 (19s ago), crashers: 1, restarts: 1/5300, execs: 519472 (15733/sec), cover: 1309, uptime: 33s
2023/03/03 17:09:28 workers: 12, corpus: 834 (22s ago), crashers: 2, restarts: 1/5200, execs: 582438 (16171/sec), cover: 1309, uptime: 36s
2023/03/03 17:09:31 workers: 12, corpus: 834 (25s ago), crashers: 2, restarts: 1/4996, execs: 724489 (18568/sec), cover: 1309, uptime: 39s
2023/03/03 17:09:34 workers: 12, corpus: 834 (28s ago), crashers: 2, restarts: 1/5112, execs: 828161 (19710/sec), cover: 1309, uptime: 42s
2023/03/03 17:09:37 workers: 12, corpus: 834 (31s ago), crashers: 2, restarts: 1/5394, execs: 933312 (20732/sec), cover: 1309, uptime: 45s
2023/03/03 17:09:40 workers: 12, corpus: 834 (34s ago), crashers: 2, restarts: 1/5587, execs: 1033722 (21528/sec), cover: 1309, uptime: 48s
^C2023/03/03 17:09:42 shutting down...

Мы видим, что go-fuzz импортировал наш корпус (831 файл) и создал файл coverprofile! Отлично, давайте сделаем html отчёт о покрытии.

# go tool cover -html=coverprofile
cover: inconsistent NumStmt: changed from 0 to 1

В go-fuzz есть проблема при сохранении покрытия. Давайте применим предложенный фикс с помощью утилиты sed:

# sed -i '/0.0,1.1/d' coverprofile

Снова запускаем go tool cover:

# go tool cover -html=coverprofile
HTML output written to /tmp/cover2240572277/coverage.html

Ого, мы получили покрытие, давайте на него посмотрим!

image-go-cov

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

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

# sydr-fuzz  -c parse-afl++.toml casr

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

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

[INFO] Analyzing 81 files...
[INFO] Timeout for target execution is 30 seconds
[INFO] Using 6 threads
[INFO] casr-san: creating ASAN reports...
[INFO] Progress: 19/81
[INFO] Progress: 41/81
[INFO] Progress: 66/81
[INFO] Casr-cluster: deduplication of casr reports...
[INFO] Reports before deduplication: 81; after: 1
[INFO] Copying inputs...
[INFO] casr-gdb: adding crash reports...
[INFO] Using 1 threads
[WARN] casr-gdb: no crash on input /fuzz/webp-out/crashes/crash-003be3ca633b2073c7a6b1c2cae1e72995a0cab3
[INFO] Done!
[INFO] ==> <casr>
[INFO] Crash: /fuzz/webp-out/casr/crash-003be3ca633b2073c7a6b1c2cae1e72995a0cab3
[INFO]   casr-san: NOT_EXPLOITABLE: GoPanic: /image/webp/decode.go:157
[INFO]   casr-gdb: No crash
[INFO]   Similar crashes: 1
[INFO] Cluster summary -> GoPanic: 1
[INFO] SUMMARY -> GoPanic: 1
[INFO] Crashes and Casr reports are saved in /fuzz/webp-out/casr

После дедупликации у нас остался только один крэш. Давайте посмотрим на его casr-отчёт.

image-go-report

Мы видим, что паника возникает при выделении памяти под массив байт. Значение w*h никак не санитизируется и контролируется пользователем. Я предложил исправление, надеюсь его примут.

Заключение

В этой небольшой статье я попытался осветить некоторые интересные аспекты фаззинга проектов на языке Go. Я показал как применять sydr-fuzz, как проводить минимизацию корпуса, собирать покрытие по исходному коду для go-fuzz с libFuzzer'ом после фаззинга, проверять предикаты безопасности и сортировать аварийные завершения. Надеюсь,что этот гайд будет вам полезен:).


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