Het vinden en analyseren van een simpele kwetsbaarheid: CVE-2017-6192

Vaak is het vinden en analyseren van kwetsbaarheden een specialisme wat als ontoegankelijk wordt ervaren.

Als ontwikkelaar kan het lastig zijn om je eigen fouten er uit te pikken, en je voor te stellen met welke situaties je geen rekening hebt gehouden. Aan de andere kant is het als beveiligingsonderzoeker evengoed een flinke taak om de broncode van een programma onder handen te nemen om daar fouten in te spotten.

Om deze fouten wel snel te ontdekken kan een fuzzer gebruikt worden. Bij fuzz testing wordt het programma herhaaldelijk met een corrupte invoer geopend om te zien of de code hier juist mee omgaat. Voor een effectieve fuzzingcampagne zijn vaak de volgende opties mogelijk:

  1. de fuzzer zonder instructies zijn gang laten gaan (willekeurig bits flippen).
    Dit kost weinig tijd maar vindt alleen de meest simpele fouten.
  2. een complete instructieset schrijven hoe de fuzzer met het programma moet omgaan
    Dit is vaak erg effectief maar kost een goede werkdag om aan de praat te krijgen.

Vanwege het werk om een goed fuzzingcampagne op te zetten zien wij vaak genoeg dat, zelfs security-gerelateerde software, niet de simpelste fuzz variant ondergaat.
Wat jammer is, want fuzzen is een erg lonende bezigheid. Als het eenmaal werkt kan de fuzzer rustig een week zijn gang gaan, om daarna de resultaten te analyseren.

Fuzzing

