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
PDFlibPas; 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 alan bir güçlendirme geçişinden geçti. Aşağıda, kapattığı ve her biri bunu mümkün kılan belirli Delphi davranışına dayanan beş kusur sınıfı yer almaktadır. Bunlar mevcut sürümlerde düzeltilmiştir ve güvenilmeyen girdileri ayrıştıran tüm Pascal kodlarında aynı şekiller tekrarlanır
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 görüntü yolu her boyutu zaten 65535 ile sınırlamıştı; bağımsız çözücülerin tümü bu sınırlamayı miras almamıştı. ByteCount * FHeight gibi bir satır baytı çarpı yükseklik ifadesi veya 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ılar olduğunda Delphi'de 32 bitlik bir çarpımdır. Büyük bir tarama için 60000 genişlik ve yükseklik makuldür, ancak bunların bayt cinsinden çarpımı işaretli 32 bitlik bir aralığı aşar ve uzunluk küçük çıkar. Aynı tuzak ZLib tahminci adımında (stride) da mevcuttu: BitsPerComponent * Colors * Columns
Çö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
// Reject before allocating, not after writing.
// Evaluate the product in Int64 so it cannot wrap at 32 bits.
RowBytes := (Int64(FWidth) * Components * BitDepth + 7) div 8;
if (RowBytes <= 0) or (RowBytes * FHeight > MaxInt) then
Exit; // hostile or unsupportable dimensions; refuse the image
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 uyaracağı 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 imkansız kılan bir 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
Sayfa sayacı, Delphi'de 0 ila 65535 tutan Word olarak bildirilmişti. Döngü, "sayfa sayısı 65535'i aştığında dur" şeklinde bir sonlandırma koruması taşıyordu; bu, işlenenin ve eşiğin bir üst sınırı paylaştığını fark edene kadar doğru olarak okunur. Bir Word asla 65535'ten büyük olamaz, bu nedenle karşılaştırma yapısal olarak her zaman yanlıştır: sayaç 65535'e ulaştığında bir sonraki artış onu tekrar 0'a sarar, koruma asla tavanın üzerinde bir değer görmez ve döngüsel bir IFD zinciri okuyucuyu döndürmeye devam ettirir
Çözüm, korumanın sayacın gerçekten tutabileceği bir değeri ifade edebilmesi için alanı genişletmekti. TPDFTIFF.FPageCount Integer olarak bildirildiğinde, aynı FPageCount > 65535 karşılaştırması ulaşılabilir hale gelir, döngü sonlanır ve genel PageCount özelliği, çağıranı bozmadan eşleşecek şekilde tür değiştirir. Bir sınır kontrolü Value > MaxValueOfType(Value) şekline sahip olduğunda ve işlenen zaten tam olarak o maksimumda yazıldığında, durum sabit bir yanlıştır: türü genişletin veya tetiklenebilmesi için maksimuma karşı eşitliği test edin
Sıcak bir 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ı olarak derlenir ve yedek deposunu doğrudan dizinler; bu nedenle aralık dışı bir dizin bir hata değil, ham bir bellek erişimidir. Dinamik dizin her zaman geçerli olduğunda iyidir ve dizinin dosyadan gelebileceği bir CFF veya Type2 karakter dizisi yorumlayıcısının içinde iyi olmaktan çıkar. Boş bir yığından bir işleneni çıkaran bir karakter dizisi, eksi bir dizini üretir; glif sayısına karşı bir eksik olan bir glif tanımlayıcısı, sonun bir yuva ötesini dizinler. Aralık kontrolü kapalıyken, her ikisi de yakalanabilir bir istisna yerine gerçek bir sınır dışı erişim haline gelir ve yuvalar referans sayımlı AnsiString değerleri tuttuğu için, başıboş bir okuma bir dizenin referans sayısını da bozabilir
Güçlendirme, sıcak yol için aralık kontrolünü tekrar açmadı. İlk olarak dizinleri kanıtlanabilir şekilde geçerli hale getirdi: yorumlayıcı, işlenen yığınının üstünü almadan önce yığının boş olmadığını kontrol eder ve her dizin koruması, bire bir hataya izin veren küçük-eşittir yerine, sayıya karşı kesin bir küçüktür olarak yazıldı. Yönerge, sınırlar için sorumluluğu derleyiciden size taşır ve kaldırdığı doğrulama, her giriş noktasında elle yerine 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ı buna karşı zaten koruma sağlıyordu. Bir çağrı derinliği sayacı ve bir tavan olan PLType1MaxCallDepth taşıyordu ve bunu aşmayı reddediyordu; bu, Type1 spesifikasyonunun kendisinin adlandırdığı derinlik sınırını yansıtır. Daha sonra eklenen ve yapısal olarak benzer olan Type2 yorumlayıcısı aynı korumayı taşımıyordu 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 (stack overflow) doğru yürüyordu
// The shape of the Type1 guard the Type2 path was missing.
// Track depth across nested calls and refuse to recurse past it.
Inc(CallDepth);
if CallDepth > PLType1MaxCallDepth then
Exit; // hostile self-referential subroutine; stop descending
// ... interpret the subroutine, then Dec(CallDepth) on the way out
Çözüm, Type2 yoluna Type1 kardeşinin zaten sahip olduğu sınırlı derinliği vermekti. İster yazı tipi alt rutinleri, ister iç içe geçmiş bir dizi veya bir çapraz referans zinciri olsun, saldırgan tarafından kontrol edilen yapı üzerindeki herhangi bir özyinelemeli iniş, girdinin kaldıramayacağı bir derinlik tavanına ihtiyaç duyar
Çıktıya sızan başlatılmamış bellek
En ince kusur, yığın (heap) içeriklerini şifresi çözülmüş çıktıya sızdırıyordu 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 buna çarptı. Çıktı arabelleği SetLength ile boyutlandırılmıştı ve şifre çözücü, şifreli metni her seferinde 16 baytlık bir blok olarak işliyordu. Şifreli metin uzunluğu 16'nın katı olmadığında (saldırganın seçebileceği bir uzunluk), sondaki kısmi blok asla yazılmıyordu, bu nedenle bu son baytlar SetLength'in geride bıraktığı yığın içeriğini koruyordu ve arabellek, bir belge nesnesinin şifresi çözülmüş düz metni olarak geri teslim ediliyordu. Çözüm iki korumadır ve hiçbiri tek başına yeterli değildir: şifre çözme giriş noktası artık uzunluğu blok boyutunun katı olmayan her şifreli metni reddeder ve bir koruma 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
Geçişin sizi bıraktığı şey
Beş kusur farklı hatalardır ancak birbirleriyle uyumludurlar. 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 devre dışı bırakılan 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 yaptı, çünkü dil size saran aritmetik, sessiz daraltma, kapatabileceğiniz aralık kontrolleri, yerleşik sınırı olmayan özyineleme ve başlatılmayan tahsis sağlar. 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 bunu karşılar: tamsayı genişliği, aralık kontrolü, özyineleme derinliği ve arabellek başlatma
Bu kusurlar, Delphi ve C++Builder motoru olan güncel PDFlibPas sürümlerinde kapatılmıştır. Çalışmanız bir dosyanın nasıl korunduğunu iddia ettiğine de uzanıyorsa, şifreleme ve izinleri denetleme ve PDF/A and PDF/UA ön uçuşu (preflight) hakkındaki notlar aynı ayrıştırıcının analiz tarafını kapsar ve tüm bunlar, bu blogun başka yerlerinde ele alınan yükleme, işleme ve imzalama API'lerinin yanı sıra PDFlibPas Delphi PDF Kitaplığı içinde sunulur