Teknik Makale

Delphi'de PDF Decode Bombaları: HotPDF Filtre Zinciri Bütçeleri

Bir hizmet sürecini OOM killer onu alana kadar kilitleyen 20 KB'lık bir PDF, kodunuzdaki bir hata değildir, bir sıkıştırma açma bombasıdır. Delphi ve C++Builder için yerel VCL PDF bileşeni olan HotPDF, bunu varsayılan olarak 268435456 byte'a ayarlı ve her decode aşamasını tek bir paylaşılan bütçeye karşı ücretlendiren filtre-zinciri-başına bir tavan olan DecodeBudgetBytes ile sınırlar

Bir worker süreci yiyen 20 KB'lık dosya

Olayın şekli her zaman aynıdır. Küçük resimler render eden bir kuyruk worker'ı bir yükleme alır, yerleşik bellek iki saniyeden kısa sürede 12 GB'ı aşar ve süreç yığın izi olmadan kaybolur. Dosya 20 KB'dır. Bir sayfası, bir içerik akışı ve beş girdili bir /Filter dizisi vardır. Dizideki her isim spesifikasyonun tanımladığı bir filtredir, her aşama hatasız decode olur ve dosyada biçimsiz hiçbir şey yoktur. Bu sınıf girdiyi garip kılan şey budur: reddedilecek bozuk bir bayt yoktur

Bu, tek bir filtreyi doğru şekilde decode etmekle aynı problem değildir. LZWDecode'u ve /DecodeParms öngörücüsünü doğru yapmak kendi başına bir konudur, yüklü belgelerde LZW, öngörücüler ve DecodeParms üzerine anlatımda ele alınmıştır. Burada her decoder zaten doğrudur. Hata, doğru decoder'ların beşini art arda çalıştırdığınızda ve kimse toplamı saymadığında ne yaptıklarıdır. ISO 32000-1 §7.4, /Filter'ın tek bir isim veya isimler dizisi olabileceğini ve bir dizinin sırayla, önce ilk girdi olmak üzere uygulandığını açıkça belirtir. Bir aşamanın girdisini ne kadar genişletebileceği hakkında hiçbir şey söylemez, zincir boyunca toplam hakkında da hiçbir şey söylemez. Bir ASCIIHexDecode aşaması, zararsız gibi görünen, girdisini kabaca yarıya indirir. Bir dizi sıfır bayt üzerinde bir FlateDecode aşaması binlerce oranına ulaşır. Bunları zincirleyin ve aritmetik çarpımsal olur: 20 KB, 20 MB olur, 20 GB olur ve her bireysel adım yasal bir akışın uyumlu bir decode'udur

Delphi'de bir PDF kod çözme bombasının anatomisi: beş geçerli iç içe /Filter aşaması taşıyan yirmi kilobaytlık bir yükleme, gigabaytlar ayrılana ve çalışıcı süreç ölene dek genişlemesini çarpar
Kurallara uygun beş filtre, 20 KB'lık bir yükleme gigabaytlar ayırana dek açılımlarını çarpar; reddedilecek tek bir bozuk bayt bile yoktur

Filtre başına bir sınır bir decode bombasını neden durduramaz?

Çünkü filtre başına bir sınır, /Filter dizisinin her elemanında yeniden silahlanır. 256 MiB aşama-başına tavan altındaki beş aşamalık bir zincir 1,25 GiB'yi yetkilendirir ve son aşama, öncesindeki dördü ne üretmiş olursa olsun yine de tamamen taze bir ödenekle başlar. Sınır dürüstçe uygulanır ve önemli olan hiçbir şeyi kısıtlamaz. HotPDF, v2.447.0'dan önce tam olarak bu şekle sahipti ve yanında ikinci bir boşluk daha vardı. LZW sıkıştırma açıcı bir MaxOutputBytes tavanı taşıyordu ve görüntü öngörücü yolu kendi satırlarını hesaba katıyordu, dolayısıyla bu ikisi yerel olarak sınırlıydı. FlateDecode, ASCIIHexDecode, ASCII85Decode ve RunLengthDecode'un hiç tavanı yoktu: her biri, girdisi bitene veya ayırıcı pes edene kadar bir TMemoryStream'e yazıyordu. Dolayısıyla düşman bir zincirin iki geçiş yolu vardı. Tamamen korumasız bir filtre kullanabilir veya korumalı olanları kullanıp basitçe daha fazlasını ekleyebilirdi