American Fuzzy Lop (afl-fuzz, http://lcamtuf.coredump.cx/afl/) biedt hier een oplossing voor, deze stelt namelijk zelf de tactieken van het fuzzen bij aan de hand van welke paden in de code worden afgelegd.

Door deze zelfstandigheid is hij net zo eenvoudig te gebruiken als een ‘domme’ fuzzer; men hoeft alleen het programma op te geven en het draait.
Gecombineerd met de snelheid van deze fuzzer wordt dit erg effectief; zelfs zonder een geldig bestand als input kan AFL binnen een dag een valide JPEG construeren, Huffman tabellen en al, zonder enige voorkennis te hebben van wat een plaatje is. Simpelweg door een ‘bruteforce’ uit te voeren en het gedrag van een JPEG bibliotheek te analyseren met zogenaamde ‘instrumentatie’.

Het starten van AFL kan op twee manieren; als men de broncode bezit kan de compiler van AFL worden gebruikt, zo niet de ingebouwde emulator.
Van het programma wat we nu als voorbeeld onder handen gaan nemen, apngdis, is de broncode beschikbaar, dus kiezen we voor de eerste optie.
(Emuleren is nog eenvoudiger, alleen gaat het fuzzen dan een stuk trager.)

Het doelwit, APNGDis

APNGDis (Animated PNG Disassembler) is een klein stukje code wat wordt gebruikt om geanimeerde PNG afbeeldingen (verwant aan GIFs) te splitsen in losse frames.
Hierin heb ik de afgelopen maand door een combinatie van fuzzen en broncodeanalyse drie kwetsbaarheden gevonden.

We hebben twee commando’s nodig om het fuzzen te starten:

een voor het compileren en ‘instrumenteren’ van het doelwit:

CC=/path/to/afl-gcc ./configure && make

en eentje om een outputmap mee te geven en het programma te starten met de juiste parameters:

afl-fuzz -i /testcases/images/png -o /fuzz-output-apngdis -- apngdis @@

..en dit gaat dan rustig met een tempo van 5000 tests per seconde het programma openen met een corrupte invoer.

Al gauw krijgen we de melding dat het programma is gecrasht, en AFL zet het bestand wat de crash heeft veroorzaak voor ons apart.

Segmentation fault

Als we het bronbestand in een hex editor openen kunnen we de opbouw van het bestand nader bekijken, en zien welke wijziging de crash heeft veroorzaakt.
Voor het overzicht hier alleen de eerste 24 bytes van het bestand, met hier boven de karakters als deze af te drukken zijn:

Origineel:

‰ P N G . . . . . . . . I H D R
89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52

. . . . . .
00 00 00 20 00 00 00 20

Crash:

‰ P N G . . . . . . . . I H D R
89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52

. . . . . . . .
00 0F 00 00 00 0F 00 00

Hier valt al snel op waar de wijzigingen aan zijn aangebracht, namelijk in de tweede regel:

00 00 00 20 00 00 00 20
v.s. ^ ^
00 0F 00 00 00 0F 00 00
^ ^

Als we de specificatie er bij pakken zien we dat de eerste acht bytes na IHDR de breedte en hoogte van de afbeelding zijn, vier bytes voor elk.

Dit heeft AFL dus gewijzigd van de originele waarde 0x20 naar 0xF0000

Waar veroorzaakt dit dan precies een fout, en van welk type? We compileren het bestand opnieuw met de ‘-g’ optie om de functienamen te kunnen lezen, en openen het in Valgrind om te zien waar de fout zich voordoet:

==3494== Invalid write of size 8
==3494== at 0x4C30265: memcpy@GLIBC_2.2.5 (vg_replace_strmem.c:1017)
==3494== by 0x109924: compose_frame(unsigned char**, unsigned char**, unsigned char, unsigned int, unsigned int, unsigned int, unsigned int) (apngdis.cpp:78)
==3494== by 0x10AA40: load_apng(char*, std::vector<APNGFrame, std::allocator >&) (apngdis.cpp:363)
==3494== by 0x10B24E: main (apngdis.cpp:498)

Regel 78 in compose_frame() roept de memcpy instructie dus aan (GLIB bevat de kwetsbaarheid niet, het is aan de ontwikkelaar om deze correct aan te roepen).
Laten we de applicatie uitvoeren in GDB en een breekpunt er vlak boven zetten om te zien hoe de waarden zich door het programma heen bewegen.

(gdb) b 77
(gdb) run
Breakpoint 1, compose_frame (rows_dst=0x7fffef54f010, rows_src=0x7ffff3150010, bop=0 '\000', x=0, y=0, w=983040, h=983040) at apngdis.cpp:77
77 if (bop == 0)
(gdb) n
78 memcpy(dp, sp, w*4);

Juist. De waarden ‘w’ en ‘h’ worden zonder te checken aan ‘memcpy’ meegegeven. Laten we het programma doorlopen om te zien hoe deze hun waarden verkrijgen.
Zoals in de Valgrind output te zien is wordt de kwetsbare functie “compose_frame()” aangeroepen door “load_apng()”. We openen de broncode, en kijken naar de werking.

Eerst wordt nagegaan of het bestand met de PNG ‘magic bytes’ begint, en of de IHDR tag conform specificatie een lengte van 25 heeft.

if (fread(sig, 1, 8, f) == 8 && png_sig_cmp(sig, 0, 8) == 0)
{
	id = read_chunk(f, &chunkIHDR);

	if (id == id_IHDR && chunkIHDR.size == 25)
	{

Daarna wordt de grootte uit het bestand gelezen:

w0 = w = png_get_uint_32(chunkIHDR.p + 8);
h0 = h = png_get_uint_32(chunkIHDR.p + 12);

Een beperking om in acht te nemen bij het ontwikkelen van een exploit; het programma faalt als de breedte of hoogte groter is dan een miljoen:

const unsigned long cMaxPNGSize = 1000000UL;

if (w > cMaxPNGSize || h > cMaxPNGSize)
{
	fclose(f);
	return res;
}

Als hieraan is voldaan, dan zal het programma gehoorzaam een paar miljoen nullen (w*4) over de heap heen schrijven.

Nu is het uitwerken van een exploit aan de hand van een heap buffer overflow niet triviaal.
Toch is hiermee een kwetsbaarheid gevonden en een analyse van de kernoorzaak uitgevoerd, en is dit een nuttige toevoeging aan de publieke databases van kwetsbaarheden. Deze stellen andere onderzoekers in staat om op deze vondst te bouwen en geven ontwikkelaars een kans om te leren van de fouten.