Teknik Makale

Delphi'de Güvenilmeyen XLSX için ZIP EOCD Doğrulama

Bir xlsx dosyası bir ZIP arşividir ve ZIP'in tek bir yetkili içindekiler tablosu yoktur. Delphi ve C++Builder için HotXLS Excel Library, bu belirsizliği bir saldırı yüzeyi olarak ele alır: merkezi dizin sonu ayrıştırıcısı, bir aday kaydı yalnızca dört bağımsız çapraz kontrol anlaştıktan sonra kabul eder, dolayısıyla bir ZIP yorumunda gizlenmiş sahte bir dizin asla kazanmaz

Bunu somut kılan senaryo sıradandır. Bir sunucu müşterilerden elektronik tablo yüklemelerini kabul eder. Dosya bir antivirüs taramasından geçer, bir kuyruk dizinine yazılır ve Delphi hizmetiniz üç sütunu çekmek için onu açar. Tarayıcı ve ayrıştırıcınız arşivin neyi içerdiği konusunda anlaşmadığı dışında her şey yolunda görünür. Tarayıcı bir üye kümesini numaralandırdı; yükleyiciniz aynı baytlardan farklı bir küme numaralandırdı. Hiçbiri sıradan anlamda hatalı değil. Yalnızca ZIP formatındaki bir belirsizliği iki farklı yönde çözdüler ve bir saldırgan baytları böyle olacak şekilde seçti

Bir ZIP arşivi hakkındaki gerçek gerçekte nerede yaşar?

Merkezi dizin sonu kaydı adı verilen 22 baytlık bir yapıda, tam en sonda yaşar. Bir ZIP dosyası baştan sona okunmaz: her üye, sıkıştırılmış verisinden hemen önce bir yerel dosya başlığı taşır, ama yetkili indeks, her girdiyi adlandıran ve yerel başlığının uzaklığını veren, sonun yakınındaki bir kayıt dizisi olan merkezi dizindir. Merkezi dizini bulmak için önce EOCD'yi bulmalısınız, çünkü dizinin nerede başladığını ve kaç kayıt tuttuğunu söyleyen EOCD'dir. HotXLS bunu, alanları disk üstü düzenine bire bir eşleyen TEndOfCentralDirectoryRecord olarak modeller: uzaklık 4'te FDiskNumber, 6'da FStartDisk, 8'de FThisDiskEntries, 10'da FTotalEntries, 12'de FSizeOfCD, 16'da FOffsetOfStartCD ve 20'de FCommentLen. O toplam, yapıcıda 4*3 + 5*2 olarak hesaplanan FMinSize'dır. Ondan sonra, 65535 bayta kadar rastgele içerik olan arşiv yorumu gelir, ki bu FMaxSize'ı 65557 yapar ve kaydın sabit bir konumda olmadığı anlamına gelir. Onu aramaya gitmeniz gerekir

EOCD imzası için geriye doğru taramak neden yeterli değil?

Çünkü aradığınız dört bayt, PK\005\006, arşiv yorumunun içinde, sıkıştırılmış verinin içinde veya bir saldırganın kasıtlı olarak eklediği ikinci bir EOCD'nin içinde yasal olarak görünebilir. Geriye doğru yürürken karşılaştığı ilk imzada duran bir ayrıştırıcı, önemsiz şekilde yönlendirilebilir: kuyruğun yakınına bir yem EOCD yerleştirin ve saf ayrıştırıcı onu takip ederken, farklı bir sırada tarayan veya dosyadaki son imzayı kanonik olarak ele alan bir ayrıştırıcı gerçeğini takip eder. Bu, ZIP belirsizlik saldırı ailesidir ve getirisi tam olarak yukarıda anlatılan, tarama motorunun ve tüketen uygulamanın bir dosyadan farklı girdi kümeleri gördüğü bölünmedir

TEndOfCentralDirectoryRecord.Parse gerçekten geriye doğru tarar. startscan'ı son bayta ayarlar, endscan'i lsize - FMaxSize'a veya sıfıra sabitler ve pencereyi, bir arabellek sınırını aşan bir imzanın asla kaçırılmaması için üç bayt üst üste binen 256 baytlık arabelleklerde dolaşır. Fark, bir isabette olanla ilgilidir. İmzayı bulmak yalnızca bir Candidate uzaklığı üretir. HotXLS daha sonra o uzaklıktaki 22 baytı okur, onları ReadEOCD ile ayrıştırır ve FOffsetEOCD hiç atanmadan önce ortaya çıkan alanların, tanımlamayı iddia ettikleri dosyayla dahili olarak tutarlı olmasını gerektirir

