HotXLS zapiše delovne zvezke Excel 5.0/95 (BIFF5), obfuskirane z XOR, ki jih Excel 16 odpre samo, kadar se tri podrobnosti točno ujemajo s [MS-OFFCRYPTO]: ključ FILEPASS mora biti CreateXorKey_Method1(password), 16-bajtna matrika XOR mora biti zgrajena z XorRor (zasuk v desno za en bit), vsak bajt pa mora uporabiti XorArrayIndex = (odmik toka + dolžina zapisa) mod 16. HotXLS je indeks uredil v v2.384.47, ključ in zasuk pa v v2.384.54. Pred tem se je vsaka geslom zaščitena datoteka BIFF5, ki jo je izdelal, v HotXLS odprla brez težav, v Excelu pa spodletela
Zadnji stavek je cela zgodba v malem. Bralnik in zapisovalnik, ki si delita isto napačno predstavo, se popolnoma strinjata med sabo, tako da testi povratnega pretvarjanja ostanejo zeleni, medtem ko edini potrošnik, ki šteje, pravi ne. Excel 16 je dvakrat rekel ne, z dvema različnima sporočiloma, in vsako sporočilo je kazalo na drugo plast sheme. Ta članek gre skozi te plasti v vrstnem redu, v katerem jih preverja Excel, s podrobnostmi na ravni bajtov, uporabnimi tako, kadar kličete HotXLS, kot kadar pišete svoj lasten bralnik BIFF
Kaj sploh shrani BIFF XOR obfuskacija?
BIFF XOR obfuskacija shrani v datoteko samo dve 16-bitni besedi, vse ostalo pa se izračuna znova iz gesla. Zapis FILEPASS ($002F) stoji takoj za globals BOF delovnega zvezka, v datoteki BIFF5 pa je njegovo telo točno 4 bajti: ključ XOR, mu sledi preverjevalnik gesla. Ni soli, ni identifikatorja algoritma in ni šifrirane oblokinke preverjevalnika, kakršno nosita shemi RC4 in AES
Iz teh dveh besed bralnik znova zgradi tri stvari:
- Preverjevalnik, 16-bitni hash bajtov gesla, združen z XOR in
$CE4B. Primerjava s shranjeno besedo je preverjanje gesla, in edino - Ključ XOR, 16-bitna vrednost iz
CreateXorKey_Method1v [MS-OFFCRYPTO] §2.3.7.2, gnana z dvema tabelama konstant (InitialCode, 15 besed, inXorMatrix, 105 besed) - Matrika XOR, 16 bajtov: bajti gesla, dopolnjeni s fiksnim 16-bajtnim polnilom, vsak združen z XOR z nižjim bajtom ključa (soda mesta) ali višjim bajtom ključa (liha mesta), nato zasukanih v desno za en bit
Glave zapisov ostanejo v čistem besedilu, prav tako peščica celih zapisov, ki jih shema izjema, med njimi BOF, FILEPASS in INTERFACEHDR. Vsako drugo telo zapisa se preoblikuje bajt za bajtom: zasuk v levo za 5 bitov, nato XOR z enim vnosom 16-bajtne matrike. Dešifriranje, ki ga [MS-OFFCRYPTO] §2.3.7.3 zapiše kot DecryptData_Method1, je zrcalna slika: najprej XOR, nato zasuk v desno za 5
V HotXLS tega vsega nikoli ne dotaknete neposredno. Nastavite geslo, izberete format, SaveAs pa izda FILEPASS in preoblikuje 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 podpira samo XOR obfuskacijo; xletAuto bi jo izbral tudi sam
Wb.EncryptionType := xletXor;
// Naj bo ASCII in največ 15 znakov (glej spodaj)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
Zakaj Excel pravi, da je geslo napačno, čeprav se preverjevalnik ujema?
Excel zavrne geslo, ker ne zaupa shranjenemu ključu: Excel izpelje ključ iz vtipičanega gesla s CreateXorKey_Method1 in ga primerja s ključno besedo FILEPASS, zato datoteka, katere ključ je karkoli drugega, padne pri preverjanju gesla, tudi če je preverjevalnik pravilen. Specifikacija opisuje ključ kot izhod gesla, ne kot prosti parameter, Excel 16 pa to branje vsiljuje
Zapisovalnik HotXLS pred v2.384.54 je ključno besedo zapolnil z dvema naključnima bajtoma. Na papirju izgleda to neškodljivo, saj je preverjevalnik dokumentirano preverjanje gesla, matrika pa je zgrajena iz kakršnega koli ključa, ki ga datoteka naznanja. HotXLS sam je te datoteke bral brez težav, ker je njegov bralnik ključ iz FILEPASS vzel kot dano. Excel 16, ki je dobil isto datoteko in pravilno geslo, je odgovoril, da geslo ni pravilno. Od v2.384.54 je ključ izpeljan, zato FILEPASS za geslo secret vedno nosi ključ $014D in preverjevalnik $DAA7, vrednosti, preverjene še z neodvisno implementacijo specifikacije
Sama izpelja je kratka, ko sta tabeli na mestu. Sprehodite se po geslu od zadaj, sedemkrat poglejte bit 6 vsakega bajta, medtem ko ga pomikate v levo, vsakič, ko je bit postavljen, pa dodajte z XOR enega vnosa XorMatrix. Naslednje je načelna skica, ki ponovi algoritem specifikacije in se ujema z implementacijo HotXLS; ni pa API HotXLS:
// Načelna skica [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// in CreateXorArray_Method1 (samo ilustracija, ni API HotXLS)
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 bajtov
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // zadnji vnos XorMatrix
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)); // zasuk v desno za en 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 to da matriko 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, priročen testni primer, če preizkušate svoj bralnik
Zakaj pravi ključ še vedno da poškodovano datoteko?
Pravilen ključ še vedno da poškodovano datoteko, kadar je matrika XOR zasukana v napačno smer: [MS-OFFCRYPTO] definira korak matrike kot XorRor, zasuk v desno za en bit, matrika zasukana v levo za dva pa dešifrira vsako telo zapisa v šum. Urejen ključ je pognal Excel 16 čez poziv za geslo in naravnost v drugo napako, poročilo, da ima datoteka težavo in je ni mogoče odpreti
Stara koda HotXLS je vsak bajt matrike zasukala v levo za 2 bita, oblika, ki kroži v nekaj implementacijah BIFF. Ker je HotXLS uporabil isti zasuk na obeh straneh, njegov lasten bralnik nikoli ni opazil. Excel 5.0/95 datotek Excel 16 ne zna več shranjevati, pri shranjevanju BIFF8 pa ne ponudi XOR, zato ni bilo izvornega vzorca Excela za primerjavo. Dokazi so morali priti iz druge smeri: zapišite en čist tok BIFF5, znova ga kodirajte na osem načinov in pustite Excelu 16, da odpre vsako različico. Osem različic je prečkalo tri neodvisne izbire:
| Izbira | Možnost A | Možnost B |
|---|---|---|
| Zasuk matrike | XorRor (zasuk v desno 1) | Zasuk v levo 2 |
| Indeks matrike | (odmik + dolžina zapisa) mod 16 | odmik mod 16 |
| Vrstni red preoblikovanja bajtov | Zasuk v levo 5, nato XOR | XOR, nato zasuk v levo 5 |
Excel 16 je od osmih odprl točno dve: XorRor z zasukom pred XOR in indeksom dolžine zapisa ter eno različico, ki je videti samo drugačna. Zasuk-v-levo-2 z XOR-pred-zasukom in istim indeksom je ista funkcija v preobleki. Zasuk se razporedi čez XOR, zato je rol5(p xor rol2(b)) enako rol5(p) xor rol7(b), na 8-bitni vrednosti pa je zasuk v levo za 7 zasuk v desno za 1. Na kratko, rol5 ∘ rol2 = ror1, zato matrika z zasukom v levo za 2 v izolaciji izgleda verjetna: pravilna je samo skupaj z obratnim vrstnim redom preoblikovanja. V paru z vrstnim redom specifikacije pokvari vsak preoblikovani bajt
Isti poskus je poravnil še drugo vprašanje. Različice, ki so iz indeksa izpustile dolžino zapisa, so vse spodletele, kar je potrdilo pravilo indeksa, ki ga je HotXLS sprejel eno izdajo prej samo na podlagi besedila specifikacije
Kako se izračuna XorArrayIndex za vsak bajt?
XorArrayIndex za bajt je njegov odmik v toku delovnega zvezka plus dolžina celotnih podatkov zapisa, ki mu bajt pripada, mod 16. Indeks se zato pri vsakem zapisu ponastavi na vrednost, odvisno od zapisa, znotraj njega pa naraste za ena na bajt. Psevdokoda specifikacije vhoda poimenuje FileOffset in Data.Length, kar zlahka preberete napačno kot samo odmik začetka zapisa, ta napačna interpretacija pa je točno to, kar je HotXLS izdal do v2.384.47
Tri podrobnosti odločajo, ali se vaši indeksi izrovnajo z Excelom:
- 4-bajtna glava zapisa se nikoli ne preoblikuje, a vseeno zaseda položaje v toku, zato prvi bajt telesa zapisa stoji pri odmiku glave + 4
- Člen dolžine je polna dolžina podatkov zapisa, ne število dejansko preoblikovanih bajtov
- BOUNDSHEET je deloma čist: njegovih prvih 4 bajtov,
lbPlyPos, odmik toka lista BOF, ostane berljiv, da razčlenjevalnik najde liste. Tistih 4 bajtov preoblikovanje preskoči, štejejo pa še vedno tako v odmik kot v dolžino zapisa
Skupaj vzeto, je preoblikovanje na zapis nekaj vrstic. Še enkrat, to je skica pravila, ničesar, kar bi morali klicati:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// Obfuskira eno telo zapisa na mestu. BodyPos je odmik toka
// od Body[0], torej odmik glave zapisa + 4. PlainPrefix je 4 za
// BOUNDSHEET, polna dolžina za BOF / FILEPASS, 0 za večino zapisov
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;
// Branje je zrcalna slika: B := Body[I] xor Arr[...];
// nato zasuk v desno za 5, torej Rol8(B, 3)
Pred v2.384.47 je bralnik HotXLS izračunal indeks samo iz položaja v toku, zapisovalnik pa uporabil čisti bajtni odmik. Oba sta ignorirala dolžino zapisa, zato sta se spet strinjali polovici med sabo in nihče drug. Neodvisno napisan dekodirnik je izhod v2.384.47 prebral pravilno, starejši izhod pa kot smeti, osemrazličični test v Excelu 16 pa je pravilo pozneje potrdil proti pravemu cilju
Kaj se zgodi z datotekami XOR, zapisanimi s starejšimi različicami HotXLS?
HotXLS še naprej bere svoje lastne datoteke XOR iz časov pred v2.384.54 tako, da preveri ključ FILEPASS: kadar se shranjeni ključ ujema s ključem, izpeljanim iz gesla, bralnik zgradi matriko XorRor po specifikaciji, kadar pa se razlikuje, datoteko obravnava kot starejšo datoteko HotXLS in znova zgradi matriko z zasukom v levo za 2. Datoteke, zapisane z Excelom, vedno nosijo izpeljani ključ, zato vedno gredo po poti specifikacije
Test je hevristika z natančno stopnjo spodletelosti. Stara datoteka, katere naključni ključ je po naključju ravno enak izpeljanemu ključu, bi se prebrala z napačno matriko, možnost za to pa je 1 od 65.536. Rezervna pot pokrije samo zasuk matrike; pravilo indeksa se ne preklopi, zato so datoteke, ki jih reši, tiste, zapisane med v2.384.47 in v2.384.53. Če še hranite datoteke BIFF5 XOR iz tega okna, jih odprite s trenutnim HotXLS in shranite znova, da dobite datoteko, ki jo Excel sprejme
Dve podrobnosti o geslih veljata za vsako datoteko, staro ali novo:
- Dolžina.
CreateXorKey_Method1prebere samo prvih 15 bajtov gesla, kar je omejitev specifikacije. HotXLS ta strop uporabi na ključ, preverjevalnik in matriko pa pusti na njunih običajnih pravilih polne dolžine in 16 bajtov, dosledno na obeh straneh. Excel sam zavrača gesla, daljša od 15 znakov, pri tem formatu, zato 15 obravnavajte kot pravo najvišjo mejo - Znakovni nabor. HotXLS pretvori geslo v bajte prek sistemske kodne strani ANSI. Specifikacija opisuje jemanje nizkega bajta vsakega znaka UTF-16, kar se pri ASCII ujema. Brez vzorcev Excela, zaščitenih z ne-ASCII gesli, za ostalo ni prave resnice, zato pri datotekah XOR ostanite pri geslih ASCII
Na strani branja vam TXLSWorkbook.OnPassword omogoča poziv za geslo, kadar Open naleti na zapis FILEPASS. Dogodek je TXLSPasswordEvent s var PassWord: WideString in var Retry: Boolean; nastavite Retry na True, da poskusite znova, do tri ponovitve:
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: geslo je zahtevano, a ni bilo podano; -1005: napačno geslo
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
Če geslo že poznate, Open(FileName, APassWord) dogodka sploh ne sproži
Ali je XOR obfuskacija za kaj sploh dovolj varna?
BIFF XOR obfuskacija ni šifriranje in ne ščiti nič pred motiviranim bralcem. Preverjanje gesla je 16-bitni preverjevalnik, ključ ima 16 bitov, 16-bajtna matrika pa se ponavlja po celem toku, tako da vnaprej uganjiva vsebina zapisov BIFF izpostavi bajte matrike brez vsakršnega gesla. HotXLS zapiše XOR samo zato, ker datoteke Excel 5.0/95 nimajo druge možnosti, razlog za izdelavo takih datotek danes pa je zastareli potrošnik, ki ne zna brati ničesar novejšega
Klasični pogon izbere shemo prek TXLSWorkbook.EncryptionType, kombinacija z granilnim formatom pa je strogo preverjena:
xletAuto(privzeto) zapiše RC4 CryptoAPI zaxlExcel97in XOR zaxlExcel5, v skladu s tem, kar je Excel sam zapisal za vsak formatxletXorvelja samo za BIFF5; prixlExcel97shranjevanje sproži izjemo in ne tihe rezervexletRC4inxletRC4CryptoAPIsta samo za BIFF8, zahteva po njiju pri shranjevanju BIFF5 pa prav tako sproži izjemo
RC4 je tudi sam zastarel, podrobnosti o medsebojni združljivosti pa so pokrite v zakaj Excel zavrne šifriran delovni zvezek s pravilnim geslom. Če prejemnik zna brati XLSX, uporabite raje pogon XLSX: TXLSXWorkbook.SaveAsEncryptedAgile zapiše Agile Encryption (hashiranje gesla SHA-512 s števcem vrtenja 100.000 ponovitev in AES-256-CBC), format, ki ga Excel 2010 in novejši zapisuje privzeto, SaveAsEncrypted pa starejše AES-128 Standard Encryption. Izmenjava kompromisov med obema je v šifriranju datotek XLSX z AES v Delphiju, stran branja pa je pokrita v brisanju z Agile šifriranih datotek Excel s HotXLS
Hiter pregled: BIFF XOR obfuskacija, ki jo Excel 16 sprejme
- FILEPASS (
$002F) sledi globals BOF; v BIFF5 je njegovo telo 4 bajti: ključ, nato preverjevalnik - Ključ =
CreateXorKey_Method1(password)po [MS-OFFCRYPTO] §2.3.7.2, nikoli naključen; zasecretje$014D - Matrika = bajti gesla + polnilo, XOR z nižjim bajtom ključa na sodih mestih in višjim na lihih, nato XorRor (zasuk v desno 1)
- Šifriranje bajta: zasuk v levo 5, nato XOR; dešifriranje: XOR, nato zasuk v desno 5 (§2.3.7.3)
- XorArrayIndex = (bajtni odmik toka + dolžina podatkov zapisa) mod 16; glave in čiste predpone štejejo v odmik
- BOUNDSHEET obdrži svojih prvih 4 bajtov čistih; BOF, FILEPASS in INTERFACEHDR ostanejo povsem čisti
- Gesla: ASCII, največ 15 znakov
- HotXLS: indeks popravljen v v2.384.47, ključ in XorRor v v2.384.54, starejše datoteke XOR HotXLS pa so prepoznane po neujemanju ključa
- Za pravo zaščito uporabite vsaj BIFF8 RC4 CryptoAPI ali XLSX Agile Encryption
HotXLS iz ene knjižnice za Delphi in C++Builder opravi zaščito z geslom BIFF5 in BIFF8, XLSX Standard in Agile Encryption ter povratni klic gesla na strani branja, zgoraj navedene podrobnosti združljivosti pa so opravljene za vas. Za izdaje, platforme in preizkusni prenos glej komponento HotXLS Delphi preglednic