Úvod do problematiky fuzzingu a fuzz testování – složení vlastního fuzzeru

12. 3. 2020
Doba čtení: 21 minut

Sdílet

Ve třetím článku o testování s využitím fuzzingu si ukážeme, jak lze vytvořit vlastní jednoduchý fuzzer. Zaměříme se přitom především na testování REST API, tedy mj. i koncových bodů, které akceptují data ve formátu JSON.

Obsah

1. Úvod do problematiky fuzzingu a fuzz testování – složení vlastního fuzzeru

2. Nejjednodušší fuzzery bez zpětné vazby

3. Základ pro vlastní fuzzer – nástroj radamsa

4. „Mutace“ dat podporované nástrojem radamsa

5. Ukázky základních možností mutace dat

6. Vygenerování dat pro otestování textových vstupů

7. Víceřádková textová data

8. Příprava pro testování REST API

9. Otestování HTTP serveru pomocí nástroje ffuf

10. Vygenerování série náhodně zvolených koncových bodů

11. Upravený HTTP server zobrazující tělo požadavku

12. Vygenerování pseudonáhodných dat poslaných v těle požadavku

13. Vygenerování JSONu s pseudonáhodnými daty pro otestování HTTP serveru

14. Třída RandomPayloadGenerator

15. Modul pro mutaci (fuzzing) vstupního souboru ve formátu JSON

16. Ukázka práce fuzzeru

17. Využití vygenerovaných dat

18. Závěr

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

20. Odkazy na Internetu

1. Úvod do problematiky fuzzingu a fuzz testování – složení vlastního fuzzeru

V předchozí dvojici článků [1] [2] a fuzzingu a fuzzerech jsme se zpočátku zabývali spíše teoriemi, na kterých jsou tyto technologie založeny. Taktéž jsme si ukázali alespoň základní způsoby využití fuzzeru nazvaného go-fuzz, který je určen pro testování aplikací vyvinutých v programovacím jazyce Go. Dnes se budeme zabývat poněkud odlišnou kategorií fuzzerů – bude se jednat o nástroje určené pro testování REST API, tedy mj. i takových koncových bodů (endpoints), které jako svůj vstup akceptují data uložená do formátu JSON, popř. XML. Používat budeme převážně programovací jazyk Python, protože fuzzery pochopitelně nejsou ani zdaleka omezeny jen na programovací jazyk Go.

2. Nejjednodušší fuzzery bez zpětné vazby

Vlastní fuzzer je možné sestavit rozličnými způsoby. Buď můžeme použít již existující nástroje a využívat jejich možnosti, nebo se skutečně můžeme pokusit vytvořit si vlastní (zpočátku pravděpodobně ten nejjednodušší možný) fuzzer, který bude přesně odpovídat požadavkům programátora. Dnes si ukážeme obě možnosti, ovšem nejprve si připomeňme, na základě jakých kategorií je možné jednotlivé typy fuzzerů rozlišit:

  1. Jakým způsobem jsou generovány vstupy použité v testech a jak je vůbec specifikováno, o jaká data se má jednat.
  2. Zda fuzzer zná a nějakým způsobem využívá informace o vnitřní struktuře testovaného systému či nikoli (rozdělení je na black-box, white-box a gray-box testování).
  3. A dále podle toho, zda a jak fuzzer rozumí struktuře vstupních dat či zda jen pseudonáhodně generuje vstupy bez dalších potřebných znalostí či zpětné vazby (ta je mnohdy důležitá pro vytvoření minimální sady vstupů, které způsobí chybu).
  4. Zda fuzz testy zjišťují pokrytí (coverage) a upravují podle toho svoji sadu testovacích dat (korpus). Obecně se jedná o nejrychlejší cestu k nalezení chyby.

3. Základ pro vlastní fuzzer – nástroj radamsa

Nejprve se seznámíme s již existujícím nástrojem, který je primárně určen pro generování vstupních dat na základě předloženého vzoru. Tato data jsou využitelná dalšími nástroji, popř. přímo použitelná při jednoduchém testování. Tento nástroj, který se jmenuje radamsa, je vlastně tím nejjednodušším fuzzerem, jenž pouze generuje sekvenci pseudonáhodných dat na základě zadaných kritérií, ovšem neobsahuje žádnou zpětnou vazbu typu „tudy už nechoď“ či „pokus se tento chybný vstup zkrátit“. Z tohoto ohledu se tedy jedná o nástroj mnohem primitivnější než minule zmíněný program go-fuzz.

