xlsx failas yra ZIP archyvas, o ZIP neturi vienintelio autoritetingo turinio sąrašo. HotXLS Excel biblioteka Delphi ir C++Builder platformoms šį nevienareikšmiškumą laiko atakos paviršiumi: jos centrinio katalogo pabaigos analizatorius priima kandidatinį įrašą tik po to, kai keturi nepriklausomi kryžminiai patikrinimai sutampa, todėl suklastotas katalogas, paslėptas ZIP komentare, niekada nelaimi
Scenarijus, paverčiantis tai konkrečiu, kasdieniškas. Serveris priima skaičiuoklių įkėlimus iš klientų. Failas praeina antivirusinį patikrinimą, įrašomas į spool katalogą, ir jūsų Delphi paslauga jį atveria, kad ištrauktų tris stulpelius. Viskas atrodo gerai, išskyrus tai, kad skeneris ir jūsų analizatorius nesutarė, ką archyvas turėjo. Skeneris išvardijo vieną narių rinkinį; jūsų įkėlimo mechanizmas išvardijo kitą rinkinį iš tų pačių baitų. Nė vienas iš jų nesuklydo įprasta prasme. Jie tiesiog išsprendė ZIP formato nevienareikšmiškumą dviem skirtingomis kryptimis, o užpuolikas pasirinko baitus taip, kad jie tai padarytų
Kur iš tikrųjų gyvena tiesa apie ZIP archyvą?
Ji gyvena pačioje pabaigoje, 22 baitų struktūroje, vadinamoje centrinio katalogo pabaigos įrašu. ZIP failas skaitomas ne nuo priekio iki galo: kiekvienas narys neša vietinę failo antraštę tiesiai prieš savo suglaudintus duomenis, bet autoritetingas indeksas — tai centrinis katalogas, įrašų sekcija netoli pabaigos, kuri įvardina kiekvieną įrašą ir nurodo jo vietinės antraštės poslinkį. Norint rasti centrinį katalogą, pirmiausia reikia rasti EOCD, nes kaip tik EOCD sako, kur prasideda katalogas ir kiek įrašų jis laiko. HotXLS jį modeliuoja kaip TEndOfCentralDirectoryRecord, kurio laukai vienas su vienu atitinka disko išdėstymą: FDiskNumber poslinkyje 4, FStartDisk poslinkyje 6, FThisDiskEntries poslinkyje 8, FTotalEntries poslinkyje 10, FSizeOfCD poslinkyje 12, FOffsetOfStartCD poslinkyje 16 ir FCommentLen poslinkyje 20. Ta suma — FMinSize, apskaičiuota konstruktoriuje kaip 4*3 + 5*2. Po jos eina archyvo komentaras, iki 65535 baitų savavališko turinio, kas daro FMaxSize lygų 65557 ir reiškia, kad įrašas nėra fiksuotoje pozicijoje. Jį reikia ieškoti
Kodėl skenavimo atgal ieškant EOCD parašo neužtenka?
Todėl, kad keturi baitai, kurių ieškote, PK\005\006, gali teisėtai pasirodyti archyvo komentare, suglaudintuose duomenyse arba antrame EOCD, kurį užpuolikas pridėjo sąmoningai. Analizatorius, kuris sustoja ties pirmu parašu, sutiktu einant atgal, yra trivialiai valdomas: padėkite netikrą EOCD arti pabaigos, ir naivus analizatorius jį seka, o analizatorius, skenuojantis kita tvarka, ar laikantis paskutinį failo parašą autoritetingu, seka tikrąjį. Tai — ZIP nevienareikšmiškumo atakų šeima, ir jos nauda kaip tik yra tas skilimas, aprašytas aukščiau, kur skenavimo variklis ir naudojanti programa mato skirtingus įrašų rinkinius iš vieno failo
TEndOfCentralDirectoryRecord.Parse iš tikrųjų skenuoja atgal. Ji nustato startscan į paskutinį baitą, apkarpo endscan iki lsize - FMaxSize ar nulio ir eina per langą 256 baitų buferiais, kurie persidengia trimis baitais, todėl parašas, kertantis buferio ribą, niekada nepraleidžiamas. Skirtumas — tai, kas nutinka radus pataikymą. Parašo suradimas duoda tik Candidate poslinkį. HotXLS tada perskaito 22 baitus tame poslinkyje, analizuoja juos su ReadEOCD ir reikalauja, kad gauti laukai būtų vidiniai nuoseklūs su failu, kurį jie teigia aprašantys, prieš FOffsetEOCD apskritai priskiriant
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;
Skaitykite predikatą kaip keturis atskirus teiginius, kuriuos suklastotė turi patenkinti vienu metu. Candidate + FMinSize + FCommentLen = lsize reikalauja, kad deklaruotas komentaro ilgis pasiektų tiksliai failo pabaigą, ir kaip tik tai nužudo apsimestinio EOCD komentare triuką: netikras EOCD, palaidotas tikrame komentare, negali taip pat apskaičiuoti kiekvieno baito po savęs. FDiskNumber = 0 ir FStartDisk = 0 atmeta kelių diskų padalijimo laukus, kurių jokia xlsx niekada teisėtai nenaudojo ir kurie sukurtuose archyvuose egzistuoja tik norint suklaidinti. FThisDiskEntries = FTotalEntries atmeta padalinto skaičiaus triuką, kur vienas analizatorius savo cikliui matuojasi pagal vieną lauką, o kitas — pagal kitą. O Int64(FOffsetOfStartCD) + FSizeOfCD = Candidate reikalauja, kad centrinis katalogas baigtųsi lygiai ten, kur prasideda EOCD, todėl katalogas negali būti nukreiptas į kokį nesusijusį dvejetainį objektą kitoje failo vietoje. Int64 konvertavimas paskutiniam svarbus: abu operandai — 32 bitų, o be plėtimo, sukurta pora galėtų aritmetiškai apsisukti ir patenkinti testą, rodydama į niekur pagrįstą
Vietinės antraštės turi sutapti su centriniu katalogu
EOCD patikrinimai fiksuoja, kuris katalogas autoritetingas; jie dar negarantuoja, kad katalogas sako tiesą apie atskirus narius. Kiekvienas įrašas ZIP faile aprašomas du kartus, kartą centrine tvarka ir kartą jo vietinėje antraštėje, ir niekas formate neverčia, kad du aprašymai sutaptų, todėl skaitytuvas, pasitikintis centriniu katalogu, ir skaitytuvas, pasitikintis vietinėmis antraštėmis, gali ištraukti skirtingą turinį iš vieno archyvo. TZipEntry.ParseLocalHeader šią spragą uždaro, analizuodamas vietinę antraštę FCdFile.LocalFileHeaderOffset ir lygindamas dvi kopijas lauką po lauko, grąžindama atskirą neigiamą kodą kiekvienam nesutapimo tipui: kanonizuotą įrašo pavadinimą, glaudinimo metodą, bendros paskirties bitų vėliavėles, o kai duomenų aprašiklio vėliavėlė nustatyta — CRC32 ir abu dydžius. Su ta vėliavėle nustatyta, vietinės kopijos gali būti nulinės, nes tikros reikšmės gyvena sekančiame aprašiklyje, bet bet kokia nenulinė vietinė reikšmė vis tiek turi sutapti. Galutinis patikrinimas atmeta įrašus, kurių duomenys tęstųsi už failo pabaigos, lygindamas Int64(FLFile.DataOffset) + Int64(FCdFile.FCompressedSize) su inputstream.Size. Bet kokia nesėkmė sklinda iš TCentralDirectory.Parse kaip ne-1 rezultatas, o TZipArchive.OpenArchive paverčia ją į Can't open zip archive, o ne perduoda jums pusiau patikimą archyvo objektą. Kai jums tereikia žinoti, kuriuos lapus turi failas, šio patvirtinimo paleidimas prieš pilną analizę yra pigus, o lengvasis lapų tikrinimo kelias duoda kaip tik tai, be ląstelių duomenų materializavimo
Kas atsitinka, kai patys baitai meluoja?
Struktūrinis sutapimas vis dar nieko nepasako apie turinį, todėl HotXLS apgaubia kiekvieną įrašo srautą į TZipVerifiedStream, kuris vykdo deklaruotą dydį ir CRC32 iškviečiančiajam skaitant. Tai sąmoningai ne patikrinimas po fakto: dekompresijos bomba, kurios deklaruotas nesuglaudintas dydis — 4 KB, bet kuri išsiplečia iki gigabaitų, sustabdoma ties 4 KB riba, ne po žalos. Įvyniotojas apkarpo kiekvieną skaitymą iki likusių deklaruotų baitų, iškelia ZIP entry ended before its declared size, jei šaltinis anksti išsenka, patikrina vieną papildomą baitą užbaigimo metu ir iškelia ZIP entry exceeds its declared size, jei kas nors liko, ir galiausiai lygina bėgantį CRC32 VerifyComplete, iškeldamas ZIP entry uncompressed size mismatch arba 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;
Verta suplanuoti vieną pasekmę. Srautas pagal dizainą — tik-į-priekį; Seek bet kur kitur, ne į dabartinę poziciją, iškelia ZIP entry stream is forward-only, su viena nuolaida soEnd su nuliniu poslinkiu, kad dydžio užklausos vis tiek veiktų. Tai — teisingas kompromisas nepatikimai įvesčiai, nes srautas, kurį galite persukti atgal, yra srautas, kurio CRC apskaitą galite nugalėti, bet tai reiškia, kad naudotojo kodui, tikinčiam sujungiamu srautu, reikia savo pačių buferio. Ta pati tik-į-priekį disciplina glūdi po srautiniu tiesioginiu skaitytuvu, kuris yra API, į kurią reikia kreiptis, kai įkelta darbaknygė pakankamai didelė, kad nenorėtumėte jos laikyti atmintyje visai
Resursų ribos prieš paskirstymą, ne po jo
Trys konstantos modulyje lxZipArchive riboja tai, ko vienas archyvas gali paprašyti iš proceso, ir TZipEntries.Add jas taiko, kol centrinis katalogas dar skaitomas, prieš paliečiant nė vieną įrašo duomenų baitą. ZipMaxEntryUncompressedSize apriboja vieną narį iki 1 GiB, ZipMaxTotalUncompressedSize apriboja archyvą iki 4 GiB, o ZipMaxCompressionRatio, lygus 10000, atmeta bet kokį suglaudintą įrašą, kurio deklaruotas išsiplėtimas viršija dešimt tūkstančių kartų, kartu su degeneruotu atveju, kai nenulinis nesuglaudintas dydis suporuotas su nuliniu suglaudintu dydžiu. Įrašų pavadinimai tuo pačiu iškvietimu eina per CanonicalZipEntryName, kuri atmeta įterptus NUL simbolius, dvitaškius ir bet kokį .. kelio segmentą su Invalid ZIP entry name, ir kuri mažosiomis raidėmis ir normalizuoja segmentus, todėl du nariai, besiskiriantys tik raidžių dydžiu ar pertekliniais skirtukais, susiduria kaip Duplicate ZIP entry name, o ne tyliai vienas kitą užstoja
Gynyba per gylį virš ZIP sluoksnio
ZIP sluoksnis — vienas sluoksnis iš kelių, ir šis raštas kartojasi visur, kur HotXLS analizuoja užpuoliko valdomą struktūrą. Aiškiausias pavyzdys — BIFF formulės analizatoriuje: TXLSFormula.GetTranslated rekursyviai eina per tMemFunc žetonus, todėl sukurta rgce žetonų srauto sena .xls faile gali įsidėti savavališkai giliai ir išsemti steką. Vartai — konstanta, MaxTranslateDepth = 256, pasirinkta pagal žinomą aukštesnės pakopos faktą, ne spėjimą. Excel apriboja formulės įdėjimą iki 64, todėl 256 palieka keturgubą atsargą ir niekada negali atmesti formulės, kurią sukūrė tikra skaičiuoklė, vis tiek nutraukiant piktavališką srautą pakankamai anksti, kol stekas dar nebaigėsi
const
MaxTranslateDepth = 256;
begin
isOuter := FTranslateDepth = 0;
if isOuter then
ResetPendingArrays;
Inc(FTranslateDepth);
try
if FTranslateDepth > MaxTranslateDepth then
begin
Result := nil;
Exit;
end;
Atkreipkite dėmesį, kad vartai grąžina nil, o ne iškelia išimtį. Per giliai įdėta formulė, kad būtų tikra, neduoda sintaksės medžio, aplinkinė analizė tęsiasi, o darbaknygė vis tiek įsikelia. Ši asimetrija sąmoninga ir verta pasiskolinti savo pačių ribose: riba, egzistuojanti tam, kad sustabdytų resursų išsekimą, turėtų degraduoti mažiausią vienetą, kurį gali, ne nutraukti dokumentą. Ta pati logika taikoma, kai plečiate skaičiavimo sluoksnį, todėl jei registruojate savo pačių tvarkytojus per formulių variklio pasirinktinių funkcijų API, suteikite jiems savo pačių argumentų ir rekursijos ribas, vietoj to, kad manytumėte, jog iškviečiantysis jau patikrino
Ko šie patikrinimai jums neduoda
Būkite tikslūs dėl šios ribos. Keturi EOCD kryžminiai patikrinimai padaro archyvo indeksą vienareikšmį, todėl HotXLS ir bet kuris kitas atitinkantis skaitytuvas išsprendžia tą patį failą į tą patį įrašų rinkinį; jie nieko nesako apie tai, ar tas įrašų rinkinys nekenksmingas. Vietinės antraštės sutapimas sustabdo dviejų-vaizdų triuką, ne piktavališką turinį, kuris nuosekliai apibūdinamas. Patvirtintas srautas sustabdo nupjovimą, perpildymą ir sugadinimą, ne visiškai gerai suformuotą XML dalį, kuri užkoduoja kažką, ko nesitikėjote. Ir nė vienas iš šių dalykų neliečia makrokomandų: VBA projektas struktūriškai nepriekaištingoje darbaknygėje vis tiek yra VBA projektas, ir sprendimas jį palikti, pašalinti ar atmesti priklauso jūsų politikos sluoksniui, ne ZIP skaitytuvui
Tai, ką gaunate mainais, — švari nesėkmės riba. Nepatikimas xlsx arba atsiveria kaip vienas vienareikšmis archyvas, kurio nariai atitinka deklaruotus dydžius ir kontrolines sumas, arba iškelia išimtį su pranešimu, įvardijančiu konkretų invariantą, kurį jis pažeidė, ir jūsų paslauga gali izoliuoti pagal išimtį, vietoj spėjimo. ZIP skaitytuvas ir virš jo esantys analizatoriaus sluoksniai dalyvauja HotXLS Excel komponente Delphi ir C++Builder platformoms, kuriam nereikia nei Excel, nei OLE automatizavimo mašinoje, atliekančioje analizę, ir šis nebuvimas savaime yra reikšmingas to, ką gali pasiekti įkeltas failas, sumažinimas