Hlavní navigace

Úvod do problematiky fuzzingu a fuzz testování – nástroj go-fuzz

Pavel Tišnovský

Ve druhém článku o fuzzy testování a o fuzzerech obecně se budeme věnovat popisu způsobů použití existujících nástrojů. Prvním z nich je go-fuzz, který byl použit pro objevení mnoha chyb nejenom ve standardní knihovně jazyka Go.

Doba čtení: 24 minut

Sdílet

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

15. Obsah třetí části seriálu

16. Repositář s demonstračními příklady

17. Odkazy na Internetu

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
Poznámka: samotná instalace může zabrat určitou relativně dlouhou dobu, protože v repositáři jsou již připraveny některé hotové korpusy. Další korpusy jsou součástí samostatného repositáře dostupného na adrese https://github.com/dvyukov/go-fuzz-corpus.

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) {
}
Poznámka: reálně testované funkce pochopitelně akceptují odlišné parametry; to je však situace, kterou je nutné zajistit v dále popsané funkci Fuzz, která celé testování řídí. Tuto možnost si ukážeme v navazujících kapitolách.

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.
Poznámka: žádné další návratové hodnoty by se používat neměly, jsou totiž rezervovány pro další rozšiřování funkcionality knihovny go-fuzz.

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"
Poznámka: fuzzer tedy správně zjistil, že funkce je naprogramovaná špatně a zhavaruje na vstupu „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:

  1. Jeden vstup je kladný a druhý záporný – zde dojde k chybě při alokaci kvůli zápornému počtu prvků
  2. 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...
Poznámka: povšimněte si, že došlo k detekci dvou „crasherů“. To je dobře, protože očekáváme výskyt dvou typů chyb – použití záporné hodnoty a použití velké kladné hodnoty.

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
}
Poznámka: povšimněte si, že vracíme hodnoty 0 nebo 1, čímž fuzzeru oznamujeme, která data se mají přidat do korpusu a která naopak nikoli.

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/is­sues/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í:

Root obecný tip

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:

# Příklad Stručný popis Cesta
1 example1.go prázdná funkce, která se má testovat (nikdy nezhavaruje) https://github.com/tisnik/fuzzing-examples/blob/master/go-fuzz/example1/example1.go
2 fuzz.go vstupní bod fuzzy testů https://github.com/tisnik/fuzzing-examples/blob/master/go-fuzz/example1/fuzz.go
     
3 example2.go funkce, která zpracovává vstupy, nikdy nezhavaruje https://github.com/tisnik/fuzzing-examples/blob/master/go-fuzz/example2/example2.go
4 fuzz.go vstupní bod fuzzy testů, může zhavarovat na základě výsledku https://github.com/tisnik/fuzzing-examples/blob/master/go-fuzz/example2/fuzz.go
     
5 example3.go funkce obsahující chybu – přístup na neexistující prvek řezu při vhodném vstupu https://github.com/tisnik/fuzzing-examples/blob/master/go-fuzz/example3/example3.go
6 fuzz.go vstupní bod fuzzy testů https://github.com/tisnik/fuzzing-examples/blob/master/go-fuzz/example3/fuzz.go
     
7 example4.go funkce obsahující dvě chyby – špatná práce se zápornými hodnotami https://github.com/tisnik/fuzzing-examples/blob/master/go-fuzz/example4/example4.go
8 fuzz.go vstupní bod fuzzy testů https://github.com/tisnik/fuzzing-examples/blob/master/go-fuzz/example4/fuzz.go
     
9 example5.go otestování, jak se provádí zápis a čtení rastrových dat ve formátu GIF https://github.com/tisnik/fuzzing-examples/blob/master/go-fuzz/example5/example5.go
10 fuzz.go vstupní bod fuzzy testů https://github.com/tisnik/fuzzing-examples/blob/master/go-fuzz/example5/fuzz.go

