Teknik Makale

Zararlı Dosyalara Karşı Pascal PDF Parser Güçlendirmek

PDF açtığınız bir belge değildir. Çalıştırdığınız küçük bir programdır. Gömülü her yazı tipi, karakter dizelerini (charstrings) bekleyen yığın tabanlı bir yorumlayıcıdır; her görüntü, dosyanın seçtiği genişlik, yükseklik ve bit derinliği alanlarıyla beslenen bir çözücüdür ve her akış, parametreleri dosyanın belirlediği filtrelere sarılmış olarak gelir. Bu sayıların hiçbiri sizin değildir. Dosyayı üreten her kimse ondan gelmiştir; bu gerçek bir iş yükünde bir müşterinin faturası veya bilinmeyen bir göndericiden gelen bir ektir. Bu baytları piksellere ve gliflere dönüştüren çözücüler saldırı yüzeyidir ve girdisine güvenen bir ayrıştırıcı, bir çökmeden veya daha kötüsünden yalnızca bir hatalı biçimlendirilmiş dosya uzaktadır

PDF Library for Delphi; yazı tipi programları (TrueType, Type1, CFF ve CMap tabloları), görüntü çözücüler (PNG, GIF, TIFF, JBIG2 ve CCITT Grup 3 ve Grup 4) ve akış filtreleri (LZW, ASCII85 ve Flate tahmincileri) genelinde tüm çözme yolunu düşmanca ele alır. Aşağıda, o yolun kapatması gereken, her biri bunu mümkün kılan belirli Delphi davranışına dayanan beş bellek güvenliği tuzağı yer alıyor; aynı şekiller, güvenilmeyen girdileri ayrıştıran herhangi bir Pascal kodunda da tekrarlanıyor

PDF Library for Delphi: Yazı tipi yorumlayıcılarını, görüntü çözücülerini ve akış filtrelerini besleyen bir Pascal PDF ayrıştırıcısının beş korumalı tuzak sınıfına uzanan saldırı yüzeyi haritası
Genişlikler, derinlikler, ofsetler, uzunluklar ve filtre bayraklarının hepsi üreticiden gelir; bu da yazı tipi, görüntü ve filtre çözücülerini, bir ayrıştırıcının düşmanca ele alması gereken saldırı yüzeyi yapar

Size yetersiz bir arabellek teslim eden bir tamsayı taşması

Bir görüntü çözücüdeki klasik bellek güvenliği hatası, saran (wrap) bir boyut çarpımıdır. Bir çözücü genişlik, yükseklik, bileşen sayısı ve bit derinliğini okur, çıktısını boyutlandırmak için bunları çarpar, o kadar bayt tahsis eder ve ardından görüntüyü gerçek boyutlarında yazar. Çarpma işlemi 32 bitlik aritmetikle yapılırsa, her bir faktör makul bir aralıkta olsa bile çarpım küçük bir değere sarabilir; böylece tahsis başarılı olur, çok küçük çıkar ve kod çözme işleminin sonundan dışarı taşar. Bu, bir adım sonra yığın sınırlarının dışında yazmaya (CWE-787) yol açan tamsayı taşmasıdır (CWE-190)

Paylaşılan bir görüntü yolunda her boyutu 65535 ile sınırlamak, bağımsız bir çözücü kendi boyutlarını kendisi hesapladığında yetmez. ByteCount * FHeight gibi bir satır-baytı-çarpı-yükseklik ifadesi ya da FWidth * Components * BitDepth gibi piksel başına bir ifade, sonucu atadığınız değişken ne kadar geniş olursa olsun, her iki işlenen de 32 bitlik tamsayı olduğunda Delphi'de 32 bitlik bir çarpımdır. Büyük bir tarama için 60000 genişlik ve yükseklik tek tek makuldür, ama bayt cinsinden çarpımları işaretli 32 bitlik aralığı aşar ve uzunluk küçük çıkar. Aynı tuzak, BitsPerComponent * Colors * Columns gibi bir tahminci adımında (stride) da oturur

