Teknik Makale

Bellek Güvenli PDF Ayrıştırma: Kötü Amaçlı Belgelere Karşı Savunma

Bir belge alım hattı, yabancılar tarafından yazılan dosyaları kabul eder. Faturalar, taramalar, bir web formundan gelen ekler: her biri bir PDF olduğunu iddia eder ve ayrıştırıcınızın üzerinde işlem yapması beklenen yüzlerce sayı taşır. Akış uzunlukları, görüntü boyutları, bayt ofsetleri, nesne referansları — bunların her biri dosyayı üreten kişi tarafından seçilmiştir ve eksik yüklenmiş veya kasıtlı olarak bozuk bir belge eninde sonunda bu sayılardan birini hasar vereceği bir yere koyacaktır. O dosyadan sağ çıkan bir ayrıştırıcı ile çöken veya bozuk bellekle çalışmaya devam eden bir ayrıştırıcı arasındaki fark, belirli bir PDF kütüphanesine bağlı olmayan küçük bir alışkanlıklar bütünüdür

Bu alışkanlıklar tek bir önermeyi paylaşır: dosyadan okunan bir değer bir ölçüm değil, bir iddiadır. Yalnızca ayrıştırıcının kendi ölçtüğü bir şeyle — dosyanın gerçek boyutu, bir kod çözücünün ürettiği gerçek bayt sayısı, bir özyinelemenin gerçek derinliği — karşılaştırıldıktan sonra kullanılabilir hale gelir. Aşağıda, belge ayrıştırıcılarının gerçekten bozulduğu yerlere uygulanan bu önerme yer almaktadır

Bildirilen bir uzunluk bir iddiadır, ölçüm değil

En basit uyumsuzluk akış uzunluğudur. Bir PDF akış nesnesi, bayt sayısını /Length anahtarında bildirir ve gerçek veri stream ve endstream anahtar kelimeleri arasında yer alır. Hiçbir şey bu ikisini uyuşmaya zorlamaz. Eksik bir dosya, bildirilen sayıdan daha az gerçek bayt barındırır; bozuk bir üreticiden gelen bir dosya, dosyanın sonunun ötesine veya komşu bir nesneye ulaşan bir uzunluk bildirebilir. Tahsis işlemini bildirilen değerden yapıp endstream'e kadar kopyalarsanız arabelleği aşarsınız; kullanılabilirliği kontrol etmeden tam olarak bildirilen sayıyı okursanız dosyanın sonundan çıkarsınız. Bildirilen değerin tahsisi yönlendirmesine ancak verinin sonuna kadar ölçülen mesafeyle sınırlandırdıktan sonra izin verin ve bir anlaşmazlığı bir karar noktası olarak ele alın — endstream için tarayarak onarın veya akışı reddedin — asla sessizce inanılacak bir şey olarak değil

Tahsis ettiğinizden daha büyük bir raster tanımlayan görüntü parametreleri

Görüntü akışları riskleri artırır çünkü aynı pikselleri iki bağımsız sayı kümesi tanımlar. Görüntü sözlüğü /Width ve /Height değerlerini taşır ve raster arabellekleri genellikle bunlardan boyutlandırılır. Kod çözme filtresi kendi geometrisini taşır: CCITTFaxDecode, DecodeParms parametresinden /Columns, /Rows ve /K alır, burada /K Group 3 veya Group 4 şemasını seçer ve kod çözücü tarama çizgisi başına (Columns + 7) div 8 bayt üretir. /Width 100 bildiren ancak filtreye /Columns 1728 (varsayılan) ileten bir dosya, kod çözücünün arabelleğin beklediğinden on altı kat daha fazla satır başına bayt üretmesine neden olur ve taşan kısım tahsisten sonra her ne varsa tarama çizgisi tarama çizgisi oraya iner. /Rows bulunmadığında kod çözücü veri dur diyene kadar çalışır, bu nedenle satır sayısını da sınırlandırın. DCTDecode aynı zafiyete sahiptir: JPEG verisi kendi genişlik ve yüksekliğini kendi SOF işaretçisinde taşır ve hiçbir şey bunların sözlükle uyuşmasını zorunlu kılmaz

