Technical Article

Bir Delphi PDF İmzalayıcısını Kötü Niyetli PKCS#12'ye Karşı Güçlendirme

Bir PDF'i imzaladığınızda, genellikle imzalama anahtarını kontrol ettiğiniz bir şey olarak düşünürsünüz. Seçtiğiniz bir şifreyle korunan, oluşturduğunuz bir .pfx dosyasında yaşar. Bu dosyayı okuyan kod, bir sınır değil, tesisat gibi hissettirir. Sertifika sizin olmaktan çıktığı an bu sezgi yanlıştır. Kullanıcının herhangi bir .pfx seçmesine izin veren bir masaüstü aracı, yüklenen bir kimlik bilgisini kabul eden bir sunucu, ağ üzerinden sertifikalarla beslenen bir toplu imzalayıcı; hepsi tek bir imza baytı üretilmeden önce saldırgandan etkilenen baytları bir ayrıştırıcıya teslim eder. Bir PKCS#12 okuyucusu, tıpkı bir görüntü çözücü veya yazı tipi yükleyici gibi bir saldırı yüzeyidir

Bu makale, imzalama kimlik bilgilerini içe aktaran yolda o okuyucuda yaşayan iki gerçek kusuru incelemektedir. İkisi de egzotik değildir. Her ikisi de, sabit genişlikte tamsayılara sahip bir dilde yazılmış neredeyse tüm ikili ayrıştırıcıları vuran aynı kök nedenden kaynaklanmaktadır: dosyadan gelen bir uzunluk veya sayı, olması gerekenden bir adım daha fazla güvenilir. Biri sınırların dışında okumaya, diğeri ise siz işlemi sonlandırana kadar askıda kalan bir işleme yol açar

Baytların seyahat ettiği yer

Bir belgeyi imzalamak üzere bir .pfx dosyasını içe aktarmak tek bir işlem değil, kısa bir boru hattıdır ve her aşama bir saldırganın yazmış olabileceği bir şeyi ayrıştırır. Kapsayıcı, RFC 7292'de tanımlandığı gibi, özel anahtarı tutan şifreli bir korumanın etrafına sarılmış bir AuthenticatedSafe torbaları yuvası olan bir PKCS#12 yapısıdır. Bunu okumak, ASN.1'i dolaşmak, şifreden bir anahtar türetmek, şifreyi çözmek ve ardından kurtarılan RSA anahtarını imzayı oluşturan koda teslim etmek anlamına gelir

HotPDF'te bu aşamalar farklı birimlerle eşleşir. PKCS#12 kapsayıcı mantığı HPDFPFX içinde yaşar. Dokunduğu her etiket, uzunluk ve değer, HPDFASN1 içindeki ASN.1 okuyucusu tarafından çözülür. Anahtar türetme ve PBES2 şifre çözme işlemleri, PBKDF2HMACSHA256 ile birlikte HPDFCrypt içinde yer alır. Anahtar kurtarıldığında, HPDFRSA ve HPDFCMS içindeki CMS SignedData oluşturucusu, bunu PDF'e gömülü ayrık imzaya dönüştürür. Tüm zinciri yönlendiren genel giriş noktası tek bir çağrıdır

// Drives the full pipeline: load the placeholder PDF, parse the PFX,
// derive the key, build CMS SignedData, write the signed output.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
     'signer.pfx', 'p@ssw0rd') then
  // signature embedded
else
  // signing did not complete
;

Herhangi bir kriptografi gerçekleşmeden önce signer.pfx dosyasının her baytı HPDFASN1 ve HPDFPFX üzerinden akar. Bu iki birim dosyanın iddia ettiği şeyler konusunda dikkatli olmazsa, akış yönündeki kriptografinin önem kazanma şansı asla olmaz

Kusur bir: korumayı aşan bir ASN.1 uzunluğu