Çözüm, tüm ifadenin 64 bitte değerlendirilmesi için en az bir işleneni Int64 yapmak, ardından SetLength çağrısı için daraltmadan önce MaxInt ile karşılaştırmak ve dosyayı reddetmektir

PDF Library for Delphi: Sarmalanmış 32 bitlik boyut çarpımının yetersiz ayırma ve sınırlar-dışı yazmaya yol açması ile dosyayı reddeden bir Int64 genişletme kontrolünün yan yana karşılaştırması
Bayt sayısı çarpımını Int64'te değerlendirmek, ayrıştırıcının MaxInt ile karşılaştırmasını ve SetLength yalan bir tampon açmadan önce düşmanca boyutları reddetmesini sağlar
// Tahsis etmeden önce reddet, yazdıktan sonra değil.
// Çarpımı Int64'te değerlendir, böylece 32 bitte saramaz.
RowBytes := (Int64(FWidth) * Components * BitDepth + 7) div 8;
if (RowBytes <= 0) or (RowBytes * FHeight > MaxInt) then
  Exit;  // düşmanca veya desteklenmeyen boyutlar; görüntüyü reddet
SetLength(Buffer, RowBytes * FHeight);

Bunu genel bir sorundan ziyade bir Delphi sorunu kılan şey sessiz daraltmadır. Çok geniş bir ifadeyi 32 bitlik bir hedefe atamak, derleyicinin varsayılan olarak uyarmayacağı yasal bir dönüşümdür ve aralık kontrolü, değer bir dizin olarak kullanılmadan önce gerçekleşen bir sarmayı yakalamaz. Çarpımı 32 bitte bırakırsanız, dil size kod çözmenin ne kadar belleğe dokunacağı konusunda yalan söyleyen bir uzunluk verir

Bir korumanın tetiklenmesini imkânsız kılan alan türü

Bir TIFF dosyası, her biri bir sonrakinin bayt kaymasını taşıyan bir görüntü dosyası dizinleri zinciridir. Kötü niyetli bir dosya bu zinciri kendisine geri döndürebilir ve durma koşulu olmadan onu dolaşan bir okuyucu sonsuza kadar çalışır. Bu, saldırgan tarafından kontrol edilen girdinin yönlendirdiği sonsuz döngüdür (CWE-835) ve savunma, yasal hiçbir dosyanın ulaşamayacağı bir sınırı geçtiğinde duran bir sayaçtır

Şimdi sayfa sayacını, Delphi'de 0 ila 65535 tutan Word olarak bildirin ve döngüye "sayfa sayısı 65535'i aşınca dur" biçiminde bir sonlandırma koruması verin. İşlenen ile eşik arasında bir üst sınır paylaşıldığını fark edene kadar bu doğru gibi okunur. Bir Word asla 65535'ten büyük olamaz, bu yüzden karşılaştırma yapısal olarak her zaman yanlıştır: sayaç 65535'e ulaştığında bir sonraki artış onu 0'a geri sarar, koruma asla tavanın üzerinde bir değer görmez ve döngüye girmiş bir IFD zinciri okuyucuyu döndürmeye devam ettirir

Çare, korumanın sayacın gerçekten tutabileceği bir değeri ifade edebilmesi için alanı genişletmektir. TPDFTIFF.FPageCount Integer olarak bildirilir, böylece FPageCount > 65535 karşılaştırması ulaşılabilir olur ve döngü sonlanır. Bir sınır kontrolü Value > MaxValueOfType(Value) şekline sahip olduğunda ve işlenen tam olarak o maksimumda yazıldığında, koşul sabit bir yanlıştır: türü genişletin ya da tetiklenebilmesi için maksimuma karşı eşitliği test edin

Kritik yolda aralık kontrolü kapatıldı