Savunma kuralı mekaniktir: beklenen raster boyutunu doğrulanmış kod çözme parametrelerinden — CCITT için filtrenin kendi /Columns ve /Rows değerleri, DCT için SOF boyutları — hesaplayın, bunu sınırlarınıza karşı kontrol edin, tahsisi bundan yapın ve kod çözme sırasında çıktının tahsisi asla aşmadığını doğrulayın. Sözlük ve filtre geometri konusunda anlaşamadığında, bunları uzlaştırın veya görüntüyü reddedin. Bir ayrıştırıcının asla yapmaması gereken şey, arabelleği bir sayı kümesinden boyutlandırmak ve kod çözücüyü diğerinde çalıştırmaktır

Delphi aritmetiği ve tahsis tuzakları

Üç Delphi davranışı, doğrulamayı amaçlayan bir ayrıştırıcının bile altını oyar. Birincisi 32-bit çarpımıdır: Delphi, hedefin genişliğine bakılmaksızın iki Integer işleneninin çarpımını 32 bitte değerlendirir, bu nedenle Width * Height * BytesPerPixel her çarpan kendi mantık testinden geçse bile başa sarabilir (wrap). Piksel başına üç baytta 30000'e 30000'lik bir tarama 2,7 milyar bayttır, bu da işaretli 32-bit aritmetikte negatife sarar; biraz farklı çarpanlar, arabelleği tahsis eden ve yetersiz boyutlandıran küçük bir pozitif uzunluğa sarar. İlk işleneni dönüştürerek tüm ifadeyi genişletmeye zorlayın — Size := Int64(Width) * Height * BytesPerPixel — ardından herhangi bir şey SetLength'e ulaşmadan önce açık bir üst sınıra karşı karşılaştırın

İkincisi, aralık kontrolüdür (range checking). Delphi'nin varsayılan yayın (release) yapılandırması kapalı olarak gelir, bu nedenle dosya verilerinden hesaplanan kapsam dışı bir dizin (index) hata fırlatmaz — diziye bitişik belleği okur veya yazar. Dosyadan elde edilen değerlerle indeksleme yapan her birimin en üstündeki {$R+} (ve aritmetik taşması için {$Q+}) ile tekrar açın. Maliyeti, bir ayrıştırıcının zaten yaptığı G/Ç'nin (I/O) yanında ölçülemez düzeydedir ve sessiz bozulmayı yakalanabilir bir ERangeError'a dönüştürür

Üçüncüsü, dosya tarafından sağlanan bir Int64 ile TMemoryStream.SetSize'dır. Mevcut bir RTL'de dosyanın istediği her şeyi tahsis eder, böylece dört gigabayt talep eden tek bir akış, alımın ortasında yetersiz bellek arızası (out-of-memory) haline gelir. SetSize'ın bir Longint aldığı eski RTL'lerde değer önce sessizce daraltılır: bildirilen bir $100000010, 16 olur, tahsis başarılı olur ve gerçek verinin yazılması bunu çok aşar. Herhangi bir tahsis çağrısı onu görmeden önce, her boyutu ölçülen kaynak boyutuna ve kesin bir sınıra karşı doğrulayın

Dosya dışını işaret eden ofsetler

Çapraz referans (xref) tablosu, nesne numaralarını mutlak bayt ofsetleriyle eşleştirir ve ayrıştırıcı işaret ettiği yere gider. Hasarlı veya düşmanca bir dosyada bu ofsetler dosyanın sonunun ilerisine veya ilgisiz yapıların içine denk gelir. TStream hatayı sessizleştirir: PositionSize'ın ötesine ayarlamak bir hata değildir ve dosyanın sonunu geçen düz bir Read, istenenden daha az bayt döndürür, böylece sayı kontrolünü atlayan kod önceki nesneden kalan eski baytları ayrıştırmaya devam eder. Savunma bir kontrol noktasıdır (chokepoint) — akış hareket etmeden önce ofset ve sayıyı ölçülen dosya boyutuna karşı doğrulayarak, dosya güdümlü her aramanın (seek) ve okumanın (read) içinden geçtiği tek bir yardımcı (helper)

