Technical Article

Perché Excel rifiuta la tua cartella di lavoro crittografata: ECB e RC4

Quando si scrive una cartella di lavoro, la si protegge con password, la si invia a un collega ed egli la apre in Excel, il programma richiede la password. Il collega la inserisce ed Excel la accetta. Fin qui il processo sembra corretto. Successivamente, Excel mostra una finestra di dialogo in cui segnala che il file è corrotto e non può essere aperto, oppure si apre mostrando celle con dati privi di significato. La password era corretta, ma il file risulta danneggiato. Questa è una delle anomalie più complesse nella crittografia di Office, poiché la sezione che convalida la password e quella che memorizza i dati sono protette da operazioni diverse, e completare correttamente la prima non garantisce il successo della seconda.

Entrambi i difetti qui analizzati presentavano proprio questo scenario. In ciascun caso il sistema di convalida superava il controllo mentre il corpo del file risultava illeggibile, spingendo a cercare anomalie nella password o nella derivazione della chiave in realtà assenti. L'errore reale risiedeva a valle, nella trasformazione dei byte del pacchetto. Le due anomalie sono indipendenti, una nel percorso AES e una in quello RC4, ma condividono la stessa difficoltà di diagnosi, motivo per cui è utile capire perché un risultato parzialmente corretto sia complesso da interpretare.

Perché la convalida della password non garantisce la correttezza del file

Il formato utilizzato dai file XLSX protetti moderni è lo standard ECMA-376 Standard Encryption, che memorizza due elementi crittografati affiancati. Il primo è l'EncryptionVerifier: una piccola area contenente un valore casuale e il rispettivo hash, crittografati con la chiave derivata dalla password. Il secondo è l'EncryptedPackage: l'intero contenitore zip della cartella di lavoro, crittografato con la stessa chiave. Il verifier esiste per consentire al lettore di verificare una password prima di decrittografare l'intero file. Decrittografando il verifier ed eseguendo l'hash del valore casuale è possibile stabilire se la password sia corretta confrontandola con l'hash memorizzato.

La particolarità risiede nel fatto che il verifier e il pacchetto vengono crittografati tramite chiamate separate su buffer differenti. Una chiave derivata correttamente decrittograferà il verifier senza considerare lo stato del pacchetto successivo. Pertanto, se la derivazione della chiave è corretta ma la trasformazione del pacchetto presenta errori, Excel accetterà la password ma segnalerà errori per il corpo del file. Il sintomo si manifesta come "password corretta, file corrotto", indirizzando l'analisi sul percorso della password, l'unico elemento che non presentava problemi. La stessa separazione governa il caso RC4 legacy: l'hash del verifier viene verificato per primo, e se il corpo del file presenta disallineamenti il controllo iniziale viene comunque superato.

Bug uno: AES in modalità ECB, non CBC

La specifica [MS-OFFCRYPTO] §2.3.4.15 indica che lo Standard Encryption protegge il pacchetto tramite AES in modalità Electronic Codebook (ECB). Ogni blocco da 16 byte del pacchetto viene crittografato in modo indipendente con la stessa chiave. Non vi è concatenamento tra i blocchi né vettori di inizializzazione. Si tratta di una scelta insolita rispetto agli standard moderni, in cui la modalità ECB viene solitamente evitata, ma la compatibilità richiede il rispetto preciso della specifica. Excel decrittografa il pacchetto in modalità ECB, quindi il produttore deve applicare tale algoritmo per garantire l'allineamento.

L'anomalia risiedeva nel fatto che il pacchetto veniva crittografato tramite AES in modalità CBC con un vettore di inizializzazione (IV) interamente impostato a zero. In questa configurazione il processo funziona parzialmente: nel CBC il primo blocco di testo in chiaro viene sottoposto a XOR con l'IV prima della crittografia. Se l'IV è impostato a zero, lo XOR non altera i dati, quindi il primo blocco in modalità CBC-con-IV-a-zero produce lo stesso testo cifrato di quello in modalità ECB. A partire dal secondo blocco, il CBC inserisce il testo cifrato precedente nel successivo, sfasando tutti i passaggi rispetto ad ECB.

Considerando la struttura, il layout del pacchetto inserisce un prefisso di lunghezza a 8 byte all'inizio, quindi le porzioni esaminate per prime da Excel risiedono nei primi blocchi. Un primo blocco allineato consente il superamento dei controlli iniziali, mentre i blocchi successivi vengono decrittografati in modo errato. La soluzione consiste nell'applicare la crittografia a blocchi da 16 byte con ECB senza concatenamento. Nel motore, XlsEncryptStdPackage analizza il buffer a passi di 16 byte richiamando AESEncryptECB128Block su ciascuno di essi, il medesimo algoritmo già in uso per i blocchi del verifier. Il codice sorgente contiene un commento specifico: la modalità CBC con IV a zero corrisponde a ECB solo per il primo blocco, quindi il resto dei dati risulterebbe illeggibile ad Excel.

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;