17. Odkazy na Internetu

  1. Fuzzing (Wikipedia)
    https://en.wikipedia.org/wiki/Fuzzing
  2. american fuzzy lop
    http://lcamtuf.coredump.cx/afl/
  3. Fuzzing: the new unit testing
    https://go-talks.appspot.com/github.com/dvyukov/go-fuzz/slides/fuzzing.slide#1
  4. Corpus for github.com/dvyukov/go-fuzz examples
    https://github.com/dvyukov/go-fuzz-corpus
  5. AFL – QuickStartGuide.txt
    https://github.com/google/AF­L/blob/master/docs/QuickStar­tGuide.txt
  6. Introduction to Fuzzing in Python with AFL
    https://alexgaynor.net/2015/a­pr/13/introduction-to-fuzzing-in-python-with-afl/
  7. Writing a Simple Fuzzer in Python
    https://jmcph4.github.io/2018/01/19/wri­ting-a-simple-fuzzer-in-python/
  8. How to Fuzz Go Code with go-fuzz (Continuously)
    https://fuzzit.dev/2019/10/02/how-to-fuzz-go-code-with-go-fuzz-continuously/
  9. Golang Fuzzing: A go-fuzz Tutorial and Example
    http://networkbit.ch/golang-fuzzing/
  10. Fuzzing Python Modules
    https://stackoverflow.com/qu­estions/20749026/fuzzing-python-modules
  11. 0×3 Python Tutorial: Fuzzer
    http://www.primalsecurity.net/0×3-python-tutorial-fuzzer/
  12. fuzzing na PyPi
    https://pypi.org/project/fuzzing/
  13. Fuzzing 0.3.2 documentation
    https://fuzzing.readthedoc­s.io/en/latest/
  14. Randomized testing for Go
    https://github.com/dvyukov/go-fuzz
  15. HTTP/2 fuzzer written in Golang
    https://github.com/c0nrad/http2fuzz
  16. 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
  17. Continuous Fuzzing Made Simple
    https://fuzzit.dev/
  18. Halt and Catch Fire
    https://en.wikipedia.org/wi­ki/Halt_and_Catch_Fire#In­tel_x86
  19. Pentium F00F bug
    https://en.wikipedia.org/wi­ki/Pentium_F00F_bug
  20. Random testing
    https://en.wikipedia.org/wi­ki/Random_testing
  21. Monkey testing
    https://en.wikipedia.org/wi­ki/Monkey_testing
  22. Fuzzing for Software Security Testing and Quality Assurance, Second Edition
    https://books.google.at/bo­oks?id=tKN5DwAAQBAJ&pg=PR15&lpg=PR15&q=%­22I+settled+on+the+term+fuz­z%22&redir_esc=y&hl=de#v=o­nepage&q=%22I%20settled%20on%20the%20ter­m%20fuzz%22&f=false
  23. Z80 Undocumented Instructions
    http://www.z80.info/z80undoc.htm
  24. The 6502/65C02/65C816 Instruction Set Decoded
    http://nparker.llx.com/a2/op­codes.html
  25. libFuzzer – a library for coverage-guided fuzz testing
    https://llvm.org/docs/LibFuzzer.html
  26. fuzzy-swagger na PyPi
    https://pypi.org/project/fuzzy-swagger/
  27. fuzzy-swagger na GitHubu
    https://github.com/namuan/fuzzy-swagger
  28. Fuzz testing tools for Python
    https://wiki.python.org/mo­in/PythonTestingToolsTaxo­nomy#Fuzz_Testing_Tools
  29. A curated list of awesome Go frameworks, libraries and software
    https://github.com/avelino/awesome-go
  30. gofuzz: a library for populating go objects with random values
    https://github.com/google/gofuzz
  31. tavor: A generic fuzzing and delta-debugging framework
    https://github.com/zimmski/tavor
  32. hypothesis na GitHubu
    https://github.com/Hypothe­sisWorks/hypothesis
  33. Hypothesis: Test faster, fix more
    https://hypothesis.works/
  34. Hypothesis
    https://hypothesis.works/ar­ticles/intro/
  35. What is Hypothesis?
    https://hypothesis.works/articles/what-is-hypothesis/
  36. Databáze CVE
    https://www.cvedetails.com/
  37. Fuzz test Python modules with libFuzzer
    https://github.com/eerimoq/pyfuzzer
  38. Taof – The art of fuzzing
    https://sourceforge.net/pro­jects/taof/
  39. JQF + Zest: Coverage-guided semantic fuzzing for Java
    https://github.com/rohanpadhye/jqf
  40. http2fuzz
    https://github.com/c0nrad/http2fuzz
  41. Demystifying hypothesis testing with simple Python examples
    https://towardsdatascience­.com/demystifying-hypothesis-testing-with-simple-python-examples-4997ad3c5294