DER ve BER'deki ASN.1, her öğeyi bir etiket, bir uzunluk ve o kadar içerik baytı olarak kodlar. Uzunluk, güvenmeniz ancak doğrulamanız gereken alandır, çünkü ayrıştırıcıya ne kadar okuyacağını söyler ve dosyayı üreten her kimse onun tarafından yazılmıştır. X.690 §8.1.3 iki kodlama tanımlar. Kısa biçim, 0 ila 127 arasındaki bir uzunluğu tek bir bayta paketler. Daha büyük her şey için kullanılan uzun biçim, düşük yedi biti takip eden uzunluk baytlarının sayısını veren bir öncü bayt harcar, ardından o kadar büyük-endian baytı gerçek değeri taşır. Dört uzunluk baytı bu nedenle dört gigabata yaklaşan bir içerik boyutu bildirebilir

Böyle bir değeri çözdükten sonra, ayrıştırıcı güvenmeden önce içeriğin gerçekten arabelleğe sığıp sığmadığını kontrol etmek zorundadır. Doğal kontrol, mevcut konum artı içerik uzunluğunun verilerin sonunu aşmadığını doğrulamaktır. Konum, içerik uzunluğu ve toplamın tümü 32 bitlik işaretli tamsayılarda tutulacak şekilde bariz bir şekilde yazıldığında, bu koruma bozulur:

// The trap: signed 32-bit arithmetic. With ContentLen near MaxInt,
// Pos + ContentLen overflows to a NEGATIVE value, so the comparison
// is false and a forged ~2 GB length sails straight through.
if Pos + ContentLen > Total then
  raise EHPDFASN1Error.Create('content overruns buffer');

Sorun karşılaştırma değil, toplamadır. ContentLen değeri MaxInt (2147483647) değerine yakın olduğunda, Pos + ContentLen işaretli 32 bitlik aralığı taşır ve negatif bir sayıya döner. Negatif bir toplam asla Total değerinden büyük değildir, bu nedenle koruma her şeyin yolunda olduğunu bildirir ve ayrıştırıcının arabellekte bulunmayan yaklaşık iki gigabaytlık bir içerik uzunluğuyla devam etmesine izin verir. Bundan sonra meydana gelen şey hasardır: okuyucu bu iddia edilen uzunluk için bir arabellek ayırır ve içine kopyalar; bir SetLength ve ardından kaynaktan okuyan bir Move. Kaynakta yalnızca birkaç yüz bayt kalmıştır, bu nedenle kopyalama girdinin sonunun çok ilerisini okur; bu, en iyi ihtimalle çöken ve en kötü ihtimalle bitişik işlem belleğini ayrıştırmaya sızdıran sınırların dışında bir okumadır

Tek doğru koruma, karşılaştırmadan önce ara toplamı genişletir, böylece toplama işlemi hesaplandığı türde taşamaz. Çözüm, her iki işleneni de Int64'e yükseltir:

// Correct: both operands widened to Int64 before the add, so the sum
// cannot wrap. A forged 2 GB length now fails the bounds check.
if ContentLen < 0 then
  raise EHPDFASN1Error.Create('negative content length after decoding.');
if Int64(Pos) + Int64(ContentLen) > Int64(Total) then
  raise EHPDFASN1Error.Create('content overruns buffer');

Bir Int64, kayıp olmadan iki 32 bitlik değerin toplamını tutar, böylece karşılaştırma gerçek sayıyı görür ve sahte uzunluğu reddeder. ContentLen üzerindeki ayrı negatif olmama kontrolü, çözülmüş bir değerin kendi başına negatif çıktığı eşleşen durumu kapatır. HotPDF'te bu koruma, diğer tüm yardımcıların üzerine kurulduğu düğümü üreten işlev olan HPDFASN1ParseNode içinde yaşar. HPDFASN1Content, SetLength ve Move işlemlerini doğrudan düğümün içerik uzunluğundan boyutlandırdığı için, bozuk bir korumayı geçen bir düğüm, ondan yapılan her okumayı zehirlemiş olurdu. Kod çözme noktasındaki sınırı düzeltmek, üzerindeki yardımcıları güvenli kılan şeydir

