En PDF er ikke et dokument, du åbner. Det er et lille program, du kører. Hver indlejret skrifttype er en stakbaseret fortolker, der venter på charstrings, hvert billede er en dekoder fodret med bredde-, højde- og bitdybdefelter, som filen har valgt, og hver strøm ankommer pakket ind i filtre, hvis parametre filen har indstillet. Ingen af disse tal er dine. De kom fra den, der oprettede filen, hvilket på en reel arbejdsbyrde er en kundes faktura eller en vedhæftet fil fra en ukendt afsender. Dekoderne, der omdanner disse bytes to pixels and glyphs are the attack surface, and a parser that trusts its input there is one malformed file away from a crash or worse.
PDFlibPas went through a hardening pass that treated the whole decode path as hostile, across the font programs (TrueType, Type1, CFF, and the CMap tables), the image decoders (PNG, GIF, TIFF, JBIG2, and CCITT Group 3 and Group 4), and the stream filters (LZW, ASCII85, and the Flate predictors). What follows are five defect classes it closed, each grounded in the specific Delphi behavior that made it possible. They are fixed in current releases, and the same shapes recur in any Pascal code that parses untrusted input.
Et heltalsoverløb, der giver dig en for lille buffer
Den klassiske hukommelsessikkerhedsfejl i en billeddekoder er et dimensionsprodukt, der wrapper. En dekoder læser bredde, højde, komponentantal og bitdybde, multiplicerer dem for at dimensionere sit output, allokerer dette antal bytes og skriver derefter billedet ved dets sande dimensioner. Hvis multiplikationen udføres i 32-bit aritmetik, produktet kan wrappe til en lille værdi, selv når hver enkelt faktor er inden for et fornuftigt område, så allokeringen lykkes, bliver alt for lille, og afkodningen går ud over slutningen af den. Dette er CWE-190, heltals-overløb, hvilket fører til en heap out-of-bounds-skrivning (CWE-787) et trin senere.
Den fælles billedsti begrænsede allerede hver dimension to 65535; de selvstændige dekodere arvede ikke alle denne begrænsning. Et udtryk som række-bytes-gange-højde såsom ByteCount * FHeight, or a per-pixel expression such as FWidth * Components * BitDepth, is a 32-bit product in Delphi when both operands are 32-bit integers, regardless of how wide the variable you assign the result to is. A width and a height of 60000 are each plausible for a large scan, but their product in bytes overruns a signed 32-bit range and the length comes out small. The same trap lived in the ZLib predictor stride, BitsPerComponent * Colors * Columns.
Løsningen er at gøre mindst én operand til Int64, så hele udtrykket evalueres i 64-bit, derefter sammenligne med MaxInt og afvise filen, før der afkortes igen for at kalde SetLength.
// Reject before allocating, not after writing.
// Evaluate the product in Int64 so it cannot wrap at 32 bits.
RowBytes := (Int64(FWidth) * Components * BitDepth + 7) div 8;
if (RowBytes <= 0) or (RowBytes * FHeight > MaxInt) then
Exit; // hostile or unsupportable dimensions; refuse the image
SetLength(Buffer, RowBytes * FHeight);
Hvad der gør dette til et Delphi-problem frem for et generisk et, er den lydløse afkortning. Tildeling af et for bredt udtryk til en 32-bit destination er en lovlig konvertering, som compileren som standard ikke vil advare om, og områdekontrol fanger ikke et wrap, der sker, før værdien overhovedet bruges som et indeks. Lad produktet forblive på 32 bits, og sproget giver dig lydløst en længde, der lyver om, hvor meget hukommelse afkodningen er ved at berøre.
En felt-type, der gør det umuligt for en beskyttelse at udløse
En TIFF-fil er een kæde af billedfil-ordbøger (directories), som hver bærer byte-forskydningen for den næste. En ondsindet fil kan pege denne kæde tilbage mod sig selv, og en læser, der gennemgår den uden en stoptilstand, kører for evigt. Dette er CWE-835, en uendelig løkke drevet af angriber-kontrolleret input, og forsvaret er en tæller, der stopper, når den passerer en grænse, som ingen legitim fil ville nå.
Sidetælleren var erklæret som Word, som i Delphi rummer 0 til 65535. Løkken bar en afslutningsbeskyttelse af formen "stop, når sideantallet overstiger 65535", hvilket lyder korrekt, indtil du bemærker, at operanden og tærsklen deler en øvre grænse. En Word kan aldrig være større end 65535, så sammenligningen er strukturelt altid falsk: når tælleren når 65535, wrapper den næste inkrementering den tilbage til 0, beskyttelsen ser aldrig en værdi over loftet, og en løkkende IFD-kæde holder læseren kørende.
Løsningen var at udvide feltet, så beskyttelsen kan udtrykke en værdi, som tælleren faktisk kan indeholde. Med TPDFTIFF.FPageCount erklæret som Integer bliver den samme sammenligning FPageCount > 65535 opnåelig, løkken afsluttes, og den offentlige egenskab PageCount ændrede type for at matche uden at ødelægge nogen kaldende kode. Hver gang en grænsekontrol har formen Value > MaxValueOfType(Value), og operanden allerede er typet til netop dette maksimum, er betingelsen en konstant falsk: udvid typen, eller test lighed mod maksimum, så den kan udløses.
Områdekontrol slået fra på en kritisk sti
Med områdekontrol (range checking) slået til indsætter Delphi en grænsekontrol på hvert array- og strengindeks, hvilket er forskellen på, om et indeks uden for rækkevidde udløser en fangelig ERangeError, eller om det samme indeks læser eller skriver hukommelse, der ikke tilhører strukturen. Kritiske stier deaktiverer det undertiden med et lokalt {$R-}-direktiv, hvilket er forsvarligt lige indtil indekserne holder op med at være pålidelige.
Listeadgangen, som skrifttypefortolkerne læner sig op ad, TPDFlibStringList.Get, is exactly such a path. On Windows it is compiled with range checking off and indexes its backing store directly, so an out-of-range index is not an error but a raw memory access. That is fine when the index is always valid, and it stops being fine inside a CFF or Type2 charstring interpreter, where the index can come from the file. A charstring that pops an operand off an empty stack produces an index of negative one; a glyph identifier off by one against the glyph count indexes one slot past the end. With range checking off, both become a genuine out-of-bounds access instead of a catchable exception, and because the slots hold reference-counted AnsiString values, a stray read can also corrupt a string's reference count.
Hærdningen slog ikke områdekontrollen til igen for den kritiske sti. Den gjorde indekserne beviseligt gyldige først: før toppen af operandstakken tages, kontrollerer fortolkeren, at stakken ikke er tom, og hver indeksbeskyttelse blev skrevet som en streng "mindre end" mod antallet i stedet for en "mindre end eller lig med", der tillader off-by-one. Direktivet flytter ansvaret for grænser fra compileren til dig, og den validering, det fjernede, skal lægges tilbage manuelt ved hvert indgangspunkt.
Ubegrænset rekursion i en charstring-fortolker
En Type2-charstring kan kalde en subrutine, og en subrutine er i sig selv en charstring, der kan kalde en anden, så de lokale og globale subrutine-kaldeoperatorer lader filen bestemme, hvor dybt den går. En subrutine, der kalder sig selv, direkte eller gennem en cyklus, rekursere uden ende, indtil den indbyggede stak er opbrugt, og processen dør. Dette er CWE-674, ukontrolleret rekursion.
Type1-fortolkeren beskyttede allerede mod dette. Den bar en kaldedybde-tæller og et loft, PLType1MaxCallDepth, og nægtede at gå dybere end dette, hvilket afspejler den dybdegrænse, som Type1-specifikationen selv navngiver. Type2-fortolkeren, der blev tilføjet senere og er strukturelt ens, bar ikke den samme beskyttelse, og en håndbygget skrifttype med en subrutine, der kalder sit eget nummer, går lige igennem den manglende kontrol og ind i et stakoverløb.
// The shape of the Type1 guard the Type2 path was missing.
// Track depth across nested calls and refuse to recurse past it.
Inc(CallDepth);
if CallDepth > PLType1MaxCallDepth then
Exit; // hostile self-referential subroutine; stop descending
// ... interpret the subroutine, then Dec(CallDepth) on the way out
Løsningen var at give Type2-stien den samme begrænsede dybde, som dens Type1-søskende allerede havde. Enhver rekursiv gennemgang over en angriberkontrolleret struktur, uanset om det er skrifttypesubrutiner, et indlejret array eller en krydsreferencekæde, har brug for et dybdeloft, som inputtet ikke kan løfte.
Uinitialiseret hukommelse, der lækker ind i outputtet
Den mest subtile fejl lækkede heap-indhold ind i dekrypteret output, og årsagen er en egenskab ved SetLength, som er nem at glemme. Når du udvider en AnsiString med SetLength, Delphi allokerer bytes, men nulstiller dem ikke, så det nye område indeholder det, der tidligere var i denne heap-hukommelse. Hvis hver byte efterfølgende skrives, betyder dette intet; hvis en sti efterlader en del af bufferen uskrevet og derefter returnerer den som data, vil disse forældede bytes følge med resultatet. Dette er CWE-457, brug af uinitialiseret hukommelse, og når resultatet krydser en tillidsgrænse, bliver det til en informationslækage.
AES-CBC-dekrypteringsstien ramte præcis dette. Output-bufferen blev dimensioneret med SetLength, og dekrypteringen behandlede cipherteksten en 16-byte blok ad gangen. Når ciphertekst-længden ikke var et multiplum af 16, en længde som en angriber kan vælge, blev den afsluttende delvise blok aldrig skrevet, så disse sidste bytes beholdt det heap-indhold, som SetLength efterlod, og bufferen blev givet tilbage som den dekrypterede klartekst af et dokumentobjekt. Løsningen er to beskyttelser, og ingen af dem er nok alene: dekrypteringsindgangspunktet afviser nu enhver ciphertekst, hvis længde ikke er et multiplum af blokstørrelsen, og som en bagstopper ryddes outputtet med FillChar før brug, så enhver sti, der ikke skriver et område, returnerer nuller i stedet for heap-rester.
Hvad processen efterlader dig med
De fem fejl er forskellige fejl, men de rimer. En heltalsbredde, der wrapper et produkt, en felt-type, der fastlåser en beskyttelse til en konstant falsk, en områdekontrol deaktiveret, hvor indekserne ophørte med at være sikre, en rekursion uden bund og en buffer, som sproget nægtede at nulstille. I hver af dem gjorde Delphi præcis, hvad det definerer, fordi sproget giver dig aritmetik, der wrapper, afkortning, der er lydløs, områdekontrol, du kan slå fra, rekursion uden indbygget grænse og allokering, der ikke initialiserer. Det er kontrakten, og en Pascal-fortolker opfylder den ved at styre fire ting manuelt ved hver grænse, som filen kontrollerer: heltalsbredde, områdekontrol, rekursionsdybde og bufferinitialisering.
Disse fejl er lukket i de aktuelle PDFlibPas-udgivelser, motoren til Delphi og C++Builder. Hvis dit arbejde også rækker ind i, hvordan en fil hævder at være beskyttet, de tilhørende noter om auditering af kryptering og tilladelser og om PDF/A- og PDF/UA-præflight analysesiden af den samme fortolker, og det hele leveres inde i PDFlibPas Delphi PDF Library sammen med API'erne til indlæsning, rendering og signering, der er dækket andre steder på denne blog.