Odborný článok

Izolácia kodekov obrázkov PDF vo worker procesoch

HotPDF dokáže dekódovať tri najrizikovejšie obrazové filtre PDF, DCTDecode, JPXDecode a JBIG2Decode, vnútri samostatného, krátko žijúceho worker procesu namiesto priamo vo vašej aplikácii. Vlastnosť, ktorá to zapína, je CodecIsolationMode, a praktickým dôsledkom je, že poškodený kódový tok JPEG 2000, ktorý by predtým zhavaroval vašu VCL aplikáciu, teraz zabije zbytočný podriadený proces, zatiaľ čo hostiteľ nahlási stavový kód a pokračuje ďalej

Tento rozdiel je najdôležitejší práve tam, odkiaľ PDF súbory skutočne prichádzajú: formulár na nahrávanie, poštová brána, skenovacie zariadenie, FTP priečinok partnera. Tieto bajty nekontrolujete vy, a práve v obrazových kodekoch sa skrýva historicky najväčšia škoda

Prečo jeden zlý obrázok zhodí celú aplikáciu?

Pretože obrazový kodek je tá jediná časť čítačky PDF, ktorá spúšťa zložitý stavový automat nad dátami kontrolovanými útočníkom, a to takmer bez akýchkoľvek štrukturálnych kontrol, o ktoré by sa dalo oprieť. Kým bajty dorazia k dekodéru JPEG 2000 alebo JBIG2, tabuľka krížových odkazov je už rozobraná, objekt vyriešený, reťaz filtrov rozvinutá, a to, čo zostáva, je surový kódový tok, ktorý hovorí, koľko dlaždíc, koľko komponentov, koľko bitov na vzorku. Zlé číslo na tomto mieste nie je chyba pri parsovaní. Je to zlá veľkosť alokácie alebo index mimo rozsahu vnútri tesnej dekódovacej slučky

Rozpočtové limity pomáhajú a mali by ste ich mať už teraz. HotPDF ohraničuje expanziu pomocou DecodeBudgetBytes a DocumentDecodeBudgetBytes a ohraničuje reťaze filtrov pomocou DecodeFilterLimit a DecodePipelineDepthLimit; dôvody za týmito limitmi sú opísané v článku ohraničené dekódovanie pre vnorené filtre a PDF bomby. Bajtový rozpočet však odpovedá len na jednu otázku — koľko výstupu je povolených. Nedokáže odpovedať na to, čo sa stane, keď dekodér zlyhá skôr, než vyprodukuje čokoľvek na výstup. Prístupová výnimka vnútri dekódovacej slučky nie je porušenie politiky, ktoré môžete odmietnuť; je to udalosť na úrovni procesu, a jediné spoľahlivé obmedzenie udalosti na úrovni procesu je iný proces

Čo HotPDF izoluje, a čo nie

HotPDF izoluje presne tri druhy kodekov, vymenované ako hckDCT, hckJPX a hckJBIG2 v jednotke HPDFCodecIsolation. Všetko ostatné — Flate, LZW, RunLength, ASCII85, CCITT — zostáva vo vnútri procesu, pretože tieto dekodéry sú dostatočne jednoduché na to, aby sa dali ohraničiť rozpočtami, a nie sú miestom, odkiaľ pochádzajú zaujímavé zlyhania

Prenosová vrstva je zámerne úzka. Hostiteľ alokuje jedno ohraničené mapovanie zdieľanej pamäte, zapíše doň pevnú štruktúru THPDFCodecSharedHeader spolu so skomprimovaným vstupom a prípadnými globálnymi segmentmi JBIG2, spustí worker a čaká. Worker zapíše dekódované pixely späť do toho istého mapovania a nastaví stavové slovo. Neexistuje žiadny pipe protokol, ktorý by sa mohol rozísť, žiadny serializačný formát na fuzzovanie, a hlavička nesie magickú hodnotu a verziu, takže nezhodný binárny súbor workeru sa odmietne namiesto toho, aby sa nesprávne prečítal

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Zlyhať uzavreto: tieto kodeky nikdy nedekódovať vo vnútri procesu
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 alebo >= 64 MiB
    Pdf.DecodeBudgetBytes := 134217728;

    if Pdf.LoadFromFile('untrusted-upload.pdf') = 1 then
      if Pdf.GetLoadedImageCount > 0 then
      begin
        Bmp := Pdf.ExtractLoadedImage(0);
        try
          if Pdf.GetLastCodecWorkerInfo(Info) then
            LogCodecOutcome(Info);
        finally
          Bmp.Free;
        end;
      end;
  finally
    Pdf.Free;
  end;