Aralık kontrolü açıkken, Delphi her dizi ve dize dizinine bir sınır kontrolü ekler; bu, yakalanabilir bir ERangeError oluşturan aralık dışı bir dizin ile yapıya ait olmayan belleği okuyan veya yazan aynı dizin arasındaki farktır. Sıcak yollar bazen bunu yerel bir {$R-} yönergesiyle devre dışı bırakır; bu, dizinler güvenilir olmayı bırakana kadar savunulabilir bir durumdur

Yazı tipi yorumlayıcılarının dayandığı liste erişimcisi TPDFlibStringList.Get, tam olarak böyle bir yoldur. Windows'ta aralık kontrolü kapalı derlenir ve yedek deposunu doğrudan dizinler; böylece aralık dışı bir dizin bir hata değil, ham bir bellek erişimi olur. Dizin her zaman geçerli olduğunda bu iyi bir şeydir ve dizinin dosyadan gelebileceği bir CFF ya da Type2 karakter dizisi yorumlayıcısının içinde dikkat ister. Boş bir yığından işlenen çıkaran bir karakter dizisi eksi bir dizin üretir; glif sayısına karşı bir eksik olan bir glif tanımlayıcısı sonun bir yuva ötesini dizinler. Kontrolsüz bırakılırsa ikisi de yakalanabilir bir istisna yerine gerçek bir sınır dışı erişim olur ve yuvalar referans sayımlı AnsiString değerleri tuttuğu için başıboş bir okuma bir dizenin referans sayısını da bozabilir

PDF Library for Delphi o sıcak yolda aralık kontrolünü kapalı tutar ve bunun yerine dizinleri kanıtlanabilir şekilde geçerli yapar: işlenen yığınının üstünü almadan önce yorumlayıcı yığının boş olmadığını kontrol eder ve her dizin koruması, bire bir hatayı kabul eden küçük-eşittir yerine sayıya karşı kesin bir küçüktür olarak yazılır. Yönerge, sınırların sorumluluğunu derleyiciden size taşır ve kaldırdığı doğrulama, her giriş noktasında elle geri konmalıdır

Bir karakter dizisi yorumlayıcısında sınırsız özyineleme

Bir Type2 karakter dizisi bir alt rutini çağırabilir ve bir alt rutin de kendisi başka birini çağırabilen bir karakter dizidir, bu nedenle yerel ve genel alt rutin çağırma operatörleri dosyanın ne kadar derine gideceğine karar vermesine izin verir. Kendini doğrudan veya bir döngü aracılığıyla çağıran bir alt rutin, yerel yığın tükenene ve işlem ölene kadar sonsuz özyineleme (recursion) yapar. Bu kontrolsüz özyinelemedir (CWE-674)

Type1 yorumlayıcısı bir çağrı derinliği sayacı ve PLType1MaxCallDepth tavanıyla buna karşı korunur ve bu aşan inişi reddeder; bu, Type1 spesifikasyonunun kendisinin adlandırdığı derinlik sınırını yansıtır. Tuzak şudur: sonradan yazılan, yapısal olarak benzer bir yorumlayıcı — Type2 olan gibi — o korumayı kendi kendine devralmaz ve kendi numarasını çağıran bir alt rutine sahip el yapımı bir yazı tipi, eksik kontrolün üzerinden doğrudan bir yığın taşmasına yürür

// Type2 yolunda eksik olan, Type1 korumasının şekli.
// İç içe çağrılar boyunca derinliği izle ve onu aşarak özyinelemeyi reddet.
Inc(CallDepth);
if CallDepth > PLType1MaxCallDepth then
  Exit;  // düşmanca kendine referanslı alt rutin; inişi durdur
// ... alt rutini yorumla, sonra çıkışta Dec(CallDepth)

PDF Library for Delphi, Type2 yoluna Type1 kardeşiyle aynı sınırlı derinliği verir. Saldırgan tarafından kontrol edilen yapı üzerindeki her özyinelemeli iniş — yazı tipi alt rutinleri, iç içe bir dizi ya da bir çapraz referans zinciri olsun — girdinin kaldıramayacağı bir derinlik tavanına ihtiyaç duyar