Candidate := pos + j - 3;
if Candidate + FMinSize <= lsize then
begin
  SetLength(RecordBuf, FMinSize);
  inputstream.Position := Candidate;
  if StreamReadExact(inputstream, RecordBuf[0], FMinSize) then
  begin
    ReadEOCD(RecordBuf[0], 0);
    if (Candidate + FMinSize + FCommentLen = lsize) and
       (FDiskNumber = 0) and (FStartDisk = 0) and
       (FThisDiskEntries = FTotalEntries) and
       (Int64(FOffsetOfStartCD) + FSizeOfCD = Candidate) then
    begin
      FOffsetEOCD := Candidate;
      Result := FOffsetEOCD;
      Exit;
    end;
  end;
end;

Bu yüklemi, bir sahtekarlığın aynı anda karşılaması gereken dört ayrı iddia olarak okuyun. Candidate + FMinSize + FCommentLen = lsize, bildirilen yorum uzunluğunun tam olarak dosyanın sonuna ulaşmasını talep eder, ki yorumdaki-yem hilesini öldüren şey budur: gerçek bir yorumun içine gömülü sahte bir EOCD, kendisinden sonraki her baytı da açıklayamaz. FDiskNumber = 0 ve FStartDisk = 0, hiçbir xlsx'in meşru olarak asla kullanmadığı ve yalnızca kafa karıştırmak için hazırlanmış arşivlerde var olan çoklu disk yayılma alanlarını reddeder. FThisDiskEntries = FTotalEntries, bir ayrıştırıcının döngüsünü bir alandan boyutlandırdığı ve diğer ayrıştırıcının diğerinden boyutlandırdığı bölünmüş-sayım hilesini reddeder. Ve Int64(FOffsetOfStartCD) + FSizeOfCD = Candidate, merkezi dizinin tam olarak EOCD'nin başladığı yerde bitmesini gerektirir, dolayısıyla dizin, dosyanın başka bir yerindeki alakasız bir blob'a işaret edemez. Sonuncudaki Int64 dönüşümü önemlidir: her iki operand da 32 bittir ve genişletme olmadan, hazırlanmış bir çift sarabilir ve makul olmayan bir yere işaret ederken aritmetik olarak testi geçebilir

Yerel başlıklar merkezi dizinle anlaşmalıdır

EOCD kontrolleri hangi dizinin yetkili olduğunu sabitler; henüz dizinin bireysel üyeler hakkında gerçeği söylediğini garanti etmezler. Her girdi bir ZIP dosyasında iki kez tanımlanır, bir kez merkezi olarak ve bir kez kendi yerel başlığında ve formatta hiçbir şey iki tanımın eşleşmesini zorlamaz, dolayısıyla merkezi dizine güvenen bir okuyucu ve yerel başlıklara güvenen bir okuyucu, bir arşivden farklı içerik çıkarabilir. TZipEntry.ParseLocalHeader, FCdFile.LocalFileHeaderOffset'teki yerel başlığı ayrıştırarak ve iki kopyayı alan alan karşılaştırarak bu boşluğu kapatır, her tür anlaşmazlık için ayrı bir negatif kod döndürür: kanonikleştirilmiş girdi adı, sıkıştırma yöntemi, genel amaç bit bayrakları ve, veri tanımlayıcı bayrağı temiz olduğunda, CRC32 ve her iki boyut. O bayrak ayarlandığında yerel kopyalar sıfır olabilir, çünkü gerçek değerler sondaki bir tanımlayıcıda yaşar, ama sıfır olmayan herhangi bir yerel değer hâlâ eşleşmelidir. Son bir kontrol, verisi dosyanın sonunun ötesine geçecek girdileri reddeder, Int64(FLFile.DataOffset) + Int64(FCdFile.FCompressedSize)inputstream.Size'a karşı karşılaştırarak. Herhangi bir başarısızlık, TCentralDirectory.Parse'dan 1 olmayan bir sonuç olarak yayılır ve TZipArchive.OpenArchive, size yarı güvenilen bir arşiv nesnesi vermek yerine bunu Can't open zip archive'a dönüştürür. Yalnızca bir dosyanın hangi sayfaları içerdiğini bilmeniz gerektiğinde, tam bir ayrıştırmadan önce bu doğrulamayı çalıştırmak ucuzdur ve hafif sayfa inceleme yolu, hücre verisini gerçekleştirmeden size tam olarak bunu verir