Ukažme si nyní překlad a instalaci . Ta je snadná a vyžaduje pouze minimum vývojářských nástrojů – překladač gcc, nástroj Make a klienta Gitu. Celý postup překladu a instalace vypadá následovně:

$ git clone https://gitlab.com/akihe/radamsa.git
$ cd radamsa
$ make
$ sudo make install
Poznámka: poslední příkaz je možné vynechat, pokud nebudete potřebovat, aby aplikaci radamsa používali i další uživatelé na stejném počítači (což je v současnosti většinou zbytečné).

Průběh stažení a instalace:

Cloning into 'radamsa'...
remote: Enumerating objects: 154, done.
remote: Counting objects: 100% (154/154), done.
remote: Compressing objects: 100% (90/90), done.
remote: Total 1724 (delta 89), reused 114 (delta 61), pack-reused 1570
Receiving objects: 100% (1724/1724), 461.26 KiB | 684.00 KiB/s, done.
Resolving deltas: 100% (1062/1062), done.
Checking connectivity... done.
test -x bin/ol || make bin/ol
make[1]: Entering directory `/home/tester/temp/out/radamsa'
test -f ol.c.gz || wget -O ol.c.gz https://gitlab.com/owl-lisp/owl/uploads/92375620fb4d570ee997bc47e2f6ddb7/ol-0.1.21.c.gz || curl -L -o ol.c.gz https://gitlab.com/owl-lisp/owl/uploads/92375620fb4d570ee997bc47e2f6ddb7/ol-0.1.21.c.gz
--2020-03-10 20:58:46--  https://gitlab.com/owl-lisp/owl/uploads/92375620fb4d570ee997bc47e2f6ddb7/ol-0.1.21.c.gz
Resolving gitlab.com (gitlab.com)... 35.231.145.151
Connecting to gitlab.com (gitlab.com)|35.231.145.151|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 321839 (314K) [application/x-gzip]
Saving to: ‘ol.c.gz’
 
100%[=============================================================================>] 321 839      497KB/s   in 0,6s
 
2020-03-10 20:58:48 (497 KB/s) - ‘ol.c.gz’ saved [321839/321839]
 
gzip -d < ol.c.gz > ol.c
mkdir -p bin
cc -Wall -O3 -o bin/ol ol.c
make[1]: Leaving directory `/home/tester/temp/out/radamsa'
bin/ol -O1 -o radamsa.c rad/main.scm
mkdir -p bin
cc -Wall -O3  -o bin/radamsa radamsa.c

Po překladu (doufejme že úspěšném) postačuje přejít do podadresáře bin a spustit:

$ ./radamsa --help
 
Usage: radamsa [arguments] [file ...]
  -h | --help, show this thing
  -a | --about, what is this thing?
  -V | --version, show program version
  -o | --output <arg>, output pattern, e.g. out.bin /tmp/fuzz-%n.%s, -, :80 or 127.0.0.1:80 or 127.0.0.1:123/udp [-]
  -n | --count <arg>, how many outputs to generate (number or inf) [1]
  -s | --seed <arg>, random seed (number, default random)
  -m | --mutations <arg>, which mutations to use [ft=2,fo=2,fn,num=5,td,tr2,ts1,tr,ts2,ld,lds,lr2,li,ls,lp,lr,lis,lrs,sr,sd,bd,bf,bi,br,bp,bei,bed,ber,uw,ui=2,xp=9,ab]
  -p | --patterns <arg>, which mutation patterns to use [od,nd=2,bu]
  -g | --generators <arg>, which data generators to use [random,file=1000,jump=200,stdin=100000]
  -M | --meta <arg>, save metadata about generated files to this file
  -r | --recursive, include files in subdirectories
  -S | --seek <arg>, start from given testcase
  -T | --truncate <arg>, take only first n bytes of each output (mainly intended for UDP)
  -d | --delay <arg>, sleep for n milliseconds between outputs
  -l | --list, list mutations, patterns and generators
  -C | --checksums <arg>, maximum number of checksums in uniqueness filter (0 disables) [10000]
  -H | --hash <arg>, hash algorithm for uniqueness checks (stream, sha1 or sha256) [stream]
  -v | --verbose, show progress during generation

