PDF nije dokument koji samo otvorite. To je mali program koji pokrećete. Svaki ugrađeni font je interpreter temeljen na stogu koji čeka znakovne nizove, svaka slika je dekoder koji se hrani poljima širine, visine i dubine bita koje je datoteka odabrala, a svaki tok dolazi umotan u filtre čije je parametre datoteka postavila. Nijedan od tih brojeva nije vaš. Došli su od onoga tko je proizveo datoteku, što je na stvarnom poslu račun kupca ili privitak od nepoznatog pošiljatelja. Dekoderi koji te bajtove pretvaraju u piksele i glifove su napadačka površina, a parser koji tu vjeruje svom ulazu je samo jednu loše oblikovanu datoteku udaljen od rušenja ili nečeg goreg
PDF Library for Delphi je prošao kroz fazu učvršćivanja koja je cijelu putanju dekodiranja tretirala kao neprijateljsku, kroz programe fontova (TrueType, Type1, CFF i CMap tablice), dekodere slika (PNG, GIF, TIFF, JBIG2 i CCITT Grupa 3 i Grupa 4) i filtre tokova (LZW, ASCII85 i Flate prediktore). Ono što slijedi je pet klasa nedostataka koje je zatvorio, od kojih je svaka utemeljena na specifičnom ponašanju Delphi-ju koje ih je omogućilo. Oni su ispravljeni u trenutnim izdanjima, a isti se oblici ponavljaju u bilo kojem Pascal kodu koji analizira nepouzdane ulaze
Prelijevanje cijelog broja koje rezultira premalim međuspremnikom
Klasični memorijski sigurnosni bug u dekoderu slika je umnožak dimenzija koji se omotava. Dekoder čita širinu, visinu, broj komponenti i dubinu bita, množi ih kako bi odredio veličinu svog izlaza, dodjeljuje toliko bajtova, a zatim zapisuje sliku u njezinim stvarnim dimenzijama. Ako se množenje vrši u 32-bitnoj aritmetici, umnožak se može omotati na malu vrijednost čak i kada je svaki pojedinačni faktor u razumnom rasponu, tako da dodjela uspijeva, ispada daleko premala, a dekodiranje izlazi izvan njezinih granica. To je CWE-190, prelijevanje cijelog broja, što vodi do pisanja izvan granica hrpe (CWE-787) korak kasnije
Zajednička putanja slike već je ograničavala svaku dimenziju na 65535; samostalni dekoderi nisu svi naslijedili to ograničenje. Izraz row-bytes-times-height kao što je ByteCount * FHeight, ili izraz po pikselu kao što je FWidth * Components * BitDepth, je 32-bitni umnožak u Delphi-ju kada su oba operanda 32-bitni cijeli brojevi, bez obzira na to koliko je široka varijabla kojoj dodjeljujete rezultat. Širina i visina od 60000 su razumne za veliko skeniranje, ali njihov umnožak u bajtovima prekoračuje označeni 32-bitni raspon i duljina ispada mala. Isti je problem živio u koraku prediktora ZLib, BitsPerComponent * Colors * Columns
Ispravak je učiniti barem jedan operand tipom Int64 kako bi se cijeli izraz procijenio u 64 bita, zatim ga usporediti s MaxInt i odbiti datoteku prije ponovnog sužavanja radi pozivanja SetLength
// Odbij prije alokacije, ne nakon pisanja.
// Izračunaj umnožak u Int64 kako ne bi mogao preliti pri 32 bita.
RowBytes := (Int64(FWidth) * Components * BitDepth + 7) div 8;
if (RowBytes <= 0) or (RowBytes * FHeight > MaxInt) then
Exit; // neprijateljske ili nepodržive dimenzije; odbij sliku
SetLength(Buffer, RowBytes * FHeight);
Ono što ovo čini problemom u Delphi-ju, a ne općim problemom, jest tiho sužavanje. Dodjeljivanje preširokog izraza u 32-bitno odredište je legalna pretvorba o kojoj kompajler prema zadanim postavkama neće upozoriti, a provjera raspona ne hvata omotavanje koje se dogodi prije nego što se vrijednost uopće upotrijebi kao indeks. Ostavite umnožak na 32 bita i jezik vam tiho daje duljinu koja laže o tome koliko će memorije dekodiranje dotaknuti
Tip polja koji onemogućuje aktiviranje zaštite
TIFF datoteka je lanac direktorija slikovnih datoteka, od kojih svaki nosi pomak bajta sljedećeg. Zlonamjerna datoteka može usmjeriti taj lanac natrag na sebe, a čitač koji prolazi kroz njega bez uvjeta zaustavljanja vrti se zauvijek. To je CWE-835, beskonačna petlja vođena ulazom pod kontrolom napadača, a obrana je brojač koji se zaustavlja čim prijeđe granicu koju nijedna legitimna datoteka ne bi dosegla
Brojač stranica bio je deklariran kao Word, koji u Delphi-ju drži vrijednosti od 0 do 65535. Petlja je nosila zaštitu od zaustavljanja u obliku "zaustavi kada broj stranica prijeđe 65535", što se čini točnim dok ne primijetite da operand i prag dijele gornju granicu. Word nikada može biti veći od 65535, pa je usporedba strukturno uvijek netočna: kada brojač dosegne 65535, sljedeće povećanje ga vraća na 0, zaštita nikada ne vidi vrijednost iznad gornje granice, a kružni lanac IFD-a drži čitač u beskonačnom vrtnju
Ispravak je bio proširiti polje kako bi zaštita mogla izraziti vrijednost koju brojač stvarno može držati. S poljem TPDFTIFF.FPageCount deklariranim kao Integer, ista usporedba FPageCount > 65535 postaje dostižna, petlja se prekida, a javno svojstvo PageCount promijenilo je tip kako bi odgovaralo bez prekidanja bilo kojeg pozivatelja. Kad god provjera granica ima oblik Value > MaxValueOfType(Value), a operand je već tipiziran točno na taj maksimum, uvjet je konstantno netočan: proširite tip ili testirajte jednakost s maksimumom kako bi se mogao aktivirati
Provjera raspona isključena na kritičnoj stazi
S uključenom provjerom raspona, Delphi umeće provjeru granica na svaki indeks niza i znakovnog niza, što je razlika između indeksa izvan raspona koji podiže uhvatljivu pogrešku ERangeError i tog istog indeksa koji čita ili piše memoriju koja ne pripada strukturi. Vruće putanje je ponekad isključuju lokalnom direktivom {$R-}, što je obranjivo sve dok indeksi ne prestanu biti pouzdani
Pristupnik popisu na koji se oslanjaju interpreteri fontova, TPDFlibStringList.Get, je upravo takva putanja. Na Windowsima se kompajlira s isključenom provjerom raspona i izravno indeksira svoju pozadinsku pohranu, tako da indeks izvan raspona nije pogreška već sirovi pristup memoriji. To je u redu kada je indeks uvijek valjan, a prestaje biti u redu unutar CFF ili Type2 charstring interpretera, gdje indeks može doći iz datoteke. Charstring koji izbacuje operand s praznog stoga proizvodi indeks minus jedan; identifikator glifa pomaknut za jedan u odnosu na broj glifova indeksira jedno mjesto iza kraja. S isključenom provjerom raspona, oba postaju stvarni pristup izvan granica umjesto uhvatljive iznimke, a budući da utori drže vrijednosti AnsiString s brojenjem referenci, zalutalo čitanje također može pokvariti broj referenci niza
Učvršćivanje nije ponovno uključilo provjeru raspona za vruću putanju. Najprije je učinilo indekse dokazivo valjanima: prije uzimanja vrha stoga operanda interpreter provjerava da stog nije prazan, a svaka zaštita indeksa napisana je kao strogo manje od u odnosu na broj, umjesto manje ili jednako što dopušta odstupanje za jedan. Direktiva pomiče odgovornost za granice s kompajlera na vas, a validacija koju je uklonila mora se ručno vratiti na svakoj ulaznoj točki
Neograničena rekurzija u interpretatoru charstringa
Type2 charstring može pozvati podrutinu, a podrutina je i sama charstring koji može pozvati drugu, tako da operatori poziva lokalne i globalne podrutine dopuštaju datoteci da odluči koliko duboko ide. Podrutina koja poziva samu sebe, izravno ili kroz ciklus, rekurzira bez kraja dok se stog ne iscrpi i proces ne umre. To je CWE-674, nekontrolirana rekurzija
Type1 interpreter se već štitio od toga. Nosio je brojač dubine poziva i gornju granicu, PLType1MaxCallDepth, i odbijao se spustiti dublje, što odražava ograničenje dubine koje sama Type1 specifikacija navodi. Type2 interpreter, dodan kasnije i strukturno sličan, nije nosio istu zaštitu, pa ručno izrađeni font s podrutinom koja poziva vlastiti broj ide ravno kroz nedostajuću provjeru u prekoračenje stoga
// Oblik Type1 zaštite koji je nedostajao na Type2 putanji.
// Prati dubinu kroz ugniježđene pozive i odbij rekurziju preko nje.
Inc(CallDepth);
if CallDepth > PLType1MaxCallDepth then
Exit; // neprijateljska samoreferencirajuća potprogram; prekini spuštanje
// ... protumači potprogram, zatim Dec(CallDepth) na izlazu
Ispravak je bio dati putanji Type2 istu ograničenu dubinu koju je već imao njegov brat Type1. Svaki rekurzivni spust preko strukture pod kontrolom napadača, bilo da se radi o podrutinama fonta, ugniježđenom nizu ili lancu unakrsnih referenci, treba gornju granicu dubine koju ulaz ne može podići
Neinicijalizirana memorija koja curi u izlaz
Najsuptilniji nedostatak propuštao je sadržaj hrpe u dešifrirani izlaz, a uzrok je svojstvo SetLength koje je lako zaboraviti. Kada povećate AnsiString pomoću SetLength, Delphi dodjeljuje bajtove, ali ih ne postavlja na nulu, tako da novo područje drži sve što je prethodno bilo u toj memoriji hrpe. Ako se svaki bajt naknadno zapiše, to nikada nije važno; ako putanja ostavi dio spremnika nezapisanim i zatim ga vrati kao podatke, ti ustajali bajtovi izlaze s rezultatom. To je CWE-457, korištenje neinicijalizirane memorije, a kada rezultat prijeđe granicu povjerenja, to postaje curenje informacija
AES-CBC putanja dešifriranja pogodila je točno ovo. Izlazni spremnik bio je veličine određene pomoću SetLength i dešifrator je obrađivao šifrirani tekst po jedan blok od 16 bajtova. Kada duljina šifriranog teksta nije bila višekratnik broja 16, a to je duljina koju napadač može odabrati, završni djelomični blok nikada nije bio zapisan, pa su ti konačni bajtovi zadržali sadržaj hrpe koji je SetLength ostavio iza sebe i spremnik je vraćen kao dešifrirani čist tekst objekta dokumenta. Lijek su dvije zaštite, i nijedna sama nije dovoljna: ulazna točka dešifriranja sada odbija bilo koji šifrirani tekst čija duljina nije višekratnik veličine bloka, a kao osiguranje izlaz se prije upotrebe briše pomoću FillChar tako da bilo koja putanja koja ne uspije zapisati regiju vraća nule umjesto ostataka hrpe
Što vam ostavlja prolaz
Pet nedostataka su različiti bugovi, ali se rimuju. Širina cijelog broja koja omotava umnožak, tip polja koji fiksira zaštitu na konstantno netočno, provjera raspona isključena tamo gdje indeksi više nisu sigurni, rekurzija bez poda i spremnik koji jezik odbija postaviti na nulu. U svakom od njih Delphi je učinio točno ono što definira, jer jezik vam daje aritmetiku koja se omotava, sužavanje koje je tiho, provjere raspona koje možete isključiti, rekurziju bez ugrađenog ograničenja i alokaciju koja se ne inicijalizira. To je ugovor, a Pascal parser ga ispunjava ručnim preuzimanjem kontrole nad četirima stvarima na svakoj granici koju datoteka kontrolira: širinom cijelog broja, provjerom raspona, dubinom rekurzije i inicijalizacijom spremnika
Ovi nedostaci su zatvoreni u trenutnim izdanjima PDF Library for Delphi-a, pogona za Delphi i C++Builder. Ako se vaš rad također dotiče načina na koji datoteka tvrdi da je zaštićena, prateće bilješke o reviziji šifriranja i dopuštenja te o preflightu za PDF/A i PDF/UA pokrivaju analitičku stranu istog parsera, a sve se to isporučuje unutar softvera PDF Library for Delphi Delphi PDF Library zajedno s API-jima za učitavanje, prikazivanje i potpisivanje koji su pokriveni drugdje na ovom blogu