HotXLS zapisuje XOR obfuskované zošity Excel 5.0/95 (BIFF5), ktoré Excel 16 otvorí len vtedy, keď tri detaily sedia s [MS-OFFCRYPTO] presne na bajt: kľúč FILEPASS musí byť CreateXorKey_Method1(password), 16-bajtové XOR pole sa musí stavať cez XorRor (rotácia doprava o jeden bit) a každý bajt musí používať XorArrayIndex = (stream offset + record length) mod 16. HotXLS mal index správne od v2.384.47 a kľúč aj rotáciu od v2.384.54. Predtým sa každý heslom chránený BIFF5 súbor, ktorý vyprodukoval, v HotXLS otváral normálne a v Exceli zlyhal
Tá posledná veta je celý príbeh v miniatúre. Čítačka a zapisovač, ktoré zdieľajú tú istú zlú predstavu, sa navzájom potvrdia dokonale, takže round-trip testy zostávajú zelené, kým jediný konzument, ktorý sa počíta, hovorí nie. Excel 16 povedal nie dvakrát, s dvomi rôznymi hláškami, a každá hláška ukazovala na inú vrstvu schémy. Tento článok prechádza tie vrstvy v poradí, v akom ich Excel kontroluje, s detailmi na úrovni bajtov, ktoré využijete, či už voláte HotXLS, alebo píšete vlastnú BIFF čítačku
Čo BIFF XOR obfuskácia v súbore vlastne uchováva?
BIFF XOR obfuskácia uchováva v súbore len dve 16-bitové slová a všetko ostatné sa prepočíta z hesla. Záznam FILEPASS ($002F) sedí hneď za workbook globals BOF a v súbore BIFF5 má jeho telo presne 4 bajty: XOR kľúč nasledovaný verifierom hesla. Žiadny salt, žiadny identifikátor algoritmu a žiadna zašifrovaná verifier blob, aké nesú schémy RC4 a AES
Z týchto dvoch slov si čítačka znovu postaví tri veci:
- Verifier, 16-bitový hash bajtov hesla XORovaný s
$CE4B. Porovnanie s uloženým slovom je kontrola hesla a jediná, ktorá tu je - XOR kľúč, 16-bitová hodnota z
CreateXorKey_Method1v [MS-OFFCRYPTO] §2.3.7.2, poháňaná dvomi konštantnými tabuľkami (InitialCode, 15 slov, aXorMatrix, 105 slov) - XOR pole, 16 bajtov zložených z bajtov hesla doplnených fixným 16-bajtovým padom, každý XORovaný s nižším bajtom kľúča (párne pozície) alebo vyšším bajtom kľúča (nepárne pozície) a potom rotovaný doprava o jeden bit
Hlavičky záznamov ostávajú v čistom texte a rovnako aj hrsť celých záznamov, ktoré schéma vynecháva, medzi nimi BOF, FILEPASS a INTERFACEHDR. Telo každého ďalšieho záznamu sa transformuje bajt po bajte: rotácia doľava o 5 bitov, potom XOR s jednou položkou 16-bajtového poľa. Dešifrovanie, ktoré [MS-OFFCRYPTO] §2.3.7.3 popisuje ako DecryptData_Method1, je zrkadlový obraz: najprv XOR, potom rotácia doprava o 5
V HotXLS sa ničoho z toho priamo nedotknete. Nastavte heslo, zvoľte formát a SaveAs emituje FILEPASS a transformuje stream:
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 podporuje len XOR obfuskáciu; xletAuto by ju tiež zvolil
Wb.EncryptionType := xletXor;
// Držte ho v ASCII a maximálne 15 znakov (viac nižšie)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
Prečo Excel hovorí, že heslo je zlé, keď verifier sedí?
Excel odmietne heslo, pretože neverí uloženému kľúču: kľúč si odvodí z napísaného hesla cez CreateXorKey_Method1 a porovná ho s kľúčovým slovom v FILEPASS, takže súbor s akýmkoľvek iným kľúčom neprejde kontrolou hesla, aj keď verifier je správny. Špecifikácia popisuje kľúč ako výstup hesla, nie ako voľný parameter, a Excel 16 toto čítanie vynucuje
Zapisovač HotXLS pred v2.384.54 naplnil kľúčové slovo dvomi náhodnými bajtmi. Na papieri to vyzerá neškodne, keďže verifier je zdokumentovaná kontrola hesla a pole sa stavia z kľúča, ktorý súbor deklaruje. HotXLS sám také súbory čítal bez problémov, lebo jeho čítačka brala kľúč z FILEPASS ako daný. Excel 16, ktorému ste podali ten istý súbor a správne heslo, odpovedal, že heslo nie je správne. Od v2.384.54 sa kľúč odvodzuje, takže FILEPASS pre heslo secret vždy drží kľúč $014D a verifier $DAA7, hodnoty skontrolované proti nezávislej implementácii špecifikácie
Samotná derivácia je krátka, akonáhle máte obe tabuľky na mieste. Prechádzajte heslo odzadu, sedemkrát sa pozrite na bit 6 každého bajtu, zatiaľ čo ho posúvate doľava, a XORujte do neho jednu položku XorMatrix vždy, keď je bit nastavený. Nasleduje náčrt princípu, ktorý reprodukuje algoritmus zo špecifikácie a sedí s implementáciou HotXLS; nie je to API HotXLS:
// Náčrt princípu [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// a CreateXorArray_Method1 (len ilustrácia, nie 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; // kľúč vidí len 15 bajtov
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // posledná položka 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)); // rotácia doprava o jeden 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;
Pre secret to vyprodukuje pole 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, čo je šikovná fixtúra, ak testujete vlastnú čítačku
Prečo aj správny kľúč dáva poškodený súbor?
Správny kľúč stále dáva poškodený súbor, keď je XOR pole rotované nesprávnym smerom: [MS-OFFCRYPTO] definuje krok poľa ako XorRor, rotáciu doprava o jeden bit, a pole rotované doľava o dva dešifruje telo každého záznamu na šum. Oprava kľúča posunula Excel 16 cez výzvu na heslo priamo k inej chybe, hláseniu, že súbor má problém a nedá sa otvoriť
Starý kód HotXLS rotuje každý bajt poľa doľava o 2 bity, forma, ktorá koluje v niekoľkých BIFF implementáciách. Keďže HotXLS používal tú istú rotáciu na oboch stranách, jeho vlastná čítačka si toho nikdy nevšimla. Excel 16 už nevie ukladať súbory Excel 5.0/95 a pri ukladaní BIFF8 XOR nenabídne, takže nebol žiadny natívny Excel vzor na diff. Dôkaz musel prísť z opačnej strany: napísať jeden čistý BIFF5 stream, prekódovať ho ôsmimi spôsobmi a nechať Excel 16 otvoriť každú variantu. Osem variánt prekrížilo tri nezávislé voľby:
| Voľba | Možnosť A | Možnosť B |
|---|---|---|
| Rotácia poľa | XorRor (rotácia doprava o 1) | Rotácia doľava o 2 |
| Index poľa | (offset + record length) mod 16 | offset mod 16 |
| Poradie bajtovej transformácie | Rotácia doľava o 5, potom XOR | XOR, potom rotácia doľava o 5 |
Excel 16 otvoril presne dve z ôsmich: XorRor s poradím najprv rotovať, potom XOR a s indexom nesúcim dĺžku záznamu, a jednu variantu, ktorá vyzerá len inak. Rotácia-doľava-2 s XOR-potom-rotuj a tým istým indexom je tá istá funkcia v prestrojení. Rotácia sa distribuuje cez XOR, takže rol5(p xor rol2(b)) sa rovná rol5(p) xor rol7(b) a na 8-bitovej hodnote je rotácia doľava o 7 rotáciou doprava o 1. Stručne, rol5 ∘ rol2 = ror1, a preto pôsobí pole s rotáciou doľava o 2 v izolácii vierohodne: je správne len spolu s opačným poradím transformácie. Spárované s poradím zo špecifikácie pokazí každý transformovaný bajt
Ten istý experiment vyriešil aj druhú otázku. Varianty, ktoré vypustili z indexu dĺžku záznamu, všetky zlyhali, čo potvrdilo pravidlo indexu, ktoré si HotXLS prijal vydanie skôr len na základe textu špecifikácie
Ako sa pre každý bajt počíta XorArrayIndex?
XorArrayIndex bajtu je jeho offset v streami zošita plus dĺžka celých dát záznamu, do ktorého bajt patrí, všetko mod 16. Index sa preto pre každý záznam reštartuje na hodnotu závislú od záznamu a vo vnútri zvyšuje o jeden na bajt. Pseudokód špecifikácie menuje vstupy FileOffset a Data.Length, čo sa ľahko prečíta ako samotný počiatočný offset záznamu, a práve toto zlé čítanie posielal HotXLS až do v2.384.47
O tom, či vaše indexy sedia s Excelom, rozhodujú tri detaily:
- 4-bajtová hlavička záznamu sa nikdy netransformuje, ale aj tak zaberá pozície v streame, takže prvý telový bajt záznamu sedí na offset hlavičky + 4
- Člen dĺžky je plná dĺžka dát záznamu, nie počet bajtov skutočne transformovaných
- BOUNDSHEET je čiastočne čistý: jeho prvé 4 bajty,
lbPlyPos, stream offset BOF hárka, ostávajú čitateľné, aby parser vedel hárky nájsť. Tých 4 bajtov transformácia preskočí, ale aj tak sa počítajú do offsetu aj do dĺžky záznamu
Zložená dohromady, transformácia na záznam je pár riadkov. Aj toto je náčrt pravidla, nie niečo, čo musíte volať:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// Obfuskuje telo jedného záznamu na mieste. BodyPos je stream offset
// Body[0], teda offset hlavičky záznamu + 4. PlainPrefix je 4 pre
// BOUNDSHEET, plná dĺžka pre BOF / FILEPASS, 0 pre väčšinu záznamov
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;
// Čítanie je zrkadlový obraz: B := Body[I] xor Arr[...];
// potom rotácia doprava o 5, teda Rol8(B, 3)
Pred v2.384.47 počítala čítačka HotXLS index len zo stream pozície a zapisovač použil čistý bajtový offset. Obe polovice ignorovali dĺžku záznamu, takže sa opäť zhodli navzájom a s nikým iným. Nezávisle napísaný dekodér čítal výstup v2.384.47 správne a starší výstup ako odpad a test ôsmich variánt v Exceli 16 neskôr potvrdil pravidlo proti skutočnému cieľu
Čo sa deje so súbormi XOR napísanými staršími verziami HotXLS?
HotXLS naďalej číta vlastné XOR súbory z čias pred v2.384.54 kontrolou kľúča FILEPASS: keď sa uložený kľúč rovná kľúču odvodenému z hesla, čítačka stavia XorRor pole zo špecifikácie, a keď sa líši, považuje súbor za starší súbor HotXLS a znovu postaví pole s rotáciou doľava o 2. Súbory písané Excelom vždy nesú odvodený kľúč, takže vždy idú cestou špecifikácie
Ten test je heuristika s presnou mierou zlyhania. Starý súbor, ktorého náhodný kľúč sa práve rovnal odvodenému kľúču, by sa čítal so zlým poľom a šanca na to je 1 zo 65 536. Fallback pokrýva len rotáciu poľa; pravidlo indexu sa neprepína, takže zachránené súbory sú tie, ktoré vznikli medzi v2.384.47 a v2.384.53. Ak ešte držíte BIFF5 XOR súbory z tohto okna, otvorte ich v aktuálnom HotXLS a uložte znova, aby ste dostali súbor, ktorý Excel prijme
Dva detaily hesla platia pre každý súbor, starý aj nový:
- Dĺžka.
CreateXorKey_Method1číta len prvých 15 bajtov hesla, čo je limit zo špecifikácie. HotXLS aplikuje ten strop na kľúč a verifier aj pole drží na svojich obvyklých pravidlách plnej dĺžky a 16 bajtov, dôsledne na oboch stranách. Excel sám odmieta heslá dlhšie ako 15 znakov pre tento formát, takže 15 berte ako skutočné maximum - Znaková sada. HotXLS prevádza heslo na bajty cez systémovú ANSI kódovú stránku. Špecifikácia popisuje branie nižšieho bajtu každého znaku UTF-16, čo sa pre ASCII zhoduje. Bez Excelových vzorov chránených ne-ASCII heslami neexistuje pre ostatné znaky pravda vyššieho rázu, takže pri XOR súboroch zostaňte pri ASCII heslách
Na čítacej strane vám TXLSWorkbook.OnPassword umožní vyzvať na heslo, keď Open narazí na záznam FILEPASS. Udalosť je TXLSPasswordEvent s var PassWord: WideString a var Retry: Boolean; nastavte Retry na True, aby ste to skúsili znova, najviac tri pokusy:
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: heslo je potrebné, ale žiadne nebolo dodané; -1005: zlé heslo
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
Ak heslo už poznáte, Open(FileName, APassWord) udalosť celkom preskočí
Je XOR obfuskácia aspoň na niečo dostatočne bezpečná?
BIFF XOR obfuskácia nie je šifrovanie a proti motivovanému čitateľovi nechráni nič. Kontrola hesla je 16-bitový verifier, kľúč má 16 bitov a 16-bajtové pole sa opakuje po celom streame, takže predvídateľný obsah BIFF záznamov vystaví bajty poľa bez akéhokoľvek hesla. HotXLS zapisuje XOR len preto, lebo súbory Excel 5.0/95 nemajú inú možnosť, a dôvodom produkovať také súbory dnes je legacy konzument, ktorý nečíta nič novšie
Klasický engine volí schému cez TXLSWorkbook.EncryptionType a kombinácia s ukladacím formátom sa kontroluje prísne:
xletAuto(predvolené) zapisuje RC4 CryptoAPI prexlExcel97a XOR prexlExcel5, presne to, čo Excel sám zapisoval pre každý formátxletXorje platný len pre BIFF5; prixlExcel97uloženie vyhodí výnimku namiesto tichého fallbackuxletRC4axletRC4CryptoAPIsú len BIFF8 a ich vyžiadanie pri ukladaní BIFF5 tiež vyhodí výnimku
RC4 má svoje roky a detaily interoperability popisuje článok o tom, prečo Excel odmietne zašifrovaný zošit so správnym heslom. Ak príjemca vie čítať XLSX, použite radšej engine XLSX: TXLSXWorkbook.SaveAsEncryptedAgile zapisuje Agile Encryption (hashovanie hesla SHA-512 so spin countom 100 000 iterácií a AES-256-CBC), formát, ktorý Excel 2010 a novšie zapisujú predvolene, zatiaľ čo SaveAsEncrypted zapisuje staršie AES-128 Standard Encryption. Rozvahy medzi oboma sú v článku o šifrovaní XLSX súborov s AES v Delphi a čítacia strana je pokrytá v článku o čítaní Agile zašifrovaných Excel súborov s HotXLS
Rýchly prehľad: XOR obfuskácia BIFF, ktorú Excel 16 prijme
- FILEPASS (
$002F) nasleduje za globals BOF; v BIFF5 má telo 4 bajty: kľúč, potom verifier - Kľúč =
CreateXorKey_Method1(password)podľa [MS-OFFCRYPTO] §2.3.7.2, nikdy náhodný; presecretje to$014D - Pole = bajty hesla + pad, XOR s nižším bajtom kľúča na párnych pozíciách a vyšším na nepárnych, potom XorRor (rotácia doprava o 1)
- Zašifrovanie bajtu: rotácia doľava o 5, potom XOR; dešifrovanie: XOR, potom rotácia doprava o 5 (§2.3.7.3)
- XorArrayIndex = (stream offset bajtu + dĺžka dát záznamu) mod 16; hlavičky a čisté prefixy sa počítajú do offsetu
- BOUNDSHEET necháva svoje prvé 4 bajty čisté; BOF, FILEPASS a INTERFACEHDR ostávajú úplne čisté
- Heslá: ASCII, maximálne 15 znakov
- HotXLS: index opravený vo v2.384.47, kľúč a XorRor opravené vo v2.384.54, staršie XOR súbory HotXLS rozpoznané podľa nezhody kľúča
- Pre skutočnú ochranu použite minimálne BIFF8 RC4 CryptoAPI alebo XLSX Agile Encryption
HotXLS obsluhuje ochranu heslom pre BIFF5 a BIFF8, XLSX Standard aj Agile Encryption a čítací heslový callback z jednej knižnice pre Delphi a C++Builder, s detailmi interoperability vyššie vyriešenými za vás. Edície, platformy a skúšobnú verziu na stiahnutie nájdete na stránke tabuľkového komponentu HotXLS pre Delphi