Technical Article

De ce respinge Excel registrul dvs. de lucru criptat: ECB și RC4

You write a workbook, encrypt it with a password, hand the file to a colleague, and the colleague opens it in Excel. Excel asks for the password. The colleague types it, and Excel accepts it. So far the encryption looks correct. Then Excel puts up a dialog that says the file is corrupt and cannot be opened, or it opens to a sheet of meaningless cells. The password was right. The file is broken anyway. This is the single most disorienting failure mode in Office encryption, because the part that tells you the password is right and the part that holds your data are protected by two different operations, and getting one correct does nothing to guarantee the other.

Ambele buguri descrise aici au avut exact această formă. În fiecare caz, verificatorul a trecut și corpul nu, ceea ce vă trimite la vânătoare de buguri de parole sau de derivare a cheilor care nu există. Defectul real a fost în aval, în modul în care au fost transformați octeții pachetului. Cele două defecțiuni sunt independente, una pe calea AES și una pe calea RC4, dar împărtășesc o problemă de diagnosticare, așa că merită să vedem de ce un rezultat pe jumătate corect este cel mai greu de interpretat.

De ce validarea parolei nu dovedește nimic despre conținutul documentului

Formatul pe care îl folosește XLSX-ul criptat modern este ECMA-376 Standard Encryption, și stochează două lucruri criptate unul lângă altul. Unul este EncryptionVerifier: un mic bloc care reține o valoare aleatorie și hash-ul acelei valori, criptat cu cheia derivată din parolă. Celălalt este EncryptedPackage: întregul container zip al registrului de lucru, criptat cu aceeași cheie. Verificatorul există pentru ca un cititor să poată confirma o parolă înainte de a consuma efort pe megaocteți de conținut. Decriptați verificatorul, calculați hash-ul valorii aleatorii, comparați-l cu hash-ul stocat și, dacă se potrivesc, parola este corectă.

Capcana este că verificatorul și pachetul sunt criptate prin apeluri separate în buffere separate. O cheie derivată corect va decripta verificatorul corect, indiferent de ce se întâmplă cu pachetul ulterior. Deci, dacă derivarea cheii este corectă, dar transformarea pachetului este greșită, Excel confirmă parola din verificator și apoi eșuează la corp. Simptomul se citește ca „parolă corectă, fișier deteriorat”, ceea ce îndreaptă investigația către calea parolei, care este singura parte care nu a fost niciodată defectă. Aceeași separare guvernează cazul RC4 vechi: hash-ul verificatorului este verificat mai întâi, iar un corp care deviază din sincronizare lasă în continuare acea verificare intactă.

Eroarea unu: AES în ECB, nu CBC

[MS-OFFCRYPTO] §2.3.4.15 specifică faptul că Standard Encryption criptă pachetul cu AES în modul Electronic Codebook. Fiecare bloc de 16 octeți al pachetului umplut este criptat independent cu aceeași cheie. Nu există nicio înlănțuire între blocuri și nu există un vector de inițializare. Aceasta este o alegere neobișnuită după standardele moderne, unde ECB este de obicei evitat, dar interoperabilitatea nu este un loc în care să ghiciți specificațiile. Excel decriptează pachetul ca ECB, așa că un producător trebuie să-l cripteze ca ECB, altfel cele două nu se vor potrivi.

Bugul a fost că pachetul a fost criptat cu AES în modul CBC folosind un vector de inițializare complet zero. Iată de ce funcționează aproape și de ce „aproape” este cel mai rău loc de unde să porniți. În CBC, primul bloc de text clar este XORat cu IV-ul înainte de criptare. Când IV-ul este format numai din zerouri, acel XOR nu schimbă nimic, așa că primul bloc de CBC-cu-IV-zero produce exact același text cifrat ca ECB. De la al doilea bloc înainte, CBC introduce textul cifrat anterior în următorul, astfel încât fiecare bloc după primul deviază de la ECB.

