Dokumentų priėmimo konvejeris (pipeline) priima nepažįstamų asmenų sukurtus failus. Sąskaitos faktūros, nuskaityti dokumentai, priedai iš žiniatinklio formos: kiekvienas iš jų teigia esąs PDF ir turi šimtus skaičių, į kuriuos jūsų analizatorius (parser) turi reaguoti. Srauto ilgiai, vaizdo matmenys, baitų poslinkiai, objektų nuorodos – kiekvieną iš jų parinko failo kūrėjas, o sutrumpintas įkėlimas arba tyčia sugadintas dokumentas galiausiai įdės vieną iš tų skaičių ten, kur jis padarys žalos. Skirtumas tarp analizatoriaus, kuris išgyvena tokį failą, ir to, kuris sugenda arba toliau veikia su sugadinta atmintimi, yra nedidelis įpročių rinkinys, kuris nepriklauso nuo jokios konkrečios PDF bibliotekos
Šie įpročiai turi vieną bendrą prielaidą: iš failo perskaityta vertė yra teiginys, o ne matavimas. Ji tampa tinkama naudoti tik po to, kai patikrinama su tuo, ką pats analizatorius išmatavo – tikruoju failo dydžiu, tikruoju dekoderio sugeneruotų baitų skaičiumi, tikruoju rekursijos gyliu. Toliau pateikiama ši prielaida, pritaikyta toms vietoms, kur dokumentų analizatoriai iš tikrųjų sugenda
Deklaruotas ilgis yra teiginys, o ne matavimas
Paprasčiausias neatitikimas yra srauto ilgis. PDF srauto objektas deklaruoja savo baitų skaičių rakte /Length, o faktiniai duomenys yra tarp raktinių žodžių stream ir endstream. Niekas neverčia šių dviejų sutapti. Sutrumpintame faile yra mažiau realių baitų nei deklaruotas skaičius; failas iš sugadinto generatoriaus gali deklaruoti ilgį, kuris siekia už failo pabaigos arba patenka į gretimą objektą. Paskirkite atmintį pagal deklaruotą vertę ir kopijuokite iki endstream, ir perpildysite buferį; nuskaitykite tiksliai deklaruotą skaičių nepatikrinę prieinamumo, ir išeisite už failo pabaigos. Leiskite deklaruotai vertei valdyti atminties skyrimą tik po to, kai apribosite ją išmatuotu atstumu iki duomenų pabaigos, ir traktuokite neatitikimą kaip sprendimo tašką – taisykite ieškodami endstream arba atmeskite srautą – bet niekada nepriimkite to kaip kažko, kuo galima aklai tikėti
Vaizdo parametrai, apibūdinantys didesnį rastrą, nei jam paskirta vietos
Vaizdo srautai padidina riziką, nes du nepriklausomi skaičių rinkiniai apibūdina tuos pačius pikselius. Vaizdų žodynas (dictionary) pateikia /Width ir /Height, ir rastro buferių dydis paprastai nustatomas pagal juos. Dekodavimo filtras turi savo geometriją: CCITTFaxDecode paima /Columns, /Rows ir /K iš savo DecodeParms, kur /K pasirenka Group 3 arba Group 4 schemą, o dekoderis išskiria (Columns + 7) div 8 baitų kiekvienai nuskaitymo eilutei. Failas, kuris deklaruoja /Width 100, bet pateikia filtrui /Columns 1728 – numatytąją vertę – priverčia dekoderį sugeneruoti daugiau nei šešiolika kartų daugiau baitų vienoje eilutėje, nei tikisi buferis, o perpildymas po vieną eilutę patenka į tai, kas yra už paskirtos atminties. Kai nėra /Rows, dekoderis veikia tol, kol duomenys nurodo sustoti, todėl taip pat apribokite eilučių skaičių. DCTDecode turi tą patį trūkumą: JPEG duomenys savo SOF žymeklyje turi savo plotį ir aukštį, ir niekas neįpareigoja jų atitikti žodyno
Gynybinė taisyklė yra mechaninė: apskaičiuokite tikėtiną rastro dydį iš patvirtintų dekodavimo parametrų – paties filtro /Columns ir /Rows (CCITT atveju), SOF matmenų (DCT atveju) – patikrinkite jį pagal savo ribas, paskirkite atmintį iš jo ir dekodavimo metu patikrinkite, ar išvestis niekada neperžengia paskirtos ribos. Kai žodynas ir filtras nesutaria dėl geometrijos, suderinkite juos arba atmeskite vaizdą. Ko analizatorius niekada neturi daryti – tai nustatyti buferio dydį pagal vieną skaičių rinkinį ir leisti dekoderiui veikti pagal kitą
„Delphi“ aritmetikos ir atminties skyrimo spąstai
Trys „Delphi“ elgsenos bruožai kenkia net ir tam analizatoriui, kuris ketina atlikti patvirtinimą. Pirmasis yra 32 bitų daugyba: „Delphi“ įvertina dviejų Integer operandų sandaugą kaip 32 bitų reikšmę nepriklausomai nuo paskirties pločio, todėl Width * Height * BytesPerPixel gali persipildyti (wrap), net jei kiekvienas veiksnys praeina savo pagrįstumo patikrinimą. 30000 x 30000 nuskaitymas po tris baitus pikseliui yra 2,7 milijardo baitų, kurie ženklintojoje (signed) 32 bitų aritmetikoje tampa neigiami; šiek tiek kitokie veiksniai persipildo iki mažo teigiamo ilgio, kuris paskiria per mažą buferį. Priverstinai išplėskite visą išraišką konvertuodami pirmąjį operandą – Size := Int64(Width) * Height * BytesPerPixel – tuomet palyginkite su aiškia riba prieš bet kam pasiekiant SetLength
Antrasis yra diapazono (range) tikrinimas. Numatytoji „Delphi“ leidimo (release) konfigūracija pateikiama su išjungtu tikrinimu, todėl iš failo duomenų apskaičiuotas indeksas už diapazono ribų nesukelia klaidos – jis nuskaito arba įrašo atmintį greta masyvo. Įjunkite jį atgal naudodami {$R+} (ir {$Q+} aritmetiniam perpildymui) kiekvieno modulio (unit), indeksuojančio failo kilmės reikšmes, viršuje. Sąnaudos yra neišmatuojamos palyginti su I/O, kurį bet kuriuo atveju atlieka analizatorius, ir tai paverčia tylų sugadinimą sugaunama ERangeError klaida
Trečiasis yra TMemoryStream.SetSize su failo pateikta Int64 reikšme. Dabartinėje RTL ji paskiria tiek, kiek paprašė failas, todėl vienas srautas, reikalaujantis keturių gigabaitų, tampa atminties trūkumo klaida pačiame priėmimo įkarštyje. Senesnėse RTL, kur SetSize priima Longint, reikšmė pirmiausia tyliai susiaurinama: deklaruotas $100000010 tampa 16, atminties skyrimas pavyksta, o tikrųjų duomenų rašymas gerokai peržengia jo ribas. Patikrinkite kiekvieną dydį, atsižvelgdami į išmatuotą šaltinio dydį ir griežtą ribą, prieš tai, kai bet koks paskyrimo iškvietimas jį pamatys
Poslinkiai, rodantys už failo ribų
Kryžminių nuorodų (cross-reference) lentelė susieja objektų numerius su absoliučiais baitų poslinkiais (offsets), ir analizatorius ieško visur, kur ji rodo. Pažeistame ar priešiškame faile šie poslinkiai patenka už failo pabaigos arba į nesusijusias struktūras. TStream paverčia gedimą tyliu: nustatyti Position už Size nėra klaida, o paprastas Read už pabaigos paprasčiausiai grąžina mažiau baitų nei prašoma, todėl kodas, praleidžiantis skaičiaus patikrinimą, toliau analizuoja pasenusius baitus iš ankstesnio objekto. Apsauga yra kontrolinis taškas (chokepoint) – vienas pagalbinis metodas, per kurį pereina kiekviena failo valdoma paieška ir skaitymas, patvirtinant poslinkį ir skaičių su išmatuotu failo dydžiu, prieš srautui judant
uses
System.SysUtils, System.Classes;
const
MAX_OBJECT_BYTES = 64 * 1024 * 1024; // no single object may exceed 64 MB
type
EPdfBoundsError = class(Exception);
// Every file-driven seek and read goes through here. Offset and Count are
// file-supplied claims; Source.Size is the measurement they must fit.
procedure ReadBounded(Source: TStream; Offset, Count: Int64;
var Buffer: TBytes);
begin
if (Offset < 0) or (Count < 0) or (Count > MAX_OBJECT_BYTES) or
(Offset > Source.Size) or (Count > Source.Size - Offset) then
raise EPdfBoundsError.CreateFmt(
'object extent %d+%d exceeds file size %d',
[Offset, Count, Source.Size]);
SetLength(Buffer, Count);
if Count = 0 then
Exit;
Source.Position := Offset;
Source.ReadBuffer(Buffer[0], Count);
end;
Nukreipkite kryžminių nuorodų poslinkius, srauto apimtis ir įterptųjų failų skaitymus per jį, ir neteisingas poslinkis taps švariu atmetimu, įvardijančiu skaičius, o ne prieigos pažeidimu (access violation) po trijų iškvietimų
Ciklai ir gylis objektų grafe
PDF yra grafas, o ne medis. Bet kokia vertė gali būti netiesioginė nuoroda, nuoroda gali vesti į kitą nuorodą – /Length 12 0 R, kur objektas 12 turi 13 0 R – ir niekas netrukdo grandinei užsidaryti pačiai savyje. Sprendiklis (resolver), naiviai sekantis nuorodomis, atlieka rekursiją tol, kol išsenka vietinis (native) stekas, o steko išsekimo neįmanoma sugauti; tai tiesiog užbaigia procesą. Giliai įdėti masyvai ir žodynai pasiekia tą patį galą net ir be jokio ciklo
Naudokite dvi apsaugas kartu: aiškus gylio skaitiklis apriboja sąžiningą, bet gilų atvejį iki ribos, kurios nepasiekia joks teisėtas failas, o aplankytų elementų rinkinys pagauna tikrą ciklą antrojo apsilankymo metu, paversdamas jį tikslia, pranešama klaida, o ne ribos pasiekimu
uses
System.SysUtils, System.Generics.Collections;
const
MAX_RESOLVE_DEPTH = 32; // far deeper than any legitimate reference chain
type
EPdfStructureError = class(Exception);
TPdfValueKind = (pvNull, pvNumber, pvName, pvString, pvArray,
pvDictionary, pvStream, pvReference);
TPdfValue = record
Kind: TPdfValueKind;
RefNumber: Integer; // meaningful when Kind = pvReference
// ... payload fields for the remaining kinds
end;
// LoadObject is your own routine: it looks up the xref offset for
// ObjNumber, reads the object with ReadBounded, and parses it.
function ResolveObject(ObjNumber, Depth: Integer;
Visited: TDictionary<Integer, Boolean>): TPdfValue;
begin
if Depth > MAX_RESOLVE_DEPTH then
raise EPdfStructureError.Create('reference chain exceeds depth limit');
if Visited.ContainsKey(ObjNumber) then
raise EPdfStructureError.CreateFmt(
'circular reference through object %d', [ObjNumber]);
Visited.Add(ObjNumber, True);
try
Result := LoadObject(ObjNumber);
if Result.Kind = pvReference then // e.g. /Length 12 0 R
Result := ResolveObject(Result.RefNumber, Depth + 1, Visited);
finally
Visited.Remove(ObjNumber); // siblings may legally share this object
end;
end;
Dekompresija yra stiprintuvas
Keli kilobaitai FlateDecode įvesties gali išsiplėsti iki gigabaitų; bendrosios paskirties glaudinimas apdovanoja pasikartojantį paprastąjį tekstą, o užpuolikas gali padaryti jį maksimaliai pasikartojantį. Apribokite kiekvieno srauto išskleistą dydį iki to, ko vartotojui realiai gali prireikti, ir turėkite antrąjį viso dokumento biudžetą: penki šimtai srautų, kurių kiekvienas yra šiek tiek žemiau vieno srauto ribos, išnaudos atmintį taip pat užtikrintai, kaip vienas milžiniškas srautas. Patikrinimas turi būti dekompresijos ciklo viduje, skaičiuojant išvesties baitus, kai jie pagaminami, ir nutraukiant pažeidimo atveju, o ne po ciklo, kai atmintis jau išnaudota. Dokumento biudžetas, išreikštas kaip suglaudinto failo dydžio kartotinis, veikia gerai, nes teisėtų dokumentų santykiai telkiasi gerokai žemiau tų, kuriuos pasiekia specialiai sukurtas srautas
Gili gynyba (Defense in depth) už jūsų pačių modulių ribų
Tos pačios defektų klasės egzistuoja ir bibliotekose. Dvi atvejų analizės šiame tinklaraštyje nagrinėja realius pavyzdžius: sveikųjų skaičių persipildymai, neribota rekursija ir neinicijuoti buferiai, ištaisyti vietiniame „Pascal“ variklyje „Pascal“ PDF analizatoriaus grūdinimas nuo kenkėjiškų failų, bei iškvietimų konvencijos, sveikųjų skaičių pločio ir nuosavybės pavojai, atsirandantys surišant C variklį, nagrinėjami „PDFium“ komponento surišimo grūdinimas. Tikrai nepatikimiems priėmimams – viešai įkėlimo formai, neautentifikuotai pašto dėžutei – analizės ir dekodavimo darbus taip pat vykdykite atskirame mažų privilegijų procese, kad failas, įveikiantis visas proceso apsaugas, kainuotų tik nepavykusią užduotį, o ne sutrikdytą paslaugą
Prieš paleidimo (preflight) kontrolinis sąrašas
Prieš išleidžiant kitą versiją (build), patikrinkite analizatorių pagal šį sąrašą: kiekvieno srauto buferio dydis nustatytas pagal apribotą ilgį, o ne deklaruotąjį; kiekvieno rastro dydis nustatytas pagal patvirtintus dekoderio parametrus ir patikrintas su dekoderio išvestimi; kiekviena matmenų sandauga įvertinta Int64 tipu ir palyginta su aiškia riba; {$R+} aktyvus kiekviename modulyje, kuris indeksuoja naudodamas iš failo gautas reikšmes; kiekvienos paieškos ribos patikrintos su išmatuotu failo dydžiu; kiekviena nuorodos rezoliucija ribojama gylio ir tikrinama dėl ciklų; kiekvienas išskleidimo ciklas skaičiuoja išvestį pagal srauto ir dokumento biudžetus. Nė vienas iš šių patikrinimų nekainuoja išmatuojamo laiko su teisėtu dokumentu, ir kiekvienas jų paverčia atminties sugadinimą švariu, užregistruojamu atmetimu
Pastaba: „losLab“ „HotPDF“ komponentas, „PDFlibPas Delphi“ PDF biblioteka ir „PDFium“ komponentas šiuos ribų patikrinimus, gylio apribojimus ir išplėtimo limitus taiko viduje, todėl jais pagrįstas priėmimo konvejeris prasideda nuo užgrūdinto pagrindo