uses
  System.SysUtils, System.Classes;

const
  MAX_OBJECT_BYTES = 64 * 1024 * 1024; // no single object may exceed 64 MB

type
  EPdfBoundsError = class(Exception);

// Every file-driven seek and read goes through here. Offset and Count are
// file-supplied claims; Source.Size is the measurement they must fit.
procedure ReadBounded(Source: TStream; Offset, Count: Int64;
  var Buffer: TBytes);
begin
  if (Offset < 0) or (Count < 0) or (Count > MAX_OBJECT_BYTES) or
     (Offset > Source.Size) or (Count > Source.Size - Offset) then
    raise EPdfBoundsError.CreateFmt(
      'object extent %d+%d exceeds file size %d',
      [Offset, Count, Source.Size]);
  SetLength(Buffer, Count);
  if Count = 0 then
    Exit;
  Source.Position := Offset;
  Source.ReadBuffer(Buffer[0], Count);
end;

Çapraz referans ofsetlerini, akış kapsamlarını ve gömülü dosya okumalarını buradan geçirin; ve kötü bir ofset, üç çağrı sonraki bir erişim ihlali yerine sayıları isimlendiren temiz bir ret (rejection) haline gelsin

Nesne grafiğindeki döngüler ve derinlik

Bir PDF bir ağaç değil, bir grafiktir. Herhangi bir değer dolaylı bir referans olabilir, bir referans başka bir referansa çözümlenebilir — örneğin nesne 12'nin 13 0 R tuttuğu yerde /Length 12 0 R — ve bir zincirin kendi üzerine kapanmasını engelleyen hiçbir şey yoktur. Referansları safça takip eden bir çözümleyici (resolver), yerel yığın (stack) tükenene kadar özyineleme (recurse) yapar ve yığın tükenmesi yakalayabileceğiniz bir şey değildir; süreci sonlandırır. Derinlemesine iç içe geçmiş diziler ve sözlükler, hiçbir döngü olmadan bile aynı sona ulaşır

İki koruyucuyu birlikte kullanın: açık bir derinlik sayacı (depth counter), meşru-ama-derin (honest-but-deep) durumu hiçbir yasal dosyanın yaklaşamayacağı bir sınırda tutar ve ziyaret edilenler kümesi (visited set), gerçek bir döngüyü ikinci ziyaretinde yakalayarak, bunu bir sınır aşımı (limit trip) yerine kesin, rapor edilebilir bir hataya dönüştürür

uses
  System.SysUtils, System.Generics.Collections;

const
  MAX_RESOLVE_DEPTH = 32; // far deeper than any legitimate reference chain

type
  EPdfStructureError = class(Exception);

  TPdfValueKind = (pvNull, pvNumber, pvName, pvString, pvArray,
    pvDictionary, pvStream, pvReference);

  TPdfValue = record
    Kind: TPdfValueKind;
    RefNumber: Integer; // meaningful when Kind = pvReference
    // ... payload fields for the remaining kinds
  end;

// LoadObject is your own routine: it looks up the xref offset for
// ObjNumber, reads the object with ReadBounded, and parses it.
function ResolveObject(ObjNumber, Depth: Integer;
  Visited: TDictionary<Integer, Boolean>): TPdfValue;
begin
  if Depth > MAX_RESOLVE_DEPTH then
    raise EPdfStructureError.Create('reference chain exceeds depth limit');
  if Visited.ContainsKey(ObjNumber) then
    raise EPdfStructureError.CreateFmt(
      'circular reference through object %d', [ObjNumber]);
  Visited.Add(ObjNumber, True);
  try
    Result := LoadObject(ObjNumber);
    if Result.Kind = pvReference then // e.g. /Length 12 0 R
      Result := ResolveObject(Result.RefNumber, Depth + 1, Visited);
  finally
    Visited.Remove(ObjNumber); // siblings may legally share this object
  end;