Acum suprapuneți acest lucru pe structură. Aspectul pachetului plasează un prefix de lungime little-endian de 8 octeți chiar la început, astfel încât părțile din fișier pe care Excel le verifică cel mai devreme se află în primul bloc sau două. Un prim bloc care se potrivește înseamnă că validările cele mai timpurii trec, în timp ce fiecare bloc ulterior se decriptează în zgomot. Remedierea nu este subtilă odată ce modul este identificat: criptați fiecare bloc de 16 octeți cu ECB și opriți înlănțuirea. În motor, XlsEncryptStdPackage parcurge bufferul umplut în pași de 16 octeți și apelează AESEncryptECB128Block pe fiecare, care este aceeași primitivă folosită deja pentru blocurile verificatorului. Sursa conține un comentariu la buclă care specifică regula în mod clar: CBC cu un IV zero se potrivește cu ECB doar pentru primul bloc, astfel încât restul pachetului s-ar decripta ca gunoi și Excel l-ar respinge.

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('report.xlsx');
    // SaveAsEncrypted serializes the workbook, then runs the
    // ECMA-376 Standard Encryption pipeline: AES-128 ECB over the
    // package per [MS-OFFCRYPTO] 2.3.4.15. Returns 1 on success.
    if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
      raise Exception.Create('Encryption failed');
  finally
    Book.Free;
  end;
end;

Eroarea doi: re-cheia RC4 își pierde sincronizarea

Calea veche .xls folosește schema RC4 CryptoAPI, iar regula sa este diferită. [MS-OFFCRYPTO] §2.3.6 specifică faptul că cifrul este re-cheiat (re-keyed) la fiecare graniță de bloc de 1024 de octeți. Fluxul este împărțit în blocuri de 1024 de octeți, o nouă cheie RC4 este derivată pentru numărul de bloc 0, 1, 2 și așa mai departe, iar în cadrul fiecărui bloc fluxul de chei (keystream) este consumat continuu de la octet la octet. Două invarianți trebuie să se mențină împreună: re-cheierea pe fiecare graniță și consumarea fluxului de chei fără lacune în interiorul unui bloc. RC4 este un cifru de flux, deci fluxul său de chei este o singură secvență ordonată; al n-lea octet pe care îl extrageți este determinat de câți octeți ați extras înainte de el. Decriptarea reprezintă același XOR împotriva aceleiași secvențe, ceea ce înseamnă că producătorul și consumatorul trebuie să extragă exact aceiași octeți la exact aceleași poziții.

Aceasta este întreaga dificultate. Un cifru de flux nu are resincronizare. Dacă risipiți un singur octet de flux de chei, fiecare octet de după el este XORat cu octetul greșit din fluxul de chei, iar eroarea nu se corectează niciodată; cascadează până la sfârșitul blocului și, odată ce poziția de rulare este greșită, la fiecare bloc de după el. Bugul de aici a făcut exact asta. Contorul de blocuri începea de la o valoare santinelă de minus unu, iar rutina de ignorare (skip) presupunea că contorul se potrivea deja cu blocul curent. Pornind de la acea santinelă, a re-cheiat și a rulat un bloc complet de 1024 de octeți de keystream care nu ar fi trebuit niciodată consumat și, în acest proces, a condus contorul rămas pe negativ. Din acel punct, decriptorul a fost decalat cu un bloc complet. Verificatorul, verificat înainte de toate acestea, a trecut în continuare, astfel încât parola părea corectă în timp ce fiecare celulă de date rezulta ca gunoi.