Bug due: il re-key RC4 non sincronizzato

Il percorso legacy .xls adotta lo schema RC4 CryptoAPI, e la regola è differente. La specifica [MS-OFFCRYPTO] §2.3.6 indica che la cifratura viene rieseguita (re-key) a ogni confine di blocco da 1024 byte. Lo stream viene suddiviso in blocchi da 1024 byte, viene derivata una nuova chiave RC4 per ciascun blocco e all'interno di ogni blocco lo stream di chiavi (keystream) viene consumato in modo continuo. È necessario garantire che il re-key avvenga a ogni confine e che lo stream di chiavi sia consumato senza interruzioni all'interno del blocco. Essendo l'RC4 un cifrario a flusso, lo stream di chiavi è una sequenza ordinata; l'ennesimo byte estratto è definito dalle operazioni precedenti. La decrittografia esegue lo XOR con la medesima sequenza, richiedendo che produttore e consumatore leggano gli stessi byte nelle medesime posizioni.

Qui risiede la complessità. Un cifrario a flusso non dispone di risincronizzazione. Se si perde la sincronizzazione di un singolo byte, tutti i byte successivi verranno decodificati con lo stream di chiavi errato, e l'errore non si correggerà, propagandosi fino alla fine del blocco e a quelli successivi. L'anomalia qui riscontrata si comportava in questo modo: il contatore dei blocchi iniziava da un valore sentinella pari a meno uno, e la routine di salto ipotizzava che il contatore corrispondesse già al blocco corrente. Partendo da tale sentinella, eseguiva il re-key ed elaborava un intero blocco da 1024 byte che non avrebbe dovuto essere consumato, sfasando il contatore e portando la decrittografia fuori allineamento. Il verifier, controllato in precedenza, veniva comunque superato, facendo apparire corretta la password ma restituendo dati illeggibili nelle celle.

La logica corretta risiede in TXLSDecrypterRC4. Sia Skip sia Decrypt condividono lo stesso ciclo: eseguono il re-key solo quando la posizione corrente entra in un nuovo blocco (calcolato dividendo la posizione per REKEY_BLOCK_SIZE (1024)), consumando dati fino al limite del blocco corrente. MakeKey viene richiamato con l'indice di blocco corretto, mai con valori obsoleti, e la posizione avanza del numero di byte elaborati per mantenere allineate le fasi di Skip e Decrypt con il produttore. Nei cifrari a flusso, la perdita di allineamento anche di un solo byte compromette l'intera decodifica successiva.

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;

Garantire la conformità byte per byte

Entrambe le anomalie si riconducono allo stesso principio generale. Quando il consumatore dell'output è un programma esterno predefinito, la modalità di cifratura e il re-key non sono dettagli opzionali che possono essere modificati o semplificati. Fanno parte del contratto di comunicazione. Excel decrittograferà con ECB ed eseguirà il re-key a confini di 1024 byte a prescindere dalle preferenze dello sviluppatore, e l'unico obiettivo richiesto è produrre byte che risultino corretti sotto tale procedura. Qualsiasi deviazione, come l'uso di una modalità più moderna o un IV diverso, si traduce in un difetto che compromette la lettura. La compatibilità con specifiche bloccate richiede un allineamento byte per byte.

Per questo motivo la convalida del verifier non rappresenta un test sufficiente. Conferma solo che la derivazione della chiave sia corretta. Un test reale richiede di decrittografare il pacchetto e confrontare i byte recuperati con quelli originali, o verificare la rilettura delle celle dopo un ciclo di scrittura e crittografia. Il verifier conferma la password; solo il corpo del file dimostra la correttezza della crittografia.

Modalità supportate per la gestione di file protetti

Le interfacce esterne sono ridotte. Per scrivere una cartella di lavoro XLSX protetta da password, si utilizza TXLSXWorkbook chiamando SaveAsEncrypted con il nome del file e la password; questa istruzione serializza la cartella ed esegue la pipeline Standard Encryption allineata con le specifiche corrette. Per la lettura, CanReadEncrypted verifica se il file sia un contenitore Compound File protetto, consentendo di indirizzare il flusso: OpenEncrypted gestisce il percorso crittografato e ripiega su Open per i file non protetti. La gestione della modalità e del ciclo di re-key descritti in precedenza operano internamente; è sufficiente fornire la password e il nome del file e il motore gestirà i passaggi per tuo conto.

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;

La struttura dell'output protetto, lo stream EncryptionInfo, i blocchi verifier e il layout del pacchetto sono descritti nella nostra guida all'output XLSX protetto con AES. Per le questioni inerenti il blocco a livello di foglio e le relazioni con il layout di pagina e la stampa, consulta l'articolo su protezione, layout di pagina e stampa. Entrambi si basano sul percorso di crittografia descritto in questa pagina, incluso all'interno del componente per fogli di calcolo HotXLS per Delphi e C++Builder alongside the reading, writing, and rendering APIs covered elsewhere on this blog.