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