Saf bir düzeltmenin kaçırdığı üçüncü bir ayrıntı var. Önem verdiğiniz sayı, son decode edilmiş çıktının boyutu değildir. Bu, doruktur ve dorukta genellikle bir ara arabellekte yaşar. Mütevazı bir 4 MB içerik akışıyla biten bir zincir, üçüncü aşamada 8 GB tahsis edebilir ve tamamen makul görünen bir şeyi geri verebilir. Sonuçtaki uzunluğu daha sonra kontrol etmek size süreci öldüren tahsis hakkında hiçbir şey söylemez

Filtre zinciri başına bir bütçe izleyici

HotPDF v2.447.0'daki düzeltme, muhasebeyi aşama yerine zincir boyunca yaymaktır. Her filtre zinciri bir THPDFDecodeBudgetTracker inşa eder ve her decoder, gerçek hedefi saran bir THPDFBudgetWriteStream aracılığıyla yazar. Sarmalayıcı, tek bir baytı yönlendirmeden önce Budget.Consume(Count)'u çağırır, dolayısıyla ret, hedef akış hâlâ eski boyutundayken gerçekleşir. Bu sıralama tüm amaçtır: arabellek zaten büyüdükten sonra yapılan bir kontrol bir savunma değil, bir tanıdır

// HotPDF zincir kod çözücüsünden sadeleştirilmiştir: tüm /Filter dizisi için
// bir izleyici, aşama başına bir sınırlı sarmalayıcı akış
Budget := THPDFDecodeBudgetTracker.Create(Doc.DecodeBudgetBytes);
try
  for I := 0 to FilterCount - 1 do
  begin
    if I = 0 then
      InputStream := StreamObj.Stream    // kaynağı oku, kopyalama
    else
      InputStream := CurrentStream;
    NextStream := TMemoryStream.Create;
    InputStream.Position := 0;
    // BeginFilter aşamayı adlandırır ve FilterCount'u artırır; sarmalayıcı
    // akış, NextStream'e yazmadan önce Budget.Consume'u çağırır
    Doc.LoadUnFlateLZW(InputStream, NextStream, Filters[I], True, Budget);
    CurrentStream.Free;
    CurrentStream := NextStream;
  end;
finally
  Budget.FinishFilter;
  Budget.Free;
end;

Yerel tavanlar ortadan kalkmadı, paylaşılan bütçenin izdüşümleri hâline geldi. LZW aşaması artık Decoder.MaxOutputBytes := Budget.RemainingBytes'i ayarlar, dolayısıyla özel tavanı bağımsız bir ödenek yerine zincirin ne kadar kalmışsa odur. Görüntü öngörücü aşaması BeginFilter ile açılır ve tahsis etmeden önce satır gereksinimini Consume üzerinden ücretlendirir, bu da öngörücü çıktısının, onu besleyen genel filtrelerle aynı bütçeye fatura edildiği anlamına gelir. Bu özellikle, filtre zinciri ve öngörücünün tek bir işlemin iki yarısı olduğu görüntü yolunda önemlidir, yüklü belgelerden decode filtreleri üzerinden görüntü çıkarmada ele alındığı gibi

Bütçe reddettiğinde çağıran ne görür?

Yığının en altında, bir ret EHPDFDecodeBudgetError fırlatır. Onun üstünde, cevap, çağrılan API'nin zaten sahip olduğu sözleşmeye bağlıdır. Başarısızlığı False veya nil üzerinden bildiren üst düzey okuma yöntemleri tam olarak bunu yapmaya devam eder, çünkü belgelenmiş bir boolean sonucu bir istisnaya dönüştürmek, biçimsiz girdiyi zaten doğru şekilde ele alan çağıranları bozardı. Yüklü sayfa içeriği yolu kasıtlı bir istisnadır: kesilmiş bir içerik akışının yalnızca boş çıkan bir sayfa olarak render edilmesine izin vermek yerine EHPDFDecodeBudgetError'ı yeniden fırlatır. Bu tasarım, çıplak bir False'un tek başına belirsiz olduğu anlamına gelir, dolayısıyla bütçe onun yanında bir tanı kaydı yayınlar: THotPDF.GetLastDecodeBudgetInfo, örneğin decode ettiği en son zincirin durumunu döndürür

