HotXLS zapisuje XOR obfuskované sešity Excel 5.0/95 (BIFF5), které Excel 16 otevře jen když tři detaily sedí přesně na [MS-OFFCRYPTO]: klíč FILEPASS musí být CreateXorKey_Method1(password), 16bajtové XOR pole se musí stavět pomocí XorRor (rotace doprava o jeden bit) a každý bajt musí použít XorArrayIndex = (offset v streamu + délka záznamu) mod 16. Index HotXLS spravil ve v2.384.47 a klíč a rotaci ve v2.384.54. Před tím se každý heslem chráněný BIFF5 soubor, který vyprodukoval, v HotXLS otevřel bez problémů a v Excelu selhal
Ta poslední věta je celý příběh v miniatuře. Čtečka a zapisovač, které sdílejí tutéž špatnou představu, se mezi sebou shodují naprosto, takže round-trip testy zůstávají zelené, zatímco jediný konzument, na kterém záleží, říká ne. Excel 16 řekl ne dvakrát, pokaždé jinou hláškou, a každá hláška ukazovala na jinou vrstvu schématu. Tenhle článek projde těmi vrstvami v pořadí, v jakém je Excel kontroluje, s bajtovými detaily, které použijete, ať voláte HotXLS, nebo píšete vlastní BIFF čtečku
Co vlastně XOR obfuskace BIFF ukládá?
XOR obfuskace BIFF ukládá v souboru jen dvě 16bitová slova a všechno ostatní se dopočítává z hesla. Záznam FILEPASS ($002F) sedí hned za globals BOF sešitu a v BIFF5 souboru má jeho tělo přesně 4 bajty: XOR klíč následovaný verifikátorem hesla. Žádná sůl, žádný identifikátor algoritmu a žádný šifrovaný verifier blob, jaké nosí schémata RC4 a AES, tu nejsou
Z těch dvou slov si čtečka postaví tři věci:
- Verifikátor, 16bitový hash bajtů hesla XORovaný s
$CE4B. Porovnání s uloženým slovem je kontrola hesla, a jediná - XOR klíč, 16bitová hodnota z
CreateXorKey_Method1v [MS-OFFCRYPTO] §2.3.7.2, řízená dvěma konstantními tabulkami (InitialCode, 15 slov, aXorMatrix, 105 slov) - XOR pole, 16 bajtů složených z bajtů hesla doplněných pevnou 16bajtovou výplní, každý XORovaný s nízkým bajtem klíče (sudé pozice) nebo vysokým (liché pozice), pak otočený doprava o jeden bit
Hlavičky záznamů zůstávají v čitelné podobě a totéž platí o hrstce celých záznamů, které schéma vynechává, mimo jiné BOF, FILEPASS a INTERFACEHDR. Každé jiné tělo záznamu se transformuje bajt po bajtu: rotace doleva o 5 bitů, pak XOR s jednou položkou 16bajtového pole. Dešifrování, které [MS-OFFCRYPTO] §2.3.7.3 popisuje jako DecryptData_Method1, je zrcadlový obraz: nejdřív XOR, pak rotace doprava o 5
V HotXLS se čehokoli z toho přímo nedotknete. Nastavte heslo, zvolte formát a SaveAs vypustí 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 jen XOR obfuskaci; xletAuto by ji zvolilo také
Wb.EncryptionType := xletXor;
// Držte se ASCII a nejvýš 15 znaků (viz níže)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
Proč Excel tvrdí, že heslo je špatně, když verifikátor sedí?
Excel heslo odmítá, protože klíči uloženému v souboru nevěří: klíč si odvozuje z napsaného hesla přes CreateXorKey_Method1 a porovnává ho s klíčovým slovem v FILEPASS, takže soubor s jakýmkoli jiným klíčem kontrolu hesla neprojde, i když je verifikátor správný. Specifikace popisuje klíč jako výstup hesla, ne jako volný parametr, a Excel 16 si tohle čtení vynucuje
Zapisovač HotXLS před v2.384.54 naplnil klíčové slovo dvěma náhodnými bajty. Na papíře to vypadá neškodně, protože verifikátor je zdokumentovaná kontrola hesla a pole se staví z klíče, který si soubor deklaruje jakýkoli. HotXLS sám tyhle soubory četl bez obtíží, protože jeho čtečka brala klíč z FILEPASS jako danost. Excel 16 dostal tentýž soubor se správným heslem a odpověděl, že heslo není správné. Od v2.384.54 se klíč odvozuje, takže FILEPASS pro heslo secret vždycky drží klíč $014D a verifikátor $DAA7, hodnoty prověřené proti nezávislé implementaci specifikace
Samotné odvození je krátké, jakmile jsou obě tabulky na místě. Projděte heslo pozpátku, sedmkrát se podívejte na bit 6 každého bajtu, zatímco ho posunete doleva, a pokaždé, když je bit nastavený, přičtěte XORem jednu položku XorMatrix. Následuje principová skica, která reprodukuje algoritmus specifikace a sedí na implementaci HotXLS; není to API HotXLS:
// Principová skica [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// a CreateXorArray_Method1 (jen ilustrace, ne 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; // klíč vidí jen 15 bajtů
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)); // rotace 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;
Pro secret z toho vypadne pole 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, šikovný fixture, pokud testujete vlastní čtečku
Proč správný klíč pořád dává poškozený soubor?
Správný klíč pořád dává poškozený soubor, když se XOR pole otočí nesprávnou stranou: [MS-OFFCRYPTO] definuje krok pole jako XorRor, rotaci doprava o jeden bit, a pole otočené doleva o dva rozšifruje každé tělo záznamu na šum. Oprava klíče posunula Excel 16 za výzvu k heslu a rovnou do jiné chyby, hlášení, že soubor má problém a nejde otevřít
Starý kód HotXLS otáčel každý bajt pole doleva o 2 bity, forma, která koluje v několika BIFF implementacích. Protože HotXLS použil stejnou rotaci na obou stranách, jeho vlastní čtečka si ničeho nevšimla. Excel 16 už neumí ukládat soubory Excel 5.0/95 a při ukládání BIFF8 XOR nenabídne, takže k dispozici nebyl žádný nativní vzorek od Excelu k porovnání. Důkaz musel přijít opačným směrem: napsat jeden čitelný BIFF5 stream, překódovat ho osmi způsoby a nechat Excel 16 otevřít každou variantu. Těch osm variant protlo tři nezávislá rozhodnutí:
| Rozhodnutí | Varianta A | Varianta B |
|---|---|---|
| Rotace pole | XorRor (rotace doprava 1) | Rotace doleva 2 |
| Index pole | (offset + délka záznamu) mod 16 | offset mod 16 |
| Pořadí bajtové transformace | Rotace doleva 5, pak XOR | XOR, pak rotace doleva 5 |
Excel 16 otevřel přesně dvě z osmi: XorRor s rotací-pak-XOR a indexem podle délky záznamu, a jednu variantu, která vypadá jen jinak. Rotace-doleva-2 s XOR-pak-rotace a stejným indexem je táž funkce v převleku. Rotace se přes XOR distribuuje, takže rol5(p xor rol2(b)) se rovná rol5(p) xor rol7(b) a na 8bitové hodnotě je rotace doleva o 7 rotací doprava o 1. Stručně, rol5 ∘ rol2 = ror1, a proto působí pole s rotací doleva o 2 osamoceně věrohodně: správné je jen spolu s opačným pořadím transformace. V páru s pořadím ze specifikace poškodí každý transformovaný bajt
Týž pokus vyřešil i druhou otázku. Varianty, které z indexu vypustily délku záznamu, selhaly všechny, čímž potvrdily pravidlo indexu, které si HotXLS osvojil vydání dřív jen na základě textu specifikace
Jak se pro každý bajt počítá XorArrayIndex?
XorArrayIndex pro bajt je jeho offset v streamu sešitu plus délka celých dat záznamu, do kterého patří, mod 16. Index se proto pro každý záznam restartuje na hodnotě závislé na záznamu a uvnitř něj narůstá o jedničku na bajt. Pseudokód specifikace jmenuje vstupy FileOffset a Data.Length, což se snadno pomístě čte jako samotný počáteční offset záznamu, a přesně tahle mylná interpretace byla tím, co HotXLS dodával až do v2.384.47
Tři detaily rozhodují, zda se vaše indexy sestaví s Excelem:
- Čtyřbajtová hlavička záznamu se netransformuje nikdy, ale streamové pozice zabírá pořád, takže první bajt těla záznamu sedí na offsetu hlavičky + 4
- Délkový člen je celá délka dat záznamu, ne počet bajtů skutečně transformovaných
- BOUNDSHEET je částečně čitelný: jeho první 4 bajty,
lbPlyPos, streamový offset BOF listu, zůstávají čitelné, aby parser mohl listy najít. Transformace tyhle 4 bajty vynechává, ale do offsetu i délky záznamu se počítají
Poskládaná, transformace na záznam je pár řádků. Zase opakuji, je to skica pravidla, nic, co byste museli volat:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// Zašumí tělo jednoho záznamu na místě. BodyPos je offset v streamu pro
// Body[0], tedy offset hlavičky záznamu + 4. PlainPrefix je 4 pro
// BOUNDSHEET, celá délka pro BOF / FILEPASS, 0 pro většinu záznamů
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;
// Čtení je zrcadlový obraz: B := Body[I] xor Arr[...];
// pak rotace doprava o 5, tedy Rol8(B, 3)
Před v2.384.47 počítala čtečka HotXLS index jen ze streamové pozice a zapisovač použil čitelný bajtový offset. Oba ignorovaly délku záznamu, takže se obě poloviny zase shodly mezi sebou a s nikým jiným. Nezávisle napsaný dekodér četl výstup v2.384.47 správně a starší výstup jako nesmysl a osmivariantní test v Excelu 16 pravidlo později potvrdil proti reálnému cíli
Co se stane s XOR soubory od starších verzí HotXLS?
HotXLS si čte vlastní XOR soubory starší než v2.384.54 kontrolou FILEPASS klíče: když uložený klíč odpovídá klíči odvozenému z hesla, staví čtečka pole XorRor podle specifikace, a když se liší, bere čtečka soubor jako starý soubor HotXLS a postaví pole s rotací doleva o 2. Soubory od Excelu vždycky nesou odvozený klíč, takže vždycky jdou cestou specifikace
Test je heuristika s přesnou mírou selhání. Starý soubor, jehož náhodný klíč náhodou odpovídal odvozenému klíči, by se četl se špatným polem a šance na to je 1 z 65,536. Fallback pokrývá jen rotaci pole; pravidlo indexu se nepřepíná, takže soubory, které zachrání, jsou ty napsané mezi v2.384.47 a v2.384.53. Máte-li pořád BIFF5 XOR soubory z toho okna, otevřete je současným HotXLS a uložte znovu, abyste dostali soubor, který Excel přijme
Dva detaily hesla platí pro každý soubor, starý i nový:
- Délka.
CreateXorKey_Method1čte jen prvních 15 bajtů hesla, což je limit ze specifikace. HotXLS aplikuje ten strop na klíč a verifikátor i pole si nechává na obvyklých pravidlech plné délky a 16 bajtů, konzistentně na obou stranách. Sám Excel u tohohle formátu odmítá hesla delší než 15 znaků, takže berte 15 jako reálné maximum - Znaková sada. HotXLS převádí heslo na bajty přes systémovou ANSI kódovou stránku. Specifikace popisuje braní nižšího bajtu každého znaku UTF-16, což se pro ASCII kryje. Bez vzorků od Excelu chráněných ne-ASCII hesly není pro zbytek žádná pravda poslední instance, takže u XOR souborů zůstaňte u ASCII hesel
Na straně čtení vám TXLSWorkbook.OnPassword umožní po heslo se zeptat, když Open narazí na záznam FILEPASS. Událost je TXLSPasswordEvent s var PassWord: WideString a var Retry: Boolean; nastavte Retry na True pro nový pokus, nejvýš tři 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: chybí heslo, když je vyžadováno; -1005: špatné 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;
Znáte-li heslo už předem, Open(FileName, APassWord) událost úplně přeskočí
Je XOR obfuskace k něčemu dost bezpečná?
XOR obfuskace BIFF není šifrování a nechrání nic proti motivovanému čtenáři. Kontrola hesla je 16bitový verifikátor, klíč má 16 bitů a 16bajtové pole se opakuje napříč celým streamem, takže předvídatelný obsah záznamů BIFF vyzradí bajty pole bez jakéhokoli hesla. HotXLS zapisuje XOR jen proto, že soubory Excel 5.0/95 nemají jinou možnost, a důvodem k jejich produkci dnes je legacy konzument, který nic novějšího neumí číst
Klasické jádro volí schéma přes TXLSWorkbook.EncryptionType a kombinace s formátem ukládání se kontroluje přísně:
xletAuto(výchozí) zapisuje RC4 CryptoAPI proxlExcel97a XOR proxlExcel5, stejně jako to Excel sám dělal pro každý formátxletXorplatí jen pro BIFF5; sxlExcel97ukládání vyvolá výjimku místo tichého ústupuxletRC4axletRC4CryptoAPIjsou jen pro BIFF8 a žádost o ně při ukládání BIFF5 taky vyvolá výjimku
RC4 je taky zastaralý a detaily jeho interoperability pokrývá článek proč Excel odmítá šifrovaný sešit se správným heslem. Umí-li příjemce číst XLSX, použijte raději XLSX jádro: TXLSXWorkbook.SaveAsEncryptedAgile zapisuje Agile Encryption (hashování hesla SHA-512 se spin countem 100 000 iterací a AES-256-CBC), formát, který od Excelu 2010 výše píše Excel defaultně, zatímco SaveAsEncrypted zapisuje starší AES-128 Standard Encryption. Kompromisy mezi oběma najdete v článku šifrování souborů XLSX pomocí AES v Delphi a stranu čtení pokrývá čtení souborů Excelu šifrovaných Agile s HotXLS
Rychlý přehled: XOR obfuskace BIFF, kterou Excel 16 přijme
- FILEPASS (
$002F) následuje za globals BOF; v BIFF5 má tělo 4 bajty: klíč, pak verifikátor - Klíč =
CreateXorKey_Method1(password)podle [MS-OFFCRYPTO] §2.3.7.2, nikdy náhodný; prosecretje to$014D - Pole = bajty hesla + výplň, XOR s nízkým bajtem klíče na sudých pozicích a vysokým na lichých, pak XorRor (rotace doprava 1)
- Zašifrování bajtu: rotace doleva 5, pak XOR; dešifrování: XOR, pak rotace doprava 5 (§2.3.7.3)
- XorArrayIndex = (bajtový offset v streamu + délka dat záznamu) mod 16; hlavičky a čitelné prefixy se do offsetu počítají
- BOUNDSHEET drží svých prvních 4 bajtů čitelných; BOF, FILEPASS a INTERFACEHDR zůstávají čitelné celé
- Hesla: ASCII, nejvýš 15 znaků
- HotXLS: index opraven ve v2.384.47, klíč a XorRor ve v2.384.54, starší XOR soubory HotXLS se poznají podle neshody klíče
- Pro skutečnou ochranu použijte minimálně BIFF8 RC4 CryptoAPI, nebo XLSX Agile Encryption
HotXLS obsluhuje ochranu heslem BIFF5 i BIFF8, XLSX Standard a Agile Encryption a čtecí callback pro heslo z jedné knihovny pro Delphi a C++Builder, s výše uvedenými interop detaily vyřešenými za vás. Edice, platformy a zkušební verzi najdete na stránce tabulková komponenta HotXLS pro Delphi