Logica corectată trăiește în TXLSDecrypterRC4. Atât Skip, cât și Decrypt partajează o singură buclă: re-cheiere numai când poziția curentă trece într-un bloc nou, unde indexul blocului este poziția împărțită la REKEY_BLOCK_SIZE (1024), apoi consumă până la restul blocului curent și nu mai mult. MakeKey este apelat cu indexul blocului, niciodată cu un index învechit sau santinelă, iar poziția avansează cu numărul exact de octeți procesați, astfel încât Skip și Decrypt rămân aliniate de fază cu producătorul. Lecția se află în cea mai mică unitate: un singur octet irosit nu este o eroare mică într-un cifru de flux, ci o pierdere totală a tot ceea ce se află în aval.

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    // CanReadEncrypted checks the Compound File (OLE2) signature so
    // you can branch before attempting a normal Open. OpenEncrypted
    // routes plain files to Open and handles the encrypted container.
    if Book.CanReadEncrypted('legacy.xls') then
      Book.OpenEncrypted('legacy.xls', 'S3cret!')
    else
      Book.Open('legacy.xls');
    // read cells here
  finally
    Book.Free;
  end;
end;

Interoperabilitatea cu o specificație fixă înseamnă potrivire la nivel de octet

Ambele buguri se reduc la același principiu fundamental, și merită enunțat singur deoarece schimbă modul în care evaluați alegerile de design. Când consumatorul rezultatului dvs. este un program extern fix pe care nu îl puteți schimba, modul cifrului și cadența de re-cheiere nu sunt detalii de implementare pe care le optimizați sau le simplificați. Ele fac parte din contractul fizic. Excel va decripta cu ECB și va re-cheia la granițele de 1024 de octeți indiferent dacă acele alegeri vă plac sau nu, iar singura dvs. sarcină este să produceți octeți care se decriptează la original în conformitate cu acea procedură exactă. Un mod mai modern, un IV care pare inofensiv, un contor care pornește de unde se simte natural; oricare dintre acestea este un defect în clipa în care deviază de la ceea ce așteaptă cititorul. Interoperabilitatea în raport cu o specificație înghețată nu este aproximativă. Este exactă la nivel de octet sau este defectă.

Acesta este și motivul pentru care verificatorul este un test slab de fum pe cont propriu. Vă spune că derivarea cheii funcționează, ceea ce este necesar, dar departe de a fi suficient. Un test care doar deschide un fișier criptat și confirmă că parola trece va raporta succes în timp ce corpul este ilizibil. Un test real decriptează pachetul și compară octeții recuperați cu datele de intrare originale, sau parcurge un registru de lucru prin criptare și decriptare și citește celulele înapoi. Verificatorul dovedește parola; numai corpul dovedește criptarea.

Metoda acceptată pentru citirea și scrierea registrelor de lucru protejate

Interfața publică este mică. Pentru a scrie un registru de lucru modern protejat prin parolă, populați sau deschideți un TXLSXWorkbook și apelați SaveAsEncrypted cu un nume de fișier și o parolă; serializează registrul de lucru și rulează conducta Standard Encryption pe care prima remediere a corectat-o, returnând 1 în caz de succes. Pentru a citi, apelați CanReadEncrypted pentru a testa dacă un fișier este un container Compound File criptat, apoi ramificați: OpenEncrypted gestionează calea criptată și recurge la Open pentru fișiere simple, iar Open cu o parolă este disponibil direct. Gestionarea modului și bucla de re-cheiere descrise mai sus se află sub aceste apeluri; furnizați parola și numele fișierului, iar motorul se potrivește cu specificația în numele dvs.

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('quarterly.xlsx');
    Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
    // Reopen on the consumer side
    Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
  finally
    Book.Free;
  end;
end;

Forma rezultatului protejat, fluxul EncryptionInfo, blocurile verificatorului și aspectul pachetului sunt acoperite în ghidul nostru despre rezultatul XLSX protejat cu AES. Pentru întrebarea separată despre blocarea la nivel de foaie și modul în care protecția interacționează cu configurarea paginii și imprimarea, consultați articolul despre protecție, configurarea paginii și imprimare. Ambele se bazează pe calea de criptare descrisă aici, care este livrată ca parte a HotXLS spreadsheet component pentru Delphi și C++Builder, alături de API-urile de citire, scriere și redare acoperite în alte părți ale acestui blog.