Popř. můžeme – přesně podle návodu – zkusit jedinou „mutaci“ vstupních dat:

$ echo "HAL 9000" | ./radamsa 
 
HAL 1701411834604692317316873037158827154

4. „Mutace“ dat podporované nástrojem radamsa

Nástroj radamsa, který jsme právě přeložili a nainstalovali, pracuje tak, že postupně „mutuje“ vstupní data, a to s využitím mnoha různých algoritmů, z nichž některé jsou určeny pro vytváření textových vstupů pro testy a jiné pro vstupy binární. Všechny podporované algoritmy lze společně s jejich možnými parametry vypsat následujícím příkazem:

$ ./radamsa -l

Poslední verze radamsy by měla vypsat tyto algoritmy:

Mutations (-m)
  ab: enhance silly issues in ASCII string data handling
  bd: drop a byte
  bf: flip one bit
  bi: insert a random byte
  br: repeat a byte
  bp: permute some bytes
  bei: increment a byte by one
  bed: decrement a byte by one
  ber: swap a byte with a random one
  sr: repeat a sequence of bytes
  sd: delete a sequence of bytes
  ld: delete a line
  lds: delete many lines
  lr2: duplicate a line
  li: copy a line closeby
  lr: repeat a line
  ls: swap two lines
  lp: swap order of lines
  lis: insert a line from elsewhere
  lrs: replace a line with one from elsewhere
  td: delete a node
  tr2: duplicate a node
  ts1: swap one node with another one
  ts2: swap two nodes pairwise
  tr: repeat a path of the parse tree
  uw: try to make a code point too wide
  ui: insert funny unicode
  num: try to modify a textual number
  xp: try to parse XML and mutate it
  ft: jump to a similar position in block
  fn: likely clone data between similar positions
  fo: fuse previously seen data elsewhere
  nop: do nothing (debug/test)
 
Mutation patterns (-p)
  od: Mutate once
  nd: Mutate possibly many times
  bu: Make several mutations closeby once
 
Generators (-g)
 stdin: read data from standard input if no paths are given or - is among them
 file: read data from given files
 random: generate random data
Poznámka: ve skutečnosti je možné algoritmy prakticky libovolným způsobem spojovat dohromady a vytvořit tak nepřeberné množství kombinací.

5. Ukázky základních možností mutace dat

Díky tomu, že radamsa nabízí mnoho typů mutace dat, je možné vytvářet pseudonáhodné vstupy pro různé typy testů. V prvním příkladu se vstup, kterým je řetězec „param=123“ podrobí libovolné mutaci, a to celkem pětkrát:

$ echo "param=123" | ./radamsa -n 5 
 
param=xyzzy
par am=123
param=-4294967297
param=340282366920938463463374607431768211457
param=ʳ256
Poznámka: povšimněte si, že se mj. použily i „zajímavé“ numerické konstanty, kterými lze obecně zjistit například přetékání číselných hodnot atd.

Ve druhém příkladu určíme, že se má použít algoritmus měnící pouze číselné hodnoty ze vstupu:

$ echo "param=123" | ./radamsa -n 5 -m num
 
param=-0
param=-14
param=-207
param=65536
param=124

Další příklad vygeneruje testy pro jazyk Python:

$ echo "print(1+2**3)" | ./radamsa -n 10 -m num
 
print(1+4294967294**3)
print(9240853959469937641+0**1)
print(1+2147483648**3)
print(1+1**256)
print(-1540+2**3)
print(1+2**0)
print(0+18446744073709581601**-18446744073709551612)
print(170141183460469231731687303715884105727+-2986**1)
print(150+2**65537)
print(1+2**3)

Testy si pochopitelně můžete spustit, i když jejich vykonání bude trvat velmi dlouho:

$ echo "print(1+2**3)" | ./radamsa -n 10 -m num > test.py
$ python3 test.py

Testy pro nástroj bc:

$ echo "1+(2*3)-(4)" | ./radamsa -n 10 -m num 
 