Kusur iki: silah olarak kullanılan bir PBKDF2 yineleme sayısı

İkinci kusur bir bellek hatası değildir; dosyanın CPU'nuza ne kadar sıkı çalışacağını söylemesidir. PKCS#12, anahtar materyalini RFC 8018'de belirtilen PKCS#5'teki şifre tabanlı şema olan PBES2 ile korur. PBES2, bir anahtar türetme işlevi (burada HMAC-SHA-256 ile PBKDF2), ardından bir şifre (burada AES-256-CBC) çalıştırır. PBKDF2 bir yineleme sayısı alır ve bu sayı dosyada taşınan bir parametredir. Tüm amacı yavaş olmaktır: daha fazla yineleme, her şifre tahmininin daha pahalıya mal olması anlamına gelir ki bu da çevrimdışı bir saldırgana karşı iyidir. RFC 8018 §4.2, daha büyük bir sayının güvenlik için daha iyi olduğunu açıkça belirtir ve kasıtlı olarak bir tavan belirlemez

Bu açıklık, dosyayı siz oluşturduğunuzda iyidir. Saldırgan oluşturduğunda ise bir silahtır. Yineleme sayısı, saldırgan tarafından kontrol edilen bir iş faktörüdür ve saldırgan tarafından kontrol edilen bir iş faktörü, algoritmik karmaşıklık hizmet dışı bırakma (DoS) saldırısıdır. Sahte bir .pfx, milyarlarca yineleme sayısını kodlayabilir; ayrıştırıcı bunu sadakatle okur ve o kadar HMAC-SHA-256 turu için PBKDF2'yi çağırır ve işlem, sağlanan tek bir dosyada dakikalarca veya saatlerce geri dönmeyecek bir döngüde kaybolur. İstek başına bir kimlik bilgisini işleyen bir imzalama sunucusunda, tek bir hazırlanmış yükleme bir çalışanı durdurur

Bu sayı, CPU'yu döndürmeden önce taşmayı daha da kötüleştirir. Yineleme değeri dosyada sabit genişliği olmayan bir ASN.1 INTEGER olarak yaşarken, PBKDF2'nin nihayetinde tükettiği alan 32 bitlik bir Integer'dır. INTEGER'ı doğrudan bu alana çözdüğünüzde büyük bir değer kırpılır ve işaret bitine denk gelecek şekilde hazırlanmış bir değer negatif veya ilgisiz küçük bir sayı olarak geri döner; böylece işin boyutu bile artık dosyanın istediği gibi görünmez. Çözüm, değeri daraltmadan önce tam genişlikte okur ve sınırlar:

// Read the iteration count as Int64 first, then clamp to a sane band
// BEFORE it is narrowed into the 32-bit Iterations field PBKDF2 uses.
LIter := HPDFASN1ToInteger(Data, Node);          // returns Int64
if (LIter < 1) or (LIter > 100000000) then
  raise EHPDFPFXError.CreateFmt(
    'PBKDF2 iteration count %d is outside the accepted range 1..100000000',
    [LIter]);
Iterations := Integer(LIter);                    // safe: already bounded

Bir Int64 içine okumak, çözülen değerin kırpılmış bir hayaleti değil, gerçeği olduğu anlamına gelir. Alt sınır, bir anahtar türetme için anlamsız olan sıfır ve negatif sayıları reddeder. Üst sınır olan yüz milyon, günümüzde on binlerce ila yüz binlerce yineleme kullanan yasal herhangi bir PKCS#12 dosyasının çok üzerindedir ve en kötü durumu sınırlı, hayatta kalınabilir miktarda bir işle sınırlar. Değer ancak bu bandı geçtikten sonra 32 bitlik alana daraltılır, böylece kırpılma artık kimseyi şaşırtamaz. HotPDF'te bu sınırlama, PBKDF2 parametrelerinin PBKDF2HMACSHA256 yolunda çözüldüğü ParsePBES2Params içinde yaşar