end;

Ponechajte CodecWorkerExecutable prázdne a HotPDF nájde worker vedľa vášho vlastného spustiteľného súboru, ako HotPDFCodecWorker.exe v adresári ParamStr(0). Nastavte ho explicitne, keď vaše nasadenie umiestňuje worker inde; hodnota sa rozbaľuje cez ExpandFileName, takže relatívna cesta sa rieši voči aktuálnemu adresáru, a nie voči adresáru aplikácie, čo je pri službe len zriedka to, čo chcete

Automaticky alebo povinne: ktoré zlyhanie preferujete?

Tri hodnoty THPDFCodecIsolationMode kódujú tri rôzne odpovede na jednu otázku — čo sa má stať, keď worker vôbec nemôže bežať. cimDisabled úplne preskočí izoláciu a dekóduje vo vnútri procesu, čo je správanie z čias pred verziou 3.x. cimAutomatic, predvolená hodnota, sa pokúsi o worker a ticho sa vráti k dekódovaniu vo vnútri procesu, keď spustiteľný súbor workeru chýba alebo sa nedá spustiť, čo sa nahlási ako stav cwsUnavailable. cimRequired tento návrat odmieta: nedostupný worker označí dekódovanie ako obslúžené a zlyhané, takže žiadny nedôveryhodný kódový tok sa nikdy nedostane do vášho adresového priestoru

Voľbu robte podľa modelu hrozieb, nie podľa pohodlnosti. Desktopová čítačka, ktorá otvára dokumenty, ktoré už používateľ má na disku, si vystačí s cimAutomatic, kde chýbajúci worker degraduje na klasické správanie namiesto toho, aby rozbil produkt. Vstupná služba, ktorá spracúva súbory z internetu, by mala bežať s cimRequired, pretože chyba pri nasadení, ktorá ticho vypne izolačnú vrstvu, je presne ten typ regresie, ktorú si nikto nevšimne, kým na tom nezáleží. Všimnite si asymetriu: návrat spustí iba cwsUnavailable. Worker, ktorý sa spustil a potom zhavaroval, presiahol časový limit alebo narazil na limit, je zlyhanie dekódovania v oboch režimoch, nikdy nie tiché opakovanie vo vnútri procesu

Čítanie verdiktu z THPDFCodecWorkerStatus

GetLastCodecWorkerInfo vracia výsledok posledného izolovaného dekódovania, a výčet stavov je dostatočne konkrétny na to, aby riadil reálne prevádzkové rozhodnutia, a nie len všeobecný riadok logu „obrázok zlyhal“. Hodnoty sú cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError a cwsOutputLimit

Vnímajte ich ako tri skupiny. Problémy s nasadením sú cwsUnavailable a cwsLaunchFailed: niekto nasadil bez workeru, alebo antivírusový produkt blokuje vytváranie procesov. Problémy s dokumentom sú cwsDecodeFailed a cwsOutputLimit: súbor je poškodený alebo väčší, než dovoľuje vaša politika, a odmietnutie je správna odpoveď. Zaujímavou skupinou je cwsTimedOut a cwsCrashed, pretože ide o udalosti, ktoré by predtým zamrzli alebo zabili hostiteľský proces. Keď sa to stane, sprievodné polia ProcessId, ExitCode a ElapsedMilliseconds vám dajú dosť na to, aby ste to prepojili so záznamom vo Windows Error Reporting a rozhodli, či je jeden zákaznícky súbor patologický, alebo vás niekto skúša

procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
  case Info.Status of
    cwsSucceeded:
      ; // nič na nahlásenie
    cwsUnavailable, cwsLaunchFailed:
      Alert('Codec worker not deployed: ' + Info.ErrorMessage);
    cwsTimedOut, cwsCrashed:
      Quarantine(Format('pid %d exit %d after %d ms',
        [Info.ProcessId, Info.ExitCode, Info.ElapsedMilliseconds]));
  else
    RejectDocument(Info.ErrorMessage);
  end;
end;

Limity, ktoré naozaj platia

Na každé izolované dekódovanie sa vzťahujú tri samostatné stropy, a vedieť, ktorý z nich zasiahol, vám ušetrí celé popoludnie hádania. CodecWorkerTimeoutMilliseconds má predvolenú hodnotu 10 000 a validuje sa do rozsahu 1 až 600 000; hodnota mimo neho vyvolá výnimku namiesto ticho orezanej hodnoty. CodecWorkerMemoryLimitBytes má predvolenú hodnotu 536 870 912 bajtov a musí byť buď nula, čo znamená bez limitu, alebo aspoň 67 108 864 bajtov, pretože menší strop by neuniesol reálnu pracovnú množinu dekodéra a zlyhal by na každom dokumente. Pamäťový strop vynucuje Windows Job Object so semantikou kill-on-close, takže worker zomrie spolu s jobom, aj keď je hostiteľ náhle ukončený

Tretí strop je výstupný limit, a ten je odvodený, nie nastavovaný. HotPDF vypočíta potrebné bajty z požadovanej oblasti, alebo z očakávanej geometrie obrázka ako šírka krát výška krát tri pre 24-bitový výstup, a potom túto hodnotu oreže na DecodeBudgetBytes, ak je nastavený rozpočet. Dekodér, ktorý nahlási vierohodnú hlavičku a potom sa pokúsi vyprodukovať oveľa viac pixelov, než dovoľuje geometria, je zastavený samotným mapovaním, a hostiteľ uvidí cwsOutputLimit. Preto sa izolačná vrstva a dekódovací rozpočet vzájomne dopĺňajú: rozpočet definuje, aký veľký smie byť obrázok, a izolačná hranica zaisťuje, že klamstvo o tejto veľkosti sa nemôže zmeniť na zápis mimo hraníc vo vašom procese

Kam to zapadá v zabezpečenej vstupnej ceste

Izolácia procesov je najvonkajšia vrstva obrannej reťaze, ktorá začína oveľa skôr. Štrukturálne limity odmietajú nevierohodné dokumenty už pri parsovaní. Rozpočty filtrov ohraničujú expanziu. Izolácia obsahuje to, čo prežije oboje. Pri dokumentoch, ktoré sa dostanú až k obrazovej vrstve, sa oplatí vedieť, ktorý kodek vlastne používate, keďže spracovanie JPXDecode a symbolové slovníky JBIG2 majú veľmi odlišné profily zlyhania, a JBIG2 obzvlášť nesie globálne segmenty naprieč stránkami, ktoré by naivný sandbox pre jednotlivé obrázky rozbil

Cena je úprimná a stojí za to ju vysloviť: spustenie procesu na každý izolovaný obrázok pridáva milisekundy, a dokument so stovkami skenovaných strán to pocíti. Porovnajte to s tým, čo za to dostanete. Pri dávkovom konvertore, ktorý beží bez dohľadu cez noc, je strata priepustnosti neviditeľná a obmedzenie havárií je celý zmysel veci. Pri interaktívnej čítačke, ktorá otvára dokumenty, ktorým používateľ už dôveruje, je cimDisabled alebo cimAutomatic rozumnou predvoľbou. Režim je obyčajná vlastnosť, takže vám nič nebráni vybrať si ho podľa triedy dokumentu za behu

HotPDF dodáva izolačnú vrstvu, dekódovacie rozpočty a štrukturálne limity parsera ako jeden natívny VCL komponent pre Delphi a C++Builder, bez potreby nasadzovať akýkoľvek externý runtime okrem samotného spustiteľného súboru workeru. Úplná dokumentácia API a skúšobná verzia sú dostupné na stránke HotPDF Delphi PDF component