1+(2*2)-(4)
1+(-1*9223372036854775807)-(1)
1+(2*4)-(340282366920938463463374607431768211455)
65535+(18446744073709551617*3)-(32773)
0+(-257*253)-(4)
1+(65535*4628165)-(4294967295)
1+(9223372036854775808*10)-(170141183460469231731687303715884105728)
4294967296+(--340282366920938463463374607349120052059*3)-(-24536405709273)
0+(2147483651*1)-(2)
1+(2*3)-(4)

Poslání testů přímo do bc:

$ echo "1+(2*3)-(4)" | ./radamsa -n 10 -m num | bc
 
18
0
9
384
18446744073709617150
4
2006146606
1020847100762815390436240682475606796917
32763
61657
Poznámka: poměrně často je nutné vytvořenou sekvenci znovu opakovat. V takovém případě použijte parametr -seed.

6. Vygenerování dat pro otestování textových vstupů

Zajímavé jsou i možnosti, které nástroj radamsa nabízí při generování pseudonáhodných dat vhodných pro otestování textových vstupů. V dalším příkladu se slovo „password“ mutuje takovým způsobem, že se některé jeho znaky nebo i sekvence znaků opakují:

$ echo "password" | ./radamsa -n 5 -m sr
 
passwordordordordordordordordordordordordordordordordordordordordordordordordordordord
passwordordordordordordord
passworrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrd
ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppassssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssword
passwordddddddddddd

