Fuzzing: software testen door het te bombarderen met chaos
Stel je voor dat je wilt weten of alle deuren en ramen van een gebouw goed op slot zitten. In plaats van elk slot zorgvuldig te inspecteren, huur je een leger robots in dat dag en nacht aan elke deurklink rammelt, tegen elk raam duwt en willekeurige sleutels probeert in elk slot. Zodra iets breekt, klemvast raakt of onverwacht opengaat, wordt dat genoteerd. Dat is in essentie wat fuzzing (ook wel fuzz testing) doet met software: een computerprogramma bestookt een ander programma met grote hoeveelheden onverwachte, willekeurige of subtiel bewerkte invoer, in de hoop dat het programma vastloopt, crasht of zich vreemd gaat gedragen.
Dat klinkt eenvoudig, en dat is voor een deel ook zo: het basisidee bestaat al sinds de jaren tachtig. Maar de manier waarop fuzzing tegenwoordig wordt toegepast, is een stuk slimmer geworden. Moderne fuzzers 'kijken' mee in de broncode van het programma dat ze testen en passen hun invoer daarop aan, waardoor ze veel efficiënter bugs en beveiligingslekken vinden dan puur willekeurig gokken ooit zou kunnen. Fuzzing is inmiddels een standaardonderdeel van hoe grote techbedrijven en open source-projecten hun software controleren voordat kwetsbaarheden door kwaadwillenden worden misbruikt.
Wat is het precies?
In de kern is een fuzzer een programma dat automatisch invoer genereert voor een ander programma (het 'doelwit') en vervolgens in de gaten houdt of dat doelwit crasht, vastloopt of andere tekenen van een fout vertoont. Bij een tekstverwerker kan die invoer bijvoorbeeld een kapot documentbestand zijn; bij een netwerkprogramma kan het een misvormd datapakketje zijn.
De eenvoudigste vorm heet random fuzzing: volledig willekeurige bytes worden het programma ingegooid. Dat vindt soms bugs, maar is inefficiënt, omdat de meeste willekeurige invoer meteen wordt afgewezen door het programma zonder ook maar in de buurt te komen van interessante code.
De grote doorbraak was coverage-guided fuzzing: hierbij houdt de fuzzer bij welke stukjes code (welke 'paden' door het programma) al zijn geraakt door eerdere test-invoer. Nieuwe invoer die nieuwe code-paden aanraakt, wordt bewaard in een verzameling die de corpus wordt genoemd. Vervolgens muteert de fuzzer die bewaarde voorbeelden lichtjes — een byte omdraaien, een stukje knippen en plakken, een getal net iets groter maken — en test opnieuw. Zo 'evolueert' de invoer geleidelijk richting steeds diepere, minder vaak geteste delen van het programma, ongeveer zoals natuurlijke selectie werkt: wat nieuwe code raakt, overleeft en wordt verder uitgebouwd.
Om fouten ook echt te kunnen zien, wordt het doelwitprogramma vaak gecompileerd met extra controles, zogeheten sanitizers. AddressSanitizer bijvoorbeeld detecteert geheugenfouten zoals een buffer overflow (het programma schrijft data buiten de grenzen van het geheugenblok dat daarvoor gereserveerd was) op het moment dat ze gebeuren, in plaats van pas veel later wanneer het lastiger te herleiden is waar de fout vandaan kwam. Zonder zo'n sanitizer merkt een fuzzer alleen de zichtbare crashes op; met sanitizer worden ook stille, sluimerende geheugenfouten zichtbaar die anders onopgemerkt zouden blijven.
Wat wil men ermee bereiken?
Het primaire doel is kwetsbaarheden vinden voordat kwaadwillenden dat doen. Veel van de ernstigste beveiligingslekken in software — met name in programma's geschreven in talen als C en C++, die programmeurs zelf verantwoordelijk maken voor geheugenbeheer — zijn geheugenfouten. Diezelfde fouten die een fuzzer laat crashen, kunnen door een aanvaller soms worden misbruikt om kwaadaardige code uit te voeren op andermans computer.
Daarnaast draait fuzzing om algemene softwarekwaliteit en betrouwbaarheid: een programma dat crasht op ongeldige invoer is simpelweg fragiel, ook los van beveiliging. Voor software die essentiële infrastructuur draait — besturingssystemen, browsers, encryptiebibliotheken, bestandsformaten die miljarden keren per dag worden geopend — is elke crash een potentieel probleem voor miljoenen gebruikers tegelijk.
Een derde, minder besproken doel is kostenbesparing: bugs die vroeg in de ontwikkeling worden gevonden, tijdens automatische tests, zijn veel goedkoper te repareren dan bugs die pas na een grote hack of dataschending aan het licht komen.
Voorbeelden uit de praktijk
AFL (American Fuzzy Lop), uitgebracht door beveiligingsonderzoeker Michal Zalewski in 2013-2014, wordt algemeen gezien als de fuzzer die coverage-guided fuzzing echt populair maakte. AFL was verrassend eenvoudig te gebruiken en vond in korte tijd tientallen kwetsbaarheden in veelgebruikte programma's zoals afbeeldingsbibliotheken en compressietools. De doorontwikkelde opvolger, AFL++, wordt nog steeds actief onderhouden door een gemeenschap van onderzoekers.
libFuzzer, onderdeel van het LLVM-compilerproject, maakte het vanaf ongeveer 2015 mogelijk om fuzzing rechtstreeks te integreren in de broncode van een programma, zonder het los te hoeven starten als apart proces. Dit maakte fuzzing sneller en toegankelijker voor ontwikkelaars.
In 2016 lanceerde Google OSS-Fuzz, een dienst die continu, gratis fuzzing aanbiedt aan belangrijke open source-projecten. Projecten als OpenSSL, de programmeertaal Python, en tal van compressie- en afbeeldingsbibliotheken worden er dag en nacht mee getest. Google meldde dat het project binnen een paar jaar duizenden bugs had gevonden in honderden projecten.
Voor besturingssystemen bestaat syzkaller, eveneens van Google, dat specifiek de Linux-kernel test door willekeurige combinaties van systeemaanroepen te genereren — de commando's waarmee programma's met de kernel communiceren. Omdat een kernelcrash het hele systeem kan platleggen, is dit een van de gevoeligste plekken om fouten te vinden.
Ook de Amerikaanse defensieonderzoeksorganisatie DARPA zette in 2016 met de Cyber Grand Challenge een grote stap: hierbij streden volledig geautomatiseerde systemen tegen elkaar, waarbij ze zelfstandig kwetsbaarheden in elkaars software moesten vinden én meteen patchen, zonder menselijke tussenkomst. Het liet zien hoe ver geautomatiseerde kwetsbaarheidsdetectie — met fuzzing als een van de bouwstenen — kon komen.
Een belangrijke kanttekening: de beruchte Heartbleed-kwetsbaarheid in OpenSSL (2014), een van de bekendste beveiligingslekken ooit, werd niet door fuzzing ontdekt maar door handmatige code-analyse. Het incident werkte wel als wake-upcall: het liet zien hoe kwetsbaar zelfs fundamentele, wereldwijd gebruikte beveiligingssoftware kon zijn, en was mede aanleiding voor initiatieven als OSS-Fuzz om dit soort code voortaan structureel te blijven fuzzen.
Hoe ver is de techniek?
Coverage-guided fuzzing is inmiddels volwassen en praktijkrijp: grote softwarebedrijven draaien fuzzers continu, als vast onderdeel van hun ontwikkelproces (vaak continuous fuzzing genoemd), naast andere geautomatiseerde tests die bij elke codewijziging draaien.
Er wordt volop onderzoek gedaan naar het slimmer maken van fuzzers met machine learning: modellen die voorspellen welke mutaties het meest kans hebben interessante nieuwe code-paden te raken, of die invoerformaten leren begrijpen (zoals een bestandsformaat met een specifieke structuur) om sneller voorbij simpele validatiechecks te komen. Dit onderzoeksgebied is actief maar nog niet uitontwikkeld; in de praktijk presteren goed afgestelde, klassieke coverage-guided fuzzers vaak nog steeds heel behoorlijk vergeleken met complexere ML-varianten.
Er zijn ook duidelijke beperkingen. Fuzzing is sterk in het vinden van geheugenfouten en crashes, maar veel minder goed in het vinden van logische fouten — situaties waarin een programma niet crasht, maar wel het verkeerde antwoord geeft of een beveiligingsregel omzeilt zonder zichtbare storing. Ook software die veel interne toestand bijhoudt (zogeheten stateful systemen, zoals netwerkprotocollen waarbij het antwoord afhangt van eerdere berichten) is lastiger te fuzzen dan simpele functies die één stuk invoer in één keer verwerken. Fuzzing van grafische gebruikersinterfaces en van hardware zelf staat eveneens nog in de kinderschoenen vergeleken met het fuzzen van programmacode. Bovendien is 'hoge codedekking' geen garantie dat er geen bugs meer zijn: een fuzzer kan een code-pad wel raken zonder de specifieke combinatie van waarden te vinden die daadwerkelijk tot een fout leidt.
Wie werken eraan?
Google is een van de grootste investeerders in fuzzing-technologie, met projecten als OSS-Fuzz, syzkaller en het beveiligingsonderzoeksteam Project Zero, dat regelmatig fuzzing gebruikt om kwetsbaarheden te vinden in zowel eigen software als die van derden. Microsoft heeft eigen fuzzing-tools ontwikkeld voor het testen van Windows en gerelateerde producten, en publiceert regelmatig onderzoek op dit vlak.
In de academische wereld doen Duitse instituten als het CISPA Helmholtz Center for Information Security en de Ruhr-Universität Bochum gerichte onderzoek naar geavanceerdere fuzzing-technieken, samen met tal van andere universiteiten wereldwijd die publiceren op grote beveiligingsconferenties. De Amerikaanse defensieorganisatie DARPA heeft met programma's als de Cyber Grand Challenge bijgedragen aan de ontwikkeling van geautomatiseerde kwetsbaarheidsdetectie.
Daarnaast draagt een brede open source-gemeenschap actief bij: tools als AFL++ en libFuzzer worden door vrijwilligers en onderzoekers wereldwijd onderhouden en uitgebreid, en veel van de vondsten worden openbaar gedeeld zodat andere ontwikkelaars ervan kunnen leren.