Asiakirjojen vastaanottoputki hyväksyy tuntemattomien kirjoittamia tiedostoja Laskuja, skannauksia, verkkolomakkeen liitteitä: jokainen väittää olevansa PDF ja kantaa satoja lukuja, joiden mukaan jäsentimen odotetaan toimivan Virtapituudet, kuvamitat, tavusiirtymät, objektiviittaukset — jokaisen valitsi se, joka tuotti tiedoston, ja katkennut lataus tai tarkoituksella virheellinen asiakirja lopulta asettaa jonkin näistä luvuista kohtaan, jossa se aiheuttaa vahinkoa Ero jäsentimen, joka selviää sellaisesta tiedostosta, ja sellaisen, joka kaatuu tai jatkaa käyntiä korruptoituneen muistin kanssa, välillä on pieni joukko tapoja, jotka eivät riipu mistään tietystä PDF-kirjastosta
Tavat jakavat yhden lähtökohdan: tiedostosta luettu arvo on väite, ei mittaus Se muuttuu käyttökelpoiseksi vasta, kun se on tarkistettu jäsentimen itsensä mittaamaa tietoa vasten — tiedoston todellista kokoa, dekooderin todella tuottamien tavujen määrää, rekursion todellista syvyyttä Seuraavassa tätä lähtökohtaa sovelletaan kohtiin, joissa asiakirjajäsentimet oikeasti hajoavat
Ilmoitettu pituus on väite, ei mittaus
Yksinkertaisin ristiriita on virran pituus PDF-virtaobjekti ilmoittaa tavumääränsä /Length-avaimessa, ja todellinen data sijaitsee stream- ja endstream-avainsanojen välissä Mikään ei pakota näitä kahta olemaan samaa mieltä Katkaistu tiedosto sisältää vähemmän todellisia tavuja kuin ilmoitettu määrä; rikkinäisen generaattorin tuottama tiedosto voi ilmoittaa pituuden, joka ulottuu tiedoston lopun yli tai naapuriobjektin sisään Jos varaat muistia ilmoitetusta arvosta ja kopioit kohtaan endstream asti, ylität puskurin; jos luet tarkalleen ilmoitetun määrän tarkistamatta saatavuutta, kävelet tiedoston lopun yli Anna ilmoitetun arvon ohjata varausta vasta sen jälkeen, kun se on rajattu mitattuun etäisyyteen datan loppuun, ja kohtele ristiriitaa päätöksentekokohtana — korjaa etsimällä endstream, tai hylkää virta — älä koskaan hiljaa usko sitä
Kuvaparametrit, jotka kuvaavat suurempaa rasteria kuin mitä varasit
Kuvavirrat nostavat panoksia, koska kaksi toisistaan riippumatonta lukujoukkoa kuvaa samoja pikseleitä Kuvasanakirja kantaa /Width- ja /Height-arvot, ja rasteripuskurit mitoitetaan yleensä niiden mukaan Dekoodaussuodattimella on oma geometriansa: CCITTFaxDecode ottaa /Columns-, /Rows- ja /K-arvot omasta DecodeParms-sanakirjastaan, jossa /K valitsee Group 3- tai Group 4 -skeeman ja dekooderi tuottaa (Columns + 7) div 8 tavua per skannausrivi Tiedosto, joka ilmoittaa /Width 100 mutta antaa suodattimelle arvon /Columns 1728 — oletusarvon — saa dekooderin tuottamaan yli kuusitoistakertaisen määrän tavuja riviä kohti verrattuna siihen, mitä puskuri odottaa, ja ylivuoto laskeutuu yksi skannausrivi kerrallaan mihin tahansa varauksen jälkeen sattuu olemaan Kun /Rows puuttuu, dekooderi jatkaa käyntiä, kunnes data sanoo pysähdy, joten rajaa myös rivimäärä DCTDecode:llä on sama sauma: JPEG-data kantaa oman leveytensä ja korkeutensa SOF-merkinnässään, eikä mikään velvoita niitä täsmäämään sanakirjan kanssa
Puolustava sääntö on mekaaninen: laske odotettu rasterikoko validoiduista dekoodausparametreista — suodattimen omat /Columns- ja /Rows-arvot CCITT:lle, SOF-mitat DCT:lle — tarkista se rajojasi vasten, varaa muisti siitä ja tarkista dekoodauksen aikana, ettei tuloste koskaan mene varauksen yli Sitä jäsentimen ei koskaan saa tehdä on mitoittaa puskuri yhdestä lukujoukosta ja antaa dekooderin ajaa toisen mukaan
Delphin aritmetiikka- ja varauskompastuskivet
Kolme Delphi-käytöstä heikentää jopa jäsennintä, joka aikoo validoida Ensimmäinen on 32-bittinen kertolasku: Delphi arvioi kahden Integer-operandin tulon 32 bitin tarkkuudella kohteen leveydestä riippumatta, joten Width * Height * BytesPerPixel voi kiertyä ympäri, vaikka jokainen tekijä läpäisee oman järkevyystarkistuksensa 30000 kertaa 30000 skannaus kolmella tavulla pikseliä kohti on 2,7 miljardia tavua, mikä kiertyy negatiiviseksi etumerkillisessä 32-bittisessä aritmetiikassa; hieman erilaiset tekijät kiertyvät pieneksi positiiviseksi pituudeksi, joka varautuu ja alimitoittaa puskurin Pakota koko lauseke leveäksi valamalla ensimmäinen operandi — Size := Int64(Width) * Height * BytesPerPixel — ja vertaa sitten eksplisiittiseen ylärajaan ennen kuin mikään saavuttaa SetLength-kutsun
Toinen on alueentarkistus Delphin oletusjulkaisuasetus toimitetaan se pois päältä, joten tiedostodatasta laskettu alueen ulkopuolinen indeksi ei nosta poikkeusta — se lukee tai kirjoittaa muistia taulukon vierestä Kytke se takaisin päälle {$R+}-direktiivillä (ja {$Q+}-direktiivillä aritmeettista ylivuotoa varten) jokaisen yksikön alkuun, joka indeksoi tiedostosta johdetuilla arvoilla Kustannus on mittaamaton verrattuna I/O:hon, jota jäsennin muutenkin tekee, ja se muuttaa hiljaisen korruption kiinniotettavaksi ERangeError-poikkeukseksi
Kolmas on TMemoryStream.SetSize tiedostosta saadulla Int64-arvolla Nykyisellä RTL:llä se varaa mitä tahansa tiedosto pyysi, joten yksi virta, joka väittää olevansa neljä gigatavua, muuttuu muistiloppumisvirheeksi kesken vastaanoton Vanhemmilla RTL-versioilla, joissa SetSize ottaa Longint-arvon, arvo kavennetaan hiljaa ensin: ilmoitettu $100000010 muuttuu luvuksi 16, varaus onnistuu, ja todellisen datan kirjoitus menee kauas sen yli Validoi jokainen koko mitattua lähdekokoa ja kovaa ylärajaa vasten ennen kuin mikään varauskutsu näkee sen
Siirtymät, jotka osoittavat tiedoston ulkopuolelle
Ristiviiteosataulukko kuvaa objektinumerot absoluuttisiksi tavusiirtymiksi, ja jäsennin siirtyy sinne, mihin se osoittaa Vaurioituneessa tai vihamielisessä tiedostossa nämä siirtymät osuvat tiedoston lopun yli tai epäolennaisten rakenteiden sisään TStream tekee virheestä hiljaisen: Position-arvon asettaminen Size-arvon yli ei ole virhe, ja tavallinen Read lopun yli yksinkertaisesti palauttaa vähemmän tavuja kuin pyydettiin, joten koodi, joka ohittaa määrän tarkistuksen, jatkaa vanhentuneiden tavujen jäsentämistä edellisestä objektista Puolustus on pullonkaula — yksi apufunktio, jonka kautta jokainen tiedostosta ohjattu siirtyminen ja luku kulkee, validoiden siirtymän ja määrän mitattua tiedostokokoa vasten ennen kuin virta liikkuu
uses
System.SysUtils, System.Classes;
const
MAX_OBJECT_BYTES = 64 * 1024 * 1024; // yksikään objekti ei saa ylittää 64 Mt
type
EPdfBoundsError = class(Exception);
// Jokainen tiedostosta ohjattu siirtyminen ja luku kulkee tämän kautta. Offset ja Count
// ovat tiedoston antamia väitteitä; Source.Size on mittaus, johon niiden on sovittava.
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;
Ohjaa ristiviitesiirtymät, virtojen laajuudet ja upotettujen tiedostojen luvut sen kautta, niin huono siirtymä muuttuu siistiksi hylkäykseksi, joka nimeää luvut, sen sijaan että se olisi käyttöoikeusrikkomus kolme kutsua myöhemmin
Syklit ja syvyys objektigraafissa
PDF on graafi, ei puu Mikä tahansa arvo voi olla epäsuora viittaus, viittaus voi ratketa toiseksi viittaukseksi — /Length 12 0 R, jossa objekti 12 sisältää 13 0 R — eikä mikään estä ketjua sulkeutumasta itseensä Ratkaisija, joka seuraa viittauksia naiivisti, rekursoi, kunnes natiivi pino loppuu, eikä pinon loppuminen ole jotain, minkä kiinniottaa; se päättää prosessin Syvästi sisäkkäiset taulukot ja sanakirjat päätyvät samaan lopputulokseen ilman yhtään sykliä
Käytä kahta vartijaa yhdessä: eksplisiittinen syvyyslaskuri rajoittaa rehellisen mutta syvän tapauksen rajaan, johon mikään laillinen tiedosto ei yllä, ja vierailtujen joukko nappaa todellisen syklin sen toisella käynnillä, muuttaen sen täsmälliseksi, raportoitavaksi virheeksi rajan laukeamisen sijaan
uses
System.SysUtils, System.Generics.Collections;
const
MAX_RESOLVE_DEPTH = 32; // paljon syvempi kuin mikään laillinen viittausketju
type
EPdfStructureError = class(Exception);
TPdfValueKind = (pvNull, pvNumber, pvName, pvString, pvArray,
pvDictionary, pvStream, pvReference);
TPdfValue = record
Kind: TPdfValueKind;
RefNumber: Integer; // merkityksellinen, kun Kind = pvReference
// ... jäljellä olevien tyyppien hyötykuormakentät
end;
// LoadObject on oma rutiinisi: se etsii xref-siirtymän
// ObjNumber:lle, lukee objektin ReadBounded-funktiolla ja jäsentää sen.
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 // esim. /Length 12 0 R
Result := ResolveObject(Result.RefNumber, Depth + 1, Visited);
finally
Visited.Remove(ObjNumber); // sisarukset voivat laillisesti jakaa tämän objektin
end;
end;
Purkaminen on vahvistin
Muutama kilotavu FlateDecode-syötettä voi paisua gigatavuiksi; yleiskäyttöinen pakkaus palkitsee toistuvasta selkokielisestä sisällöstä, ja hyökkääjä voi tehdä siitä mahdollisimman toistuvaa Rajaa jokaisen virran puretun koon siihen, mitä sen kuluttaja voi uskottavasti tarvita, ja pidä toinen, asiakirjakohtainen budjetti: viisisataa virtaa, kukin juuri alle virtakohtaisen ylärajan, tyhjentävät muistin yhtä varmasti kuin yksi jättimäinen virta Tarkistuksen paikka on purkusilmukan sisällä, laskien tulostetavuja niitä tuotettaessa ja keskeyttäen rikkomuksen kohdalla, ei silmukan jälkeen, kun muisti on jo käytetty Asiakirjakohtainen budjetti, joka ilmaistaan pakatun tiedoston koon kerrannaisena, toimii hyvin, koska lailliset asiakirjat jäävät kauas alle suhdeluvuista, joihin muokattu virta yltää
Syväpuolustus omien yksiköittesi ulkopuolella
Samat vikaluokat asuvat kirjastojen sisällä Kaksi tapaustutkimusta tällä blogilla käy läpi todellisia tapauksia: kokonaislukujen kiertymiset, rajoittamaton rekursio ja alustamattomat puskurit korjattuna natiivissa Pascal-moottorissa artikkelissa Pascal-PDF-jäsentimen suojaaminen haitallisilta tiedostoilta, sekä kutsutavan, kokonaisluvun leveyden ja omistajuuden vaarat C-moottorin sitomisessa artikkelissa PDFium-komponenttisidoksen suojaaminen Aidosti epäluotettavaa vastaanottoa varten — julkinen latauslomake, todentamaton postilaatikko — aja jäsennys- ja dekoodaustyö myös erillisessä matalan käyttöoikeuden prosessissa, jotta tiedosto, joka voittaa jokaisen prosessin sisäisen vartijan, maksaa epäonnistuneen työn eikä kaatunutta palvelua
Lentoonlähtöä edeltävä tarkistuslista
Ennen seuraavaa julkaisua käy jäsennin läpi tätä listaa vasten: jokainen virtapuskuri mitoitettu rajatusta pituudesta eikä ilmoitetusta; jokainen rasteri mitoitettu validoiduista dekooderiparametreista ja tarkistettu dekooderin tulostetta vasten; jokainen mittatulo laskettu Int64-tarkkuudella ja verrattu eksplisiittiseen ylärajaan; {$R+} aktiivisena jokaisessa yksikössä, joka indeksoi tiedostosta johdetuilla arvoilla; jokainen siirtyminen rajatarkistettu mitattua tiedostokokoa vasten; jokainen viittauksen ratkaisu syvyysrajoitettu ja syklitarkistettu; jokainen purkusilmukka laskee tulostetta virta- ja asiakirjakohtaisia budjetteja vasten Mikään näistä tarkistuksista ei maksa mitattavaa aikaa laillisella asiakirjalla, ja jokainen muuttaa muistikorruption siistiksi, lokitettavaksi hylkäykseksi
Huom: losLabin HotPDF Delphi Component, PDF Library for Delphi Delphi PDF Library ja PDFium Component soveltavat näitä rajatarkistuksia, syvyysrajoja ja laajenemisylärajoja sisäisesti, joten niiden päälle rakennettu vastaanottoputki alkaa vahvistetulta perustalta