Naopak je možné vstup (opět slovo „password“ postupně zmenšovat mazáním náhodně vybraných znaků:

$ echo "password" | ./radamsa -n 5 -m sd
 
pswo
 
pa
passwod
password

Kombinace obou možností:

$ echo "password" | ./radamsa -n 5 -m sd -m sr
 
pppppppppppppppppppasssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssword
passwordrd
ppassswowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowoword
passsssssssswordordordordordordordordordordordordordordordordordordordordorororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordord
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppapassworrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrd

Další algoritmus mutace dat, tentokrát založený na permutaci:

$ echo "password" | ./radamsa -n 5 -m bp
 
pasrws
odpodaw
srspasdsrw
opassodrw
wro
spdas

Generování numerických dat, ovšem jen z číslovek 1–6:

$ echo "123456" | ./radamsa -n 10 -m bp
 
12
3456123456
613524
2
13546124536
621
453124
36513
64251354
62513246

7. Víceřádková textová data

Šablony mohou být specifikovány i v souborech. Následující příklad vytvoří sérii pěti souborů s pseudonáhodnými daty odvozenými od šablon uložených v souborech „example1.txt“ a „example2.txt“:

$ ./radamsa -n 5 -o %n.txt example1.txt example2.txt

Ve vstupním textovém souboru lze prohazovat řádky a vytvořit tak například sérii vstupů pro otestování překladače:

$ ./radamsa http_server.go -m ls

Řádky lze pochopitelně i mazat, a to naprosto stejným způsobem:

$ ./radamsa http_server.go -m ld

Kombinace s předchozími možnostmi:

$ ./radamsa http_server.go -m ld -m bp

Skutečně váš program dokáže zpracovat Unicode?:

$ echo "hello" | ./radamsa -n 10 -m ui

Ideální vstup pro programy napsané v céčku:

$ echo "hello" | ./radamsa -n 10 -m ab

8. Příprava pro testování REST API

Ve druhé polovině článků se budeme zabývat testováním REST API. Připravíme si tedy jednoduchý HTTP server, který bude zpracovávat všechny endpointy a bude na požadavky odpovídat stále stejným řetězcem. Vzhledem k tomu, že jsme již minule použili jazyk Go, napíšeme i webový server v tomto programovacím jazyce:

package main
 
import (
        "io"
        "log"
        "net/http"
)
 
func mainEndpoint(writer http.ResponseWriter, request *http.Request) {
        log.Println(request.URL)
        io.WriteString(writer, "Hello world!\n\n")
}
 
func main() {
        http.HandleFunc("/", mainEndpoint)
        http.ListenAndServe(":8080", nil)
}

Spustíme ho:

$ go run http_server.go

A otestujeme:

$ curl -v localhost:8080
 
* Rebuilt URL to: localhost:8080/
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Wed, 11 Mar 2020 19:24:29 GMT
< Content-Length: 14
< Content-Type: text/plain; charset=utf-8
<
Hello world!
 
* Connection #0 to host localhost left intact

9. Otestování HTTP serveru pomocí nástroje ffuf

Nyní již můžeme náš HTTP server otestovat, a to s použitím nástroje nazvaného ffuf. Ten lze nainstalovat přímo z dodávaného tarballu dostupného na adrese https://github.com/ffuf/ffuf/re­leases/tag/v1.0.2. Nejdříve si připravíme vstupní soubor obsahující seznam všech koncových bodů (soubor může obsahovat i parametry endpointů atd.):

 
endpoint1
endpoint2
foo/bar
foo/bar/baz

Tento seznam uložíme do souboru pojmenovaného „endpoints.txt“. Následně spustíme nástroj fuff, kterému se musí předat jak jméno souboru, tak i adresa testovaného HTTP serveru. Povšimněte si, že se na konci adresy nachází řetězec „FUFF“. Tento řetězec je postupně nahrazován řetězci načítanými ze souboru „endpoints.txt“:

$ ffuf -w endpoints.txt -u http://localhost:8080/FUZZ

Průběh celého testování vypadá následovně:

 
        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/
 
       v1.1.0-git
________________________________________________
 
 :: Method           : GET
 :: URL              : http://localhost:8080/FUZZ
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403
________________________________________________
 
endpoint2               [Status: 200, Size: 14, Words: 2, Lines: 3]
                        [Status: 200, Size: 14, Words: 2, Lines: 3]
foo/bar/baz             [Status: 200, Size: 14, Words: 2, Lines: 3]
foo/bar                 [Status: 200, Size: 14, Words: 2, Lines: 3]
endpoint1               [Status: 200, Size: 14, Words: 2, Lines: 3]
:: Progress: [5/5] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

Můžeme vidět, že se vytvořilo celkem pět požadavků a na všechny tyto požadavky server odpověděl HTTP kódem 200 OK, což je v pořádku (ostatně by bylo podezřelé, kdyby tak malý HTTP server zhavaroval).

10. Vygenerování série náhodně zvolených koncových bodů

Nyní si vytvoříme soubor nazvaný „example.txt“ a uložíme do něj jediný řádek:

endpoint

Z tohoto souboru je možné vygenerovat libovolný počet koncových bodů (endpointů):

$ ./radamsa example.txt -n 5
 
endpoint
endp�oint
mpoint
nendpoi�nt
eodpoendpoint
Poznámka: povšimněte si Unicode „paznaků“.

Takto vytvořené koncové body by bylo možné použít pro testování HTTP serveru, ovšem ve skutečnosti není nutné nástroj radamsa volat přímo. Můžeme ho totiž zavolat automaticky přes ffuf – nyní se pseudonáhodné endpointy připojí na konec URL namísto řetězce „FUZZ“:

$ ffuf --input-cmd './radamsa example.txt' -u http://localhost:8080/FUZZ

Taktéž lze snadno vytvářet pseudonáhodné hodnoty parametru/parametrů:

$ ffuf --input-cmd './radamsa example.txt' -u http://localhost:8080/parameter1=FUZZ

11. Upravený HTTP server zobrazující tělo požadavku

Náš HTTP server můžeme nepatrně upravit a vylepšit takovým způsobem, aby zobrazoval těla požadavků (takzvaný payload). Nová varianta serveru může vypadat následovně:

package main
 
import (
        "io"
        "io/ioutil"
        "log"
        "net/http"
)
 
func mainEndpoint(writer http.ResponseWriter, request *http.Request) {
        log.Println(request.URL)
        body, err := ioutil.ReadAll(request.Body)
        if err != nil {
                log.Printf("Error reading body: %v", err)
                http.Error(writer, "can't read body", http.StatusBadRequest)
                return
        }
        log.Println(string(body))
        io.WriteString(writer, "Hello world!\n\n")
}
 
func main() {
        http.HandleFunc("/", mainEndpoint)
        http.ListenAndServe(":8080", nil)
}

Server spustíme:

$ go run http_server_post.go

12. Vygenerování pseudonáhodných dat poslaných v těle požadavku

V tomto okamžiku již můžeme využít kombinaci nástrojů ffuf a radamsa pro otestování koncového bodu HTTP serveru, kterému se předají data v těle požadavku (tedy metodou POST). Povšimněte si, že náhodnou hodnotu bude mít parametr „name“:

$ ffuf --input-cmd './radamsa values.txt' -u http://localhost:8080/ -X POST -H "Content-Type: application/json" -d '{"name": "FUZZ", "anotherkey": "anothervalue"}' -fr "error"

Průběh testování:

 
        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/
 
       v1.1.0-git
________________________________________________
 
 :: Method           : POST
 :: URL              : http://localhost:8080/
 :: Header           : Content-Type: application/json
 :: Data             : {"name": "FUZZ", "anotherkey": "anothervalue"}
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403
 :: Filter           : Regexp: error
________________________________________________
 
1                       [Status: 200, Size: 14, Words: 2, Lines: 3]
2                       [Status: 200, Size: 14, Words: 2, Lines: 3]
3                       [Status: 200, Size: 14, Words: 2, Lines: 3]
4                       [Status: 200, Size: 14, Words: 2, Lines: 3]
...
...
...
96                      [Status: 200, Size: 14, Words: 2, Lines: 3]
97                      [Status: 200, Size: 14, Words: 2, Lines: 3]
98                      [Status: 200, Size: 14, Words: 2, Lines: 3]
99                      [Status: 200, Size: 14, Words: 2, Lines: 3]
100                     [Status: 200, Size: 14, Words: 2, Lines: 3]
:: Progress: [100/100] :: Job [1/1] :: 50 req/sec :: Duration: [0:00:02] :: Errors: 0 ::

13. Vygenerování JSONu s pseudonáhodnými daty pro otestování HTTP serveru

V následujících dvou kapitolách si ukážeme jednoduchou utilitu určenou pro vygenerování hodnot ve formátu JSON s pseudonáhodnými daty, která lze využít pro otestování HTTP serveru. Tato utilitka je umístěna v repositáři na adrese https://github.com/tisnik/payload-fuzzer a skládá se ze dvou zdrojových souborů:

  1. random_payload_generator.py
  2. json_fuzzer.py
Poznámka: v Pythonu již přes půl roku aktivně nepracuji, tak se předem omlouvám za všechny nedostatky, které tato utilitka má – nicméně svůj základní účel splňuje.

14. Třída RandomPayloadGenerator

Ve třídě RandomPayloadGenerator je implementován generátor náhodných dat, a to včetně seznamů a slovníků. A právě různým způsobem vnořené seznamy a slovníky tvoří struktury vhodné pro serializaci do formátu JSON:

"""Generator of random payload for testing API."""
 
import string
import random
 
 
class RandomPayloadGenerator:
    """Generator of random payload for testing API."""
 
    def __init__(self):
        """Initialize the random payload generator."""
        self.iteration_deep = 0
        self.max_iteration_deep = 2
        self.max_dict_key_length = 10
        self.max_string_length = 20
        self.dict_key_characters = string.ascii_lowercase + string.ascii_uppercase + "_"
        self.string_value_characters = (string.ascii_lowercase + string.ascii_uppercase +
                                        "_" + string.punctuation + " ")
 
    def generate_random_string(self, n, uppercase=False, punctuations=False):
        """Generate random string of length=n."""
        prefix = random.choice(string.ascii_lowercase)
        mix = string.ascii_lowercase + string.digits
 
        if uppercase:
            mix += string.ascii_uppercase
        if punctuations:
            mix += string.punctuation
 
        suffix = ''.join(random.choice(mix) for _ in range(n - 1))
        return prefix + suffix
 
    def generate_random_key_for_dict(self, data):
        """Generate a string key to be used in dictionary."""
        existing_keys = data.keys()
        while True:
            new_key = self.generate_random_string(10)
            if new_key not in existing_keys:
                return new_key
 
    def generate_random_list(self, n):
        """Generate list filled in with random values."""
        return [self.generate_random_payload((int, str, float, bool, list, dict)) for i in range(n)]
 
    def generate_random_dict(self, n):
        """Generate dictionary filled in with random values."""
        dict_content = (int, str, list, dict)
        return {self.generate_random_string(10): self.generate_random_payload(dict_content)
                for i in range(n)}
 
    def generate_random_list_or_string(self):
        """Generate list filled in with random strings."""
        if self.iteration_deep < self.max_iteration_deep:
            self.iteration_deep += 1
            value = self.generate_random_list(5)
            self.iteration_deep -= 1
        else:
            value = self.generate_random_value(str)
        return value
 
    def generate_random_dict_or_string(self):
        """Generate dict filled in with random strings."""
        if self.iteration_deep < self.max_iteration_deep:
            self.iteration_deep += 1
            value = self.generate_random_dict(5)
            self.iteration_deep -= 1
        else:
            value = self.generate_random_value(str)
        return value
 
    def generate_random_value(self, type):
        """Generate one random value of given type."""
        generators = {
            str: lambda: self.generate_random_string(20, uppercase=True, punctuations=True),
            int: lambda: random.randrange(100000),
            float: lambda: random.random() * 100000.0,
            bool: lambda: bool(random.getrandbits(1)),
            list: lambda: self.generate_random_list_or_string(),
            dict: lambda: self.generate_random_dict_or_string()
        }
        generator = generators[type]
        return generator()
 
    def generate_random_payload(self, restrict_types=None):
        """Generate random payload with possibly restricted data types."""
        if restrict_types:
            types = restrict_types
        else:
            types = (str, int, float, list, dict, bool)
 
        selected_type = random.choice(types)
 
        return self.generate_random_value(selected_type)

15. Modul pro mutaci (fuzzing) vstupního souboru ve formátu JSON

Druhý zdrojový soubor obsahuje modul provádějící mutaci (či fuzzing) vstupního souboru ve formátu JSON. Ze vstupního souboru jsou vytvořeny tři nové třídy souborů:

  1. JSON s odstraněnými položkami
  2. JSON s přidanými pseudonáhodnými položkami
  3. JSON s mutovanými pseudonáhodnými položkami

Následuje výpis zdrojového kódu tohoto modulu:

import json
import os
import sys
import itertools
import copy
import random
 
from random_payload_generator import RandomPayloadGenerator
 
 
output_num = 0
 
 
def load_json(filename):
    """Load and decode JSON file."""
    with open(filename) as fin:
        return json.load(fin)
 
 
def generate_output(payload):
    global output_num
    filename = "generated_{}.json".format(output_num)
    with open(filename, 'w') as f:
        json.dump(payload, f, indent=4)
        output_num += 1
 
 
def remove_items_one_iter(original_payload, items_count, remove_flags):
    keys = list(original_payload.keys())
    # deep copy
    new_payload = copy.deepcopy(original_payload)
    for i in range(items_count):
        remove_flag = remove_flags[i]
        if remove_flag:
            key = keys[i]
            del new_payload[key]
 
    generate_output(new_payload)
 
 
def fuzz_remove_items(original_payload):
    items_count = len(original_payload)
    # lexicographics ordering
    remove_flags_list = list(itertools.product([True, False],
                             repeat=items_count))
    # the last item contains (False, False, False...) and we are not interested
    # in removing ZERO items
    remove_flags_list = remove_flags_list[:-1]
 
    for remove_flags in remove_flags_list:
        remove_items_one_iter(original_payload, items_count, remove_flags)
 
 
def add_items_one_iter(original_payload, how_many):
    # deep copy
    new_payload = copy.deepcopy(original_payload)
    rpg = RandomPayloadGenerator()
 
    for i in range(how_many):
        new_key = rpg.generate_random_key_for_dict(new_payload)
        new_value = rpg.generate_random_payload()
        new_payload[new_key] = new_value
 
    generate_output(new_payload)
 
 
def fuzz_add_items(original_payload, min, max, mutations):
    for how_many in range(min, max):
        for i in range(1, mutations):
            add_items_one_iter(original_payload, how_many)
 
 
def change_items_one_iteration(original_payload, how_many):
    # deep copy
    new_payload = copy.deepcopy(original_payload)
    rpg = RandomPayloadGenerator()
 
    for i in range(how_many):
        selected_key = random.choice(list(original_payload.keys()))
        new_value = rpg.generate_random_payload()
        new_payload[selected_key] = new_value
 
    generate_output(new_payload)
 
 
def fuzz_change_items(original_payload, min, max):
    for how_many in range(1, len(original_payload)):
        for i in range(min, max):
            change_items_one_iteration(original_payload, how_many)
 
 
def main(filename):
    original_payload = load_json(filename)
    fuzz_remove_items(original_payload)
    fuzz_add_items(original_payload, 1, 4, 2)
    fuzz_change_items(original_payload, 1, 4)
 
 
if len(sys.argv) < 2:
    print("Usage: python json_fuzzer.py input_file.json")
    sys.exit(1)
 
main(sys.argv[1])

16. Ukázka práce fuzzeru

Vstupní soubor, který bude tvořit základní šablonu dat, může vypadat následovně:

{
    "name": "baf",
    "type": "fuzzer",
    "version": 2,
    "subversion": 0
}

Příklady vygenerovaných výsledků:

{
    "type": "fuzzer"
}
 
{
    "subversion": 0,
    "version": 2,
    "t0i1nzhfih": "rXZY^w%Yr#p6Gk:!vpMs",
    "name": "baf",
    "type": "fuzzer",
    "qaao0f1o3d": 46589.933317956624
}
 
{
    "subversion": 0,
    "name": "baf",
    "version": [
        "d#7QZu,UCQc.;F8w/!B",
        [
            3572,
            "u|~n\"V$I+.(,b}bE,!",
            "uaKuZQL;FyIleF*!h(;",
            "g}P0h6o.UKFY8sIV_C%~",
            true
        ],
        "gerL~`EgPN1-})JjH\"L",
        13805.792659417404,
        [
            31934.701190647098,
            "t8*!{B;c3ZFfg(aK[,\\",
            13634,
            "i|L'vuP=Lk(:56\\uhVn",
            "h+7M5OtTM\\LcPPP3z)i"
        ]
    ],
    "type": "fuzzer"
}

17. Využití vygenerovaných dat

Využití vygenerovaných dat je již jednoduché:

for file in *.json
do
    echo $file
    curl -X POST -d @$file localhost:8080/
done

Jen pro malou ukázku je zde vypsána část logů serveru:

ict ve školství 24

2020/03/11 21:21:03 /
2020/03/11 21:21:03 {    "subversion": 7168.838223613449,    "name": false,    "version": true,    "type": "fuzzer"}
2020/03/11 21:21:03 /
2020/03/11 21:21:03 {    "subversion": 83238.604257801,    "name": "tsvbG17xbD5NM1::!^>'",    "version": 2,    "type": "fuzzer"}
2020/03/11 21:21:03 /
2020/03/11 21:21:03 {    "version": 2}
2020/03/11 21:21:03 /
2020/03/11 21:21:03 {    "version": 2,    "type": "fuzzer"}
2020/03/11 21:21:03 /
2020/03/11 21:21:03 {    "name": "baf"}
2020/03/11 21:21:03 /
2020/03/11 21:21:03 {    "name": "baf",    "type": "fuzzer"}
2020/03/11 21:21:03 /
2020/03/11 21:21:03 {    "name": "baf",    "version": 2}
2020/03/11 21:21:03 /
2020/03/11 21:21:03 {    "name": "baf",    "version": 2,    "type": "fuzzer"}
2020/03/11 21:21:03 /
2020/03/11 21:21:03 {    "subversion": 0}
2020/03/11 21:21:03 /
2020/03/11 21:21:03 {    "subversion": 0,    "type": "fuzzer"}
2020/03/11 21:21:03 /
2020/03/11 21:21:03 {    "name": "baf",    "type": "fuzzer",    "version": 2,    "subversion": 0}

18. Závěr

V dnešním článku jsme si ukázali dvě možnosti sestavení vlastního jednoduchého fuzzeru. První možnost spočívá v kombinaci radamsa+ffuf, druhá pak ve využití ffuf společně s jednoduchým projektem payload-fuzzer.

19. 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 http_server.go jednoduchý HTTP server https://github.com/tisnik/fuzzing-examples/blob/master/simple_ser­ver/http_server.go
2 http_server_post.go HTTP server, který vypisuje celé tělo požadavku https://github.com/tisnik/fuzzing-examples/blob/master/simple_ser­ver/http_server_post.go

20. Odkazy na Internetu

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

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.