Obsah
1. Úvod do problematiky fuzzingu a fuzz testování – nástroj go-fuzz
2. Základní algoritmus používaný nástrojem go-fuzz
4. První demonstrační příklad – prázdná funkce
5. Vytvoření základního korpusu a zahájení testování (fuzzingu)
6. Druhý demonstrační příklad – funkce zpracovávající vstupní data
7. Získání podezřelých vstupních dat pro druhý příklad
8. Třetí demonstrační příklad s klasickou chybou „±1“
9. Získání vzorku vstupních dat způsobujících pád
10. Funkce chybně testující vstupní data
11. Příprava vstupů pro testovanou funkci a spuštění testů
12. Analýza výsledků zjištěných fuzzerem
13. Otestování funkčnosti dekodéru grafického formátu GIF
14. Vytvoření reproduceru na základě chyb nalezených fuzzerem
16. Repositář s demonstračními příklady
1. Úvod do problematiky fuzzingu a fuzz testování – nástroj go-fuzz
Ve druhém článku o fuzzy testování se již budeme věnovat popisu praktických způsobů použití existujících nástrojů – fuzzerů. Prvním z těchto nástrojů je go-fuzz, jehož autorem je Dmitrij Vyukov (zaměstnanec Googlu, který pracuje na vývoji toolingu pro programovací jazyk Go). Prezentace o go-fuzz (i o dalších nástrojích určených pro testování) je k dispozici na adrese https://talks.golang.org/2015/dynamic-tools.slide#1, ovšem velmi zajímavá je i Dmitrijova prezentace dostupná na Youtube (používá v ní stejné slajdy). go-fuzz je sice primárně určen pro programovací jazyk Go, ovšem základní myšlenky, které v něm nalezneme, je možné nalézt i v mnoha dalších podobně koncipovaných nástrojích, s nimiž se seznámíme později.
2. Základní algoritmus používaný nástrojem go-fuzz
Základní algoritmus používaný nástrojem go-fuzz je možné popsat následujícím (dosti zjednodušeným) pseudokódem:
proveď instrumentaci programu takovým způsobem, aby bylo možné zjišťovat pokrytí kódu for { zvol náhodný vstup z korpusu vhodným způsobem tento vstup modifikuj (mutuj) zavolej volaný kód a zjisti pokrytí kódu vstupními daty pokud se zvýšilo pokrytí, popř. se nalezla nová cesta v kódu, přidej tento vstup do korpusu }
Cílem základního algoritmu je tedy vytvořit takzvaný korpus, jinými slovy (ve stručnosti řečeno) sadu vstupních dat, která ideálně pokryje všechny možné cesty v programovém kódu, tedy i ty části, v nichž by se (pokud je program korektní) měly testovat hodnoty nil, záporné hodnoty, NaN, nekonečna, nekorektní vstupy atd. atd.
Následně se další algoritmus snaží o minimalizaci korpusu, resp. o nalezení nejkratší sekvence vstupních dat, které vedou k chybě či k pádu testované aplikace.
Nástroj go-fuzz dokáže detekovat mj. i pády programu (například při alokaci paměti), zavolání funkce panic(), ukončení aplikace funkcí os.Exit(), souběh (deadlock) při práci s gorutinami atd.
3. Instalace nástroje go-fuzz
Instalace nástroje go-fuzz je (na rozdíl od mnoha jiných fuzzerů) stejně snadná, jako instalace jakékoli jiné knihovny určené pro ekosystém programovacího jazyka Go. Postačuje nám použít standardní příkaz go get:
$ go get github.com/google/gofuzz
Následně je dobré zkontrolovat, zda je korektně nastavena proměnná prostředí GOPATH a PATH. V prvním případě lze použít příkaz:
$ go env
V proměnné prostředí PATH by se měl objevit i adresář $GOPATH/bin. V tomto adresáři by měly být umístěny spustitelné soubory pojmenované go-fuzz a go-fuzz-build, jejichž existenci a spustitelnost si ostatně můžeme velmi snadno ověřit:
$ go-fuzz --help Usage of ./go-fuzz: -bin string test binary built with go-fuzz-build -connectiontimeout duration time limit for worker to try to connect coordinator (default 1m0s) -coordinator string coordinator mode (value is coordinator address) -covercounters use coverage hit counters (default true) -dumpcover dump coverage profile into workdir -dup collect duplicate crashers -func string function to fuzz -http string HTTP server listen address (coordinator mode only) -minimize duration time limit for input minimization (default 1m0s) -procs int parallelism level (default 4) -sonar use sonar hints (default true) -testoutput print test binary output to stdout (for debugging only) -timeout int test timeout, in seconds (default 10) -v int verbosity level -workdir string dir with persistent work data (default ".") -worker string worker mode (value is coordinator address)
a:
$ go-fuzz-build --help Usage of go-fuzz-build: -cpuprofile generate cpu profile in cpu.pprof -func string preferred entry function -libfuzzer output static archive for use with libFuzzer -o string output file -preserve string a comma-separated list of import paths not to instrument -race enable race detector -tags string a space-separated list of build tags to consider satisfied during the build -work don't remove working directory -x print the commands if build fails
4. První demonstrační příklad – prázdná funkce
V dnešním prvním demonstračním příkladu, který naleznete na adrese https://github.com/tisnik/fuzzing-examples/tree/master/go-fuzz/example1, si pouze ukážeme, jaká vlastně vypadá základní struktura fuzzy testů a jaké funkce dokáže nástroj go-fuzz (bez dalších úprav a přidaných vylepšení) testovat. Nejprve vytvoříme nový balíček nazvaný example1, který bude obsahovat testovanou funkci pojmenovanou pro jednoduchost přímočaře TestedFunction. Tato funkce akceptuje řez bajtů, tj. libovolně dlouhou (ale na začátku testování i prázdnou) sekvenci bajtů. Samotná funkce má – alespoň prozatím – prázdné tělo, takže celý balíček vypadá značně primitivně:
package example1 func TestedFunction(data []byte) { }
Následně je nutné vytvořit druhý soubor, v němž bude deklarována funkce nazvaná Fuzz (toto jméno je nutné dodržet). Tato funkce, která řídí celé testování, taktéž akceptuje parametr, jehož typ je řez bajtů. Důležitá je i návratová hodnota (typu int), kterou je možné řídit další kroky testování, a to konkrétně následujícím způsobem:
- Návratová hodnota rovna 1 značí, že by fuzzer měl zvýšit prioritu právě použitých vstupních dat. Touto hodnotou lze označit všechny vstupy, které jsou testovanou funkcí zpracovány korektně.
- Návratová hodnota –1 naopak značí, že právě použitá vstupní data nemají být přidána do korpusu, ať již jsou důvody jakékoli (například byla vstupní data testovanou funkcí odmítnuta, což je zcela korektní).
- A konečně hodnota 0 znamená, že se jedná o běžná data, která lze do korpusu přidat, ale s nenastavenou prioritou.
Velmi jednoduchá forma testovací funkce Fuzz může přímo volat funkci testovanou, tedy funkci nazvanou TestedFunction. Jedná se o nejjednodušší možný příklad:
// +build gofuzz package example1 func Fuzz(data []byte) int { TestedFunction(data) return 0 }
5. Vytvoření základního korpusu a zahájení testování (fuzzingu)
Před vlastním spuštěním testů je nejprve nutné provést přípravu projektu, a to konkrétně zavoláním příkazu:
$ go-fuzz-build
Tento příkaz vytvoří pomocný soubor nazvaný example1-fuzz.zip. Uvnitř tohoto souboru jsou mj. zabaleny spustitelné soubory pojmenované cover.exe a sonar.exe. I přes neobvyklé koncovky se jedná o soubory spustitelné na dané architektuře a operačním systému (tedy i na Linuxu, pochopitelně jen pokud se výše uvedený příkaz spouštěl taktéž na Linuxu).
Ve druhém kroku již můžeme spustit vlastní testy, a to příkazem:
$ go-fuzz
Po spuštění se začnou vypisovat informace o probíhajících testech:
2020/03/02 22:31:54 workers: 4, corpus: 1 (3s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s 2020/03/02 22:31:57 workers: 4, corpus: 1 (6s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 2, uptime: 6s 2020/03/02 22:32:00 workers: 4, corpus: 1 (9s ago), crashers: 0, restarts: 1/3180, execs: 25446 (2827/sec), cover: 2, uptime: 9s 2020/03/02 22:32:03 workers: 4, corpus: 1 (12s ago), crashers: 0, restarts: 1/4266, execs: 51202 (4266/sec), cover: 2, uptime: 12s 2020/03/02 22:32:06 workers: 4, corpus: 1 (15s ago), crashers: 0, restarts: 1/6441, execs: 77294 (5152/sec), cover: 2, uptime: 15s
Povšimněte si, že se mj. vypisují i informace o čtyřech „workerech“, což na mém počítači odpovídá počtu jader, ve skutečnosti se však postupně spouští a opět zastavuje mnoho dalších procesů, což nám prozradí například příkaz pstree:
go-fuzz─┬─4*[go-fuzz93667065───4*[{go-fuzz93667065}]] ├─3*[go-fuzz94444180───4*[{go-fuzz94444180}]] ├─go-fuzz94444180───5*[{go-fuzz94444180}] └─13*[{go-fuzz}]
Dále je ve výpisu patrné, že počet takzvaných „crasherů“ je nulový, což je pochopitelné, protože testovaná funkce je prázdná, tudíž s velkou pravděpodobností za žádných (běžných) okolností nezhavaruje.
Testy, které lze mít puštěny libovolně dlouhou dobu (pouze narůstá účet za elektřinu), můžeme kdykoli přerušit klávesovou zkratkou Ctrl+C:
^C2020/03/02 22:32:09 shutting down...
Po ukončení testů si povšimněte, že se vytvořila trojice adresářů, přičemž adresář se jménem corpus obsahuje prázdný soubor s korpusem (prázdný je proto, že se nenašel problematický vstup). Adresáře crashers a suppressions budou zcela prázdné:
├── corpus │ └── da39a3ee5e6b4b0d3255bfef95601890afd80709 ├── crashers ├── example1-fuzz.zip ├── example1.go ├── fuzz.go └── suppressions
6. Druhý demonstrační příklad – funkce zpracovávající vstupní data
Druhý demonstrační příklad, který si dnes ukážeme a který je uložen na adrese https://github.com/tisnik/fuzzing-examples/tree/master/go-fuzz/example2, již bude nepatrně složitější (i když stále umělý), protože testovaná funkce bude vstupní data generovaná fuzzerem zpracovávat. Konkrétně bude vracet pravdivostní hodnotu true za podmínky, kdy sekvence vstupních dat začíná bajty s hodnotami 0×03 0×02 a 0×01. Pro všechny ostatní kombinace se vrátí pravdivostní hodnota false. Implementace je snadná:
package example2 func TestedFunction(data []byte) bool { if len(data) >= 3 { if data[0] == 3 && data[1] == 2 && data[2] == 1 { return true } } return false }
Testovací funkce pojmenovaná Fuzz bude vypadat odlišně – v závislosti na návratové hodnotě testované funkce se buď běžným způsobem vrátí nulová hodnota, nebo funkce zhavaruje zavoláním panic(), což je mimochodem zcela legální, protože fuzzer musí umět zareagovat i na podobné situace:
// +build gofuzz package example2 func Fuzz(data []byte) int { if TestedFunction(data) { panic("wrong input") } return 0 }
7. Získání podezřelých vstupních dat pro druhý příklad
Nyní si tedy otestujme druhý příklad, a to nám již známou sekvencí příkazů:
$ go-fuzz-build $ go-fuzz
Samotný průběh testování je již v tomto případě odlišný, protože se již prakticky od začátku vypisuje informace o tom, že byl nalezen jeden „crasher“. Navíc je jiná (tedy nenulová) i velikost korpusu:
2020/03/02 22:29:14 workers: 4, corpus: 4 (2s ago), crashers: 1, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s 2020/03/02 22:29:17 workers: 4, corpus: 4 (5s ago), crashers: 1, restarts: 1/0, execs: 0 (0/sec), cover: 9, uptime: 6s 2020/03/02 22:29:20 workers: 4, corpus: 4 (8s ago), crashers: 1, restarts: 1/288, execs: 23371 (2596/sec), cover: 9, uptime: 9s 2020/03/02 22:29:23 workers: 4, corpus: 4 (11s ago), crashers: 1, restarts: 1/280, execs: 47101 (3925/sec), cover: 9, uptime: 12s 2020/03/02 22:29:26 workers: 4, corpus: 4 (14s ago), crashers: 1, restarts: 1/279, execs: 69568 (4638/sec), cover: 9, uptime: 15s 2020/03/02 22:29:29 workers: 4, corpus: 4 (17s ago), crashers: 1, restarts: 1/300, execs: 93328 (5185/sec), cover: 9, uptime: 18s
Testování lze po chvíli přerušit:
^C2020/03/02 22:29:30 shutting down...
V adresáři s projektem se opět vytvořilo několik podadresářů, ty již však nejsou prázdné:
├── corpus │ ├── 685ad06a33b3db3330ad4b19cf95fdd6acf3eceb-1 │ ├── 888693d736b5508655198129dc0ec8cf6d0e7757-2 │ ├── a6cd288e027237b261f24b1d140960ec48b6d63b-1 │ └── da39a3ee5e6b4b0d3255bfef95601890afd80709 ├── crashers │ ├── 134aead1d2020adfb1d2352b1dffb2afd8fe0dc5 │ ├── 134aead1d2020adfb1d2352b1dffb2afd8fe0dc5.output │ └── 134aead1d2020adfb1d2352b1dffb2afd8fe0dc5.quoted ├── example2-fuzz.zip ├── example2.go ├── fuzz.go └── suppressions └── a5d1237652e2eab23ab4f89b64348a150d2d77fa
Z hlediska programátora testujícího svoji aplikaci (resp. prozatím jedinou funkci z této aplikace) je nejdůležitější obsah podadresáře crashers, protože ten obsahuje ta vstupní data, která způsobila chybu nebo dokonce pád testované funkce/aplikace/programu. Tyto soubory můžeme prozkoumat (resp. měli bychom, protože se jedná právě o ty informace, kvůli kterým se fuzzer pouští).
První z těchto souborů obsahuje binární podobu vstupních dat:
$ hd 134aead1d2020adfb1d2352b1dffb2afd8fe0dc5 00000000 03 02 01 |...| 00000003
Můžeme zde vidět, že se skutečně jedná o naši „speciální“ sekvenci tří bajtů.
V mnoha případech je vstup chápán jako text, což je reflektováno třetím souborem, který obsahuje vstupní data, ovšem tentokrát v řetězcové podobě:
$ cat 134aead1d2020adfb1d2352b1dffb2afd8fe0dc5.quoted "\x03\x02\x01"
A konečně v posledním souboru jsou uloženy podrobnější informace o tom, jak vypadal pád testované aplikace (v našem případě jediné funkce):
$ cat 134aead1d2020adfb1d2352b1dffb2afd8fe0dc5.output panic: wrong input goroutine 1 [running]: _/home/tester/temp/out/fuzz/example2.Fuzz(0x7f182f247000, 0x3, 0x3, 0x3) /home/tester/temp/out/fuzz/example2/fuzz.go:7 +0xdc go-fuzz-dep.Main(0xc000036780, 0x1, 0x1) /tmp/ramdisk/go-fuzz-build514602391/goroot/src/go-fuzz-dep/main.go:36 +0x1ad main.main() /tmp/ramdisk/go-fuzz-build514602391/gopath/src/_/home/tester/temp/out/fuzz/example2/go.fuzz.main/main.go:15 +0x52 exit status 2
8. Třetí demonstrační příklad s klasickou chybou „±1“
Třetí testovaná funkce vypadá zdánlivě nevinně – pokud se ve vstupní sekvenci nachází bajty s obsahem ‚r‘, ‚o‘, ‚o‘, ‚t‘, vypíše se na standardní výstup zpráva. Ovšem ve skutečnosti je funkce naprogramována špatně – obsahuje klasickou „chybu ±1“, protože testovaná délka řezu by měla být větší nebo rovna čtyřem a nikoli třem:
package example3 import "fmt" func TestedFunction(data []byte) { if len(data) >= 3 { if data[0] == 'r' && data[1] == 'o' && data[2] == 'o' && data[3] == 't' { fmt.Println("Spravny vstup") } } }
Tuto funkci budeme testovat prakticky stejným způsobem, jako obě funkce předchozí, tj. vytvoříme si vlastní implementaci funkce Fuzz:
// +build gofuzz package example3 func Fuzz(data []byte) int { TestedFunction(data) return 0 }
9. Získání vzorku vstupních dat způsobujících pád
Nyní přišel čas na to, aby fuzzer správně rozpoznal, která vstupní sekvence způsobí pád aplikace. Je nám již dopředu jasné, že se jedná o jedinou sekvenci, ale bude úkolem fuzzeru tuto sekvenci zjistit. Opět použijeme nám již známé dva příkazy:
$ go-fuzz-build $ go-fuzz 2020/03/03 08:27:15 workers: 4, corpus: 6 (3s ago), crashers: 1, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s 2020/03/03 08:27:18 workers: 4, corpus: 6 (6s ago), crashers: 1, restarts: 1/0, execs: 0 (0/sec), cover: 112, uptime: 6s 2020/03/03 08:27:21 workers: 4, corpus: 6 (9s ago), crashers: 1, restarts: 1/532, execs: 22345 (2483/sec), cover: 112, uptime: 9s 2020/03/03 08:27:24 workers: 4, corpus: 6 (12s ago), crashers: 1, restarts: 1/359, execs: 44203 (3683/sec), cover: 112, uptime: 12s 2020/03/03 08:27:27 workers: 4, corpus: 6 (15s ago), crashers: 1, restarts: 1/326, execs: 65232 (4349/sec), cover: 112, uptime: 15s 2020/03/03 08:27:30 workers: 4, corpus: 6 (18s ago), crashers: 1, restarts: 1/306, execs: 86878 (4826/sec), cover: 112, uptime: 18s 2020/03/03 08:27:33 workers: 4, corpus: 6 (21s ago), crashers: 1, restarts: 1/298, execs: 107631 (5125/sec), cover: 112 ... ... ... ^C2020/03/03 08:27:48 shutting down...
V adresáři crashers by se měly nacházet soubory se sekvencí bajtů, kvůli které funkce zhavaruje:
$ ls -1 crashers/ dc76e9f0c0006e8f919e0c515c66dbba3982f785 dc76e9f0c0006e8f919e0c515c66dbba3982f785.output dc76e9f0c0006e8f919e0c515c66dbba3982f785.quoted
Nejdůležitější je hned první soubor s binární sekvencí, kterou skutečně tvoří znaky „roo“:
$ cat crashers/dc76e9f0c0006e8f919e0c515c66dbba3982f785 roo
Dále zkontrolujeme, jakým způsobem a na základě jaké chyby vlastně aplikace zhavarovala:
$ cat crashers/dc76e9f0c0006e8f919e0c515c66dbba3982f785.output panic: runtime error: index out of range goroutine 1 [running]: _/home/tester/temp/out/fuzz/example4.TestedFunction.func3(...) /home/tester/temp/out/fuzz/example4/example3.go:7 _/home/tester/temp/out/fuzz/example4.TestedFunction(0x7f29a71b7000, 0x3, 0x3) /home/tester/temp/out/fuzz/example4/example3.go:7 +0x167 _/home/tester/temp/out/fuzz/example4.Fuzz(0x7f29a71b7000, 0x3, 0x3, 0x3) /home/tester/temp/out/fuzz/example4/fuzz.go:6 +0x57 go-fuzz-dep.Main(0xc000096f80, 0x1, 0x1) /tmp/ramdisk/go-fuzz-build367173585/goroot/src/go-fuzz-dep/main.go:36 +0x1ad main.main() /tmp/ramdisk/go-fuzz-build367173585/gopath/src/_/home/tester/temp/out/fuzz/example4/go.fuzz.main/main.go:15 +0x52 exit status 2
A nakonec pro jistotu zjistíme, jak vypadají vstupní data převedená na řetězec. Ani zde se o žádné překvapení nebude jednat:
$ cat crashers/dc76e9f0c0006e8f919e0c515c66dbba3982f785.quoted "roo"
10. Funkce chybně testující vstupní data
Čtvrtý příklad vznikl zjednodušením reálného programu. Je v něm deklarována funkce, která na základě dvou vstupních parametrů alokuje paměť pro bitmapu o zadaných rozměrech. Každý pixel bitmapy je uložen ve čtyřech bajtech. Funkce (zdánlivě správně) testuje, zda nejsou rozměry bitmapy příliš velké, ale již se zapomnělo na to, že vstupem mohou být záporná čísla. A ta jsou nebezpečná ve dvou případech:
- Jeden vstup je kladný a druhý záporný – zde dojde k chybě při alokaci kvůli zápornému počtu prvků
- Oba vstupy jsou záporné. Toto je větší problém, protože vynásobením vznikne kladné číslo, které je buď relativně malé a program nezhavaruje, nebo je naopak příliš velké a dojde k chybě při alokaci paměti
Zdrojový kód problematické funkce je dostupný na adrese https://github.com/tisnik/fuzzing-examples/blob/master/go-fuzz/example4/example4.go:
package example4 import "fmt" const maxWidth = 1024 const maxHeight = 1024 func TestedFunction(width int32, height int32) { if width < maxWidth && height < maxHeight { size := 4 * width * height bitmap := make([]byte, size) fmt.Println(len(bitmap)) } }
11. Příprava vstupů pro testovanou funkci a spuštění testů
Nyní je již nutné připravit vhodná vstupní data, tedy dvojici hodnot typu int32 ze sekvence bajtů. Zvolme si ten nejpřímější a nejprimitivnější způsob založený na vygenerování int32 ze čtveřice bajtů. Implementace funkce Fuzz může vypadat následovně:
// +build gofuzz package example4 func Fuzz(data []byte) int { if len(data) >= 8 { width := int32(data[0]) + 256*int32(data[1]) + 65536*int32(data[2]) + 16777216*int32(data[3]) height := int32(data[4]) + 256*int32(data[5]) + 65536*int32(data[6]) + 16777216*int32(data[7]) TestedFunction(width, height) } return 0 }
Testy samozřejmě musíme spustit, abychom získali potřebné „crashery“:
$ go-fuzz-build $ go-fuzz
S výsledky:
2020/03/04 08:13:48 workers: 4, corpus: 3 (2s ago), crashers: 1, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s 2020/03/04 08:13:53 workers: 4, corpus: 4 (2s ago), crashers: 1, restarts: 1/0, execs: 0 (0/sec), cover: 7, uptime: 6s 2020/03/04 08:13:54 workers: 4, corpus: 4 (4s ago), crashers: 1, restarts: 1/18, execs: 458 (51/sec), cover: 111, uptime: 9s 2020/03/04 08:13:57 workers: 4, corpus: 4 (7s ago), crashers: 1, restarts: 1/22, execs: 901 (75/sec), cover: 111, uptime: 12s 2020/03/04 08:14:00 workers: 4, corpus: 4 (10s ago), crashers: 1, restarts: 1/22, execs: 901 (60/sec), cover: 111, uptime: 15s 2020/03/04 08:14:03 workers: 4, corpus: 4 (13s ago), crashers: 1, restarts: 1/22, execs: 901 (50/sec), cover: 111, uptime: 18s 2020/03/04 08:14:06 workers: 4, corpus: 4 (16s ago), crashers: 2, restarts: 1/22, execs: 913 (43/sec), cover: 111, uptime: 21s 2020/03/04 08:14:09 workers: 4, corpus: 4 (19s ago), crashers: 2, restarts: 1/43, execs: 1880 (78/sec), cover: 111, uptime: 24s 2020/03/04 08:14:12 workers: 4, corpus: 4 (22s ago), crashers: 2, restarts: 1/44, execs: 2032 (75/sec), cover: 111, uptime: 27s 2020/03/04 08:14:15 workers: 4, corpus: 4 (25s ago), crashers: 2, restarts: 1/44, execs: 2032 (68/sec), cover: 111, uptime: 30s 2020/03/04 08:14:18 workers: 4, corpus: 4 (28s ago), crashers: 2, restarts: 1/44, execs: 2032 (62/sec), cover: 111, uptime: 33s ^C2020/03/04 08:14:20 shutting down...
12. Analýza výsledků zjištěných fuzzerem
Fuzzer našel obě chyby, což je patrné již při pohledu na počet vygenerovaných souborů:
├── corpus │ ├── 287ceb7f19a3d5a9ca72916101d863bef2d5fe66-1 │ ├── c487e35e15cf096317bfcd33cf6a95811b3e0b01-1 │ ├── da39a3ee5e6b4b0d3255bfef95601890afd80709 │ └── da67c6d8fb9354c0b9d0391663be4ab82974a0db-1 ├── crashers │ ├── 5a6899fa69a5a0ae44f2cf286c2dee3d83d5ff5c │ ├── 5a6899fa69a5a0ae44f2cf286c2dee3d83d5ff5c.output │ ├── 5a6899fa69a5a0ae44f2cf286c2dee3d83d5ff5c.quoted │ ├── d14623983a778683ea98c8e411a1088aa29a1bdc │ ├── d14623983a778683ea98c8e411a1088aa29a1bdc.output │ └── d14623983a778683ea98c8e411a1088aa29a1bdc.quoted ├── example4-fuzz.zip ├── example4.go ├── fuzz.go └── suppressions ├── 3d5a46d793c051d77b04f76d630edef78291da54 └── a596442269a13f32d85889a173f2d36187a768c6
Podívejme se nyní nejprve na oba soubory s koncovkou .output. Ty obsahují informace o nalezené chybě.
První soubor:
2018609124 358989248 442121188 panic: runtime error: makeslice: len out of range goroutine 1 [running]: _/home/tester/temp/fuzzing-examples/go-fuzz/example4.TestedFunction(0xbd30efbdbfef30ed) /home/tester/temp/fuzzing-examples/go-fuzz/example4/example4.go:11 +0xc5 _/home/tester/temp/fuzzing-examples/go-fuzz/example4.Fuzz(0x7f738baed000, 0x8, 0x8, 0x3) /home/tester/temp/fuzzing-examples/go-fuzz/example4/fuzz.go:9 +0xec go-fuzz-dep.Main(0xc0785acf80, 0x1, 0x1) /tmp/ramdisk/go-fuzz-build858165217/goroot/src/go-fuzz-dep/main.go:36 +0x1ad main.main() /tmp/ramdisk/go-fuzz-build858165217/gopath/src/_/home/tester/temp/fuzzing-examples/go-fuzz/example4/go.fuzz.main/main.go:15 +0x52 exit status 2
Druhý soubor:
program hanged (timeout 10 seconds) 840167040 1513871568 SIGABRT: abort PC=0x451b33 m=0 sigcode=0 goroutine 0 [idle]: runtime.memclrNoHeapPointers(0xc0321f4000, 0x548f6000) /tmp/ramdisk/go-fuzz-build858165217/goroot/src/runtime/memclr_amd64.s:46 +0x83 runtime.(*mheap).alloc(0x57ebc0, 0x2a47b, 0x7ffd47010101, 0x413325) /tmp/ramdisk/go-fuzz-build858165217/goroot/src/runtime/mheap.go:764 +0xda runtime.largeAlloc(0x548f5efc, 0x101, 0xc0321e8e58) /tmp/ramdisk/go-fuzz-build858165217/goroot/src/runtime/malloc.go:1019 +0x97 runtime.mallocgc.func1() /tmp/ramdisk/go-fuzz-build858165217/goroot/src/runtime/malloc.go:914 +0x46 runtime.systemstack(0x44ec09) /tmp/ramdisk/go-fuzz-build858165217/goroot/src/runtime/asm_amd64.s:351 +0x66 runtime.mstart() /tmp/ramdisk/go-fuzz-build858165217/goroot/src/runtime/proc.go:1229 goroutine 1 [running]: runtime.systemstack_switch() /tmp/ramdisk/go-fuzz-build858165217/goroot/src/runtime/asm_amd64.s:311 fp=0xc0321e8d80 sp=0xc0321e8d78 pc=0x44ed00 runtime.mallocgc(0x548f5efc, 0x4b31e0, 0x464201, 0x4) /tmp/ramdisk/go-fuzz-build858165217/goroot/src/runtime/malloc.go:913 +0x896 fp=0xc0321e8e20 sp=0xc0321e8d80 pc=0x40ab06 runtime.makeslice(0x4b31e0, 0x548f5efc, 0x548f5efc, 0x13a2d062, 0x13a2d06200000000, 0x5e5f552f) /tmp/ramdisk/go-fuzz-build858165217/goroot/src/runtime/slice.go:70 +0x77 fp=0xc0321e8e50 sp=0xc0321e8e20 pc=0x43af07 _/home/tester/temp/fuzzing-examples/go-fuzz/example4.TestedFunction(0x853b95bfef5c3e01) /home/tester/temp/fuzzing-examples/go-fuzz/example4/example4.go:11 +0xc5 fp=0xc0321e8ea0 sp=0xc0321e8e50 pc=0x4a27e5 _/home/tester/temp/fuzzing-examples/go-fuzz/example4.Fuzz(0x7fc302a26000, 0xa, 0xa, 0x3) /home/tester/temp/fuzzing-examples/go-fuzz/example4/fuzz.go:9 +0xec fp=0xc0321e8eb8 sp=0xc0321e8ea0 pc=0x4a293c go-fuzz-dep.Main(0xc0321e8f80, 0x1, 0x1) /tmp/ramdisk/go-fuzz-build858165217/goroot/src/go-fuzz-dep/main.go:36 +0x1ad fp=0xc0321e8f68 sp=0xc0321e8eb8 pc=0x46400d main.main() /tmp/ramdisk/go-fuzz-build858165217/gopath/src/_/home/tester/temp/fuzzing-examples/go-fuzz/example4/go.fuzz.main/main.go:15 +0x52 fp=0xc0321e8f98 sp=0xc0321e8f68 pc=0x4a2a12 runtime.main() /tmp/ramdisk/go-fuzz-build858165217/goroot/src/runtime/proc.go:201 +0x207 fp=0xc0321e8fe0 sp=0xc0321e8f98 pc=0x428bf7 runtime.goexit() /tmp/ramdisk/go-fuzz-build858165217/goroot/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc0321e8fe8 sp=0xc0321e8fe0 pc=0x450c61 rax 0x0 rbx 0x90ac000 rcx 0x548f6000 rdx 0x0 rdi 0xc07da3e000 rsi 0x2a47b rbp 0x7ffd47930250 rsp 0x7ffd47930208 r8 0x7fc304e9fbe8 r9 0x2a47b r10 0x1574 r11 0x2a47a r12 0x0 r13 0x2 r14 0x7 r15 0x4 rip 0x451b33 rflags 0x10206 cs 0x33 fs 0x0 gs 0x0 exit status 2
Vidíme, že se v prvním případě jednalo o použití záporného indexu a v případě druhém o použití dvou záporných čísel, které po vynásobení vytvořily poměrně velkou kladnou hodnotu. To lze zjistit i z binárních souborů s obsahem vstupních dat:
$ hd 611d6b397393720522e9fafe1f95081e64a586ff 00000000 ed 30 ef bf bd ef 30 bd |.0....0.| 00000008 $ hd f2d89e79c00e2ab6dfbc262a5ca9fbb30a5e236c 00000000 01 3e 5c ef bf 95 3b 85 32 b1 |.>\...;.2.| 0000000a
13. Otestování funkčnosti dekodéru grafického formátu GIF
Další příklad je již skutečně získán z reálného světa, protože podobným způsobem Dmitrij zjistil problémy ve standardní knihovně jazyka Go. Tyto problémy jsou již opraveny, ovšem teoreticky si můžete spustit fuzzer oproti Go verze 1.5:
package example5 import ( "bytes" "image/gif" ) func TestedFunction(data []byte) int { img, err := gif.Decode(bytes.NewReader(data)) // chyba nastat muze - na vstupu jsou nahodna data if err != nil { // ovsem img by melo byt rovno nil if img != nil { panic("img != nil on error") } // jinak ok return 0 } // pokus o zpetne vytvoreni obrazku // pro vsechny vstupy, ktere probehly "korektne" var w bytes.Buffer err = gif.Encode(&w, img, nil) if err != nil { panic(err) } return 1 }
Testovací funkce je shodná s prvními příklady:
// +build gofuzz package example5 func Fuzz(data []byte) int { return TestedFunction(data) }
Testování na novější verzi Go (než 1.5) by mělo proběhnout za všech okolností v pořádku, ovšem když budete trpěliví, možná nějakou chybu nakonec objevíte:
2020/03/04 08:42:09 workers: 4, corpus: 57 (0s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s 2020/03/04 08:42:12 workers: 4, corpus: 59 (2s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 276, uptime: 6s 2020/03/04 08:42:15 workers: 4, corpus: 59 (5s ago), crashers: 0, restarts: 1/2506, execs: 20048 (2227/sec), cover: 276, uptime: 9s ... ... ... 2020/03/04 08:42:33 workers: 4, corpus: 59 (23s ago), crashers: 0, restarts: 1/6702, execs: 160850 (5957/sec), cover: 276, uptime: 27s 2020/03/04 08:42:36 workers: 4, corpus: 59 (26s ago), crashers: 0, restarts: 1/7481, execs: 179551 (5985/sec), cover: 276, uptime: 30s 2020/03/04 08:42:39 workers: 4, corpus: 59 (29s ago), crashers: 0, restarts: 1/8228, execs: 197480 (5984/sec), cover: 276, uptime: 33s ^C2020/03/04 08:42:40 shutting down...
14. Vytvoření reproduceru na základě chyb nalezených fuzzerem
Pokud fuzzer nalezne sekvenci bajtů, která způsobuje pád nějaké funkce, je již snadné vytvořit na základě těchto dat reproducer, tj. co nejkratší kód, který chybu demonstruje a každý si ji může ověřit. Následující reproducer byl skutečně podobným způsobem vytvořen a naleznete ho na adrese https://github.com/golang/go/issues/11150:
package main import ( "bytes" "image/gif" ) func main() { data := []byte("GIF89a000\x00000,00\x00\x00\x00\x000\x000\x02\b\r0000000\x00;") img, err := gif.Decode(bytes.NewReader(data)) if err != nil { panic(err) } var w bytes.Buffer err = gif.Encode(&w, img, nil) if err != nil { panic(err) } _, err = gif.Decode(&w) if err != nil { panic(err) } }
Chybové hlášení:
panic: gif: cannot encode image block with empty palette goroutine 1 [running]: main.main() gif.go:17 +0x219
15. Obsah třetí části seriálu
Klasické fuzzery jsou založeny na generování pseudonáhodných sekvencí bajtů, což je vhodné pro testování binárních API a ABI. Ovšem v praxi se často setkáme s API akceptující formáty JSON, XML atd., tedy nějakým způsobem strukturovaná a mnohdy textová data. Touto problematikou se budeme zabývat příště.
16. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/fuzzing-examples. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně šest až sedm megabajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
17. Odkazy na Internetu
- Fuzzing (Wikipedia)
https://en.wikipedia.org/wiki/Fuzzing - american fuzzy lop
http://lcamtuf.coredump.cx/afl/ - Fuzzing: the new unit testing
https://go-talks.appspot.com/github.com/dvyukov/go-fuzz/slides/fuzzing.slide#1 - Corpus for github.com/dvyukov/go-fuzz examples
https://github.com/dvyukov/go-fuzz-corpus - AFL – QuickStartGuide.txt
https://github.com/google/AFL/blob/master/docs/QuickStartGuide.txt - Introduction to Fuzzing in Python with AFL
https://alexgaynor.net/2015/apr/13/introduction-to-fuzzing-in-python-with-afl/ - Writing a Simple Fuzzer in Python
https://jmcph4.github.io/2018/01/19/writing-a-simple-fuzzer-in-python/ - How to Fuzz Go Code with go-fuzz (Continuously)
https://fuzzit.dev/2019/10/02/how-to-fuzz-go-code-with-go-fuzz-continuously/ - Golang Fuzzing: A go-fuzz Tutorial and Example
http://networkbit.ch/golang-fuzzing/ - Fuzzing Python Modules
https://stackoverflow.com/questions/20749026/fuzzing-python-modules - 0×3 Python Tutorial: Fuzzer
http://www.primalsecurity.net/0×3-python-tutorial-fuzzer/ - fuzzing na PyPi
https://pypi.org/project/fuzzing/ - Fuzzing 0.3.2 documentation
https://fuzzing.readthedocs.io/en/latest/ - Randomized testing for Go
https://github.com/dvyukov/go-fuzz - HTTP/2 fuzzer written in Golang
https://github.com/c0nrad/http2fuzz - Ffuf (Fuzz Faster U Fool) – An Open Source Fast Web Fuzzing Tool
https://hacknews.co/hacking-tools/20191208/ffuf-fuzz-faster-u-fool-an-open-source-fast-web-fuzzing-tool.html - Continuous Fuzzing Made Simple
https://fuzzit.dev/ - Halt and Catch Fire
https://en.wikipedia.org/wiki/Halt_and_Catch_Fire#Intel_x86 - Pentium F00F bug
https://en.wikipedia.org/wiki/Pentium_F00F_bug - Random testing
https://en.wikipedia.org/wiki/Random_testing - Monkey testing
https://en.wikipedia.org/wiki/Monkey_testing - Fuzzing for Software Security Testing and Quality Assurance, Second Edition
https://books.google.at/books?id=tKN5DwAAQBAJ&pg=PR15&lpg=PR15&q=%22I+settled+on+the+term+fuzz%22&redir_esc=y&hl=de#v=onepage&q=%22I%20settled%20on%20the%20term%20fuzz%22&f=false - Z80 Undocumented Instructions
http://www.z80.info/z80undoc.htm - The 6502/65C02/65C816 Instruction Set Decoded
http://nparker.llx.com/a2/opcodes.html - libFuzzer – a library for coverage-guided fuzz testing
https://llvm.org/docs/LibFuzzer.html - fuzzy-swagger na PyPi
https://pypi.org/project/fuzzy-swagger/ - fuzzy-swagger na GitHubu
https://github.com/namuan/fuzzy-swagger - Fuzz testing tools for Python
https://wiki.python.org/moin/PythonTestingToolsTaxonomy#Fuzz_Testing_Tools - A curated list of awesome Go frameworks, libraries and software
https://github.com/avelino/awesome-go - gofuzz: a library for populating go objects with random values
https://github.com/google/gofuzz - tavor: A generic fuzzing and delta-debugging framework
https://github.com/zimmski/tavor - hypothesis na GitHubu
https://github.com/HypothesisWorks/hypothesis - Hypothesis: Test faster, fix more
https://hypothesis.works/ - Hypothesis
https://hypothesis.works/articles/intro/ - What is Hypothesis?
https://hypothesis.works/articles/what-is-hypothesis/ - Databáze CVE
https://www.cvedetails.com/ - Fuzz test Python modules with libFuzzer
https://github.com/eerimoq/pyfuzzer - Taof – The art of fuzzing
https://sourceforge.net/projects/taof/ - JQF + Zest: Coverage-guided semantic fuzzing for Java
https://github.com/rohanpadhye/jqf - http2fuzz
https://github.com/c0nrad/http2fuzz - Demystifying hypothesis testing with simple Python examples
https://towardsdatascience.com/demystifying-hypothesis-testing-with-simple-python-examples-4997ad3c5294