Baytların kendisi yalan söylediğinde ne olur?

Yapısal anlaşma hâlâ yük hakkında hiçbir şey söylemez, dolayısıyla HotXLS her girdi akışını, çağıran okurken bildirilen boyutu ve CRC32'yi zorunlu kılan TZipVerifiedStream ile sarar. Bu kasıtlı olarak sonradan yapılan bir kontrol değildir: bildirilen sıkıştırılmamış boyutu 4 KB olan ama gigabaytlara açılan bir sıkıştırma açma bombası, hasardan sonra değil 4 KB işaretinde durdurulur. Sarmalayıcı, her okumayı kalan bildirilen baytlara sabitler, kaynak erken tükenirse ZIP entry ended before its declared size fırlatır, tamamlanmada bir ekstra bayt için yoklar ve bir şey kalmışsa ZIP entry exceeds its declared size fırlatır ve son olarak VerifyComplete'de çalışan CRC32'yi karşılaştırır, ZIP entry uncompressed size mismatch veya ZIP entry CRC32 mismatch fırlatır

if Count > 0 then
begin
  Result := FSource.Read(Buffer, Count);
  if Result <= 0 then
    raise Exception.Create('ZIP entry ended before its declared size');
  FCRC32 := ZLibCRC32(FCRC32, Buffer, Result);
  Inc(FPosition, Result);
end
else
  Result := 0;

if FPosition = FExpectedSize then
begin
  if FSource.Read(Probe, 1) <> 0 then
    raise Exception.Create('ZIP entry exceeds its declared size');
  VerifyComplete;
end;

Planlamaya değer bir sonuç var. Akış tasarım gereği yalnızca ileri yöndedir; geçerli konumun dışında herhangi bir yere bir Seek, boyut sorgularının hâlâ çalışması için sıfır uzaklıklı soEnd'e tek bir tavizle, ZIP entry stream is forward-only fırlatır. Bu, güvenilmeyen girdi için doğru değiş tokuştur, çünkü geri sarabildiğiniz bir akış, CRC muhasebesini yenebileceğiniz bir akıştır, ama aranabilir bir akış bekleyen tüketici kodunun kendi arabelleğine ihtiyacı olduğu anlamına gelir. Aynı yalnızca-ileri disiplini, yüklenen çalışma kitabı belleğe yerleştirmek istemeyeceğiniz kadar büyük olduğunda ulaşılacak API olan akış tabanlı doğrudan okuyucunun temelini oluşturur

Tahsisten önce kaynak sınırları, sonra değil

lxZipArchive'daki üç sabit, tek bir arşivin sürece yaptırabileceği şeyi sınırlar ve TZipEntries.Add, herhangi bir girdi verisi baytına dokunulmadan önce, merkezi dizin hâlâ okunurken bunları uygular. ZipMaxEntryUncompressedSize, bir üyeyi 1 GiB'ye sabitler, ZipMaxTotalUncompressedSize arşivi 4 GiB'ye sabitler ve 10000 olan ZipMaxCompressionRatio, bildirilen genişlemesi on binlik kat aşan herhangi bir deflate girdisini, sıfır sıkıştırılmış boyutla eşleştirilmiş sıfır olmayan sıkıştırılmamış boyutun dejenere durumuyla birlikte reddeder. Girdi adları aynı çağrıda CanonicalZipEntryName'den geçer, ki bu, gömülü NUL karakterlerini, iki noktaları ve herhangi bir .. yol segmentini Invalid ZIP entry name ile reddeder ve segmentleri küçük harfe çevirip normalleştirir, dolayısıyla yalnızca büyük/küçük harf veya gereksiz ayırıcılarda farklılaşan iki üye, birbirlerini sessizce gölgelemek yerine Duplicate ZIP entry name olarak çarpışır

