Een PDF is niet zomaar een document dat u opent. Het is een klein programma dat u uitvoert. Elk ingesloten lettertype is een op een stack gebaseerde interpreter die wacht op charstrings, elke afbeelding is een decoder die wordt gevoed met breedte-, hoogte- en kleurdieptevelden die het bestand heeft bepaald, en elke stream komt binnen verpakt in filters waarvan het bestand de parameters heeft ingesteld. Geen van die getallen is van u. Ze zijn afkomstig van degene die het bestand heeft gegenereerd, wat in de praktijk een factuur van een klant kan zijn of een bijlage van een onbekende afzender. De decoders die die bytes omzetten in pixels en glyphs vormen het aanvalsoppervlak, en een parser die zijn invoer blindelings vertrouwt, is één misvormd bestand verwijderd van een crash of erger
PDF Library for Delphi behandelt het hele decodeerpad als vijandig: de lettertypeprogramma's (TrueType, Type1, CFF en de CMap-tabellen), de afbeeldingsdecoders (PNG, GIF, TIFF, JBIG2 en CCITT Groep 3 en Groep 4) en de stream-filters (LZW, ASCII85 en de Flate-predictors). Wat volgt zijn vijf geheugenveiligheidsvalkuilen die dat pad moet dichten, elk geworteld in het specifieke Delphi-gedrag dat ze mogelijk maakt, en dezelfde vormen keren terug in elke Pascal-code die niet-vertrouwde invoer parst
Een integer-overflow die resulteert in een te kleine buffer
De klassieke geheugenveiligheidsfout in een afbeeldingsdecoder is een product van de dimensies dat wrapt. Een decoder leest breedte, hoogte, aantal componenten en kleurdiepte, vermenigvuldigt deze om de uitvoer te dimensioneren, alloceert dat aantal bytes en schrijft vervolgens de afbeelding met zijn werkelijke afmetingen. Als de vermenigvuldiging wordt uitgevoerd met 32-bits berekeningen, kan het product wrappen naar een kleine waarde, zelfs als elke afzonderlijke factor binnen een acceptabel bereik ligt. De allocatie slaagt dan wel, maar is veel te klein, waarna de decodering buiten de grenzen schrijft. Dit is CWE-190 (integer-overflow), die één stap later leidt tot een heap-out-of-bounds-schrijfactie (CWE-787)
Elke dimensie op 65535 begrenzen op een gedeeld afbeeldingspad is niet genoeg wanneer een standalone decoder zijn eigen afmetingen berekent. Een rijbytes-maal-hoogte-expressie zoals ByteCount * FHeight, of een expressie per pixel zoals FWidth * Components * BitDepth, is in Delphi een 32-bits product wanneer beide operanden 32-bits integers zijn, ongeacht hoe breed de variabele is waaraan u het resultaat toewijst. Een breedte en een hoogte van 60000 zijn elk plausibel voor een grote scan, maar hun product in bytes overstijgt het signed 32-bits bereik en de lengte komt klein uit. Dezelfde valkuil zit in een predictor-stride zoals BitsPerComponent * Colors * Columns
De verdediging is om ten minste één operand Int64 te maken zodat de hele expressie in 64 bits wordt geëvalueerd, vervolgens tegen MaxInt te vergelijken en het bestand te weigeren voordat u terug versmalt om SetLength aan te roepen
// Wijs af vóór het alloceren, niet na het schrijven.
// Evalueer het product in Int64 zodat het niet kan wrappen op 32 bits.
RowBytes := (Int64(FWidth) * Components * BitDepth + 7) div 8;
if (RowBytes <= 0) or (RowBytes * FHeight > MaxInt) then
Exit; // vijandige of niet-ondersteunde afmetingen; weiger de afbeelding
SetLength(Buffer, RowBytes * FHeight);
Wat maakt dit tot een specifiek Delphi-probleem en niet tot een generiek probleem, is de stille versmalling. Het toewijzen van een te brede expressie aan een 32-bits bestemming is een legale conversie waarvoor de compiler standaard niet waarschuwt, en range-checking vangt geen wrap op die plaatsvindt voordat de waarde als index wordt gebruikt. Als u het product op 32 bits laat staan, geeft de taal u geruisloos een lengte die niet overeenkomt met de hoeveelheid geheugen die de decodering daadwerkelijk gaat gebruiken
Een veldtype dat de activering van een controle onmogelijk maakt
Een TIFF-bestand is een keten van afbeeldingsdirectories (image file directories), die elk de byte-offset van de volgende bevatten. Een kwaadaardig bestand kan die keten naar zichzelf laten terugwijzen, waardoor een lezer die de keten doorloopt zonder stopconditie oneindig blijft draaien. Dat is CWE-835, een oneindige lus aangedreven door invoer onder controle van een aanvaller. De verdediging is een teller die stopt zodra hij een limiet passeert die geen enkel legitiem bestand zou bereiken
Declareer de paginateller nu als Word, dat in Delphi 0 tot 65535 bevat, en geef de lus een beëindigingsguard van de vorm "stop wanneer het aantal pagina's boven 65535 uitkomt". Dat leest als correct totdat u merkt dat de operand en de drempelwaarde dezelfde bovengrens delen. Een Word kan nooit groter zijn dan 65535, dus de vergelijking is structureel altijd onwaar: wanneer de teller 65535 bereikt, wikkelt de volgende verhoging hem terug naar 0, de guard ziet nooit een waarde boven het plafond, en een ronddraaiende IFD-keten houdt de lezer aan het spinnen
De remedie is het veld te verbreden zodat de guard een waarde kan uitdrukken die de teller daadwerkelijk kan bevatten. TPDFTIFF.FPageCount is gedeclareerd als Integer, dus de vergelijking FPageCount > 65535 is bereikbaar en de lus termineert. Wanneer een grenscontrole de vorm Value > MaxValueOfType(Value) heeft en de operand al precies op dat maximum getypeerd is, is de conditie constant onwaar: verbreed het type, of test op gelijkheid met het maximum zodat hij kan afgaan
Range-checking uitgeschakeld op een kritiek pad
Wanneer range-checking is ingeschakeld, voegt Delphi een grenzencontrole toe aan elke array- en stringindex. Dit is het verschil tussen een index buiten bereik die een opvangbare ERangeError veroorzaakt, en diezelfde index die geheugen leest of schrijft dat niet tot de structuur behoort. Kritieke prestatiepaden schakelen dit soms uit met een lokale {$R-}-richtlijn, wat verdedigbaar is totdat de indexen niet meer betrouwbaar zijn
De lijst-accessor waar de fontinterpreters op leunen, TPDFlibStringList.Get, is precies zo'n pad. Op Windows wordt hij gecompileerd met range-checking uit en indexeert hij zijn onderliggende opslag rechtstreeks, dus een index buiten bereik zou een rauwe geheugentoegang zijn in plaats van een fout. Dat is prima zolang de index altijd geldig is, en het vergt zorg binnen een CFF- of Type2-charstring-interpreter, waar de index uit het bestand kan komen. Een charstring die een operand van een lege stack haalt, produceert een index van min één; een glyph-identificatie die er één naast zit ten opzichte van het aantal glyphs indexeert één positie voorbij het einde. Ongecontroleerd zijn beide een echte out-of-bounds-toegang in plaats van een opvangbare uitzondering, en omdat de slots referentiegetelde AnsiString-waarden bevatten, kan een verdwaalde leesactie ook de referentietelling van een string corrumperen
PDF Library for Delphi houdt range-checking op dat hot path uit en maakt de indexen in plaats daarvan aantoonbaar geldig: voordat de interpreter de bovenkant van de operand-stack pakt, controleert hij dat de stack niet leeg is, en elke indexguard is een strikte kleiner-dan tegen het aantal in plaats van een kleiner-dan-of-gelijk-aan die de off-by-one toestaat. De richtlijn verplaatst de verantwoordelijkheid voor grenzen van de compiler naar u, en de validatie die hij wegnam moet met de hand teruggezet worden op elk ingangspunt
Onbegrensde recursie in een charstring-interpreter
Een Type2-charstring kan een subroutine aanroepen, en een subroutine is zelf een charstring die weer een andere kan aanroepen. De lokale en globale subroutine-aanroepers zorgen er dus voor dat het bestand bepaalt hoe diep dit gaat. Een subroutine die zichzelf aanroept, rechtstreeks of via een lus, recurseert eindeloos totdat de systeemslag (native stack) uitgeput raakt en het proces stopt. Dat is CWE-674, ongecontroleerde recursie
De Type1-interpreter guardt hiertegen met een aanroepdiepteteller en een plafond, PLType1MaxCallDepth, en weigert daarlangs af te dalen, wat de dieptelimiet weerspiegelt die de Type1-specificatie zelf noemt. De valkuil is dat een structureel vergelijkbare interpreter die later is geschreven, zoals de Type2-variant, die guard niet vanzelf erft, en een handgebouwd font met een subroutine die zijn eigen nummer aanroept loopt rechtstreeks door een ontbrekende controle heen een stack-overflow in
// De vorm van de diepteguard die beide charstring-interpreters nodig hebben.
// Houd de diepte bij over geneste aanroepen en weiger daarlangs te recursen.
Inc(CallDepth);
if CallDepth > PLType1MaxCallDepth then
Exit; // vijandige zelfverwijzende subroutine; stop met afdalen
// ... interpreteer de subroutine, en doe Dec(CallDepth) bij het teruggaan
PDF Library for Delphi geeft het Type2-pad dezelfde begrensde diepte als zijn Type1-broer. Elke recursieve afdaling over een door een aanvaller beheerste structuur — of het nu font-subroutines zijn, een geneste array of een cross-reference-keten — heeft een diepteplafond nodig dat de invoer niet kan verhogen
Niet-geïnitialiseerd geheugen dat naar de uitvoer lekt
De subtielste valkuil lekt heap-inhoud naar de ontsleutelde uitvoer, en de oorzaak is een eigenschap van SetLength die makkelijk vergeten wordt. Wanneer u een AnsiString laat groeien met SetLength, alloceert Delphi de bytes maar zet ze niet op nul, dus het nieuwe gebied bevat wat er voorheen in dat heap-geheugen stond. Als elke byte daarna geschreven wordt, speelt dit nooit; laat een pad een deel van de buffer ongeschreven en geeft het daarna als data terug, dan rijden die verouderde bytes met het resultaat mee. Dat is CWE-457, gebruik van niet-geïnitialiseerd geheugen, en zodra het resultaat een vertrouwensgrens oversteekt wordt het een informatielek
Een AES-CBC-ontsleutelingspad is precies waar dit bijt. Dimensioneer de uitvoerbuffer met SetLength, verwerk de cijfertekst blok voor blok van 16 bytes, en wanneer de cijfertekstlengte geen veelvoud van 16 is — een lengte die een aanvaller kan kiezen — wordt het resterende deelblok nooit geschreven, dus die laatste bytes behouden de heap-inhoud die SetLength achterliet en de buffer gaat terug als de ontsleutelde klaartekst van een documentobject. De remedie bestaat uit twee guards, en geen van beide alleen is genoeg: het ontsleutelingsingangspunt van PDF Library for Delphi weigert elke cijfertekst waarvan de lengte geen veelvoud van de blokgrootte is, en als vangnet wordt de uitvoer voor gebruik gewist met FillChar zodat elk pad dat er niet in slaagt een gebied te schrijven nullen teruggeeft in plaats van heap-resten
Wat de vijf valkuilen gemeen hebben
De vijf valkuilen zijn verschillende bugs, maar ze rijmen. Een integer-breedte die een product laat wrappen, een veldtype dat een guard vastzet op constant onwaar, een uitgeschakelde range-check waar de indexen niet meer veilig waren, een recursie zonder bodem, en een buffer die de taal weigerde op nul te zetten. In elk geval doet Delphi precies wat het definieert, want de taal geeft je berekeningen die wrappen, stille versmalling, range-checks die je kunt uitzetten, recursie zonder ingebouwde limiet, en allocatie die niet initialiseert. Dat is de afspraak, en een Pascal-parser komt die na door met de hand vier dingen te beheren op elke grens die het bestand beheerst: integer-breedte, range-checking, recursiediepte en bufferinitialisatie
PDF Library for Delphi, de engine voor Delphi en C++Builder, guardt zijn decodeerpad tegen alle vijf. Reikt uw werk ook tot hoe een bestand claimt beschermd te zijn, dan behandelen de companion-notities over het auditen van versleuteling en machtigingen en over PDF/A- en PDF/UA-preflight de analysekant van dezelfde parser, en dat alles wordt geleverd binnen de PDF Library for Delphi Delphi PDF Library samen met de laad-, render- en signeer-API's die elders op deze blog worden behandeld