end;

Sıkıştırma açma bir yükselticidir

Birkaç kilobaytlık FlateDecode girdisi gigabaytlara şişebilir; genel amaçlı sıkıştırma, tekrarlayan düz metni ödüllendirir ve bir saldırgan onu maksimum düzeyde tekrarlayan hale getirebilir. Her akışın şişirilmiş boyutunu, tüketicisinin makul bir şekilde ihtiyaç duyabileceği bir değere sınırlandırın ve ikinci bir belge bazında bütçe (per-document budget) tutun: her biri akış başına sınırın hemen altında olan beş yüz akış, belleği tıpkı devasa bir akış gibi kesin olarak tüketir. Kontrol işlemi şişirme döngüsünün içinde olmalı, üretildikçe çıktı baytlarını saymalı ve ihlal durumunda iptal etmelidir; hafıza zaten harcandıktan sonra döngünün bitiminde değil. Sıkıştırılmış dosya boyutunun bir katı olarak ifade edilen bir belge bütçesi iyi çalışır, çünkü meşru belgeler hazırlanmış (crafted) bir akışın ulaştığı oranların çok altında kümelenir

Kendi birimlerinizin ötesinde derinlemesine savunma

Aynı kusur sınıfları kütüphanelerin içinde de yaşar. Bu blogdaki iki vaka çalışması, gerçek örnekleri incelemektedir: tamsayı sarmaları (integer wraps), sınırsız özyineleme ve Hardening a Pascal PDF Parser Against Malicious Files içindeki yerel bir Pascal motorunda kapatılan başlatılmamış arabellekler (uninitialized buffers) ile Hardening a PDFium Component Binding içindeki bir C motorunu bağlamanın çağırma kuralı (calling-convention), tamsayı genişliği (integer-width) ve sahiplik (ownership) tehlikeleri. Gerçekten güvenilmeyen alımlar için — genel bir yükleme formu, kimliği doğrulanmamış bir posta kutusu — ayrıştırma ve kod çözme işini ayrıca düşük ayrıcalıklı ayrı bir süreçte (process) çalıştırın; böylece tüm süreç içi korumaları alt eden dosya, çöken bir hizmet yerine başarısız bir işe mal olur

Bir uçuş öncesi kontrol listesi

Bir sonraki sürüm çıkmadan önce, ayrıştırıcıyı bu listeye göre gözden geçirin: bildirilen uzunluk yerine sınırlandırılmış (clamped) bir uzunluktan boyutlandırılan her akış arabelleği; doğrulanmış kod çözücü parametrelerinden boyutlandırılan ve kod çözücü çıktısına karşı kontrol edilen her raster; Int64 olarak değerlendirilen ve açık bir sınırla karşılaştırılan her boyut çarpımı; dosyadan elde edilen değerlerle indeksleyen her birimde aktif olan {$R+}; ölçülen dosya boyutuna karşı sınırları kontrol edilen her arama (seek); derinlik sınırlaması ve döngü kontrolü yapılmış her referans çözümü; çıktıları akış başına ve belge başına bütçelere göre sayan her şişirme döngüsü. Bu kontrollerin hiçbiri yasal bir belgede ölçülebilir bir zaman maliyetine yol açmaz ve her biri bellek bozulmasını temiz, günlüğe kaydedilebilir (loggable) bir redde dönüştürür

Not: losLab HotPDF Bileşeni, PDFlibPas Delphi PDF Kütüphanesi ve PDFium Bileşeni, bu sınır kontrollerini, derinlik sınırlarını ve genişleme kapaklarını dahili olarak uygular, bu nedenle bunlar üzerine inşa edilen bir alım hattı (intake pipeline) zaten güçlendirilmiş bir temelden başlar