ZIP katmanının üzerinde derinlemesine savunma

ZIP katmanı birkaçından biridir ve desen, HotXLS'in saldırgan kontrollü yapıyı ayrıştırdığı her yerde tekrarlanır. En açık örnek BIFF formül ayrıştırıcısında oturur: TXLSFormula.GetTranslated, tMemFunc tokenları boyunca özyineler, dolayısıyla eski bir .xls'te hazırlanmış bir rgce token akışı keyfi olarak derinlemesine iç içe geçebilir ve yığını tüketebilir. Kapı bir sabit, MaxTranslateDepth = 256, tahmin edilmek yerine bilinen bir yukarı akış gerçeğine karşı seçilmiştir. Excel formül iç içe geçmesini 64'e sınırlar, dolayısıyla 256, dört katı bir marj bırakır ve gerçek bir elektronik tablonun ürettiği bir formülü asla reddedemez, aynı zamanda kötü niyetli bir akışı yığın tükenmeden yeterince önce sonlandırır

const
  MaxTranslateDepth = 256;
begin
  isOuter := FTranslateDepth = 0;
  if isOuter then
    ResetPendingArrays;
  Inc(FTranslateDepth);
  try
    if FTranslateDepth > MaxTranslateDepth then
    begin
      Result := nil;
      Exit;
    end;

Kapının fırlatmak yerine nil döndürdüğünü not edin. Gerçek olamayacak kadar derin bir formül hiçbir söz dizimi ağacı vermez, çevredeki ayrıştırma devam eder ve çalışma kitabı yine de yüklenir. Bu asimetri kasıtlıdır ve kendi sınırlarınızda kopyalamaya değer: kaynak tükenmesini durdurmak için var olan bir sınır, belgeyi iptal etmek yerine durdurabileceği en küçük birimi bozulmalıdır. Aynı gerekçe hesaplama katmanını genişlettiğinizde de geçerlidir, dolayısıyla kendi handler'larınızı formül motoru özel fonksiyon API'si üzerinden kaydediyorsanız, çağıranın zaten kontrol ettiğini varsaymak yerine onlara kendi argüman ve özyineleme sınırlarını verin

Bu kontrollerin size satın almadığı şey

Sınır konusunda kesin olun. Dört EOCD çapraz kontrolü, arşiv indeksini belirsiz olmaktan çıkarır, dolayısıyla HotXLS ve diğer herhangi bir uyumlu okuyucu, aynı dosyayı aynı girdi kümesine çözer; o girdi kümesinin zararsız olup olmadığı hakkında hiçbir şey söylemezler. Yerel başlık anlaşması, iki-görünüm hilesini durdurur, tutarlı bir şekilde tanımlanmış kötü niyetli bir yükü değil. Doğrulanmış akış, kesilmeyi, taşmayı ve bozulmayı durdurur, beklemediğiniz bir şeyi kodlayan mükemmel derecede iyi biçimlendirilmiş bir XML parçasını değil. Ve bunların hiçbiri makrolara dokunmaz: yapısal olarak kusursuz bir çalışma kitabının içindeki bir VBA projesi hâlâ bir VBA projesidir ve onu tutma, çıkarma veya reddetme kararı, ZIP okuyucusuna değil politika katmanınıza aittir

Karşılığında elde ettiğiniz şey temiz bir hata sınırıdır. Güvenilmeyen bir xlsx, ya üyeleri bildirilen boyutları ve sağlama toplamlarıyla eşleşen tek bir belirsiz olmayan arşiv olarak açılır, ya da bozduğu belirli değişmezi adlandıran bir mesajla fırlatır ve hizmetiniz tahmin etmek yerine istisna üzerinde karantinaya alabilir. ZIP okuyucusu ve üzerindeki ayrıştırıcı katmanları, ayrıştırmayı yapan makinede ne Excel ne de OLE otomasyonuna ihtiyaç duyan, Delphi ve C++Builder için HotXLS Excel Component'inin bir parçası olarak gönderilir ve bu yokluk kendisi, yüklenen bir dosyanın erişebileceği şeyde anlamlı bir azalmadır