-
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 являются хорошими примерами для фаззинга. Перед тем как я начну, есть уже готовый для сборки докер контейнер со всем необходимым окружением для фаззинга: фазиннг-цели для 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
Ого, мы получили покрытие, давайте на него посмотрим!
Для сортировки аварийных завершений я использую
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-отчёт.
Мы видим, что паника возникает при выделении памяти под массив байт. Значение w*h никак не санитизируется и контролируется пользователем. Я предложил исправление, надеюсь его примут.
В этой небольшой статье я попытался осветить некоторые интересные аспекты фаззинга проектов на языке Go. Я показал как применять sydr-fuzz, как проводить минимизацию корпуса, собирать покрытие по исходному коду для go-fuzz с libFuzzer'ом после фаззинга, проверять предикаты безопасности и сортировать аварийные завершения. Надеюсь,что этот гайд будет вам полезен:).
Андрей Федотов