Çıktıya sızan başlatılmamış bellek

En ince tuzak, yığın (heap) içeriklerini şifresi çözülmüş çıktıya sızdırır ve bunun nedeni, unutulması kolay bir SetLength özelliğidir. Bir AnsiString'i SetLength ile büyüttüğünüzde, Delphi baytları tahsis eder ancak sıfırlamaz; bu nedenle yeni bölge, daha önce o yığın belleğinde her ne varsa onu tutar. Sonradan her bayt yazılırsa, bu hiç önemli değildir; bir yol arabelleğin bir kısmını yazılmamış olarak bırakır ve ardından veri olarak döndürürse, o eski baytlar sonuçla birlikte dışarı çıkar. Bu, başlatılmamış belleğin kullanılmasıdır (CWE-457) ve sonuç bir güven sınırını geçtiğinde bir bilgi sızıntısına dönüşür

AES-CBC şifre çözme yolu, tam olarak bunun geçerli olduğu yerdir. Çıktı arabelleğini SetLength ile boyutlandırın, şifreli metni her seferinde 16 baytlık bir blok olarak işleyin; şifreli metin uzunluğu, saldırganın seçebileceği bir uzunluk olan 16'nın katı olmadığında, sondaki kısmi blok asla yazılmaz, o son baytlar SetLength'in geride bıraktığı yığın içeriğini taşır ve arabellek, bir belge nesnesinin şifresi çözülmüş düz metni olarak geri döner. Çare iki korumadır ve hiçbiri tek başına yeterli değildir: PDF Library for Delphi şifre çözme giriş noktası, uzunluğu blok boyutunun katı olmayan her şifreli metni reddeder; ve bir emniyet olarak çıktı kullanılmadan önce FillChar ile temizlenir, böylece bir bölgeyi yazamayan herhangi bir yol yığın kalıntısı yerine sıfır döndürür

PDF Library for Delphi şeması: AES-CBC şifre çözme; sonlandırılmamış bir sondaki blok, giriş uzunluğu kontrolleri ve FillChar tampon temizliği yolu korumadıkça başlatılmamış yığın baytları bırakır
İki bağımsız koruma sızıntıyı kapatır: uzunluğu 16 baytlık sınıra uymayan şifreli metni reddedin ve çıktıyı önceden temizleyin; böylece kaçan bir yazma bile yığın artığı yerine sıfırlar üretir

Beş tuzağın ortak noktası

Beş tuzak farklı hatalardır ama birbirleriyle kafiyelidir. Bir çarpımı saran bir tamsayı genişliği, bir korumayı sabit bir yanlışa sabitleyen bir alan türü, dizinlerin güvenli olmayı bıraktığı yerde kapatılmış bir aralık kontrolü, tabanı olmayan bir özyineleme ve dilin sıfırlamayı reddettiği bir arabellek. Her birinde Delphi tam olarak tanımladığı şeyi yapar, çünkü dil size saran aritmetik, sessiz daraltma, kapatabileceğiniz aralık kontrolleri, yerleşik sınırı olmayan özyineleme ve başlatılmayan tahsis verir. Sözleşme budur ve bir Pascal ayrıştırıcısı, dosyanın kontrol ettiği her sınırda dört şeyi elle sahiplenerek bu sözleşmeyi karşılar: tamsayı genişliği, aralık kontrolü, özyineleme derinliği ve arabellek başlatma

PDF Library for Delphi, Delphi ve C++Builder motoru, çözme yolunu beş tuzağın tamamına karşı korur. Çalışmanız bir dosyanın nasıl korunduğunu iddia ettiğine de uzanıyorsa, şifreleme ve izinleri denetleme ile PDF/A ve PDF/UA ön uçuşu üzerine notlar aynı ayrıştırıcının analiz tarafını kapsar; bunların tamamı, bu blogun başka yerlerinde ele alınan yükleme, işleme ve imzalama API'lerinin yanında PDF Library for Delphi Delphi PDF Kitaplığı içinde sunulur