Artykuł techniczny

Izolacja kodeków obrazu PDF w procesach HotPDF

HotPDF potrafi dekodować trzy najbardziej ryzykowne filtry obrazów PDF, DCTDecode, JPXDecode i JBIG2Decode, wewnątrz osobnego, krótkotrwałego procesu roboczego zamiast wewnątrz twojej aplikacji. Właściwość, która to włącza, to CodecIsolationMode, a praktyczny efekt jest taki, że zniekształcony strumień kodowy JPEG 2000, który wcześniej zawiesiłby twoją aplikację VCL, teraz zabija jednorazowy proces potomny, podczas gdy host zgłasza kod stanu i działa dalej

Ta różnica ma największe znaczenie tam, skąd pliki PDF faktycznie napływają: formularz przesyłania, brama pocztowa, urządzenie skanujące, katalog FTP partnera. Nie kontrolujesz tych bajtów, a kodeki obrazów to miejsce, gdzie mieszkają historyczne szkody

Dlaczego jeden zły obraz wywraca całą aplikację?

Ponieważ kodek obrazu to jedyna część czytnika PDF, która uruchamia złożoną maszynę stanów nad danymi kontrolowanymi przez atakującego, przy niemal zerowej liczbie kontroli strukturalnych, na których można się jeszcze oprzeć. Zanim bajty dotrą do dekodera JPEG 2000 lub JBIG2, tablica odsyłaczy krzyżowych jest już przeanalizowana, obiekt rozwiązany, łańcuch filtrów rozwinięty, a to, co zostaje, to surowy strumień kodowy mówiący, ile jest kafli, ile komponentów, ile bitów na próbkę. Zła liczba w tym miejscu to nie błąd analizy składniowej. To zła wielkość alokacji albo indeks poza zakresem wewnątrz ciasnej pętli dekodowania

Limity budżetu pomagają i powinieneś już je mieć. HotPDF ogranicza ekspansję za pomocą DecodeBudgetBytes i DocumentDecodeBudgetBytes, a łańcuchy filtrów ogranicza za pomocą DecodeFilterLimit i DecodePipelineDepthLimit; rozumowanie stojące za tymi limitami zostało omówione w ograniczonym dekodowaniu dla zagnieżdżonych filtrów i bomb PDF. Ale budżet bajtowy odpowiada tylko na jedno pytanie: ile wyjścia jest dozwolone. Nie potrafi odpowiedzieć, co się dzieje, gdy dekoder ulega awarii, zanim wyprodukuje jakiekolwiek wyjście. Naruszenie dostępu wewnątrz pętli dekodowania nie jest naruszeniem zasady, które możesz po prostu odrzucić; to zdarzenie na poziomie procesu, a jedyną wiarygodną izolacją dla zdarzenia na poziomie procesu jest inny proces

Co HotPDF izoluje, a czego nie

HotPDF izoluje dokładnie trzy rodzaje kodeków, wyliczone jako hckDCT, hckJPX i hckJBIG2 w jednostce HPDFCodecIsolation. Wszystko inne, Flate, LZW, RunLength, ASCII85, CCITT, pozostaje w procesie głównym, ponieważ te dekodery są na tyle proste, że da się je ograniczyć budżetami, i to nie stamtąd biorą się interesujące awarie

Transport jest celowo wąski. Host przydziela jedno ograniczone odwzorowanie pamięci współdzielonej, zapisuje stały nagłówek THPDFCodecSharedHeader, skompresowane dane wejściowe oraz ewentualne globalne segmenty JBIG2, uruchamia proces roboczy i czeka. Proces roboczy zapisuje zdekodowane piksele z powrotem do tego samego odwzorowania i ustawia słowo statusu. Nie ma protokołu potokowego, który mógłby się rozsynchronizować, ani formatu serializacji do fuzzowania, a nagłówek niesie wartość magiczną i wersję, dzięki czemu niepasująca binarka procesu roboczego zostaje odrzucona, a nie błędnie odczytana

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Reguła fail-closed: nigdy nie dekoduj tych kodeków w procesie głównym
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 albo >= 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;

Pozostaw CodecWorkerExecutable puste, a HotPDF rozwiąże proces roboczy obok twojego własnego pliku wykonywalnego, jako HotPDFCodecWorker.exe w katalogu ParamStr(0). Ustaw go jawnie, gdy twoje wdrożenie umieszcza proces roboczy gdzie indziej; wartość jest rozwijana przez ExpandFileName, więc ścieżka względna rozwiązuje się względem bieżącego katalogu, a nie katalogu aplikacji, co rzadko jest tym, czego chcesz w usłudze

Automatyczny czy wymagany: którą awarię wolisz?

Trzy wartości THPDFCodecIsolationMode kodują trzy różne odpowiedzi na jedno pytanie: co powinno się stać, gdy proces roboczy w ogóle nie może działać. cimDisabled pomija izolację całkowicie i dekoduje w procesie głównym, czyli zachowanie sprzed wersji 3.x. cimAutomatic, wartość domyślna, próbuje uruchomić proces roboczy i po cichu przechodzi na dekodowanie w procesie głównym, gdy plik wykonywalny procesu roboczego jest niedostępny albo nie chce się uruchomić, co zostaje zgłoszone jako status cwsUnavailable. cimRequired odmawia tego zapasowego zachowania: niedostępny proces roboczy oznacza dekodowanie jako obsłużone i zakończone niepowodzeniem, więc żaden niezaufany strumień kodowy nigdy nie trafia do twojej przestrzeni adresowej

