Datoteka xlsx je arhiv ZIP, ZIP pa nima ene same avtoritativne kazalo vsebine. Excel Library HotXLS za Delphi in C++Builder to nejasnost obravnava kot napadalno površino: njegov razčlenjevalnik konca osrednjega imenika sprejme kandidatni zapis šele, ko se štiri neodvisna navzkrižna preverjanja strinjajo, tako da ponarejen imenik, skrit v komentarju ZIP, nikoli ne zmaga
Scenarij, ki to naredi konkreten, je vsakdanji. Strežnik sprejema nalaganja preglednic od strank. Datoteka prestane protivirusni pregled, se zapiše v čakalni imenik, vaša storitev Delphi pa jo odpre, da izvleče tri stolpce. Vse je videti v redu, razen da se skener in vaš razčlenjevalnik nista strinjala o tem, kaj je arhiv vseboval. Skener je naštel en nabor članov; vaš nalagalnik je naštel drug nabor iz istih bajtov. Nobeden od njiju ni hrošč v navadnem smislu. Preprosto sta nejasnost v obliki ZIP razrešila v dve različni smeri, napadalec pa je bajte izbral tako, da bi se to zgodilo
Kje dejansko živi resnica o arhivu ZIP?
Živi na samem koncu, v 22-bajtni strukturi, imenovani zapis konca osrednjega imenika. Datoteka ZIP se ne bere od začetka do konca: vsak član nosi lokalno glavo datoteke neposredno pred svojimi stisnjenimi podatki, avtoritativen indeks pa je osrednji imenik, niz zapisov blizu konca, ki poimenuje vsak vnos in poda odmik njegove lokalne glave. Da bi našli osrednji imenik, morate najprej najti EOCD, saj EOCD pove, kje se imenik začne in koliko zapisov ima. HotXLS ga modelira kot TEndOfCentralDirectoryRecord, čigar polja se preslikajo ena na ena na postavitev na disku: FDiskNumber na odmiku 4, FStartDisk na 6, FThisDiskEntries na 8, FTotalEntries na 10, FSizeOfCD na 12, FOffsetOfStartCD na 16 in FCommentLen na 20. Ta vsota je FMinSize, izračunana v konstruktorju kot 4*3 + 5*2. Za tem pride komentar arhiva, do 65535 bajtov poljubne vsebine, zaradi česar je FMaxSize 65557 in zapis ni na fiksnem položaju. Morate ga iskati
Zakaj pregledovanje nazaj za podpisom EOCD ne zadošča?
Ker se lahko štirje bajti, ki jih iščete, PK\005\006, zakonito pojavijo znotraj komentarja arhiva, znotraj stisnjenih podatkov ali znotraj drugega EOCD, ki ga je napadalec namerno pripel. Razčlenjevalnik, ki se ustavi pri prvem podpisu, ki ga sreča med sprehodom nazaj, je trivialno vodljiv: postavite lažni EOCD blizu repa, in naiven razčlenjevalnik mu sledi, medtem ko razčlenjevalnik, ki pregleduje v drugem vrstnem redu ali ki obravnava zadnji podpis v datoteki kot kanoničnega, sledi pravemu. To je družina napadov nejasnosti ZIP, njen donos pa je natanko delitev, opisana zgoraj, kjer skenirni pogon in porabniška aplikacija iz ene datoteke vidita različne nabore vnosov
TEndOfCentralDirectoryRecord.Parse dejansko pregleduje nazaj. Nastavi startscan na zadnji bajt, priklene endscan na lsize - FMaxSize ali nič, ter se sprehodi po oknu v 256-bajtnih medpomnilnikih, ki se prekrivajo za tri bajte, tako da podpis, ki razpolovi mejo medpomnilnika, ni nikoli spregledan. Razlika je v tem, kaj se zgodi ob zadetku. Najdba podpisa proizvede le odmik Candidate. HotXLS nato prebere 22 bajtov na tem odmiku, jih razčleni z ReadEOCD in zahteva, da so nastali podatki notranje skladni z datoteko, ki jo trdijo, da opisujejo, preden je FOffsetEOCD sploh dodeljen
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;
Preberite predikat kot štiri ločene trditve, ki jim mora ponaredek zadostiti hkrati. Candidate + FMinSize + FCommentLen = lsize zahteva, da izjavljena dolžina komentarja doseže natanko konec datoteke, kar je tisto, kar ubije trik z vabo v komentarju: lažen EOCD, zakopan znotraj pravega komentarja, ne more hkrati odgovarjati za vsak bajt za seboj. FDiskNumber = 0 in FStartDisk = 0 zavrneta polja za razpenjanje čez več diskov, ki jih noben xlsx nikoli ni zakonito uporabil in obstajajo v izdelanih arhivih le za zmedo. FThisDiskEntries = FTotalEntries zavrne trik z razdeljenim štetjem, kjer en razčlenjevalnik meri svojo zanko iz enega polja, drug razčlenjevalnik pa iz drugega. In Int64(FOffsetOfStartCD) + FSizeOfCD = Candidate zahteva, da se osrednji imenik konča natanko tam, kjer se EOCD začne, tako da imenik ne more biti usmerjen na kakšen nepovezan blob drugje v datoteki. Pretvorba Int64 na zadnjem je pomembna: oba operanda sta 32-bitna, brez razširitve pa bi lahko izdelan par preplaval in aritmetično zadostil testu, medtem ko kaže nikamor razumnega
Lokalne glave se morajo ujemati z osrednjim imenikom
Preverjanja EOCD določijo, kateri imenik je avtoritativen; še ne zagotavljajo, da imenik pove resnico o posameznih članih. Vsak vnos je v datoteki ZIP opisan dvakrat, enkrat centralno in enkrat v svoji lokalni glavi, nič v obliki pa ne prisili obeh opisov, da bi se ujemala, zato lahko bralnik, ki zaupa osrednjemu imeniku, in bralnik, ki zaupa lokalnim glavam, iz enega arhiva izvlečeta različno vsebino. TZipEntry.ParseLocalHeader to vrzel zapre tako, da razčleni lokalno glavo na FCdFile.LocalFileHeaderOffset in primerja obe kopiji polje za poljem, ter vrne ločeno negativno kodo za vsako vrsto neskladja: kanonizirano ime vnosa, metodo stiskanja, splošne bite zastavic in, kadar je zastavica opisovalca podatkov počiščena, CRC32 in obe velikosti. S to nastavljeno zastavico so lahko lokalne kopije nič, saj resnične vrednosti živijo v sledečem opisovalcu, katera koli neničelna lokalna vrednost pa se mora vseeno ujemati. Zadnje preverjanje zavrne vnose, katerih podatki bi tekli mimo konca datoteke, s primerjavo Int64(FLFile.DataOffset) + Int64(FCdFile.FCompressedSize) proti inputstream.Size. Vsaka odpoved se razširi iz TCentralDirectory.Parse kot rezultat, ki ni 1, TZipArchive.OpenArchive pa jo pretvori v Can't open zip archive, namesto da bi vam izročil polovično zaupan objekt arhiva. Kadar potrebujete le vedeti, katere liste datoteka vsebuje, je izvedba tega preverjanja pred polnim razčlenjevanjem poceni, lahka pot pregleda listov pa vam da natanko to, ne da bi materializirala podatke celic
Kaj se zgodi, ko sami bajti lažejo?
Strukturno ujemanje še vedno ne pove ničesar o vsebini, zato HotXLS ovije vsak tok vnosa v TZipVerifiedStream, ki med branjem klicatelja uveljavlja izjavljeno velikost in CRC32. To namerno ni naknadno preverjanje: dekompresijska bomba, katere izjavljena nestisnjena velikost je 4 KB, se pa napihne na gigabajte, se ustavi pri 4 KB, ne po škodi. Ovoj vsako branje priklene na preostale izjavljene bajte, sproži ZIP entry ended before its declared size, če vir prezgodaj usahne, ob dokončanju preveri en dodaten bajt in sproži ZIP entry exceeds its declared size, če kaj ostane, nazadnje pa primerja tekoč CRC32 v VerifyComplete, sproži ZIP entry uncompressed size mismatch ali ZIP entry CRC32 mismatch
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;
Ena posledica je vredna načrtovanja. Tok je po zasnovi le naprej; Seek kamor koli razen na trenutni položaj sproži ZIP entry stream is forward-only, z eno izjemo za soEnd z odmikom nič, tako da povpraševanja po velikosti še vedno delujejo. To je pravi kompromis za nezaupanja vreden vhod, saj je tok, ki ga lahko previjete nazaj, tok, katerega obračun CRC lahko premagate, pomeni pa tudi, da porabniška koda, ki pričakuje sposoben za iskanje tok, potrebuje svoj lasten medpomnilnik. Ista disciplina samo naprej podpira pretakajoč neposredni bralnik, ki je API, po katerem sezite, kadar je naložen delovni zvezek dovolj velik, da ga sploh ne želite imeti v pomnilniku
Omejitve virov pred dodelitvijo, ne po njej
Tri konstante v lxZipArchive omejijo, kaj lahko en arhiv zahteva od procesa, TZipEntries.Add pa jih uveljavlja, medtem ko se osrednji imenik še bere, preden se dotakne enega samega bajta podatkov vnosa. ZipMaxEntryUncompressedSize omeji en član na 1 GiB, ZipMaxTotalUncompressedSize omeji arhiv na 4 GiB, ZipMaxCompressionRatio vrednosti 10000 pa zavrne vsak deflate vnos, katerega izjavljena razširitev presega desettisočkratnik, skupaj z degeneriranim primerom neničelne nestisnjene velikosti, uparjene z ničelno stisnjeno velikostjo. Imena vnosov gredo v istem klicu skozi CanonicalZipEntryName, ki zavrne vgrajene znake NUL, dvopičja in vsak potni segment .. s Invalid ZIP entry name, ter razlikuje velike in male črke ter normalizira segmente, tako da se dva člana, ki se razlikujeta le po velikosti črk ali odvečnih ločilih, trčita kot Duplicate ZIP entry name namesto da bi se tiho senčila
Globinska obramba nad slojem ZIP
Sloj ZIP je ena od več plasti, vzorec pa se ponovi povsod, kjer HotXLS razčlenjuje strukturo, ki jo nadzoruje napadalec. Najjasnejši primer sedi v razčlenjevalniku formul BIFF: TXLSFormula.GetTranslated rekurzira skozi žetone tMemFunc, tako da lahko izdelan tok žetonov rgce v starejšem .xls poljubno globoko gnezdi in izčrpa sklad. Vrata so konstanta, MaxTranslateDepth = 256, izbrana na podlagi znanega dejstva vzvodno v verigi, ne uganjena. Excel omeji gnezdenje formul na 64, zato 256 pusti štirikratno rezervo in nikoli ne more zavrniti formule, ki jo je proizvedla prava preglednica, hkrati pa dovolj zgodaj konča sovražen tok, preden zmanjka sklada
const
MaxTranslateDepth = 256;
begin
isOuter := FTranslateDepth = 0;
if isOuter then
ResetPendingArrays;
Inc(FTranslateDepth);
try
if FTranslateDepth > MaxTranslateDepth then
begin
Result := nil;
Exit;
end;
Upoštevajte, da vrata vrnejo nil namesto da bi sprožila izjemo. Formula, pregloboka, da bi bila prava, ne da sintaktičnega drevesa, okoliška razčlenitev se nadaljuje, delovni zvezek pa se še vedno naloži. Ta asimetrija je namerna in vredna posnemanja pri vaših lastnih omejitvah: meja, ki obstaja, da ustavi izčrpanje virov, mora degradirati najmanjšo enoto, ki jo lahko, ne prekiniti dokumenta. Ista logika velja, kadar razširjate raven izračuna, zato če registrirate svoje lastne obravnavalnike prek API za funkcije po meri formulskega pogona, jim dajte lastne omejitve argumentov in rekurzije namesto da bi predpostavljali, da je klicatelj že preveril
Kaj vam ta preverjanja ne prinesejo
Bodite natančni glede meje. Štiri navzkrižna preverjanja EOCD naredijo indeks arhiva nedvoumen, tako da HotXLS in vsak drug skladen bralnik razrešita isto datoteko v isti nabor vnosov; nič ne povejo o tem, ali je ta nabor vnosov nedolžen. Ujemanje lokalnih glav ustavi trik z dvema pogledoma, ne zlonamernega tovora, ki je dosledno opisan. Preverjeni tok ustavi prekinitev, prekoračitev in poškodbo, ne popolnoma pravilno oblikovanega dela XML, ki kodira nekaj, česar niste pričakovali. Nič od tega se ne dotakne makrov: projekt VBA znotraj strukturno neoporečnega delovnega zvezka je še vedno projekt VBA, odločitev, ali ga obdržati, odstraniti ali zavrniti, pa spada v vaš sloj politike, ne v bralnik ZIP
Kar dobite v zameno, je čista meja odpovedi. Nezaupanja vreden xlsx se bodisi odpre kot en nedvoumen arhiv, katerega člani se ujemajo s svojimi izjavljenimi velikostmi in kontrolnimi vsotami, bodisi sproži izjemo s sporočilom, ki poimenuje specifično invarianto, ki jo je prekršil, vaša storitev pa lahko karantenira ob izjemi namesto ugibanja. Bralnik ZIP in plasti razčlenjevalnika nad njim dostavlja komponenta HotXLS Excel za Delphi in C++Builder, ki na stroju, ki opravlja razčlenjevanje, ne potrebuje niti Excela niti avtomatizacije OLE, ta odsotnost pa je sama po sebi smiselno zmanjšanje tega, kar lahko naložena datoteka doseže