type
  THPDFDecodeBudgetInfo = record
    LimitBytes: Int64;
    DecodedBytes: Int64;
    PeakStageBytes: Int64;
    FilterCount: Integer;
    Exceeded: Boolean;
    ExceededFilter: AnsiString;
  end;

var
  Pdf: THotPDF;
  Info: THPDFDecodeBudgetInfo;
  PageText: UnicodeString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.DecodeBudgetBytes := 64 * 1024 * 1024;   // varsayılandan daha sıkı
    Pdf.LoadFromFile('untrusted.pdf');
    if not Pdf.ExtractLoadedPageText(0, PageText) then
      if Pdf.GetLastDecodeBudgetInfo(Info) and Info.Exceeded then
        LogWarning(Format(
          'decode refused in %s after %d bytes, peak stage %d, %d filters',
          [String(Info.ExceededFilter), Info.DecodedBytes,
           Info.PeakStageBytes, Info.FilterCount]));
  finally
    Pdf.Free;
  end;
end;

Bu alanları birlikte okuyun, iki saldırı biçimini ayırırlar. PeakStageBytes, DecodedBytes'e yakın olduğunda, tek bir aşama tüm hasarı yapmıştır ve tek bir yüksek oranlı filtreye bakıyorsunuzdur. PeakStageBytes, DecodedBytes'in küçük bir kesri olduğunda ve FilterCount yüksek olduğunda, hiçbir bireysel aşama aşırı değildi ve zincir tavanın ötesine birikerek geçti, ki bu tam olarak filtre başına bir sınırın göremediği durumdur. Handler'ınıza yazmaya değer bir uyarı: GetLastDecodeBudgetInfo, örnek en az bir filtre decode edene kadar False döndürür, dolayısıyla ondan gelen bir False, belgenin temiz olduğuna dair kanıt değildir

Bütçenin sıfırlandığı yer ve sıfırın dürüst cevap olduğu zaman

DecodeBudgetBytes, bir belgeyi değil, bir akış zincirini sınırlar ve bu sınır kasıtlıdır ama yanlış okunması kolaydır. Her içerik akışı, her gömülü dosya, her çapraz referans akışı ve her nesne akışı taze bir 256 MiB ile başlar. 4.000 sayfalık bir belgenin bu yüzden tam tavanı harcamak için 4.000 bağımsız şansı vardır ve nesne akışları, her biri kendisi birçok nesne tutan sıkıştırılmış bir konteyner olduğu için sayıyı daha da çoğaltır, nesne akışları ve artımlı güncellemeler üzerine notlarda anlatıldığı gibi. Gerçek gereksiniminiz toplam süreç belleği üzerinde bir sınırsa, bu özellik onun tamamı değil bir girdisidir ve bir iş düzeyi veya konteyner düzeyi tavanın arkasında oturmalıdır

/Filter dizisinin her aşamasında yeniden kurulan filtre başına üst sınırlar ile Delphi'de baytlar iletilmeden önce tüm filtre zincirini Budget.Consume üzerinden sınırlayan paylaşımlı HotPDF THPDFDecodeBudgetTracker'ın karşılaştırması
Paylaşılan bir izleyici her aşamayı tek bir üst sınıra karşı fatur eder ve her sarmalayıcı akış tek bir bayt iletmeden önce Budget.Consume çağırır

Sıfır sınırsız anlamına gelir ve bir kaçış kapağı değil meşru bir ayardır. Girdiye sahip olduğunuzda ayarlayın: kendi sisteminizin ürettiği belgeler üzerinde bir arşiv yeniden işleme boru hattı, veya tek bir 600 dpi renkli tarama zincirinin gerçekten rahat kodlayacağınız herhangi bir tavandan daha fazlasına ihtiyaç duyduğu bir rasterleştirme adımı. Negatif değerler, negatif bir bütçenin tutarlı bir anlamı olmadığı ve onu sessizce sınırlamanın bir yapılandırma hatasını gizleyeceği için önceden ERangeError ile reddedilir

