PDF nije dokument koji samo otvorite. To je mali program koji pokrećete. Svaki ugrađeni font je stekovni interpretator koji čeka definicije karaktera (charstrings), svaka slika je dekoder koji se hrani poljima širine, visine i dubine bita koje je datoteka izabrala, a svaki tok stiže obmotan filterima čije je parametre datoteka postavila. Nijedan od tih brojeva nije vaš. Došli su od onoga ko je proizveo datoteku, što je u realnom radu faktura kupca ili prilog od nepoznatog pošiljaoca. Dekoderi koji pretvaraju te bajtove u piksele i glifove su površina napada, a parser koji veruje svom ulazu na tom mestu udaljen je jednu neispravnu datoteku od rušenja ili nečeg goreg.
PDFlibPas je prošao kroz fazu ojačavanja koja je tretirala celu putanju dekodiranja kao neprijateljsku, kroz programe fontova (TrueType, Type1, CFF i CMap tabele), dekodere slika (PNG, GIF, TIFF, JBIG2 i CCITT Group 3 i Group 4) i filtere tokova (LZW, ASCII85 i Flate prediktore). Ono što sledi je pet klasa defekata koje je zatvorio, a svaka je zasnovana na specifičnom Delphi ponašanju koje je to omogućilo. Oni su ispravljeni u trenutnim izdanjima, a isti oblici se ponavljaju u bilo kom Pascal kodu koji analizira nepouzdan unos.
Prepunjavanje celog broja koje vam daje premali bafer
Klasičan memorijski bezbednosni bag u dekoderu slika je proizvod dimenzija koji se obmotava. Dekoder čita širinu, visinu, broj komponenti i dubinu bita, množi ih da bi dimenzionisao svoj izlaz, alocira toliko bajtova, a zatim upisuje sliku u njenim stvarnim dimenzijama. Ako se množenje vrši u 32-bitnoj aritmetici, proizvod se može obmotati na malu vrednost čak i kada je svaki pojedinačni faktor u razumnom opsegu, pa alokacija uspeva, ispada previše mala, a dekodiranje izlazi van njenih granica. Ovo je CWE-190, prepunjavanje celog broja, što korak kasnije dovodi do upisa van granica hipa (CWE-787).
Zajednička putanja slike je već ograničavala svaku dimenziju na 65535; samostalni dekoderi nisu svi nasledili to ograničenje. Izraz row-bytes-puta-visina kao što je ByteCount * FHeight, ili izraz po pikselu kao što je FWidth * Components * BitDepth, je 32-bitni proizvod u Delphi-ju kada su oba operanda 32-bitni celi brojevi, bez obzira na to koliko je široka promenljiva kojoj dodeljujete rezultat. Širina i visina od 60000 su pojedinačno uverljive za veliko skeniranje, ali njihov proizvod u bajtovima prekoračuje označeni 32-bitni opseg i dužina ispada mala. Ista zamka je postojala u koraku ZLib prediktora, BitsPerComponent * Colors * Columns.
Rešenje je da se bar jedan operand učini tipom Int64 kako bi se ceo izraz izračunao u 64 bita, a zatim uporedi sa MaxInt i odbije datoteka pre sužavanja nazad za poziv 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);
Ono što ovo čini Delphi problemom, a ne opštim, jeste tiho sužavanje. Dodeljivanje preširokog izraza 32-bitnom odredištu je dozvoljena konverzija o kojoj kompajler podrazumevano neće upozoriti, a provera opsega ne hvata obmotavanje koje se dešava pre nego što se vrednost uopšte upotrebi kao indeks. Ostavite proizvod na 32 bita i jezik vam tiho daje dužinu koja laže o tome koliko će memorije dekodiranje dodirnuti.
Tip polja koji onemogućava aktiviranje zaštite
TIFF datoteka je lanac direktorijuma slikovnih datoteka, od kojih svaki nosi bajt-ofset sledećeg. Zlonamerna datoteka može usmeriti taj lanac nazad na sebe, a čitač koji prolazi kroz njega bez uslova zaustavljanja vrti se zauvek. To je CWE-835, beskonačna petlja vođena ulazom pod kontrolom napadača, a odbrana je brojač koji se zaustavlja kada pređe limit koji nijedna legitimna datoteka ne bi dostigla.
Brojač stranica je deklarisan kao Word, što u Delphi-ju drži vrednosti od 0 do 65535. Petlja je imala zaštitu za prekid u obliku "zaustavi se kada broj stranica pređe 65535", što zvuči ispravno dok ne primetite da operand i prag dele istu gornju granicu. Word nikada ne može biti veći od 65535, pa je poređenje strukturno uvek netačno: kada brojač dostigne 65535, sledeće uvećanje ga vraća na 0, zaštita nikada ne vidi vrednost iznad plafona, a kružni IFD lanac drži čitača u beskonacnom vrtljaju.
Rešenje je bilo proširenje polja kako bi zaštita mogla da izrazi vrednost koju brojač stvarno može da drži. Sa TPDFTIFF.FPageCount deklarisanim kao Integer, isto poređenje FPageCount > 65535 postaje dostižno, petlja se završava, a javno svojstvo PageCount je promenilo tip kako bi se uskladilo bez narušavanja pozivnih programa. Kad god provera granica ima oblik Value > MaxValueOfType(Value) a operand je već tipiziran na tačno taj maksimum, uslov je konstantno netačan: proširite tip ili testirajte jednakost sa maksimumom kako bi se mogao aktivirati.
Provera opsega isključena na kritičnoj putanji
Sa uključenom proverom opsega, Delphi umeće proveru granica na svaki indeks niza i stringa, što je razlika između indeks van opsega koji podiže uhvatljivu grešku ERangeError i tog istog indeks koji čita ili upisuje u memoriju koja ne pripada strukturi. Kritične putanje (hot paths) je ponekad isključuju lokalnom direktivom {$R-}, što se može braniti sve dok indeksi ne prestanu da budu pouzdani.
Pristupnik listi na koji se oslanjaju interpretatori fontova, TPDFlibStringList.Get, je upravo takva putanja. Na Windows-u se kompajlira sa isključenom proverom opsega i direktno indeksira svoju memoriju, tako da indeks van opsega nije greška već sirovi pristup memoriji. To je u redu kada je indeks uvek validan, ali prestaje da bude u redu unutar CFF ili Type2 interpretatora definicija karaktera (charstring), gde indeks može doći iz datoteke. Charstring koji uklanja operand sa praznog steka proizvodi indeks minus jedan; identifikator glifa koji odstupa za jedan u odnosu na broj glifova indeksira jedno mesto preko kraja. Sa isključenom proverom opsega, oba postaju stvarni pristup van granica umesto uhvatljivog izuzetka, a pošto mesta drže reference-counted AnsiString vrednosti, zalutalo čitanje može oštetiti i broj referenci stringa.
Ojačavanje nije ponovo uključilo proveru opsega za kritičnu putanju. Ono je prvo učinilo indekse dokazano validnim: pre uzimanja vrha steka operanda, interpretator proverava da li je stek neprazan, a svaka zaštita indeksa napisana je kao strogo manje-od u odnosu na ukupan broj, umesto manje-ili-jednako što dopušta odstupanje za jedan. Direktiva pomera odgovornost za granice sa kompajlera na vas, a validacija koju je uklonila mora se ručno vratiti na svakoj ulaznoj tački.
Neograničena rekurzija u interpretatoru definicija karaktera
Type2 charstring može pozvati podprogram, a podprogram je i sam charstring koji može pozvati drugi, tako da lokalni i globalni operatori poziva podprograma dozvoljavaju datoteci da odluči koliko duboko ide. Podprogram koji poziva sam sebe, direktno ili kroz ciklus, rekurzira se bez kraja dok se izvorni stek ne iscrpi i proces ne umre. To je CWE-674, nekontrolisana rekurzija.
Interpretator za Type1 je već imao zaštitu od ovoga. Nosio je brojač dubine poziva i plafon, PLType1MaxCallDepth, i odbijao da se spusti dalje, što odražava limit dubine koji sama Type1 specifikacija navodi. Type2 interpretator, dodat kasnije i strukturno sličan, nije imao istu zaštitu, i ručno napravljen font sa podprogramom koji poziva sopstveni broj prolazi pravo kroz nedostajuću proveru u prepunjavanje steka (stack overflow).
// 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
Rešenje je bilo davanje Type2 putanji iste ograničene dubine koju je njen Type1 blizanac već imao. Bilo koji rekurzivni spust preko strukture pod kontrolom napadača, bilo da se radi o podprogramima fonta, ugnežđenom nizu ili lancu unakrsnih referenci, zahteva plafon dubine koji ulaz ne može da pomeri.
Neinicijalizovana memorija koja curi u izlaz
Najsuptilniji defekt je propuštao sadržaj hipa u dešifrovani izlaz, a uzrok je svojstvo SetLength funkcije koje je lako zaboraviti. Kada proširite AnsiString sa SetLength, Delphi alocira bajtove ali ih ne postavlja na nulu, tako da novi region drži ono što je prethodno bilo u toj memoriji hipa. Ako se svaki bajt naknadno upiše, ovo nikada nije važno; ako putanja ostavi deo bafera neupisanim i potom ga vrati kao podatke, ti bajtovi odlaze sa rezultatom. To je CWE-457, korišćenje neinicijalizovane memorije, a kada rezultat pređe granicu poverenja, pretvara se u curenje informacija.
Putanja dešifrovanja AES-CBC je pogodila upravo ovo. Izlazni bafer je dimenzionisan sa SetLength i dekriptor je obrađivao šifrovani tekst blok po blok od po 16 bajtova. Kada dužina šifrovanog teksta nije bila deljiva sa 16, što je dužina koju napadač može da izabere, prateći delimični blok nikada nije bio upisan, tako da su ti završni bajtovi zadržavali sadržaj hipa koji je SetLength ostavio za sobom, a bafer je vraćen kao dešifrovani čisti tekst objekta dokumenta. Lek su dve zaštite, i nijedna sama nije dovoljna: ulazna tačka dešifrovanja sada odbija bilo koji šifrovani tekst čija dužina nije deljiva sa veličinom bloka, i kao dodatna sigurnost izlaz se čisti pomoću FillChar pre upotrebe, tako da svaka putanja koja ne uspe da upiše region vraća nule umesto ostataka hipa.
Sa čime vas ovaj prolaz ostavlja
Pet defekata su različiti bagovi, ali se rimuju. Širina celog broja koja obmotava proizvod, tip polja koji fiksira zaštitu na konstantno netačno stanje, provera opsega isključena tamo gde su indeksi prestali da budu bezbedni, rekurzija bez dna i bafer koji jezik odbija da postavi na nulu. U svakom od njih Delphi je uradio tačno ono što definiše, jer vam jezik daje aritmetiku koja se obmotava, sužavanje koje je tiho, provere opsega koje možete isključiti, rekurziju bez ugrađenog limita i alokaciju koja ne inicijalizuje. To je ugovor, i Pascal parser ga ispunjava tako što ručno upravlja sa četiri stvari na svakoj granici koju datoteka kontroliše: širinom celog broja, proverom opsega, dubinom rekurzije i inicijalizacijom bafera.
Ovi defekti su zatvoreni u trenutnim izdanjima PDFlibPas-a, endžina za Delphi i C++Builder. Ako se vaš rad takođe bavi načinom na koji datoteka tvrdi da je zaštićena, prateće beleške o reviziji šifrovanja i dozvola i o PDF/A i PDF/UA preflight analizi pokrivaju stranu analize istog parsera, a sve to se isporučuje unutar PDFlibPas Delphi PDF Library zajedno sa API-jima za učitavanje, renderovanje i potpisivanje koji su pokriveni na drugim mestima na ovom blogu.