Wybieraj według modelu zagrożeń, nie wygody. Przeglądarka desktopowa otwierająca dokumenty, które użytkownik już ma na dysku, jest odpowiednia z cimAutomatic, gdzie brakujący proces roboczy degraduje się do klasycznego zachowania, zamiast psuć produkt. Usługa przyjmowania plików z internetu powinna działać w trybie cimRequired, ponieważ błąd wdrożenia, który po cichu usuwa warstwę izolacji, to dokładnie taki rodzaj regresji, którego nikt nie zauważa, dopóki nie ma to znaczenia. Zwróć uwagę na asymetrię: tylko cwsUnavailable wyzwala zapasowe zachowanie. Proces roboczy, który się uruchomił, a potem uległ awarii, przekroczył limit czasu albo trafił na limit, to niepowodzenie dekodowania w obu trybach, nigdy ciche ponowienie w procesie głównym

Odczytywanie wyniku z THPDFCodecWorkerStatus

GetLastCodecWorkerInfo zwraca wynik ostatniego izolowanego dekodowania, a wyliczenie statusu jest na tyle szczegółowe, że może napędzać rzeczywiste decyzje operacyjne, a nie tylko ogólny wpis w dzienniku „obraz się nie powiódł”. Wartości to cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError i cwsOutputLimit

Traktuj je jako trzy grupy. Problemy wdrożeniowe to cwsUnavailable i cwsLaunchFailed: ktoś wysłał wdrożenie bez procesu roboczego albo antywirus blokuje tworzenie procesów. Problemy z dokumentem to cwsDecodeFailed i cwsOutputLimit: plik jest zniekształcony albo większy, niż pozwala twoja polityka, a odrzucenie go jest właściwą odpowiedzią. Interesująca grupa to cwsTimedOut i cwsCrashed, ponieważ to zdarzenia, które wcześniej zawiesiłyby albo zabiłyby proces hosta. Gdy to się zdarza, towarzyszące pola ProcessId, ExitCode i ElapsedMilliseconds dają wystarczająco dużo, by skorelować zdarzenie z wpisem Windows Error Reporting i zdecydować, czy jeden plik klienta jest patologiczny, czy ktoś cię sonduje

procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
  case Info.Status of
    cwsSucceeded:
      ; // nie ma nic do zgłoszenia
    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, które faktycznie obowiązują

Do każdego izolowanego dekodowania odnoszą się trzy osobne pułapy, a wiedza, który z nich zadziałał, oszczędza popołudnie zgadywania. CodecWorkerTimeoutMilliseconds domyślnie wynosi 10 000 i jest walidowany w zakresie od 1 do 600 000; wartość spoza tego zakresu powoduje wyjątek zamiast cichego przycięcia. CodecWorkerMemoryLimitBytes domyślnie wynosi 536 870 912 bajtów i musi być albo zerem, co oznacza brak limitu, albo co najmniej 67 108 864 bajtów, ponieważ mniejszy pułap nie pomieści realistycznego zestawu roboczego dekodera i zawiódłby przy każdym dokumencie. Limit pamięci jest egzekwowany przez obiekt zadania Windows z semantyką zabicia przy zamknięciu, więc proces roboczy umiera razem z zadaniem, nawet jeśli host zostanie brutalnie zakończony

Trzeci pułap to limit wyjścia i jest on wyliczany, a nie konfigurowany. HotPDF oblicza wymagane bajty na podstawie żądanego regionu albo oczekiwanej geometrii obrazu, jako szerokość razy wysokość razy trzy dla wyjścia 24-bitowego, a następnie przycina tę wartość do DecodeBudgetBytes, gdy budżet jest ustawiony. Dekoder, który zgłasza wiarygodny nagłówek, a potem próbuje wyemitować znacznie więcej pikseli, niż pozwala geometria, jest zatrzymywany przez samo odwzorowanie, a host widzi cwsOutputLimit. Dlatego warstwa izolacji i budżet dekodowania się uzupełniają: budżet określa, jak duży może być obraz, a granica izolacji dba o to, by kłamstwo na temat tego rozmiaru nie zamieniło się w zapis poza zakresem w twoim procesie

Gdzie to pasuje w utwardzonej ścieżce przyjmowania plików

Izolacja procesów to zewnętrzna warstwa łańcucha obrony, który zaczyna się dużo wcześniej. Limity strukturalne odrzucają nieprawdopodobne dokumenty już w trakcie analizy składniowej. Budżety filtrów ograniczają ekspansję. Izolacja zawiera to, co przetrwa oba te etapy. Dla dokumentów, które docierają do warstwy obrazu, warto wiedzieć, który kodek faktycznie testujesz, ponieważ obsługa JPXDecode i słowniki symboli JBIG2 mają bardzo różne profile awarii, a JBIG2 w szczególności niesie globalne segmenty między stronami, które naiwna izolacja per obraz by złamała

Koszt jest uczciwy i wart wspomnienia: uruchamianie procesu na każdy izolowany obraz dodaje milisekundy, a dokument z setkami zeskanowanych stron to odczuje. Zmierz to w odniesieniu do tego, co daje w zamian. W konwerterze wsadowym działającym bez nadzoru całą noc strata przepustowości jest niewidoczna, a powstrzymanie awarii to cały sens. W interaktywnej przeglądarce otwierającej dokumenty, którym użytkownik już ufa, cimDisabled albo cimAutomatic to rozsądna wartość domyślna. Tryb to zwykła właściwość, więc nic nie stoi na przeszkodzie, by wybierać go w czasie działania osobno dla każdej klasy dokumentów

HotPDF dostarcza warstwę izolacji, budżety dekodowania i strukturalne limity analizatora jako jeden natywny komponent VCL dla Delphi i C++Builder, bez żadnego zewnętrznego środowiska uruchomieniowego do wdrożenia poza samym plikiem wykonywalnym procesu roboczego. Pełna dokumentacja API i wersja próbna są dostępne na stronie komponentu HotPDF dla Delphi