// Güvenilir arşiv iş hattı: bir tavan tahmin etmek yerine niyeti belirt
ArchivePdf.DecodeBudgetBytes := 0;              // açıkça sınırsız

// Güvenilmeyen yükleme: tavanı korpusunuzun gerçekte ihtiyaç duyduğuna göre boyutlandırın
IngestPdf.DecodeBudgetBytes := 96 * 1024 * 1024;

// Yapılandırma hataları kırpma yerine gürültülü şekilde başarısız olur
try
  IngestPdf.DecodeBudgetBytes := -1;
except
  on E: ERangeError do
    LogWarning('DecodeBudgetBytes cannot be negative');
end;

Sayıyı seçmek genellikle aldığından daha fazla özen hak eder, çünkü çok düşük ayarlanmış bir bütçe kendi kendine verilen bir kesintidir. Mevcut korpusunuzu varsayılanla çalıştırın, her zincir için PeakStageBytes ve DecodedBytes'i kaydedin ve tavanı gözlemlenen maksimumun üzerinde, gerçek bir marj ile ayarlayın. Güvenli göründüğü için seçilen yuvarlak bir sayı, meşru büyük bir taramayı en kötü anda reddeder ve hata, günlüklerinizde tam olarak bir saldırı gibi görünür

HotPDF Delphi'de iç içe filtreli bir kod çözmeyi reddettikten sonra çağıranların GetLastDecodeBudgetInfo'yu nasıl okuduğunu gösteren karar akışı: çözülen baytlara yakın zirve aşama baytları tek bir aç gözlü filtreyi, çok sayıda filtreye sahip düşük bir zirve ise birikmiş bir zinciri gösterir
Zirve-aşaması ile toplam çözülmüş bayt karşılaştırması, tek bir aç gözlü filtreyle üst sınırı aşan bir zinciri birbirinden ayırır

Artık gerçekleşmeyen kopya

Her aşamayı bir bütçe sarmalayıcısı üzerinden yönlendirmek, zinciri daha pahalı yerine daha ucuz kılmayı başardı. Bir akışın filtreleri olduğunda, ilk aşama artık kodlanmış baytları önce bir taslak arabelleğe kopyalamak yerine kaynak akışı doğrudan okur ve buradan itibaren yalnızca iki arabellek aynı anda canlıdır: geçerli girdi ve yazılan aşama çıktısı. Ham kopya, buna ihtiyaç duyan iki durumda hayatta kalır, yani hiç filtresi olmayan bir akış ve çağıranın son kodlamanın korunmasını istediği bir görüntü, çünkü ikisi de çağıranın sahip olduğu ve bağımsız olarak arayabildiği bir akışı geri verir. Bu kodun korumasız versiyonu daha fazla tahsis ediyordu ve daha az sınırlıyordu, ki bu ikisi arasındaki olağan ilişkidir. Açıkça belirtmeye değer: bunların hiçbiri rastgele bir PDF'i yüklemeyi güvenli hâle getirmez. Küçük bir dosyanın iç içe geçmiş filtreler yoluyla büyük bir tahsis satın aldığı, belirli ve çok ucuz bir hizmet reddi vektörünü kapatır. Byte muhasebesindeki tamsayı taşması ayrı olarak korunur ve düşman belgeleri iç uzaklıklarına güvenmeden ayrıştırmanın daha geniş sorusu farklı bir disiplindir. Bir decode bütçesi birkaç sınırdan biridir ve değeri, dosyaya dokunmadan önce tek bir özellikten ayarlayabileceğiniz bir sınır olmasıdır

Zincir başına bütçe, tanı kaydı ve koruduğu yüklü-belge decode yolları, yapılandırılacak veya yamalanacak harici bir sıkıştırma açma bağımlılığı olmadan bileşenin kendisiyle birlikte gönderilir. Bir Delphi veya C++Builder hizmeti içinde güvenilmeyen PDF girdisini nasıl sınırlayacağınızı değerlendiriyorsanız, HotPDF Delphi PDF bileşeni sayfası, bu sınırların uygulandığı yüklü-belge araç setini listeler