Neden her iki çözüm de aynı çözümdür

İki kusur farklı görünüyor; biri arabellek taşması, diğeri ise askıda kalan bir işlem, ancak bunlar aynı hatadır. Her durumda, güvenilmeyen bir dosyadan gelen bir sayı, gerçekliğe karşı kontrol edilmeden önce bir adım çok erken sabit genişlikte bir türe taşınmıştır. Uzunluk, sınır testinden önce 32 bitte toplandı; yineleme sayısı, aralık testinden önce 32 bite daraltıldı. Her ikisi de aynı disipline boyun eğer: tam genişlikte çözün, gerçek sınıra karşı kontrol edin ve ancak ondan sonra daraltın. Ara Int64 bir stil seçimi değildir; korumanın, saldırganın gerçekte yazdığı değeri görebildiği tek genişliktir. Taşma yapan bir sınır, bir sınır değildir ve tavanı olmayan bir sayı bir parametre değildir, kendi CPU'nuzda uzaktan kumandalı bir kısıcıdır

İmzalama boru hattı için pratik kılavuz

Çıkarılması gereken ders, güvenilmeyen sertifika girdilerini, güvenilmeyen herhangi bir yüklemeyi doğrulayacağınız şekilde doğrulamaktır. Kabul ettiğiniz bir .pfx dosyasının boyutunu sınırlayın, çünkü yasal bir dosya megabaytlarca değil, kilobaytlardır. Ayrıştırma hatasını, kullanıcıya bir yığın izlemesi göstermeye değer bir hata olarak değil, rutin olarak reddedilen bir girdi olarak ele alın. Bir sunucuda imzalama yapıyorsanız, içe aktarma işlemini durdurulan bir çalışanın hizmeti beraberinde çökertemeyeceği bir yerde çalıştırın ve işlemin etrafına bir zaman aşımı koyun, böylece beklenmedik derecede pahalı bir dosya, yineleme sınırının yanı sıra duvar saatiyle de sınırlanır

Daha geniş kapsamlı ders sertifikaların ötesine geçer. Ayrıştırma güçlendirmesi, bir birimin tek seferlik denetimi değildir; kitaplığınızın yazmadığı baytları okuduğu her yerin bir özelliğidir. Bir PDF kitaplığı, güvenilmeyen kaynaklardan çok şey ayrıştırır: bir belgeye gömülü yazı tipleri, yarım düzine codec bileşenindeki görüntüler, akış filtreleri ve imzalama yolunda sertifikalar. Bunların her biri saldırı yüzeyidir ve her biri her uzunluk ve her sayı hakkında aynı şüpheyi hak eder. HotPDF, içe aktarma ve imzalama yolunu burada açıklanan güçlendirilmiş HPDFASN1, HPDFPFX, HPDFCrypt ve HPDFCMS birimleri üzerine inşa eder; böylece ona teslim ettiğiniz kimlik bilgisi, nereden gelirse gelsin, güvenilmeden önce savunma amaçlı olarak ayrıştırılır

Bu kontrollerin koruduğu imzalama iş akışı, Delphi'de PAdES dijital imzaları kılavuzumuzda uçtan uca ele alınmıştır ve bu kod tabanını paylaşan AES-256 anahtar yolu dahil olmak üzere belge şifrelemesine uygulanan aynı savunma duruşu, AES-256 şifreleme ve güvenlik hakkındaki makalede açıklanmıştır. Tüm bunlar, bu blogun başka yerlerinde ele alınan yükleme, düzenleme, şifreleme ve imzalama API'lerinin yanı sıra Delphi ve C++Builder için HotPDF Bileşeni'nin bir parçası olarak sunulmaktadır