HotXLS piše Excel 5.0/95 (BIFF5) radne sveske obfuskovane XOR-om koje Excel 16 otvara samo kada se tri detalja tačno poklope s [MS-OFFCRYPTO]: FILEPASS ključ mora biti CreateXorKey_Method1(password), 16-bajtni XOR niz mora se graditi s XorRor (rotacija u desno za jedan bit), a svaki bajt mora koristiti XorArrayIndex = (ofset u toku + dužina zapisa) mod 16. HotXLS je indeks pogodio u v2.384.47, a ključ i rotaciju u v2.384.54. Pre toga, svaki BIFF5 fajl zaštićen lozinkom koji je proizveo otvarao se lepo u HotXLS-u i padao u Excelu
Ta poslednja rečenica je cela priča u malom. Čitač i writer koji dele istu pogrešnu ideju savršeno se slažu međusobno, pa round-trip testovi ostaju zeleni dok jedini potrošač koji je važan govori ne. Excel 16 je rekao ne dva puta, s dve različite poruke, i svaka poruka je ukazivala na drugi sloj šeme. Ovaj tekst prolazi kroz te slojeve redom kojim ih Excel proverava, s detaljima na nivou bajta koji vam koriste bilo da pozivate HotXLS bilo da pišete svoj BIFF čitač
Šta BIFF XOR obfuskacija zapravo čuva?
BIFF XOR obfuskacija čuva u fajlu samo dve 16-bitne reči, a sve ostalo se preračunava iz lozinke. FILEPASS zapis ($002F) sedi odmah iza workbook globals BOF-a, i u BIFF5 fajlu njegovo telo je tačno 4 bajta: XOR ključ pa verifier lozinke. Nema salta, nema identifikatora algoritma, nema šifrovanog verifier bloba kakav nose RC4 i AES šeme
Iz te dve reči čitač ponovo gradi tri stvari:
- verifier, 16-bitni heš bajtova lozinke XOR-ovan s
$CE4B. Poređenje sa sačuvanom rečju jeste provera lozinke, i jedina - XOR ključ, 16-bitna vrednost iz
CreateXorKey_Method1u [MS-OFFCRYPTO] §2.3.7.2, vođen dvema konstantnim tabelama (InitialCode, 15 reči, iXorMatrix, 105 reči) - XOR niz, 16 bajtova napravljenih od bajtova lozinke dopunjenih fiksnim 16-bajtnim padom, svaki XOR-ovan s nižim bajtom ključa (parne pozicije) ili višim bajtom ključa (neparne pozicije), pa rotiran u desno za jedan bit
Zaglavlja zapisa ostaju u čistom tekstu, kao i nekolicina celih zapisa koje šema izuzima, među njima BOF, FILEPASS i INTERFACEHDR. Svako drugo telo zapisa transformiše se bajt po bajt: rotacija u levo za 5 bitova, pa XOR s jednim elementom 16-bajtnog niza. Dešifrovanje, koje [MS-OFFCRYPTO] §2.3.7.3 izlaže kao DecryptData_Method1, je ogledalo: prvo XOR, pa rotacija u desno za 5
U HotXLS-u ničeg od ovoga ne dirate direktno. Postavite lozinku, izaberite format, i SaveAs emituje FILEPASS i transformiše tok:
uses
SysUtils, lxHandle;
procedure SaveLegacyProtectedBook(const FileName: string);
var
Wb: IXLSWorkbook;
begin
Wb := TXLSWorkbook.Create;
Wb.Sheets.Add.Name := 'Ledger';
Wb.Sheets[1].Range['A1', 'A1'].Value := 'Account';
Wb.Sheets[1].Range['B1', 'B1'].Value := 1250.75;
// BIFF5 podržava samo XOR obfuskaciju; xletAuto bi je i sam izabrao
Wb.EncryptionType := xletXor;
// Držite se ASCII-a i najviše 15 znakova (vidi dole)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
Zašto Excel kaže da je lozinka pogrešna kad se verifier poklapa?
Excel odbija lozinku jer ne veruje sačuvanom ključu: Excel izvodi ključ iz ukucene lozinke s CreateXorKey_Method1 i poredi ga s ključnom reči iz FILEPASS-a, pa fajl čiji je ključ bilo šta drugo pada na proveri lozinke čak i kad je verifier ispravan. Specifikacija opisuje ključ kao izlaz lozinke, a ne kao slobodan parametar, i Excel 16 to tumačenje nameće
HotXLS writer pre v2.384.54 popunjavao je ključnu reč s dva slučajna bajta. Na papiru to deluje bezopasno, jer je verifier dokumentovana provera lozinke, a niz se gradi od ključa koji fajl sam deklariše. HotXLS je te fajlove čitao bez problema, jer je njegov čitač uzimao ključ iz FILEPASS-a kao dato. Excel 16, kad mu se pruži isti fajl i ispravna lozinka, odgovarao je da lozinka nije ispravna. Od v2.384.54 ključ se izvodi, pa FILEPASS za lozinku secret uvek nosi ključ $014D i verifier $DAA7, vrednosti proverene upoređivanjem s nezavisnom implementacijom specifikacije
Sama izvedba je kratka kad su dve tabele na mestu. Prođite lozinku od kraja, gledajte bit 6 svakog bajta sedam puta dok ga pomerate u levo, i XOR-ujte po jedan XorMatrix element svaki put kad je bit postavljen. Sledeće je načelna skica koja reprodukuje algoritam iz specifikacije i poklapa se s HotXLS implementacijom; nije HotXLS API:
// Načelna skica [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// i CreateXorArray_Method1 (samo ilustracija, nije HotXLS API)
type
TXorArray = array [0..15] of Byte;
function DemoCreateXorKey(const Password: AnsiString): Word;
const
InitialCode: array [0..14] of Word = ($E1F0, $1D0F, $CC9C, $84C0, $110C,
$0E10, $F1CE, $313E, $1872, $E139, $D40F, $84F9, $280C, $A96A, $4EC3);
XorMatrix: array [0..104] of Word = (
$AEFC, $4DD9, $9BB2, $2745, $4E8A, $9D14, $2A09,
$7B61, $F6C2, $FDA5, $EB6B, $C6F7, $9DCF, $2BBF,
$4563, $8AC6, $05AD, $0B5A, $16B4, $2D68, $5AD0,
$0375, $06EA, $0DD4, $1BA8, $3750, $6EA0, $DD40,
$D849, $A0B3, $5147, $A28E, $553D, $AA7A, $44D5,
$6F45, $DE8A, $AD35, $4A4B, $9496, $390D, $721A,
$EB23, $C667, $9CEF, $29FF, $53FE, $A7FC, $5FD9,
$47D3, $8FA6, $0F6D, $1EDA, $3DB4, $7B68, $F6D0,
$B861, $60E3, $C1C6, $93AD, $377B, $6EF6, $DDEC,
$45A0, $8B40, $06A1, $0D42, $1A84, $3508, $6A10,
$AA51, $4483, $8906, $022D, $045A, $08B4, $1168,
$76B4, $ED68, $CAF1, $85C3, $1BA7, $374E, $6E9C,
$3730, $6E60, $DCC0, $A9A1, $4363, $86C6, $1DAD,
$3331, $6662, $CCC4, $89A9, $0373, $06E6, $0DCC,
$1021, $2042, $4084, $8108, $1231, $2462, $48C4);
var
Len, I, Bit, Element: Integer;
Ch: Byte;
begin
Result := 0;
Len := Length(Password);
if Len > 15 then
Len := 15; // ključ vidi samo 15 bajtova
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // poslednji XorMatrix element
for I := Len downto 1 do
begin
Ch := Ord(Password[I]);
for Bit := 1 to 7 do
begin
if (Ch and $40) <> 0 then
Result := Result xor XorMatrix[Element];
Ch := Byte(Ch shl 1);
Dec(Element);
end;
end;
end;
function XorRor(B, KeyByte: Byte): Byte;
begin
B := B xor KeyByte;
Result := Byte((B shr 1) or (B shl 7)); // rotiraj u desno za jedan bit
end;
procedure DemoCreateXorArray(const Password: AnsiString; out Arr: TXorArray);
const
PadArray: TXorArray = ($BB, $FF, $FF, $BA, $FF, $FF, $B9, $80,
$00, $BE, $0F, $00, $BF, $0F, $00, $00);
var
Key: Word;
Len, I: Integer;
begin
Key := DemoCreateXorKey(Password);
Len := Length(Password);
if Len > 16 then
Len := 16;
for I := 0 to Len - 1 do
Arr[I] := Ord(Password[I + 1]);
for I := Len to 15 do
Arr[I] := PadArray[I - Len];
for I := 0 to 15 do
if Odd(I) then
Arr[I] := XorRor(Arr[I], Byte(Key shr 8))
else
Arr[I] := XorRor(Arr[I], Byte(Key and $FF));
end;
Za secret ovo daje niz 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, zgodan fixture ako testirate svoj čitač
Zašto ispravan ključ i dalje daje oštećen fajl?
Ispravan ključ i dalje daje oštećen fajl kad se XOR niz rotira na pogrešnu stranu: [MS-OFFCRYPTO] definiše korak niza kao XorRor, rotaciju u desno za jedan bit, a niz rotiran u levo za dva dešifruje svako telo zapisa u šum. Ispravka ključa je gurnula Excel 16 preko upita za lozinku pravo u drugu grešku, izveštaj da fajl ima problem i ne može se otvoriti
Stari HotXLS kod rotirao je svaki bajt niza u levo za 2 bita, oblik koji kruži u nekoliko BIFF implementacija. Pošto je HotXLS koristio istu rotaciju s obe strane, njegov sopstveni čitač nikada nije primetio. Excel 16 više ne može da čuva fajlove Excel 5.0/95, i ne nudi XOR pri čuvanju BIFF8, pa nije bilo nativnog Excel uzorka za poređenje. Dokaz je morao doći iz drugog smera: napisati jedan čist tekstualni BIFF5 tok, ponovo ga enkodovati na osam načina i pustiti Excel 16 da otvori svaku varijantu. Osam varijanti ukrstilo je tri nezavisne odluke:
| Odluka | Opcija A | Opcija B |
|---|---|---|
| Rotacija niza | XorRor (rotacija u desno 1) | Rotacija u levo 2 |
| Indeks niza | (ofset + dužina zapisa) mod 16 | ofset mod 16 |
| Red transformacije bajta | Rotacija u levo 5, pa XOR | XOR, pa rotacija u levo 5 |
Excel 16 je otvorio tačno dve od osam: XorRor s rotiraj-pa-XOR i indeksom dužine zapisa, i jednu varijantu koja se samo čini drugačijom. Rotacija u levo 2 s XOR-pa-rotiraj i istim indeksom je ista funkcija u prerušavanju. Rotacija se distribuje preko XOR-a, pa je rol5(p xor rol2(b)) jednako rol5(p) xor rol7(b), a na 8-bitnoj vrednosti rotacija u levo za 7 je rotacija u desno za 1. Ukratko, rol5 ∘ rol2 = ror1, pa niz s rotacijom u levo 2 deluje uverljivo izolovan: ispravan je samo uz obrnuti red transformacije. U paru s redom iz specifikacije pokvari svaki transformisani bajt
Isti eksperiment rešio je i drugo pitanje. Varijante koje su izbacile dužinu zapisa iz indeksa sve su pale, čime je potvrđeno pravilo indeksa koje je HotXLS usvojio izdanje ranije, na snagu samog teksta specifikacije
Kako se XorArrayIndex računa za svaki bajt?
XorArrayIndex za bajt je njegov ofset u toku radne sveske plus dužina celokupnih podataka zapisa kojem pripada, mod 16. Indeks se dakle restartuje na vrednost zavisnu od zapisa za svaki zapis i uvećava se po jedan za svaki bajt unutar njega. Pseudokod specifikacije imenuje ulaze FileOffset i Data.Length, što se lako pročita kao sam ofset početka zapisa, i to pogrešno čitanje je upravo ono što je HotXLS isporučivao do v2.384.47
Tri detalja odlučuju da li se vaši indeksi poklapaju s Excelom:
- Četvorobajtno zaglavlje zapisa se nikada ne transformiše, ali i dalje zauzima pozicije u toku, pa prvi bajt tela zapisa sedi na ofsetu zaglavlja + 4
- Član dužine je puna dužina podataka zapisa, ne broj bajtova zaista transformisanih
- BOUNDSHEET je delimično čist: njegova prva 4 bajta,
lbPlyPos, ofset sheet BOF-a u toku, ostaju čitljivi da parser može da locira listove. Tih 4 bajta transformacija preskače, ali i dalje računaju i u ofset i u dužinu zapisa
Sve skupa, transformacija po zapisu je nekoliko redova. Opet, ovo je skica pravila, ne nešto što treba da zovete:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// Šifruje telo jednog zapisa na mestu. BodyPos je ofset u toku za
// Body[0], tj. ofset zaglavlja zapisa + 4. PlainPrefix je 4 za
// BOUNDSHEET, puna dužina za BOF / FILEPASS, 0 za većinu zapisa
procedure DemoObfuscateRecord(var Body: array of Byte; RecordLength: Word;
PlainPrefix: Integer; BodyPos: LongWord; const Arr: TXorArray);
var
I: Integer;
begin
for I := PlainPrefix to RecordLength - 1 do
Body[I] := Rol8(Body[I], 5) xor
Arr[(BodyPos + LongWord(I) + RecordLength) mod 16];
end;
// Čitanje je ogledalna slika: B := Body[I] xor Arr[...];
// pa rotacija u desno za 5, tj. Rol8(B, 3)
Pre v2.384.47 HotXLS čitač računao je indeks samo iz pozicije u toku, a writer je koristio čisti bajt-ofset. Obojica su ignorisali dužinu zapisa, pa su se opet dve polovine slagale jedna s drugom i ni s kim drugim. Nezavisno napisan dekoder čitao je izlaz v2.384.47 ispravno a stariji izlaz kao đubre, i test s osam varijanti u Excelu 16 kasnije je potvrdio pravilo protiv pravog cilja
Šta je sa XOR fajlovima koje su napisale starije HotXLS verzije?
HotXLS i dalje čita svoje XOR fajlove starije od v2.384.54 proverom FILEPASS ključa: kad je sačuvani ključ jednak ključu izvedenom iz lozinke, čitač gradi XorRor niz iz specifikacije, a kad se razlikuje, čitač tretira fajl kao stariji HotXLS fajl i ponovo gradi niz s rotacijom u levo 2. Fajlovi napisani iz Excela uvek nose izvedeni ključ, pa uvek idu putem specifikacije
Ta provera je heuristika s tačnom stopom greške. Stari fajl čiji se slučajni ključ desio jednakim izvedenom ključu čitao bi se s pogrešnim nizom, a šansa za to je 1 u 65.536. Fallback pokriva samo rotaciju niza; pravilo indeksa se ne prebacuje, pa fajlovi koje spašava su oni napisani između v2.384.47 i v2.384.53. Ako još držite BIFF5 XOR fajlove iz tog prozora, otvorite ih trenutnim HotXLS-om i sačuvajte ih ponovo da dobijete fajl koji Excel prihvata
Dva detalja o lozinci važe za svaki fajl, stari ili novi:
- Dužina.
CreateXorKey_Method1čita samo prvih 15 bajtova lozinke, što je granica iz specifikacije. HotXLS tu granicu primenjuje na ključ i drži verifier i niz na njihovim uobičajenim pravilima pune dužine i 16 bajtova, dosledno s obe strane. Excel sam odbija lozinke duže od 15 znakova za ovaj format, pa tretirajte 15 kao pravi maksimum - Skup znakova. HotXLS prevodi lozinku u bajtove kroz sistemsku ANSI code page. Specifikacija opisuje uzimanje nižeg bajta svakog UTF-16 znaka, što se poklapa za ASCII. Bez Excel uzoraka zaštićenih ne-ASCII lozinkama nema istine o ostatku, pa se držite ASCII lozinki za XOR fajlove
Na strani čitanja, TXLSWorkbook.OnPassword vam omogućava da upitite lozinku kad Open naiđe na FILEPASS zapis. Događaj je TXLSPasswordEvent s var PassWord: WideString i var Retry: Boolean; postavite Retry na True da pokušate ponovo, do tri ponovna pokušaja:
procedure TImportForm.WorkbookPassword(Sender: TObject;
var PassWord: WideString; var Retry: Boolean);
var
S: string;
begin
S := '';
Retry := InputQuery('Protected workbook', 'Password:', S);
PassWord := S;
end;
procedure TImportForm.ImportLegacyFile(const FileName: string);
var
Wb: IXLSWorkbook;
Rc: Integer;
begin
Wb := TXLSWorkbook.Create;
Wb.OnPassword := WorkbookPassword;
Rc := Wb.Open(FileName);
// -1003: traži se lozinka ali nije data; -1005: pogrešna lozinka
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
Ako lozinku već znate, Open(FileName, APassWord) preskače događaj sasvim
Da li je XOR obfuskacija dovoljno sigurna za išta?
BIFF XOR obfuskacija nije šifrovanje i ne štiti ništa od čitača s voljom. Provera lozinke je 16-bitni verifier, ključ je 16 bitova, a 16-bajtni niz se ponavlja kroz ceo tok, pa predvidljivi sadržaji BIFF zapisa izlažu bajtove niza bez ikakve lozinke. HotXLS piše XOR samo zato što fajlovi Excel 5.0/95 nemaju drugu opciju, a razlog za proizvodnju takvih fajlova danas je legacy potrošač koji ne čita ništa novije
Klasični engine bira šemu kroz TXLSWorkbook.EncryptionType, i kombinacija sa formatom čuvanja proverava se strogo:
xletAuto(podrazumevano) piše RC4 CryptoAPI zaxlExcel97i XOR zaxlExcel5, u skladu s tim što je Excel sam pisao za svaki formatxletXorvaži samo za BIFF5; uzxlExcel97čuvanje baca izuzetak umesto da tiho padne na stariju opcijuxletRC4ixletRC4CryptoAPIsu samo za BIFF8, i traženje njih pri BIFF5 čuvanju takođe baca izuzetak
RC4 je isto zastareo, a detalji oko njegove interoperabilnosti pokriveni su u tekstu o tome zašto Excel odbija šifrovanu radnu svesku s ispravnom lozinkom. Ako primač ume da čita XLSX, koristite XLSX engine: TXLSXWorkbook.SaveAsEncryptedAgile piše Agile Encryption (SHA-512 heširanje lozinke sa spin count od 100.000 iteracija i AES-256-CBC), format koji Excel 2010 i kasnije pišu podrazumevano, dok SaveAsEncrypted piše stariji AES-128 Standard Encryption. Kompromisi između ta dva su u tekstu o šifrovanju XLSX fajlova s AES-om u Delphiju, a strana čitanja pokrivena je u tekstu o čitanju Agile-šifrovanih Excel fajlova s HotXLS-om
Brzi podsetnik: BIFF XOR obfuskacija koju Excel 16 prihvata
- FILEPASS (
$002F) ide iza globals BOF-a; u BIFF5 njegovo telo je 4 bajta: ključ, pa verifier - Ključ =
CreateXorKey_Method1(password)po [MS-OFFCRYPTO] §2.3.7.2, nikad slučajan; zasecretje$014D - Niz = bajtovi lozinke + pad, XOR s nižim bajtom ključa na parnim pozicijama i višim na neparnim, pa XorRor (rotacija u desno 1)
- Šifruj bajt: rotacija u levo 5, pa XOR; dešifruj: XOR, pa rotacija u desno 5 (§2.3.7.3)
- XorArrayIndex = (ofset bajta u toku + dužina podataka zapisa) mod 16; zaglavlja i čisti prefiksi računaju se u ofset
- BOUNDSHEET drži prvih 4 bajta čistim; BOF, FILEPASS i INTERFACEHDR ostaju sasvim čisti
- Lozinke: ASCII, najviše 15 znakova
- HotXLS: indeks ispravljen u v2.384.47, ključ i XorRor u v2.384.54, stariji HotXLS XOR fajlovi se prepoznaju po nepoklapanju ključa
- Za pravu zaštitu koristite bar BIFF8 RC4 CryptoAPI, ili XLSX Agile Encryption
HotXLS pokriva BIFF5 i BIFF8 zaštitu lozinkom, XLSX Standard i Agile Encryption i callback za lozinku na strani čitanja iz jedne Delphi i C++Builder biblioteke, s interoperabilnim detaljima iz gore rešenim umesto vas. Pogledajte HotXLS Delphi spreadsheet component za izdanja, platforme i probnu verziju